Skip to content
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 internal/handlers/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ type HandlerOptions struct {
Client *http.Client
HelmClientOptions *helmclient.RestConfClientOptions
DynamicClient dynamic.Interface
KrateoNamespace string
}
16 changes: 16 additions & 0 deletions internal/handlers/resources/get/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/krateoplatformops/chart-inspector/internal/getter"
"github.com/krateoplatformops/chart-inspector/internal/handlers"
"github.com/krateoplatformops/chart-inspector/internal/helmclient"
"github.com/krateoplatformops/chart-inspector/internal/helmclient/tools"
"github.com/krateoplatformops/chart-inspector/internal/helper"
"github.com/krateoplatformops/chart-inspector/internal/tracer"
"github.com/krateoplatformops/plumbing/http/response"
Expand All @@ -23,6 +24,10 @@ import (
sigsyaml "sigs.k8s.io/yaml"
)

const (
AnnotationKeyReconciliationGracefullyPaused = "krateo.io/gracefully-paused"
)

type handler struct {
handlers.HandlerOptions
}
Expand Down Expand Up @@ -159,6 +164,17 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}

bValues, err = tools.InjectValues(bValues, tools.CompositionValues{
KrateoNamespace: h.KrateoNamespace,
CompositionName: compositionName,
CompositionNamespace: compositionNamespace,
CompositionId: string(composition.GetUID()),
CompositionGroup: compositionGroup,
CompositionResource: compositionResource,
CompositionKind: composition.GetKind(),
GracefullyPaused: composition.GetAnnotations()[AnnotationKeyReconciliationGracefullyPaused] == "true",
})

