Skip to content

Commit 894830a

Browse files
DeanBruntThirdfortjulienduchesne
authored andcommitted
feat: Add support for overriding environment label
This change adds support for overriding the environment label using fields on the environment as per grafana#824. To achieve this, the NameLabel() function that generates this needed to be lifted to the Environment struct (from Metadata) to ensure it can access the fields it needs. Additionally, its signature now returns an error as there are ways errors could occur during this generation now that should be surfaced neatly to the user.
1 parent 06d542e commit 894830a

File tree

6 files changed

+285
-12
lines changed

6 files changed

+285
-12
lines changed

pkg/kubernetes/apply.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,13 @@ See https://tanka.dev/garbage-collection for more details`)
6363
// get all resources matching our label
6464
start = time.Now()
6565
log.Info().Msg("fetching resources previously created by this env")
66+
67+
nameLabel, err := k.Env.NameLabel()
68+
if err != nil {
69+
return nil, err
70+
}
6671
matched, err := k.ctl.GetByLabels("", kinds, map[string]string{
67-
process.LabelEnvironment: k.Env.Metadata.NameLabel(),
72+
process.LabelEnvironment: nameLabel,
6873
})
6974
if err != nil {
7075
return nil, err

pkg/process/process.go

+12-4
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ func Process(cfg v1alpha1.Environment, exprs Matchers) (manifest.List, error) {
4545
out = Namespace(out, cfg.Spec.Namespace)
4646

4747
// tanka.dev/** labels
48-
out = Label(out, cfg)
48+
out, err = Label(out, cfg)
49+
if err != nil {
50+
return nil, err
51+
}
4952

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

6467
// Label conditionally adds tanka.dev/** labels to each manifest in the List
65-
func Label(list manifest.List, cfg v1alpha1.Environment) manifest.List {
68+
func Label(list manifest.List, cfg v1alpha1.Environment) (manifest.List, error) {
6669
for i, m := range list {
6770
// inject tanka.dev/environment label
6871
if cfg.Spec.InjectLabels {
69-
m.Metadata().Labels()[LabelEnvironment] = cfg.Metadata.NameLabel()
72+
label, err := cfg.NameLabel()
73+
if err != nil {
74+
return nil, fmt.Errorf("failed to get name label: %w", err)
75+
}
76+
77+
m.Metadata().Labels()[LabelEnvironment] = label
7078
}
7179
list[i] = m
7280
}
7381

74-
return list
82+
return list, nil
7583
}
7684

7785
func ResourceDefaults(list manifest.List, cfg v1alpha1.Environment) manifest.List {

pkg/process/process_test.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,10 @@ func TestProcess(t *testing.T) {
116116

117117
if env.Spec.InjectLabels {
118118
for i, m := range c.flat {
119-
m.Metadata().Labels()[LabelEnvironment] = env.Metadata.NameLabel()
119+
nameLabel, err := env.NameLabel()
120+
require.NoError(t, err)
121+
122+
m.Metadata().Labels()[LabelEnvironment] = nameLabel
120123
c.flat[i] = m
121124
}
122125
}

pkg/spec/v1alpha1/environment.go

+43-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package v1alpha1
33
import (
44
"crypto/sha256"
55
"encoding/hex"
6+
"errors"
67
"fmt"
8+
"strings"
79
)
810

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

36+
func (e Environment) NameLabel() (string, error) {
37+
envLabelFields := e.Spec.TankaEnvLabelFromFields
38+
if len(envLabelFields) == 0 {
39+
envLabelFields = []string{
40+
".metadata.name",
41+
".metadata.namespace",
42+
}
43+
}
44+
45+
envLabelFieldValues, err := e.getFieldValuesByLabel(envLabelFields)
46+
if err != nil {
47+
return "", fmt.Errorf("failed to retrieve field values for label: %w", err)
48+
}
49+
50+
labelParts := strings.Join(envLabelFieldValues, ":")
51+
partsHash := sha256.Sum256([]byte(labelParts))
52+
chars := []rune(hex.EncodeToString(partsHash[:]))
53+
return string(chars[:48]), nil
54+
}
55+
56+
func (e Environment) getFieldValuesByLabel(labels []string) ([]string, error) {
57+
if len(labels) == 0 {
58+
return nil, errors.New("labels must be set")
59+
}
60+
61+
fieldValues := make([]string, len(labels))
62+
for idx, label := range labels {
63+
keyPath := strings.Split(strings.TrimPrefix(label, "."), ".")
64+
65+
labelValue, err := getDeepFieldAsString(e, keyPath)
66+
if err != nil {
67+
return nil, fmt.Errorf("could not get struct value at path: %w", err)
68+
}
69+
70+
fieldValues[idx] = labelValue
71+
}
72+
73+
return fieldValues, nil
74+
}
75+
3476
// Metadata is meant for humans and not parsed
3577
type Metadata struct {
3678
Name string `json:"name,omitempty"`
@@ -49,12 +91,6 @@ func (m Metadata) Get(label string) (value string) {
4991
return m.Labels[label]
5092
}
5193

52-
func (m Metadata) NameLabel() string {
53-
partsHash := sha256.Sum256([]byte(fmt.Sprintf("%s:%s", m.Name, m.Namespace)))
54-
chars := []rune(hex.EncodeToString(partsHash[:]))
55-
return string(chars[:48])
56-
}
57-
5894
// Spec defines Kubernetes properties
5995
type Spec struct {
6096
APIServer string `json:"apiServer,omitempty"`
@@ -63,6 +99,7 @@ type Spec struct {
6399
DiffStrategy string `json:"diffStrategy,omitempty"`
64100
ApplyStrategy string `json:"applyStrategy,omitempty"`
65101
InjectLabels bool `json:"injectLabels,omitempty"`
102+
TankaEnvLabelFromFields []string `json:"tankaEnvLabelFromFields,omitempty"`
66103
ResourceDefaults ResourceDefaults `json:"resourceDefaults"`
67104
ExpectVersions ExpectVersions `json:"expectVersions"`
68105
ExportJsonnetImplementation string `json:"exportJsonnetImplementation,omitempty"`

pkg/spec/v1alpha1/environment_test.go

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package v1alpha1
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/hex"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestEnvironmentNameLabel(t *testing.T) {
12+
type testCase struct {
13+
name string
14+
inputEnvironment *Environment
15+
expectedLabelPreHash string
16+
expectError bool
17+
}
18+
19+
testCases := []testCase{
20+
{
21+
name: "Default environment label hash",
22+
inputEnvironment: &Environment{
23+
Spec: Spec{
24+
Namespace: "default",
25+
},
26+
Metadata: Metadata{
27+
Name: "environments/a-nice-go-test",
28+
Namespace: "main.jsonnet",
29+
},
30+
},
31+
expectedLabelPreHash: "environments/a-nice-go-test:main.jsonnet",
32+
},
33+
{
34+
name: "Overriden single nested field",
35+
inputEnvironment: &Environment{
36+
Spec: Spec{
37+
Namespace: "default",
38+
TankaEnvLabelFromFields: []string{
39+
".metadata.name",
40+
},
41+
},
42+
Metadata: Metadata{
43+
Name: "environments/another-nice-go-test",
44+
},
45+
},
46+
expectedLabelPreHash: "environments/another-nice-go-test",
47+
},
48+
{
49+
name: "Overriden multiple nested field",
50+
inputEnvironment: &Environment{
51+
Spec: Spec{
52+
Namespace: "default",
53+
TankaEnvLabelFromFields: []string{
54+
".metadata.name",
55+
".spec.namespace",
56+
},
57+
},
58+
Metadata: Metadata{
59+
Name: "environments/another-nice-go-test",
60+
},
61+
},
62+
expectedLabelPreHash: "environments/another-nice-go-test:default",
63+
},
64+
{
65+
name: "Override field of map type",
66+
inputEnvironment: &Environment{
67+
Spec: Spec{
68+
TankaEnvLabelFromFields: []string{
69+
".metadata.labels.project",
70+
},
71+
},
72+
Metadata: Metadata{
73+
Name: "environments/another-nice-go-test",
74+
Labels: map[string]string{
75+
"project": "an-equally-nice-project",
76+
},
77+
},
78+
},
79+
expectedLabelPreHash: "an-equally-nice-project",
80+
},
81+
{
82+
name: "Label value not primitive type",
83+
inputEnvironment: &Environment{
84+
Spec: Spec{
85+
TankaEnvLabelFromFields: []string{
86+
".metadata",
87+
},
88+
},
89+
Metadata: Metadata{
90+
Name: "environments/another-nice-go-test",
91+
},
92+
},
93+
expectError: true,
94+
},
95+
{
96+
name: "Attempted descent past non-object like type",
97+
inputEnvironment: &Environment{
98+
Spec: Spec{
99+
TankaEnvLabelFromFields: []string{
100+
".metadata.name.nonExistent",
101+
},
102+
},
103+
Metadata: Metadata{
104+
Name: "environments/not-an-object",
105+
},
106+
},
107+
expectError: true,
108+
},
109+
}
110+
111+
for _, tc := range testCases {
112+
t.Run(tc.name, func(t *testing.T) {
113+
expectedLabelHashParts := sha256.Sum256([]byte(tc.expectedLabelPreHash))
114+
expectedLabelHashChars := []rune(hex.EncodeToString(expectedLabelHashParts[:]))
115+
expectedLabelHash := string(expectedLabelHashChars[:48])
116+
actualLabelHash, err := tc.inputEnvironment.NameLabel()
117+
118+
if tc.expectedLabelPreHash != "" {
119+
assert.Equal(t, expectedLabelHash, actualLabelHash)
120+
} else {
121+
assert.Equal(t, "", actualLabelHash)
122+
}
123+
124+
if tc.expectError {
125+
assert.Error(t, err)
126+
} else {
127+
assert.NoError(t, err)
128+
}
129+
})
130+
}
131+
}

pkg/spec/v1alpha1/reflect_utils.go

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package v1alpha1
2+
3+
import (
4+
"errors"
5+
"reflect"
6+
"strconv"
7+
"strings"
8+
)
9+
10+
func getDeepFieldAsString(obj interface{}, keyPath []string) (string, error) {
11+
if !isSupportedType(obj, []reflect.Kind{reflect.Struct, reflect.Pointer, reflect.Map}) {
12+
return "", errors.New("intermediary objects must be object types")
13+
}
14+
15+
objValue := reflectValue(obj)
16+
objType := objValue.Type()
17+
18+
var nextFieldValue reflect.Value
19+
20+
switch objType.Kind() {
21+
case reflect.Struct, reflect.Pointer:
22+
fieldsCount := objType.NumField()
23+
24+
for i := 0; i < fieldsCount; i++ {
25+
candidateType := objType.Field(i)
26+
candidateValue := objValue.Field(i)
27+
jsonTag := candidateType.Tag.Get("json")
28+
29+
if strings.Split(jsonTag, ",")[0] == keyPath[0] {
30+
nextFieldValue = candidateValue
31+
break
32+
}
33+
}
34+
35+
case reflect.Map:
36+
for _, key := range objValue.MapKeys() {
37+
nextFieldValue = objValue.MapIndex(key)
38+
}
39+
}
40+
41+
if len(keyPath) == 1 {
42+
return getReflectValueAsString(nextFieldValue)
43+
}
44+
45+
if nextFieldValue.Type().Kind() == reflect.Pointer {
46+
nextFieldValue = nextFieldValue.Elem()
47+
}
48+
49+
return getDeepFieldAsString(nextFieldValue.Interface(), keyPath[1:])
50+
}
51+
52+
func getReflectValueAsString(val reflect.Value) (string, error) {
53+
switch val.Type().Kind() {
54+
case reflect.String:
55+
return val.String(), nil
56+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
57+
return strconv.FormatInt(val.Int(), 10), nil
58+
case reflect.Float32:
59+
return strconv.FormatFloat(val.Float(), 'f', -1, 32), nil
60+
case reflect.Float64:
61+
return strconv.FormatFloat(val.Float(), 'f', -1, 64), nil
62+
case reflect.Bool:
63+
return strconv.FormatBool(val.Bool()), nil
64+
default:
65+
return "", errors.New("unsupported value type")
66+
}
67+
}
68+
69+
func reflectValue(obj interface{}) reflect.Value {
70+
var val reflect.Value
71+
72+
if reflect.TypeOf(obj).Kind() == reflect.Pointer {
73+
val = reflect.ValueOf(obj).Elem()
74+
} else {
75+
val = reflect.ValueOf(obj)
76+
}
77+
78+
return val
79+
}
80+
81+
func isSupportedType(obj interface{}, types []reflect.Kind) bool {
82+
for _, t := range types {
83+
if reflect.TypeOf(obj).Kind() == t {
84+
return true
85+
}
86+
}
87+
88+
return false
89+
}

0 commit comments

Comments
 (0)