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

Add chartutil package and ValuesReference type to APIs #836

Merged
merged 1 commit into from
Dec 10, 2024
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
47 changes: 46 additions & 1 deletion apis/meta/reference_types.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2020, 2021 The Flux authors
Copyright 2020, 2024 The Flux authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -83,3 +83,48 @@ type KubeConfigReference struct {
// +required
SecretRef SecretKeyReference `json:"secretRef"`
}

// ValuesReference contains a reference to a resource containing Helm values,
// and optionally the key they can be found at.
type ValuesReference struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the context of this package, the type seems to be generically named. LocalObjectReference, NamespacedObjectReference, SecretKeyReference, KubeConfigReference, etc have the context in the name prefix. Something like HelmValuesReference would have been better.
I'm assuming that because it originated in helm-controller API, it was named this way. But in this package, the name seems confusing compared to others in the package.

Since helm-controller now has an alias to this, if we want to, we can rename this to anything without breaking the helm-controller API.

It feels like it's too late for the change now. I wouldn't insist on the change.

// Kind of the values referent, valid values are ('Secret', 'ConfigMap').
// +kubebuilder:validation:Enum=Secret;ConfigMap
// +required
Kind string `json:"kind"`

// Name of the values referent. Should reside in the same namespace as the
// referring resource.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +required
Name string `json:"name"`

// ValuesKey is the data key where the values.yaml or a specific value can be
// found at. Defaults to 'values.yaml'.
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Pattern=`^[\-._a-zA-Z0-9]+$`
// +optional
ValuesKey string `json:"valuesKey,omitempty"`

// TargetPath is the YAML dot notation path the value should be merged at. When
// set, the ValuesKey is expected to be a single flat value. Defaults to 'None',
// which results in the values getting merged at the root.
// +kubebuilder:validation:MaxLength=250
// +kubebuilder:validation:Pattern=`^([a-zA-Z0-9_\-.\\\/]|\[[0-9]{1,5}\])+$`
// +optional
TargetPath string `json:"targetPath,omitempty"`

// Optional marks this ValuesReference as optional. When set, a not found error
// for the values reference is ignored, but any ValuesKey, TargetPath or
// transient error will still result in a reconciliation failure.
// +optional
Optional bool `json:"optional,omitempty"`
}

// GetValuesKey returns the defined ValuesKey, or the default ('values.yaml').
func (in ValuesReference) GetValuesKey() string {
if in.ValuesKey == "" {
return "values.yaml"
}
return in.ValuesKey
}
15 changes: 15 additions & 0 deletions apis/meta/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 59 additions & 0 deletions chartutil/digest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
Copyright 2024 The Flux authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package chartutil

import (
"github.com/opencontainers/go-digest"
"helm.sh/helm/v3/pkg/chartutil"
)

// DigestValues calculates the digest of the values using the provided algorithm.
// The caller is responsible for ensuring that the algorithm is supported.
func DigestValues(algo digest.Algorithm, values chartutil.Values) digest.Digest {
digester := algo.Digester()
if values = valuesOrNil(values); values != nil {
if err := Encode(digester.Hash(), values, SortMapSlice); err != nil {
return ""
}
}
return digester.Digest()
}

// VerifyValues verifies the digest of the values against the provided digest.
func VerifyValues(digest digest.Digest, values chartutil.Values) bool {
if digest.Validate() != nil {
return false
}

verifier := digest.Verifier()
if values = valuesOrNil(values); values != nil {
if err := Encode(verifier, values, SortMapSlice); err != nil {
return false
}
}
return verifier.Verified()
}