chartSpec := helmclient.ChartSpec{
InsecureSkipTLSverify: compositionDefinition.Spec.Chart.InsecureSkipVerifyTLS,
ReleaseName: meta.GetReleaseName(composition),
Expand Down
105 changes: 105 additions & 0 deletions internal/helmclient/tools/tools.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package tools

import (
"fmt"

sigsyaml "sigs.k8s.io/yaml"
)

func AddOrUpdateFieldInValues(values []byte, value interface{}, fields ...string) ([]byte, error) {
var valuesMap map[string]interface{}
if err := sigsyaml.Unmarshal(values, &valuesMap); err != nil {
return nil, err
}

// Recursive function to add the value to the map creating nested maps if needed
var addOrUpdateField func(map[string]interface{}, []string, interface{}) error
addOrUpdateField = func(m map[string]interface{}, fields []string, value interface{}) error {
if len(fields) == 1 {
m[fields[0]] = value
return nil
}

if _, ok := m[fields[0]]; !ok {
m[fields[0]] = map[string]interface{}{}
}

if nestedMap, ok := m[fields[0]].(map[string]interface{}); ok {
return addOrUpdateField(nestedMap, fields[1:], value)
} else {
return fmt.Errorf("field %s is not a map", fields[0])
}
}

if err := addOrUpdateField(valuesMap, fields, value); err != nil {
return nil, err
}

return sigsyaml.Marshal(valuesMap)
}

type CompositionValues struct {
KrateoNamespace string
CompositionName string
CompositionNamespace string
CompositionId string
compositionApiVersion string // DEPRECATED: Remove in future versions in favor of compositionGroup and compositionInstalledVersion
CompositionGroup string
compositionInstalledVersion string
CompositionResource string
CompositionKind string
GracefullyPaused bool
}

// InjectValues injects composition related values into the provided Helm chart values. It adds fields under the "global" key.
// The modified values are returned as a byte slice.
func InjectValues(dat []byte, opts CompositionValues) ([]byte, error) {
var err error
if opts.GracefullyPaused {
dat, err = AddOrUpdateFieldInValues(dat, true, "global", "gracefullyPaused")
if err != nil {
return dat, fmt.Errorf("failed to add gracefullyPaused to values: %w", err)
}
}

dat, err = AddOrUpdateFieldInValues(dat, opts.CompositionNamespace, "global", "compositionNamespace")
if err != nil {
return dat, fmt.Errorf("failed to add compositionNamespace to values: %w", err)
}
dat, err = AddOrUpdateFieldInValues(dat, opts.CompositionName, "global", "compositionName")
if err != nil {
return dat, fmt.Errorf("failed to add compositionName to values: %w", err)
}
dat, err = AddOrUpdateFieldInValues(dat, opts.KrateoNamespace, "global", "krateoNamespace")
if err != nil {
return dat, fmt.Errorf("failed to add krateoNamespace to values: %w", err)
}
dat, err = AddOrUpdateFieldInValues(dat, opts.CompositionId, "global", "compositionId")
if err != nil {
return dat, fmt.Errorf("failed to add compositionId to values: %w", err)
}
// DEPRECATED: Remove in future versions in favor of compositionGroup and compositionInstalledVersion
dat, err = AddOrUpdateFieldInValues(dat, opts.compositionApiVersion, "global", "compositionApiVersion")
if err != nil {
return dat, fmt.Errorf("failed to add compositionApiVersion to values: %w", err)
}
// END DEPRECATED
dat, err = AddOrUpdateFieldInValues(dat, opts.CompositionGroup, "global", "compositionGroup")
if err != nil {
return dat, fmt.Errorf("failed to add compositionGroup to values: %w", err)
}
dat, err = AddOrUpdateFieldInValues(dat, opts.compositionInstalledVersion, "global", "compositionInstalledVersion")
if err != nil {
return dat, fmt.Errorf("failed to add compositionInstalledVersion to values: %w", err)
}
dat, err = AddOrUpdateFieldInValues(dat, opts.CompositionResource, "global", "compositionResource")
if err != nil {
return dat, fmt.Errorf("failed to add compositionResource to values: %w", err)
}
dat, err = AddOrUpdateFieldInValues(dat, opts.CompositionKind, "global", "compositionKind")
if err != nil {
return dat, fmt.Errorf("failed to add compositionKind to values: %w", err)
}
return dat, nil

}
248 changes: 248 additions & 0 deletions internal/helmclient/tools/tools_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
package tools

import (
"testing"

sigsyaml "sigs.k8s.io/yaml"
)

func TestAddOrUpdateFieldInValues(t *testing.T) {
tests := []struct {
name string
values string
value interface{}
fields []string
expected string
expectError bool
}{
{
name: "add single field to empty values",
values: "{}",
value: "test-value",
fields: []string{"testField"},
expected: "testField: test-value\n",
},
{
name: "add nested field to empty values",
values: "{}",
value: "test-value",
fields: []string{"global", "testField"},
expected: "global:\n testField: test-value\n",
},
{
name: "update existing field",
values: "testField: old-value",
value: "new-value",
fields: []string{"testField"},
expected: "testField: new-value\n",
},
{
name: "add to existing nested structure",
values: "global:\n existing: value",
value: "test-value",
fields: []string{"global", "newField"},
expected: "global:\n existing: value\n newField: test-value\n",
},
{
name: "deeply nested field",
values: "{}",
value: "test-value",
fields: []string{"level1", "level2", "level3", "testField"},
expected: "level1:\n level2:\n level3:\n testField: test-value\n",
},
{
name: "field exists but is not a map",
values: "testField: string-value",
value: "test-value",
fields: []string{"testField", "nested"},
expectError: true,
},
{
name: "invalid yaml",
values: "invalid: yaml: content:",
value: "test-value",
fields: []string{"testField"},
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := AddOrUpdateFieldInValues([]byte(tt.values), tt.value, tt.fields...)

if tt.expectError {
if err == nil {
t.Errorf("expected error but got none")
}
return
}

if err != nil {
t.Errorf("unexpected error: %v", err)
return
}

if string(result) != tt.expected {
t.Errorf("expected:\n%s\ngot:\n%s", tt.expected, string(result))
}
})
}
}

