Skip to content

Commit 7f92fe5

Browse files
committed
#53 Process helm metadata
Extend CR with field mappedValues to process helm values which need metadata from their charts
1 parent 1b828b7 commit 7f92fe5

File tree

9 files changed

+163
-8
lines changed

9 files changed

+163
-8
lines changed

config/samples/dogu-operator.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ metadata:
44
name: k8s-dogu-operator
55
spec:
66
name: k8s-dogu-operator
7-
namespace: k8s
7+
namespace: k8s
8+
version: 0.41.11-dev
9+
mappedValues:
10+
log-level: error

k8s/helm-crd/templates/k8s.cloudogu.com_components.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ spec:
3838
deployNamespace:
3939
description: DeployNamespace is the namespace where the helm chart should be deployed in. This value is optional. If it is empty the operator deploys the helm chart in the namespace where the operator is deployed.
4040
type: string
41+
mappedValues:
42+
additionalProperties:
43+
type: string
44+
description: MappedValues contains helm values which require a mapping. Components hold metadata information in a file `component-metadata-values.yaml` for these fields. The component-operator reads the file in install or upgrade process the sets the corresponding values in the chart spec. Possible values in this map are forbidden to set via the field ValuesYamlOverwrite.
45+
type: object
4146
name:
4247
description: Name of the component (e.g. k8s-dogu-operator)
4348
type: string

pkg/api/v1/ces_component_types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ type ComponentSpec struct {
4848
// It can be used to overwrite specific configurations. Lists are overwritten, maps are merged.
4949
// +optional
5050
ValuesYamlOverwrite string `json:"valuesYamlOverwrite,omitempty"`
51+
// MappedValues contains helm values which require a mapping.
52+
// Components hold metadata information in a file `component-metadata-values.yaml` for these fields.
53+
// The component-operator reads the file in install or upgrade process the sets the corresponding values in the chart spec.
54+
// Possible values in this map are forbidden to set via the field ValuesYamlOverwrite.
55+
// +optional
56+
MappedValues map[string]string `json:"mappedValues,omitempty"`
5157
}
5258

5359
type HealthStatus string

pkg/controllers/componentController.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ func (r *ComponentReconciler) isValuesChanged(deployedRelease *release.Release,
277277
return false, fmt.Errorf("failed to get values.yaml from release %s: %w", deployedRelease.Name, err)
278278
}
279279

