Skip to content

Commit

Permalink
Merge pull request #231 from shalb/kubernetes-provider
Browse files Browse the repository at this point in the history
Kubernetes provider
  • Loading branch information
romanprog authored Oct 24, 2023
2 parents 4b3c6a3 + 4cb46b6 commit a2d14c7
Show file tree
Hide file tree
Showing 20 changed files with 1,476 additions and 119 deletions.
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

0 comments on commit a2d14c7

Please sign in to comment.