Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kubernetes provider #231

Merged
merged 2 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"grafana",
"GREEDYDATA",
"gsub",
"hclwrite",
"HTTPDATE",
"id",
"inittf",
Expand Down
2 changes: 1 addition & 1 deletion docs/units-kubernetes.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
!!! Info
This unit is deprecated and will be removed soon. Please use the k8s-manifest unit instead.

Describes [Terraform kubernetes-alpha provider](https://github.com/hashicorp/terraform-provider-kubernetes-alpha) invocation.
Describes [Terraform kubernetes provider](https://github.com/hashicorp/terraform-provider-kubernetes-alpha) invocation.

Example:

Expand Down
248 changes: 165 additions & 83 deletions go.mod

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions pkg/hcltools/hcl_block.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package hcltools

import (
"strings"

"github.com/apex/log"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/shalb/cluster.dev/pkg/hcltools/tfkschema"

// "github.com/shalb/cluster.dev/pkg/hcltools/tfkschema"

"github.com/zclconf/go-cty/cty"
)

// hclBlock is a wrapper for hclwrite.Block that allows tagging some extra
// metadata to each block.
type hclBlock struct {
//
name string

//
fieldName string

// The parent hclBlock to this hclBlock
parent *hclBlock

// The wrapped HCL block
hcl *hclwrite.Block

// hasValue means a child field of this block had a non-nil / non-zero value.
// If this is false when closeBlock() is called, the block won't be appended to
// parent
hasValue bool

// inlined flags whether this block is "transparent" Some Structs in the
// Kubernetes API structure are marked as "inline", meaning they don't create
// a new block, and their child value is propagated up the hierarchy.
// See v1.Volume as an example
inlined bool

// inlined flags whether this block is supported in the Terraform Provider schema
// Unsupported blocks will be excluded from HCL rendering
unsupported bool

// isMap flags whether the output of this block will be map syntax rather than a sub-block
// e.g.
// mapName = {
// key = "value"
// }
// vs.
// mapName {
// key = "value"
// }
// In TF0.12.0 Schema attributes of type schema.TypeMap must be written with the former syntax, and sub-blocks
// most use the latter.
// However, there are some cases where a Golang map on the Kubernetes object side is not defined
// as schema.TypeMap on the Terraform side (e.g. Container.Limits) so isMap is used to track how this block
// should be outputted.
isMap bool
hclMap map[string]cty.Value
}

// A child block is adding a sub-block, write HCL to:
// - this hclBlock's hcl Body if this block is not inlined
// - parent's HCL body if this block is "inlined"
func (b *hclBlock) AppendBlock(hcl *hclwrite.Block) {
if b.inlined {
// append to parent
b.parent.AppendBlock(hcl)

} else {
b.hcl.Body().AppendBlock(hcl)

}
}

// A child block is adding an attribute, write HCL to:
// - this hclBlock's hcl Body if this block is not inlined
// - parent's HCL body if this block is "inlined"
func (b *hclBlock) SetAttributeValue(name string, val cty.Value) {
if b.isMap {
if b.hclMap == nil {
b.hclMap = map[string]cty.Value{name: val}
} else {
b.hclMap[name] = val
}

} else if tfkschema.IsAttributeSupported(b.FullSchemaName() + "." + name) {
if b.inlined {
// append to parent
b.parent.SetAttributeValue(name, val)
} else {
b.hcl.Body().SetAttributeValue(name, val)
}
} else {
log.Debugf("skipping attribute: %s - not supported by provider", name)
}
}

func (b *hclBlock) FullSchemaName() string {
parentName := ""
if b.parent != nil {
parentName = b.parent.FullSchemaName()
}

if b.inlined {
return parentName
}
return strings.TrimLeft(parentName+"."+b.name, ".")
}

func (b *hclBlock) isRequired() bool {
if b.parent == nil {
// top level resource block is always required.
return true
}

required := tfkschema.IsAttributeRequired(b.FullSchemaName())

if required && !b.hasValue && !b.parent.isRequired() {
// If current attribute has no value, only flag as required if parent(s) are also required.
// This is to match how Terraform handles the Required flag of nested attributes.
required = false
}

return required
}

func (b *hclBlock) FullFieldName() string {
parentName := ""
if b.parent != nil {
parentName = b.parent.FullFieldName()
}

if b.inlined {
return parentName
}
return strings.TrimLeft(parentName+"."+b.fieldName, ".")
}
151 changes: 151 additions & 0 deletions pkg/hcltools/k2tf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package hcltools

import (
"reflect"
"strings"

"github.com/apex/log"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/iancoleman/strcase"
"github.com/jinzhu/inflection"
"github.com/mitchellh/reflectwalk"
"gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"

// "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/scheme"
)

func Kubernetes2HCLCustom(manifest interface{}, key string, rootBody *hclwrite.Body) error {
unitBlock := rootBody.AppendNewBlock("resource", []string{"kubernetes_manifest", key})
unitBody := unitBlock.Body()
tokens := hclwrite.Tokens{&hclwrite.Token{Type: hclsyntax.TokenQuotedLit, Bytes: []byte(" kubernetes"), SpacesBefore: 1}}
unitBody.SetAttributeRaw("provider", tokens)
ctyVal, err := InterfaceToCty(manifest)
if err != nil {
return err
}
unitBody.SetAttributeValue("manifest", ctyVal)
return nil
}

func Kubernetes2HCL(manifest interface{}, dst *hclwrite.Body) error {
manifestRaw, err := yaml.Marshal(manifest)
if err != nil {
return err
}
d := scheme.Codecs.UniversalDeserializer()
obj, _, err := d.Decode([]byte(manifestRaw), nil, nil)
if err != nil {
log.Debug("Could not decode YAML object, malformed manifest or unknown k8s API/Kind, generating common resource")
return err
}
w, err := NewObjectWalker(obj, dst)
if err != nil {
return err
}

return reflectwalk.Walk(obj, w)
}

func ToTerraformSubBlockName(field *reflect.StructField, path string) string {
name := extractProtobufName(field)

return NormalizeTerraformName(name, true, path)
}

func ToTerraformAttributeName(field *reflect.StructField, path string) string {
name := extractProtobufName(field)

return NormalizeTerraformName(name, false, path)
}

// ToTerraformResourceType converts a Kubernetes API Object Type name to the
// equivalent `terraform-provider-kubernetes` schema name.
// Src: https://github.com/sl1pm4t/k2tf
func ToTerraformResourceType(obj runtime.Object, blockKind *schema.GroupVersionKind) string {
if blockKind == nil {
return ""
}
kind := blockKind.Kind
switch kind {
case "Ingress":
if blockKind.Version == "networking.k8s.io/v1" {
kind = "ingress_v1"
} else {
kind = "ingress"
}
default:
kind = NormalizeTerraformName(blockKind.Kind, false, "")
}
return "kubernetes_" + kind
}

// normalizeTerraformName converts the given string to snake case
// and optionally to singular form of the given word
// s is the string to normalize
// set toSingular to true to singularize the given word
// path is the full schema path to the named element
// Src: https://github.com/sl1pm4t/k2tf
func NormalizeTerraformName(s string, toSingular bool, path string) string {
switch s {
case "DaemonSet":
return "daemonset"

case "nonResourceURLs":
if strings.Contains(path, "role.rule") {
return "non_resource_urls"
}

case "updateStrategy":
if !strings.Contains(path, "stateful") {
return "strategy"
}

case "limits":
if strings.Contains(path, "limit_range.spec") {
return "limit"
}

case "ports":
if strings.Contains(path, "kubernetes_network_policy.spec") {
return "ports"
}

case "externalIPs":
if strings.Contains(path, "kubernetes_service.spec") {
return "external_ips"
}
}

if toSingular {
s = inflection.Singular(s)
}
s = strcase.ToSnake(s)

// colons and dots are not allowed by Terraform
s = strings.ReplaceAll(s, ":", "_")
s = strings.ReplaceAll(s, ".", "_")

return s
}

func extractProtobufName(field *reflect.StructField) string {
protoTag := field.Tag.Get("protobuf")
if protoTag == "" {
log.Warnf("field [%s] has no protobuf tag", field.Name)
return field.Name
}

tagParts := strings.Split(protoTag, ",")
for _, part := range tagParts {
if strings.Contains(part, "name=") {
return part[5:]
}
}

log.Warnf("field [%s] protobuf tag has no 'name'", field.Name)
return field.Name
}
13 changes: 13 additions & 0 deletions pkg/hcltools/tfkschema/attr_overrides.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package tfkschema

// IncludedOnZero checks the attribute name against a lookup table to determine if it can be included
// when it is zero / empty.
func IncludedOnZero(attrName string) bool {
switch attrName {
case "EmptyDir":
return true
case "RunAsUser":
return true
}
return false
}
38 changes: 38 additions & 0 deletions pkg/hcltools/tfkschema/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package tfkschema

import (
"reflect"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)

type K8sObject struct {
Object runtime.Object
GroupVersionKind *schema.GroupVersionKind
}

func ObjectMeta(obj runtime.Object) metav1.ObjectMeta {
v := reflect.ValueOf(obj)

if v.Kind() == reflect.Ptr {
v = v.Elem()
}

metaF := v.FieldByName("ObjectMeta")

return metaF.Interface().(metav1.ObjectMeta)
}

func TypeMeta(obj runtime.Object) metav1.TypeMeta {
v := reflect.ValueOf(obj)

if v.Kind() == reflect.Ptr {
v = v.Elem()
}

metaF := v.FieldByName("TypeMeta")

return metaF.Interface().(metav1.TypeMeta)
}
Loading
Loading