// valuesOrNil returns nil if the values are empty, otherwise the values are
// returned. This is used to ensure that the digest is calculated against nil
// opposed to an empty object.
func valuesOrNil(values chartutil.Values) chartutil.Values {
if values != nil && len(values) == 0 {
return nil
}
return values
}
244 changes: 244 additions & 0 deletions chartutil/digest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
/*
Copyright 2024 The Flux authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package chartutil

import (
"testing"

"github.com/opencontainers/go-digest"
"helm.sh/helm/v3/pkg/chartutil"
)

func TestDigestValues(t *testing.T) {
tests := []struct {
name string
algo digest.Algorithm
values chartutil.Values
want digest.Digest
}{
{
name: "empty",
algo: digest.SHA256,
values: chartutil.Values{},
want: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
},
{
name: "nil",
algo: digest.SHA256,
values: nil,
want: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
},
{
name: "value map",
algo: digest.SHA256,
values: chartutil.Values{
"replicas": 3,
"image": map[string]interface{}{
"tag": "latest",
"repository": "nginx",
},
"ports": []interface{}{
map[string]interface{}{
"protocol": "TCP",
"port": 8080,
},
map[string]interface{}{
"port": 9090,
"protocol": "UDP",
},
},
},
want: "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6",
},
{
name: "value map in different order",
algo: digest.SHA256,
values: chartutil.Values{
"image": map[string]interface{}{
"repository": "nginx",
"tag": "latest",
},
"ports": []interface{}{
map[string]interface{}{
"port": 8080,
"protocol": "TCP",
},
map[string]interface{}{
"port": 9090,
"protocol": "UDP",
},
},
"replicas": 3,
},
want: "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6",
},
{
// Explicit test for something that does not work with sigs.k8s.io/yaml.
// See: https://go.dev/play/p/KRyfK9ZobZx
name: "values map with numeric keys",
algo: digest.SHA256,
values: chartutil.Values{
"replicas": 3,
"test": map[string]interface{}{
"632bd80235a05f4192aefade": "value1",
"632bd80ddf416cf32fd50679": "value2",
"632bd817c559818a52307da2": "value3",
"632bd82398e71231a98004b6": "value4",
},
},
want: "sha256:8a980fcbeadd6f05818f07e8aec14070c22250ca3d96af1fcd5f93b3e85b4d70",
},
{
name: "values map with numeric keys in different order",
algo: digest.SHA256,
values: chartutil.Values{
"test": map[string]interface{}{
"632bd82398e71231a98004b6": "value4",
"632bd817c559818a52307da2": "value3",
"632bd80ddf416cf32fd50679": "value2",
"632bd80235a05f4192aefade": "value1",
},
"replicas": 3,
},
want: "sha256:8a980fcbeadd6f05818f07e8aec14070c22250ca3d96af1fcd5f93b3e85b4d70",
},
{
name: "using different algorithm",
algo: digest.SHA512,
values: chartutil.Values{
"foo": "bar",
"baz": map[string]interface{}{
"cool": "stuff",
},
},
want: "sha512:b5f9cd4855ca3b08afd602557f373069b1732ce2e6d52341481b0d38f1938452e9d7759ab177c66699962b592f20ceded03eea3cd405d8670578c47842e2c550",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := DigestValues(tt.algo, tt.values); got != tt.want {
t.Errorf("DigestValues() = %v, want %v", got, tt.want)
}
})
}
}

func TestVerifyValues(t *testing.T) {
tests := []struct {
name string
digest digest.Digest
values chartutil.Values
want bool
}{
{
name: "empty values",
digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
values: chartutil.Values{},
want: true,
},
{
name: "nil values",
digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
values: nil,
want: true,
},
{
name: "empty digest",
digest: "",
want: false,
},
{
name: "invalid digest",
digest: "sha512:invalid",
values: nil,
want: false,
},
{
name: "matching values",
digest: "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6",
values: chartutil.Values{
"image": map[string]interface{}{
"repository": "nginx",
"tag": "latest",
},
"ports": []interface{}{
map[string]interface{}{
"port": 8080,
"protocol": "TCP",
},
map[string]interface{}{
"port": 9090,
"protocol": "UDP",
},
},
"replicas": 3,
},
want: true,
},
{
name: "matching values in different order",
digest: "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6",
values: chartutil.Values{
"replicas": 3,
"image": map[string]interface{}{
"tag": "latest",
"repository": "nginx",
},
"ports": []interface{}{
map[string]interface{}{
"protocol": "TCP",
"port": 8080,
},
map[string]interface{}{
"port": 9090,
"protocol": "UDP",
},
},
},
want: true,
},
{
name: "matching values with numeric keys",
digest: "sha256:8a980fcbeadd6f05818f07e8aec14070c22250ca3d96af1fcd5f93b3e85b4d70",
values: chartutil.Values{
"replicas": 3,
"test": map[string]interface{}{
"632bd80235a05f4192aefade": "value1",
"632bd80ddf416cf32fd50679": "value2",
"632bd817c559818a52307da2": "value3",
"632bd82398e71231a98004b6": "value4",
},
},
want: true,
},
{
name: "mismatching values",
digest: "sha256:3f3641788a2d4abda3534eaa90c90b54916e4c6e3a5b2e1b24758b7bfa701ecd",
values: chartutil.Values{
"foo": "bar",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := VerifyValues(tt.digest, tt.values); got != tt.want {
t.Errorf("VerifyValues() = %v, want %v", got, tt.want)
}
})
}
}
Loading
Loading