func TestInjectValues(t *testing.T) {
tests := []struct {
name string
input string
opts CompositionValues
expected map[string]interface{}
}{
{
name: "inject all values to empty yaml",
input: "{}",
opts: CompositionValues{
KrateoNamespace: "krateo-system",
CompositionName: "test-composition",
CompositionNamespace: "default",
CompositionId: "comp-123",
CompositionGroup: "test.group",
CompositionResource: "testresources",
CompositionKind: "TestResource",
GracefullyPaused: true,
},
expected: map[string]interface{}{
"global": map[string]interface{}{
"gracefullyPaused": true,
"compositionNamespace": "default",
"compositionName": "test-composition",
"krateoNamespace": "krateo-system",
"compositionId": "comp-123",
"compositionApiVersion": "",
"compositionGroup": "test.group",
"compositionInstalledVersion": "",
"compositionResource": "testresources",
"compositionKind": "TestResource",
},
},
},
{
name: "inject values without gracefully paused",
input: "{}",
opts: CompositionValues{
KrateoNamespace: "krateo-system",
CompositionName: "test-composition",
CompositionNamespace: "default",
CompositionId: "comp-123",
CompositionGroup: "test.group",
CompositionResource: "testresources",
CompositionKind: "TestResource",
GracefullyPaused: false,
},
expected: map[string]interface{}{
"global": map[string]interface{}{
"compositionNamespace": "default",
"compositionName": "test-composition",
"krateoNamespace": "krateo-system",
"compositionId": "comp-123",
"compositionApiVersion": "",
"compositionGroup": "test.group",
"compositionInstalledVersion": "",
"compositionResource": "testresources",
"compositionKind": "TestResource",
},
},
},
{
name: "inject values to existing yaml",
input: "existing:\n field: value\nglobal:\n existingGlobal: value",
opts: CompositionValues{
KrateoNamespace: "krateo-system",
CompositionName: "test-composition",
CompositionNamespace: "default",
CompositionId: "comp-123",
CompositionGroup: "test.group",
CompositionResource: "testresources",
CompositionKind: "TestResource",
},
expected: map[string]interface{}{
"existing": map[string]interface{}{
"field": "value",
},
"global": map[string]interface{}{
"existingGlobal": "value",
"compositionNamespace": "default",
"compositionName": "test-composition",
"krateoNamespace": "krateo-system",
"compositionId": "comp-123",
"compositionApiVersion": "",
"compositionGroup": "test.group",
"compositionInstalledVersion": "",
"compositionResource": "testresources",
"compositionKind": "TestResource",
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := InjectValues([]byte(tt.input), tt.opts)
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}

var resultMap map[string]interface{}
if err := sigsyaml.Unmarshal(result, &resultMap); err != nil {
t.Errorf("failed to unmarshal result: %v", err)
return
}

if !deepEqual(resultMap, tt.expected) {
t.Errorf("expected:\n%+v\ngot:\n%+v", tt.expected, resultMap)
}
})
}
}

func TestInjectValuesError(t *testing.T) {
invalidYaml := "invalid: yaml: content:"
opts := CompositionValues{
KrateoNamespace: "krateo-system",
}

_, err := InjectValues([]byte(invalidYaml), opts)
if err == nil {
t.Errorf("expected error for invalid yaml but got none")
}
}

// Helper function to compare maps deeply
func deepEqual(a, b map[string]interface{}) bool {
if len(a) != len(b) {
return false
}

for key, valueA := range a {
valueB, exists := b[key]
if !exists {
return false
}

switch va := valueA.(type) {
case map[string]interface{}:
if vb, ok := valueB.(map[string]interface{}); ok {
if !deepEqual(va, vb) {
return false
}
} else {
return false
}
default:
if valueA != valueB {
return false
}
}
}

return true
}
Loading