-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #231 from shalb/kubernetes-provider
Kubernetes provider
- Loading branch information
Showing
20 changed files
with
1,476 additions
and
119 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,6 +51,7 @@ | |
"grafana", | ||
"GREEDYDATA", | ||
"gsub", | ||
"hclwrite", | ||
"HTTPDATE", | ||
"id", | ||
"inittf", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, ".") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.