280+
// TODO Check changes for mappedValues. Merge them here. Maybe extend chartSpec and reuse them later.
280281
chartSpecValues, err := r.helmClient.GetChartSpecValues(component.GetHelmChartSpec())
281282
if err != nil {
282283
return false, fmt.Errorf("failed to get values.yaml from component %s: %w", component.GetHelmChartSpec().ChartName, err)

pkg/controllers/componentInstallManager.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func (cim *ComponentInstallManager) Install(ctx context.Context, component *k8sv
6161
// create a new context that does not get canceled immediately on SIGTERM
6262
helmCtx := context.Background()
6363

64-
if err := cim.helmClient.InstallOrUpgrade(helmCtx, component.GetHelmChartSpec()); err != nil {
64+
if err := cim.helmClient.InstallOrUpgradeWithMappedValues(helmCtx, component.GetHelmChartSpec(), component.Spec.MappedValues); err != nil {
6565
return &genericRequeueableError{"failed to install chart for component " + component.Spec.Name, err}
6666
}
6767

pkg/controllers/componentUpgradeManager.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func (cum *ComponentUpgradeManager) Upgrade(ctx context.Context, component *k8sv
5050
// this allows self-upgrades
5151
helmCtx := context.WithoutCancel(ctx)
5252

53-
if err := cum.helmClient.InstallOrUpgrade(helmCtx, component.GetHelmChartSpec()); err != nil {
53+
if err := cum.helmClient.InstallOrUpgradeWithMappedValues(helmCtx, component.GetHelmChartSpec(), component.Spec.MappedValues); err != nil {
5454
return fmt.Errorf("failed to upgrade chart for component %s: %w", component.Spec.Name, err)
5555
}
5656

pkg/controllers/interfaces.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ type healthManager interface {
4141
type helmClient interface {
4242
// InstallOrUpgrade takes a helmChart and applies it.
4343
InstallOrUpgrade(ctx context.Context, chart *client.ChartSpec) error
44+
// InstallOrUpgradeWithMappedValues takes a helmChart and applies it with custom values which should be in the metadata file of the helm chart.
45+
InstallOrUpgradeWithMappedValues(ctx context.Context, chart *client.ChartSpec, mappedValues map[string]string) error
4446
// Uninstall removes the helmRelease for the given name
4547
Uninstall(releaseName string) error
4648
// ListDeployedReleases returns all deployed helm releases

pkg/helm/client.go

Lines changed: 117 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package helm
33
import (
44
"context"
55
"fmt"
6+
"helm.sh/helm/v3/pkg/chartutil"
7+
"k8s.io/apimachinery/pkg/util/yaml"
68
"sort"
79
"strings"
810

@@ -19,10 +21,11 @@ import (
1921
)
2022

2123
const (
22-
helmRepositoryCache = "/tmp/.helmcache"
23-
helmRepositoryConfig = "/tmp/.helmrepo"
24-
helmRegistryConfigFile = "/tmp/.helmregistry/config.json"
25-
ociSchemePrefix = string(config.EndpointSchemaOCI + "://")
24+
helmRepositoryCache = "/tmp/.helmcache"
25+
helmRepositoryConfig = "/tmp/.helmrepo"
26+
helmRegistryConfigFile = "/tmp/.helmregistry/config.json"
27+
ociSchemePrefix = string(config.EndpointSchemaOCI + "://")
28+
helmValuesMetadataFileName = "component-values-metadata.yaml"
2629
)
2730

2831
// HelmClient embeds the client.Client interface for usage in this package.
@@ -67,6 +70,10 @@ func NewClient(namespace string, helmRepoData *config.HelmRepositoryData, debug
6770

6871
// InstallOrUpgrade takes a helmChart and applies it.
6972
func (c *Client) InstallOrUpgrade(ctx context.Context, chart *client.ChartSpec) error {
73+
return c.InstallOrUpgradeWithMappedValues(ctx, chart, nil)
74+
}
75+
76+
func (c *Client) InstallOrUpgradeWithMappedValues(ctx context.Context, chart *client.ChartSpec, mappedValues map[string]string) error {
7077
// This helm-client currently only works with OCI-Helm-Repositories.
7178
// Therefore, the chartName has to include the FQDN of the repository (e.g. "oci://my.repo/...")
7279
// If in the future non-oci-repositories need to be used, this should be done here...
@@ -76,6 +83,10 @@ func (c *Client) InstallOrUpgrade(ctx context.Context, chart *client.ChartSpec)
7683
return fmt.Errorf("error patching chart-version for chart %s: %w", chart.ChartName, err)
7784
}
7885

86+
if err := c.patchMappedValues(ctx, chart, mappedValues); err != nil {
87+
return err
88+
}
89+
7990
_, err := c.helmClient.InstallOrUpgradeChart(ctx, chart)
8091
if err != nil {
8192
return fmt.Errorf("error while installOrUpgrade chart %s: %w", chart.ChartName, err)
@@ -154,6 +165,107 @@ func (c *Client) GetChartSpecValues(spec *client.ChartSpec) (map[string]interfac
154165
return c.helmClient.GetChartSpecValues(spec)
155166
}
156167

168+
type helmValuesMetadata struct {
169+
ApiVersion string `yaml:"apiVersion"`
170+
Metadata map[string]helmValuesMetadataEntry `yaml:"metadata"`
171+
}
172+
173+
type helmValuesMetadataEntry struct {
174+
Name string `yaml:"name"`
175+
Description string `yaml:"description"`
176+
Keys []helmValuesMetadataKey `yaml:"keys"`
177+
}
178+
179+
type helmValuesMetadataKey struct {
180+
Path string `yaml:"path"`
181+
}
182+
183+
func (c *Client) patchMappedValues(ctx context.Context, chartSpec *client.ChartSpec, mappedValues map[string]string) error {
184+
logger := log.FromContext(ctx)
185+
if mappedValues == nil {
186+
logger.Info("return patching values because mapped values are nil")
187+
return nil
188+
}
189+
190+
helmChart, err := c.getChart(ctx, chartSpec)
191+
if err != nil {
192+
return err
193+
}
194+
195+
files := helmChart.Files
196+
var metadataFile *helmValuesMetadata
197+
for _, file := range files {
198+
logger.Info(file.Name)
199+
if file.Name == helmValuesMetadataFileName {
200+
err := yaml.Unmarshal(file.Data, &metadataFile)
201+
if err != nil {
202+
return fmt.Errorf("failed to unmarshal %s: %w", helmValuesMetadataFileName, err)
203+
}
204+
logger.Info(fmt.Sprintf("found metadata file %q for chart %q", helmValuesMetadataFileName, chartSpec.ChartName))
205+
logger.Info(fmt.Sprintf("%+v", metadataFile))
206+
break
207+
}
208+
}
209+
210+
if metadataFile == nil {
211+
logger.Info(fmt.Sprintf("found no metadata file %q for chart %q", helmValuesMetadataFileName, chartSpec.ChartName))
212+
return nil
213+
}
214+
215+
values, err := chartutil.ReadValues([]byte(chartSpec.ValuesYaml))
216+
if err != nil {
217+
return fmt.Errorf("failed to read current values: %w", err)
218+
}
219+
220+
// TODO Exclude this check because it has to be done if the mappedValues are nil too
221+
for _, metadataEntry := range metadataFile.Metadata {
222+
// Check if metadata values are already set in valuesYamlOverwrite
223+
for _, key := range metadataEntry.Keys {
224+
// key.Path is somethings like controllerManager.env.logLevel (dot-separated)
225+
path := key.Path
226+
if isMetadataPathInValuesMap(path, values) {
227+
return fmt.Errorf("values contains path %s which should only be set in field mappedValues", path)
228+
}
229+
230+
value, ok := mappedValues[metadataEntry.Name]
231+
if !ok {
232+
// Return nil because metadata is nil in mappedValue
233+
logger.Info(fmt.Sprintf("found no value in mappedValues for metadata %q", metadataEntry.Name))
234+
return nil
235+
}
236+
237+
setValueInChartSpec(ctx, chartSpec, path, value)
238+
}
239+
}
240+
241+
return nil
242+
}
243+
244+
func setValueInChartSpec(ctx context.Context, chartSpec *client.ChartSpec, path string, value interface{}) {
245+
// TODO Mapping
246+
// This is easier than reading the current values as string and merging them.
247+
logger := log.FromContext(ctx)
248+
option := fmt.Sprintf("%s=%s", path, value)
249+
logger.Info(fmt.Sprintf("set option %q for chart %q", option, chartSpec.ChartName))
250+
chartSpec.ValuesOptions.Values = append(chartSpec.ValuesOptions.Values, option)
251+
}
252+
253+
func isMetadataPathInValuesMap(path string, values chartutil.Values) bool {
254+
before, after, _ := strings.Cut(path, ".")
255+
256+
i, ok := values[before]
257+
if !ok {
258+
return false
259+
}
260+
261+
ii, ok := i.(map[string]interface{})
262+
if !ok && after == "" {
263+
return true
264+
}
265+
266+
return isMetadataPathInValuesMap(after, ii)
267+
}
268+
157269
func (c *Client) patchOciEndpoint(chart *client.ChartSpec) {
158270
if strings.HasPrefix(chart.ChartName, ociSchemePrefix) {
159271
return
@@ -173,7 +285,7 @@ func (c *Client) patchChartVersion(chart *client.ChartSpec) error {
173285
return fmt.Errorf("error resolving tags for chart %s: %w", chart.ChartName, err)
174286
}
175287

176-
//sort tags by version
288+
// sort tags by version
177289
sortedTags := sortByVersionDescending(tags)
178290

179291
if len(sortedTags) <= 0 {

pkg/helm/client_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"helm.sh/helm/v3/pkg/chartutil"
78
"strings"
89
"testing"
910

@@ -628,3 +629,28 @@ func TestClient_GetReleaseValues(t *testing.T) {
628629
assert.Equal(t, "val", values["key"])
629630
})
630631
}
632+
633+
func Test_isMetadataPathInValuesMap(t *testing.T) {
634+
type args struct {
635+
path string
636+
values chartutil.Values
637+
}
638+
tests := []struct {
639+
name string
640+
args args
641+
want bool
642+
}{
643+
{"empty path should result in false with empty values", args{"", map[string]interface{}{}}, false},
644+
{"empty path should result in false", args{"", map[string]interface{}{"controllerManager": "value"}}, false},
645+
{"simple path is in values", args{"controllerManager", map[string]interface{}{"controllerManager": "value"}}, true},
646+
{"path is in values", args{"controllerManager.env", map[string]interface{}{"controllerManager": map[string]interface{}{"env": "value"}}}, true},
647+
{"path is not in values", args{"controllerManager.env", map[string]interface{}{"controllerManager": "value"}}, false},
648+
{"path is not in values because of bool value", args{"controllerManager.env", map[string]interface{}{"controllerManager": false}}, false},
649+
{"path is not in values because of slice value", args{"controllerManager.env", map[string]interface{}{"controllerManager": []byte{}}}, false},
650+
}
651+
for _, tt := range tests {
652+
t.Run(tt.name, func(t *testing.T) {
653+
assert.Equalf(t, tt.want, isMetadataPathInValuesMap(tt.args.path, tt.args.values), "isMetadataPathInValuesMap(%v, %v)", tt.args.path, tt.args.values)
654+
})
655+
}
656+
}

0 commit comments

Comments
 (0)