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

feat: add support for overriding environment label (#824) #975

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
7 changes: 6 additions & 1 deletion pkg/kubernetes/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,13 @@ See https://tanka.dev/garbage-collection for more details`)
// get all resources matching our label
start = time.Now()
log.Info().Msg("fetching resources previously created by this env")

nameLabel, err := k.Env.NameLabel()
if err != nil {
return nil, err
}
matched, err := k.ctl.GetByLabels("", kinds, map[string]string{
process.LabelEnvironment: k.Env.Metadata.NameLabel(),
process.LabelEnvironment: nameLabel,
})
if err != nil {
return nil, err
Expand Down
16 changes: 12 additions & 4 deletions pkg/process/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ func Process(cfg v1alpha1.Environment, exprs Matchers) (manifest.List, error) {
out = Namespace(out, cfg.Spec.Namespace)

// tanka.dev/** labels
out = Label(out, cfg)
out, err = Label(out, cfg)
if err != nil {
return nil, err
}

// arbitrary labels and annotations from spec
out = ResourceDefaults(out, cfg)
Expand All @@ -62,16 +65,21 @@ func Process(cfg v1alpha1.Environment, exprs Matchers) (manifest.List, error) {
}

// Label conditionally adds tanka.dev/** labels to each manifest in the List
func Label(list manifest.List, cfg v1alpha1.Environment) manifest.List {
func Label(list manifest.List, cfg v1alpha1.Environment) (manifest.List, error) {
for i, m := range list {
// inject tanka.dev/environment label
if cfg.Spec.InjectLabels {
m.Metadata().Labels()[LabelEnvironment] = cfg.Metadata.NameLabel()
label, err := cfg.NameLabel()
if err != nil {
return nil, fmt.Errorf("failed to get name label: %w", err)
}

m.Metadata().Labels()[LabelEnvironment] = label
}
list[i] = m
}

return list
return list, nil
}

func ResourceDefaults(list manifest.List, cfg v1alpha1.Environment) manifest.List {
Expand Down
5 changes: 4 additions & 1 deletion pkg/process/process_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,10 @@ func TestProcess(t *testing.T) {

if env.Spec.InjectLabels {
for i, m := range c.flat {
m.Metadata().Labels()[LabelEnvironment] = env.Metadata.NameLabel()
nameLabel, err := env.NameLabel()
require.NoError(t, err)

m.Metadata().Labels()[LabelEnvironment] = nameLabel
c.flat[i] = m
}
}
Expand Down
49 changes: 43 additions & 6 deletions pkg/spec/v1alpha1/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package v1alpha1
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"strings"
)

// New creates a new Environment object with internal values already set
Expand Down Expand Up @@ -31,6 +33,46 @@ type Environment struct {
Data interface{} `json:"data,omitempty"`
}

func (e Environment) NameLabel() (string, error) {
envLabelFields := e.Spec.TankaEnvLabelFromFields
if len(envLabelFields) == 0 {
envLabelFields = []string{
".metadata.name",
".metadata.namespace",
}
}

envLabelFieldValues, err := e.getFieldValuesByLabel(envLabelFields)
if err != nil {
return "", fmt.Errorf("failed to retrieve field values for label: %w", err)
}

labelParts := strings.Join(envLabelFieldValues, ":")
partsHash := sha256.Sum256([]byte(labelParts))
chars := []rune(hex.EncodeToString(partsHash[:]))
return string(chars[:48]), nil
}

func (e Environment) getFieldValuesByLabel(labels []string) ([]string, error) {
if len(labels) == 0 {
return nil, errors.New("labels must be set")
}

fieldValues := make([]string, len(labels))
for idx, label := range labels {
keyPath := strings.Split(strings.TrimPrefix(label, "."), ".")

labelValue, err := getDeepFieldAsString(e, keyPath)
if err != nil {
return nil, fmt.Errorf("could not get struct value at path: %w", err)
}

fieldValues[idx] = labelValue
}

return fieldValues, nil
}

// Metadata is meant for humans and not parsed
type Metadata struct {
Name string `json:"name,omitempty"`
Expand All @@ -49,12 +91,6 @@ func (m Metadata) Get(label string) (value string) {
return m.Labels[label]
}

func (m Metadata) NameLabel() string {
partsHash := sha256.Sum256([]byte(fmt.Sprintf("%s:%s", m.Name, m.Namespace)))
chars := []rune(hex.EncodeToString(partsHash[:]))
return string(chars[:48])
}

// Spec defines Kubernetes properties
type Spec struct {
APIServer string `json:"apiServer,omitempty"`
Expand All @@ -63,6 +99,7 @@ type Spec struct {
DiffStrategy string `json:"diffStrategy,omitempty"`
ApplyStrategy string `json:"applyStrategy,omitempty"`
InjectLabels bool `json:"injectLabels,omitempty"`
TankaEnvLabelFromFields []string `json:"tankaEnvLabelFromFields,omitempty"`
ResourceDefaults ResourceDefaults `json:"resourceDefaults"`
ExpectVersions ExpectVersions `json:"expectVersions"`
ExportJsonnetImplementation string `json:"exportJsonnetImplementation,omitempty"`
Expand Down
131 changes: 131 additions & 0 deletions pkg/spec/v1alpha1/environment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package v1alpha1

import (
"crypto/sha256"
"encoding/hex"
"testing"

"github.com/stretchr/testify/assert"
)

func TestEnvironmentNameLabel(t *testing.T) {
type testCase struct {
name string
inputEnvironment *Environment
expectedLabelPreHash string
expectError bool
}

testCases := []testCase{
{
name: "Default environment label hash",
inputEnvironment: &Environment{
Spec: Spec{
Namespace: "default",
},
Metadata: Metadata{
Name: "environments/a-nice-go-test",
Namespace: "main.jsonnet",
},
},
expectedLabelPreHash: "environments/a-nice-go-test:main.jsonnet",
},
{
name: "Overridden single nested field",
inputEnvironment: &Environment{
Spec: Spec{
Namespace: "default",
TankaEnvLabelFromFields: []string{
".metadata.name",
},
},
Metadata: Metadata{
Name: "environments/another-nice-go-test",
},
},
expectedLabelPreHash: "environments/another-nice-go-test",
},
{
name: "Overridden multiple nested field",
inputEnvironment: &Environment{
Spec: Spec{
Namespace: "default",
TankaEnvLabelFromFields: []string{
".metadata.name",
".spec.namespace",
},
},
Metadata: Metadata{
Name: "environments/another-nice-go-test",
},
},
expectedLabelPreHash: "environments/another-nice-go-test:default",
},
{
name: "Override field of map type",
inputEnvironment: &Environment{
Spec: Spec{
TankaEnvLabelFromFields: []string{
".metadata.labels.project",
},
},
Metadata: Metadata{
Name: "environments/another-nice-go-test",
Labels: map[string]string{
"project": "an-equally-nice-project",
},
},
},
expectedLabelPreHash: "an-equally-nice-project",
},
{
name: "Label value not primitive type",
inputEnvironment: &Environment{
Spec: Spec{
TankaEnvLabelFromFields: []string{
".metadata",
},
},
Metadata: Metadata{
Name: "environments/another-nice-go-test",
},
},
expectError: true,
},
{
name: "Attempted descent past non-object like type",
inputEnvironment: &Environment{
Spec: Spec{
TankaEnvLabelFromFields: []string{
".metadata.name.nonExistent",
},
},
Metadata: Metadata{
Name: "environments/not-an-object",
},
},
expectError: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
expectedLabelHashParts := sha256.Sum256([]byte(tc.expectedLabelPreHash))
expectedLabelHashChars := []rune(hex.EncodeToString(expectedLabelHashParts[:]))
expectedLabelHash := string(expectedLabelHashChars[:48])
actualLabelHash, err := tc.inputEnvironment.NameLabel()

if tc.expectedLabelPreHash != "" {
assert.Equal(t, expectedLabelHash, actualLabelHash)
} else {
assert.Equal(t, "", actualLabelHash)
}

if tc.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
89 changes: 89 additions & 0 deletions pkg/spec/v1alpha1/reflect_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package v1alpha1

import (
"errors"
"reflect"
"strconv"
"strings"
)

func getDeepFieldAsString(obj interface{}, keyPath []string) (string, error) {
if !isSupportedType(obj, []reflect.Kind{reflect.Struct, reflect.Pointer, reflect.Map}) {
return "", errors.New("intermediary objects must be object types")
}

objValue := reflectValue(obj)
objType := objValue.Type()

var nextFieldValue reflect.Value

switch objType.Kind() {
case reflect.Struct, reflect.Pointer:
fieldsCount := objType.NumField()

for i := 0; i < fieldsCount; i++ {
candidateType := objType.Field(i)
candidateValue := objValue.Field(i)
jsonTag := candidateType.Tag.Get("json")

if strings.Split(jsonTag, ",")[0] == keyPath[0] {
nextFieldValue = candidateValue
break
}
}

case reflect.Map:
for _, key := range objValue.MapKeys() {
nextFieldValue = objValue.MapIndex(key)
}
}

if len(keyPath) == 1 {
return getReflectValueAsString(nextFieldValue)
}

if nextFieldValue.Type().Kind() == reflect.Pointer {
nextFieldValue = nextFieldValue.Elem()
}

return getDeepFieldAsString(nextFieldValue.Interface(), keyPath[1:])
}

func getReflectValueAsString(val reflect.Value) (string, error) {
switch val.Type().Kind() {
case reflect.String:
return val.String(), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(val.Int(), 10), nil
case reflect.Float32:
return strconv.FormatFloat(val.Float(), 'f', -1, 32), nil
case reflect.Float64:
return strconv.FormatFloat(val.Float(), 'f', -1, 64), nil
case reflect.Bool:
return strconv.FormatBool(val.Bool()), nil
default:
return "", errors.New("unsupported value type")
}
}

func reflectValue(obj interface{}) reflect.Value {
var val reflect.Value

if reflect.TypeOf(obj).Kind() == reflect.Pointer {
val = reflect.ValueOf(obj).Elem()
} else {
val = reflect.ValueOf(obj)
}

return val
}

func isSupportedType(obj interface{}, types []reflect.Kind) bool {
for _, t := range types {
if reflect.TypeOf(obj).Kind() == t {
return true
}
}

return false
}
Loading