Refactor values composition to use `pkg/chartutil`

Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
This commit is contained in:
Stefan Prodan 2024-12-10 12:04:54 +02:00
parent f48671c020
commit 9c58e02b62
No known key found for this signature in database
GPG Key ID: 3299AEB0E4085BAF
33 changed files with 54 additions and 5813 deletions

View File

@ -4,7 +4,7 @@ go 1.22.7
require (
github.com/fluxcd/pkg/apis/kustomize v1.7.0
github.com/fluxcd/pkg/apis/meta v1.7.0
github.com/fluxcd/pkg/apis/meta v1.8.0
k8s.io/apiextensions-apiserver v0.31.3
k8s.io/apimachinery v0.31.3
sigs.k8s.io/controller-runtime v0.19.3

View File

@ -4,8 +4,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fluxcd/pkg/apis/kustomize v1.7.0 h1:4N23LccihQ3Ose/1FYZGwhrFSh63C9uOVFhwmSInOfQ=
github.com/fluxcd/pkg/apis/kustomize v1.7.0/go.mod h1:CqWLBcY2ZPW5f3k2sEypSfjIhz2hFs70PTTYIdKTMaY=
github.com/fluxcd/pkg/apis/meta v1.7.0 h1:pDbPrBGgsiWV4bx8j+hodwv1Ysbj/pHP+FH46aTZOfs=
github.com/fluxcd/pkg/apis/meta v1.7.0/go.mod h1:OJGH7I//SNO6zcso80oBRuf5H8oU8etZDeTgCcH7qHo=
github.com/fluxcd/pkg/apis/meta v1.8.0 h1:wF7MJ3mu5ds9Y/exWU1yU0YyDb8s1VwwQnZYuMWli3c=
github.com/fluxcd/pkg/apis/meta v1.8.0/go.mod h1:OJGH7I//SNO6zcso80oBRuf5H8oU8etZDeTgCcH7qHo=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=

View File

@ -189,7 +189,7 @@ type HelmReleaseSpec struct {
// ValuesFrom holds references to resources containing Helm values for this HelmRelease,
// and information about how they should be merged.
ValuesFrom []ValuesReference `json:"valuesFrom,omitempty"`
ValuesFrom []meta.ValuesReference `json:"valuesFrom,omitempty"`
// Values holds the values for this Helm release.
// +optional

View File

@ -68,48 +68,3 @@ type CrossNamespaceSourceReference struct {
// +optional
Namespace string `json:"namespace,omitempty"`
}
// ValuesReference contains a reference to a resource containing Helm values,
// and optionally the key they can be found at.
type ValuesReference struct {
// 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
}

View File

@ -326,7 +326,7 @@ func (in *HelmReleaseSpec) DeepCopyInto(out *HelmReleaseSpec) {
}
if in.ValuesFrom != nil {
in, out := &in.ValuesFrom, &out.ValuesFrom
*out = make([]ValuesReference, len(*in))
*out = make([]meta.ValuesReference, len(*in))
copy(*out, *in)
}
if in.Values != nil {
@ -717,18 +717,3 @@ func (in *UpgradeRemediation) DeepCopy() *UpgradeRemediation {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ValuesReference) DeepCopyInto(out *ValuesReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValuesReference.
func (in *ValuesReference) DeepCopy() *ValuesReference {
if in == nil {
return nil
}
out := new(ValuesReference)
in.DeepCopyInto(out)
return out
}

View File

@ -350,8 +350,8 @@ Uninstall
<td>
<code>valuesFrom</code><br>
<em>
<a href="#helm.toolkit.fluxcd.io/v2.ValuesReference">
[]ValuesReference
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#ValuesReference">
[]github.com/fluxcd/pkg/apis/meta.ValuesReference
</a>
</em>
</td>
@ -1360,8 +1360,8 @@ Uninstall
<td>
<code>valuesFrom</code><br>
<em>
<a href="#helm.toolkit.fluxcd.io/v2.ValuesReference">
[]ValuesReference
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#ValuesReference">
[]github.com/fluxcd/pkg/apis/meta.ValuesReference
</a>
</em>
</td>
@ -2887,92 +2887,6 @@ RemediationStrategy
</table>
</div>
</div>
<h3 id="helm.toolkit.fluxcd.io/v2.ValuesReference">ValuesReference
</h3>
<p>
(<em>Appears on:</em>
<a href="#helm.toolkit.fluxcd.io/v2.HelmReleaseSpec">HelmReleaseSpec</a>)
</p>
<p>ValuesReference contains a reference to a resource containing Helm values,
and optionally the key they can be found at.</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>kind</code><br>
<em>
string
</em>
</td>
<td>
<p>Kind of the values referent, valid values are (&lsquo;Secret&rsquo;, &lsquo;ConfigMap&rsquo;).</p>
</td>
</tr>
<tr>
<td>
<code>name</code><br>
<em>
string
</em>
</td>
<td>
<p>Name of the values referent. Should reside in the same namespace as the
referring resource.</p>
</td>
</tr>
<tr>
<td>
<code>valuesKey</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>ValuesKey is the data key where the values.yaml or a specific value can be
found at. Defaults to &lsquo;values.yaml&rsquo;.</p>
</td>
</tr>
<tr>
<td>
<code>targetPath</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>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 &lsquo;None&rsquo;,
which results in the values getting merged at the root.</p>
</td>
</tr>
<tr>
<td>
<code>optional</code><br>
<em>
bool
</em>
</td>
<td>
<em>(Optional)</em>
<p>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.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="admonition note">
<p class="last">This page was automatically generated with <code>gen-crd-api-reference-docs</code></p>
</div>

5
go.mod
View File

@ -21,8 +21,9 @@ require (
github.com/fluxcd/pkg/apis/acl v0.4.0
github.com/fluxcd/pkg/apis/event v0.11.0
github.com/fluxcd/pkg/apis/kustomize v1.7.0
github.com/fluxcd/pkg/apis/meta v1.7.0
github.com/fluxcd/pkg/runtime v0.50.0
github.com/fluxcd/pkg/apis/meta v1.8.0
github.com/fluxcd/pkg/chartutil v1.0.0
github.com/fluxcd/pkg/runtime v0.50.1
github.com/fluxcd/pkg/ssa v0.41.1
github.com/fluxcd/pkg/testserver v0.8.0
github.com/fluxcd/source-controller/api v1.4.1

10
go.sum
View File

@ -113,10 +113,12 @@ github.com/fluxcd/pkg/apis/event v0.11.0 h1:blvUbgko8EqqjMn1mju2U8aBXUntn3EWbMNc
github.com/fluxcd/pkg/apis/event v0.11.0/go.mod h1:AjoDg8Au7RpZbk8B5t3Q2Kq/6kXgmhtdXz6P1y2teAU=
github.com/fluxcd/pkg/apis/kustomize v1.7.0 h1:4N23LccihQ3Ose/1FYZGwhrFSh63C9uOVFhwmSInOfQ=
github.com/fluxcd/pkg/apis/kustomize v1.7.0/go.mod h1:CqWLBcY2ZPW5f3k2sEypSfjIhz2hFs70PTTYIdKTMaY=
github.com/fluxcd/pkg/apis/meta v1.7.0 h1:pDbPrBGgsiWV4bx8j+hodwv1Ysbj/pHP+FH46aTZOfs=
github.com/fluxcd/pkg/apis/meta v1.7.0/go.mod h1:OJGH7I//SNO6zcso80oBRuf5H8oU8etZDeTgCcH7qHo=
github.com/fluxcd/pkg/runtime v0.50.0 h1:FKJQaOFv8SKp/t7yRE0EkHxA4RIr650SGTLJa1HY3AU=
github.com/fluxcd/pkg/runtime v0.50.0/go.mod h1:NEjX8/1DL8B/dsjH1/FD9PjCLPhgdvsffSvzuFrgjys=
github.com/fluxcd/pkg/apis/meta v1.8.0 h1:wF7MJ3mu5ds9Y/exWU1yU0YyDb8s1VwwQnZYuMWli3c=
github.com/fluxcd/pkg/apis/meta v1.8.0/go.mod h1:OJGH7I//SNO6zcso80oBRuf5H8oU8etZDeTgCcH7qHo=
github.com/fluxcd/pkg/chartutil v1.0.0 h1:Hj5mPiUp/nanZPVK7Ur0TDN4BCMhuoxKjvAmBbnX7DE=
github.com/fluxcd/pkg/chartutil v1.0.0/go.mod h1:GBo3G78aiK48BppJ/YoDUv8L1NDLHrMpK3K5uiazQ0A=
github.com/fluxcd/pkg/runtime v0.50.1 h1:VQeIJ2iq/BjsboGATRTCUQYMU737R0DboKXWVGyBhAI=
github.com/fluxcd/pkg/runtime v0.50.1/go.mod h1:lBLhK6y/3kppfBsqmBs8wZ97dEmd44WzLp0iCci4DnY=
github.com/fluxcd/pkg/ssa v0.41.1 h1:VW87zsLYAKUvCxJhuEH7VzxVh3SxaU+PyApCT6gKjTk=
github.com/fluxcd/pkg/ssa v0.41.1/go.mod h1:7cbyLHqFd5FpcKvhxbHG3DkMm3cZteW45Mi78B0hg8g=
github.com/fluxcd/pkg/testserver v0.8.0 h1:ndlCjNpIueEmsLbyg97Dbkq/0Mfzxn4Kq4HSPEb71V8=

View File

@ -22,7 +22,7 @@ import (
"helm.sh/helm/v3/pkg/chartutil"
v2 "github.com/fluxcd/helm-controller/api/v2"
intchartutil "github.com/fluxcd/helm-controller/internal/chartutil"
intchartutil "github.com/fluxcd/pkg/chartutil"
)
const (

View File

@ -28,8 +28,8 @@ import (
helmdriver "helm.sh/helm/v3/pkg/storage/driver"
v2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/helm-controller/internal/chartutil"
"github.com/fluxcd/helm-controller/internal/release"
"github.com/fluxcd/pkg/chartutil"
)
var (

View File

@ -1,61 +0,0 @@
/*
Copyright 2022 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"
intyaml "github.com/fluxcd/helm-controller/internal/yaml"
)
// 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 := intyaml.Encode(digester.Hash(), values, intyaml.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 := intyaml.Encode(verifier, values, intyaml.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
}

View File

@ -1,244 +0,0 @@
/*
Copyright 2022 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)
}
})
}
}

View File

@ -1,275 +0,0 @@
/*
Copyright 2022 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 (
"context"
"errors"
"fmt"
"strings"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/strvals"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
kubeclient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/fluxcd/pkg/runtime/transform"
v2 "github.com/fluxcd/helm-controller/api/v2"
)
// ErrValuesRefReason is the descriptive reason for an ErrValuesReference.
type ErrValuesRefReason error
var (
// ErrResourceNotFound signals the referenced values resource could not be
// found.
ErrResourceNotFound = errors.New("resource not found")
// ErrKeyNotFound signals the key could not be found in the referenced
// values resource.
ErrKeyNotFound = errors.New("key not found")
// ErrUnsupportedRefKind signals the values reference kind is not
// supported.
ErrUnsupportedRefKind = errors.New("unsupported values reference kind")
// ErrValuesDataRead signals the referenced resource's values data could
// not be read.
ErrValuesDataRead = errors.New("failed to read values data")
// ErrValueMerge signals a single value could not be merged into the
// values.
ErrValueMerge = errors.New("failed to merge value")
// ErrUnknown signals the reason an error occurred is unknown.
ErrUnknown = errors.New("unknown error")
)
// ErrValuesReference is returned by ChartValuesFromReferences
type ErrValuesReference struct {
// Reason for the values reference error. Nil equals ErrUnknown.
// Can be used with Is to reason about a returned error:
// err := &ErrValuesReference{Reason: ErrResourceNotFound, ...}
// errors.Is(err, ErrResourceNotFound)
Reason ErrValuesRefReason
// Kind of the values reference the error is being reported for.
Kind string
// Name of the values reference the error is being reported for.
Name types.NamespacedName
// Key of the values reference the error is being reported for.
Key string
// Optional indicates if the error is being reported for an optional values
// reference.
Optional bool
// Err contains the further error chain leading to this error, it can be
// nil.
Err error
}
// Error returns an error string constructed out of the state of
// ErrValuesReference.
func (e *ErrValuesReference) Error() string {
b := strings.Builder{}
b.WriteString("could not resolve")
if e.Optional {
b.WriteString(" optional")
}
if kind := e.Kind; kind != "" {
b.WriteString(" " + kind)
}
b.WriteString(" chart values reference")
if name := e.Name.String(); name != "" {
b.WriteString(fmt.Sprintf(" '%s'", name))
}
if key := e.Key; key != "" {
b.WriteString(fmt.Sprintf(" with key '%s'", key))
}
reason := e.Reason.Error()
if reason == "" && e.Err == nil {
reason = ErrUnknown.Error()
}
if e.Err != nil {
reason = e.Err.Error()
}
b.WriteString(": " + reason)
return b.String()
}
// Is returns if target == Reason, or target == Err.
// Can be used to Reason about a returned error:
//
// err := &ErrValuesReference{Reason: ErrResourceNotFound, ...}
// errors.Is(err, ErrResourceNotFound)
func (e *ErrValuesReference) Is(target error) bool {
reason := e.Reason
if reason == nil {
reason = ErrUnknown
}
if reason == target {
return true
}
return errors.Is(e.Err, target)
}
// Unwrap returns the wrapped Err.
func (e *ErrValuesReference) Unwrap() error {
return e.Err
}
// NewErrValuesReference returns a new ErrValuesReference constructed from the
// provided values.
func NewErrValuesReference(name types.NamespacedName, ref v2.ValuesReference, reason ErrValuesRefReason, err error) *ErrValuesReference {
return &ErrValuesReference{
Reason: reason,
Kind: ref.Kind,
Name: name,
Key: ref.GetValuesKey(),
Optional: ref.Optional,
Err: err,
}
}
const (
kindConfigMap = "ConfigMap"
kindSecret = "Secret"
)
// ChartValuesFromReferences attempts to construct new chart values by resolving
// the provided references using the client, merging them in the order given.
// If provided, the values map is merged in last overwriting values from references,
// unless a reference has a targetPath specified, in which case it will overwrite all.
// It returns the merged values, or an ErrValuesReference error.
func ChartValuesFromReferences(ctx context.Context, client kubeclient.Client, namespace string,
values map[string]interface{}, refs ...v2.ValuesReference) (chartutil.Values, error) {
log := ctrl.LoggerFrom(ctx)
result := chartutil.Values{}
resources := make(map[string]kubeclient.Object)
for _, ref := range refs {
namespacedName := types.NamespacedName{Namespace: namespace, Name: ref.Name}
var valuesData []byte
switch ref.Kind {
case kindConfigMap, kindSecret:
index := ref.Kind + namespacedName.String()
resource, ok := resources[index]
if !ok {
// The resource may not exist, but we want to act on a single version
// of the resource in case the values reference is marked as optional.
resources[index] = nil
switch ref.Kind {
case kindSecret:
resource = &corev1.Secret{}
case kindConfigMap:
resource = &corev1.ConfigMap{}
}
if resource != nil {
if err := client.Get(ctx, namespacedName, resource); err != nil {
if apierrors.IsNotFound(err) {
err := NewErrValuesReference(namespacedName, ref, ErrResourceNotFound, err)
if err.Optional {
log.Info(err.Error())
continue
}
return nil, err
}
return nil, err
}
resources[index] = resource
}
}
if resource == nil {
if ref.Optional {
continue
}
return nil, NewErrValuesReference(namespacedName, ref, ErrResourceNotFound, nil)
}
switch typedRes := resource.(type) {
case *corev1.Secret:
data, ok := typedRes.Data[ref.GetValuesKey()]
if !ok {
err := NewErrValuesReference(namespacedName, ref, ErrKeyNotFound, nil)
if ref.Optional {
log.Info(err.Error())
continue
}
return nil, NewErrValuesReference(namespacedName, ref, ErrKeyNotFound, nil)
}
valuesData = data
case *corev1.ConfigMap:
data, ok := typedRes.Data[ref.GetValuesKey()]
if !ok {
err := NewErrValuesReference(namespacedName, ref, ErrKeyNotFound, nil)
if ref.Optional {
log.Info(err.Error())
continue
}
return nil, err
}
valuesData = []byte(data)
default:
return nil, NewErrValuesReference(namespacedName, ref, ErrUnsupportedRefKind, nil)
}
default:
return nil, NewErrValuesReference(namespacedName, ref, ErrUnsupportedRefKind, nil)
}
if ref.TargetPath != "" {
result = transform.MergeMaps(result, values)
// TODO(hidde): this is a bit of hack, as it mimics the way the option string is passed
// to Helm from a CLI perspective. Given the parser is however not publicly accessible
// while it contains all logic around parsing the target path, it is a fair trade-off.
if err := ReplacePathValue(result, ref.TargetPath, string(valuesData)); err != nil {
return nil, NewErrValuesReference(namespacedName, ref, ErrValueMerge, err)
}
continue
}
values, err := chartutil.ReadValues(valuesData)
if err != nil {
return nil, NewErrValuesReference(namespacedName, ref, ErrValuesDataRead, err)
}
result = transform.MergeMaps(result, values)
}
return transform.MergeMaps(result, values), nil
}
// ReplacePathValue replaces the value at the dot notation path with the given
// value using Helm's string value parser using strvals.ParseInto. Single or
// double-quoted values are merged using strvals.ParseIntoString.
func ReplacePathValue(values chartutil.Values, path string, value string) error {
const (
singleQuote = "'"
doubleQuote = `"`
)
isSingleQuoted := strings.HasPrefix(value, singleQuote) && strings.HasSuffix(value, singleQuote)
isDoubleQuoted := strings.HasPrefix(value, doubleQuote) && strings.HasSuffix(value, doubleQuote)
if isSingleQuoted || isDoubleQuoted {
value = strings.Trim(value, singleQuote+doubleQuote)
value = path + "=" + value
return strvals.ParseIntoString(value, values)
}
value = path + "=" + value
return strvals.ParseInto(value, values)
}

View File

@ -1,186 +0,0 @@
//go:build gofuzz_libfuzzer
// +build gofuzz_libfuzzer
/*
Copyright 2022 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 (
"context"
"testing"
"github.com/go-logr/logr"
"helm.sh/helm/v3/pkg/chartutil"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
v2 "github.com/fluxcd/helm-controller/api/v2"
)
func FuzzChartValuesFromReferences(f *testing.F) {
scheme := testScheme()
tests := []struct {
targetPath string
valuesKey string
hrValues string
createObject bool
secretData []byte
configData string
}{
{
targetPath: "flat",
valuesKey: "custom-values.yaml",
secretData: []byte(`flat:
nested: value
nested: value
`),
configData: `flat: value
nested:
configuration: value
`,
hrValues: `
other: values
`,
createObject: true,
},
{
targetPath: "'flat'",
valuesKey: "custom-values.yaml",
secretData: []byte(`flat:
nested: value
nested: value
`),
configData: `flat: value
nested:
configuration: value
`,
hrValues: `
other: values
`,
createObject: true,
},
{
targetPath: "flat[0]",
secretData: []byte(``),
configData: `flat: value`,
hrValues: `
other: values
`,
createObject: true,
},
{
secretData: []byte(`flat:
nested: value
nested: value
`),
configData: `flat: value
nested:
configuration: value
`,
hrValues: `
other: values
`,
createObject: true,
},
{
targetPath: "some-value",
hrValues: `
other: values
`,
createObject: false,
},
}
for _, tt := range tests {
f.Add(tt.targetPath, tt.valuesKey, tt.hrValues, tt.createObject, tt.secretData, tt.configData)
}
f.Fuzz(func(t *testing.T,
targetPath, valuesKey, hrValues string, createObject bool, secretData []byte, configData string) {
// objectName and objectNamespace represent a name reference to a core
// Kubernetes object upstream (Secret/ConfigMap) which is validated upstream,
// and also validated by us in the OpenAPI-based validation set in
// v2.ValuesReference. Therefore, a static value here suffices, and instead
// we just play with the objects presence/absence.
objectName := "values"
objectNamespace := "default"
var resources []runtime.Object
if createObject {
resources = append(resources,
mockConfigMap(objectName, map[string]string{valuesKey: configData}),
mockSecret(objectName, map[string][]byte{valuesKey: secretData}),
)
}
references := []v2.ValuesReference{
{
Kind: kindConfigMap,
Name: objectName,
ValuesKey: valuesKey,
TargetPath: targetPath,
},
{
Kind: kindSecret,
Name: objectName,
ValuesKey: valuesKey,
TargetPath: targetPath,
},
}
c := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(resources...)
var values chartutil.Values
if hrValues != "" {
values, _ = chartutil.ReadValues([]byte(hrValues))
}
_, _ = ChartValuesFromReferences(logr.NewContext(context.TODO(), logr.Discard()), c.Build(), objectNamespace, values, references...)
})
}
func mockSecret(name string, data map[string][]byte) *corev1.Secret {
return &corev1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: kindSecret,
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{Name: name},
Data: data,
}
}
func mockConfigMap(name string, data map[string]string) *corev1.ConfigMap {
return &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: kindConfigMap,
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{Name: name},
Data: data,
}
}
func testScheme() *runtime.Scheme {
scheme := runtime.NewScheme()
_ = corev1.AddToScheme(scheme)
_ = v2.AddToScheme(scheme)
return scheme
}

View File

@ -1,437 +0,0 @@
/*
Copyright 2022 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 (
"context"
"testing"
"github.com/go-logr/logr"
. "github.com/onsi/gomega"
"helm.sh/helm/v3/pkg/chartutil"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
v2 "github.com/fluxcd/helm-controller/api/v2"
)
func TestChartValuesFromReferences(t *testing.T) {
scheme := testScheme()
tests := []struct {
name string
resources []runtime.Object
namespace string
references []v2.ValuesReference
values string
want chartutil.Values
wantErr bool
}{
{
name: "merges",
resources: []runtime.Object{
mockConfigMap("values", map[string]string{
"values.yaml": `flat: value
nested:
configuration: value
`,
}),
mockSecret("values", map[string][]byte{
"values.yaml": []byte(`flat:
nested: value
nested: value
`),
}),
},
references: []v2.ValuesReference{
{
Kind: kindConfigMap,
Name: "values",
},
{
Kind: kindSecret,
Name: "values",
},
},
values: `
other: values
`,
want: chartutil.Values{
"flat": map[string]interface{}{
"nested": "value",
},
"nested": "value",
"other": "values",
},
},
{
name: "with target path",
resources: []runtime.Object{
mockSecret("values", map[string][]byte{"single": []byte("value")}),
},
references: []v2.ValuesReference{
{
Kind: kindSecret,
Name: "values",
ValuesKey: "single",
TargetPath: "merge.at.specific.path",
},
},
want: chartutil.Values{
"merge": map[string]interface{}{
"at": map[string]interface{}{
"specific": map[string]interface{}{
"path": "value",
},
},
},
},
},
{
name: "target path precedence over all",
resources: []runtime.Object{
mockConfigMap("values", map[string]string{
"values.yaml": `flat: value
nested:
configuration:
- one
- two
- three
`,
}),
mockSecret("values", map[string][]byte{"key": []byte("value")}),
},
references: []v2.ValuesReference{
{
Kind: kindSecret,
Name: "values",
ValuesKey: "key",
TargetPath: "nested.configuration[0]",
},
{
Kind: kindConfigMap,
Name: "values",
},
},
values: `
nested:
configuration:
- list
- item
- option
`,
want: chartutil.Values{
"flat": "value",
"nested": map[string]interface{}{
"configuration": []interface{}{"value", "item", "option"},
},
},
},
{
name: "target path for string type array item",
resources: []runtime.Object{
mockConfigMap("values", map[string]string{
"values.yaml": `flat: value
nested:
configuration:
- list
- item
- option
`,
}),
mockSecret("values", map[string][]byte{
"values.yaml": []byte(`foo`),
}),
},
references: []v2.ValuesReference{
{
Kind: kindConfigMap,
Name: "values",
},
{
Kind: kindSecret,
Name: "values",
TargetPath: "nested.configuration[1]",
},
},
values: `
other: values
`,
want: chartutil.Values{
"flat": "value",
"nested": map[string]interface{}{
"configuration": []interface{}{"list", "foo", "option"},
},
"other": "values",
},
},
{
name: "values reference to non existing secret",
references: []v2.ValuesReference{
{
Kind: kindSecret,
Name: "missing",
},
},
wantErr: true,
},
{
name: "optional values reference to non existing secret",
references: []v2.ValuesReference{
{
Kind: kindSecret,
Name: "missing",
Optional: true,
},
},
want: chartutil.Values{},
wantErr: false,
},
{
name: "values reference to non existing config map",
references: []v2.ValuesReference{
{
Kind: kindConfigMap,
Name: "missing",
},
},
wantErr: true,
},
{
name: "optional values reference to non existing config map",
references: []v2.ValuesReference{
{
Kind: kindConfigMap,
Name: "missing",
Optional: true,
},
},
want: chartutil.Values{},
wantErr: false,
},
{
name: "missing secret key",
resources: []runtime.Object{
mockSecret("values", nil),
},
references: []v2.ValuesReference{
{
Kind: kindSecret,
Name: "values",
ValuesKey: "nonexisting",
},
},
wantErr: true,
},
{
name: "missing config map key",
resources: []runtime.Object{
mockConfigMap("values", nil),
},
references: []v2.ValuesReference{
{
Kind: kindConfigMap,
Name: "values",
ValuesKey: "nonexisting",
},
},
wantErr: true,
},
{
name: "unsupported values reference kind",
references: []v2.ValuesReference{
{
Kind: "Unsupported",
},
},
wantErr: true,
},
{
name: "invalid values",
resources: []runtime.Object{
mockConfigMap("values", map[string]string{
"values.yaml": `
invalid`,
}),
},
references: []v2.ValuesReference{
{
Kind: kindConfigMap,
Name: "values",
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
c := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(tt.resources...)
var values map[string]interface{}
if tt.values != "" {
m, err := chartutil.ReadValues([]byte(tt.values))
g.Expect(err).ToNot(HaveOccurred())
values = m
}
ctx := logr.NewContext(context.TODO(), logr.Discard())
got, err := ChartValuesFromReferences(ctx, c.Build(), tt.namespace, values, tt.references...)
if tt.wantErr {
g.Expect(err).To(HaveOccurred())
g.Expect(got).To(BeNil())
return
}
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).To(Equal(tt.want))
})
}
}
// This tests compatability with the formats described in:
// https://helm.sh/docs/intro/using_helm/#the-format-and-limitations-of---set
func TestReplacePathValue(t *testing.T) {
tests := []struct {
name string
value []byte
path string
want map[string]interface{}
wantErr bool
}{
{
name: "outer inner",
value: []byte("value"),
path: "outer.inner",
want: map[string]interface{}{
"outer": map[string]interface{}{
"inner": "value",
},
},
},
{
name: "inline list",
value: []byte("{a,b,c}"),
path: "name",
want: map[string]interface{}{
// TODO(hidde): figure out why the cap is off by len+1
"name": append(make([]interface{}, 0, 4), []interface{}{"a", "b", "c"}...),
},
},
{
name: "with escape",
value: []byte(`value1\,value2`),
path: "name",
want: map[string]interface{}{
"name": "value1,value2",
},
},
{
name: "target path with boolean value",
value: []byte("true"),
path: "merge.at.specific.path",
want: chartutil.Values{
"merge": map[string]interface{}{
"at": map[string]interface{}{
"specific": map[string]interface{}{
"path": true,
},
},
},
},
},
{
name: "target path with set-string behavior",
value: []byte(`"true"`),
path: "merge.at.specific.path",
want: chartutil.Values{
"merge": map[string]interface{}{
"at": map[string]interface{}{
"specific": map[string]interface{}{
"path": "true",
},
},
},
},
},
{
name: "target path with array item",
value: []byte("value"),
path: "merge.at[2]",
want: chartutil.Values{
"merge": map[string]interface{}{
"at": []interface{}{nil, nil, "value"},
},
},
},
{
name: "dot sequence escaping path",
value: []byte("master"),
path: `nodeSelector.kubernetes\.io/role`,
want: map[string]interface{}{
"nodeSelector": map[string]interface{}{
"kubernetes.io/role": "master",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
values := map[string]interface{}{}
err := ReplacePathValue(values, tt.path, string(tt.value))
if tt.wantErr {
g.Expect(err).To(HaveOccurred())
g.Expect(values).To(BeNil())
return
}
g.Expect(err).ToNot(HaveOccurred())
g.Expect(values).To(Equal(tt.want))
})
}
}
func mockSecret(name string, data map[string][]byte) *corev1.Secret {
return &corev1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: kindSecret,
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{Name: name},
Data: data,
}
}
func mockConfigMap(name string, data map[string]string) *corev1.ConfigMap {
return &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: kindConfigMap,
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{Name: name},
Data: data,
}
}
func testScheme() *runtime.Scheme {
scheme := runtime.NewScheme()
_ = corev1.AddToScheme(scheme)
_ = v2.AddToScheme(scheme)
return scheme
}

View File

@ -58,10 +58,11 @@ import (
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1beta2 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/fluxcd/pkg/chartutil"
v2 "github.com/fluxcd/helm-controller/api/v2"
intacl "github.com/fluxcd/helm-controller/internal/acl"
"github.com/fluxcd/helm-controller/internal/action"
"github.com/fluxcd/helm-controller/internal/chartutil"
"github.com/fluxcd/helm-controller/internal/digest"
interrors "github.com/fluxcd/helm-controller/internal/errors"
"github.com/fluxcd/helm-controller/internal/features"
@ -307,7 +308,12 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context, patchHelpe
}
// Compose values based from the spec and references.
values, err := chartutil.ChartValuesFromReferences(ctx, r.Client, obj.Namespace, obj.GetValues(), obj.Spec.ValuesFrom...)
values, err := chartutil.ChartValuesFromReferences(ctx,
log,
r.Client,
obj.Namespace,
obj.GetValues(),
obj.Spec.ValuesFrom...)
if err != nil {
conditions.MarkFalse(obj, meta.ReadyCondition, "ValuesError", "%s", err)
r.Eventf(obj, corev1.EventTypeWarning, "ValuesError", err.Error())

View File

@ -49,6 +49,7 @@ import (
aclv1 "github.com/fluxcd/pkg/apis/acl"
"github.com/fluxcd/pkg/apis/kustomize"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/chartutil"
"github.com/fluxcd/pkg/runtime/conditions"
feathelper "github.com/fluxcd/pkg/runtime/features"
"github.com/fluxcd/pkg/runtime/patch"
@ -58,7 +59,6 @@ import (
v2 "github.com/fluxcd/helm-controller/api/v2"
intacl "github.com/fluxcd/helm-controller/internal/acl"
"github.com/fluxcd/helm-controller/internal/action"
"github.com/fluxcd/helm-controller/internal/chartutil"
"github.com/fluxcd/helm-controller/internal/features"
"github.com/fluxcd/helm-controller/internal/kube"
"github.com/fluxcd/helm-controller/internal/postrender"
@ -331,7 +331,7 @@ func TestHelmReleaseReconciler_reconcileRelease(t *testing.T) {
Namespace: "mock",
},
Spec: v2.HelmReleaseSpec{
ValuesFrom: []v2.ValuesReference{
ValuesFrom: []meta.ValuesReference{
{
Kind: "Secret",
Name: "missing",
@ -1537,7 +1537,7 @@ func TestHelmReleaseReconciler_reconcileReleaseFromOCIRepositorySource(t *testin
Name: "ocirepo",
Namespace: "mock",
},
ValuesFrom: []v2.ValuesReference{
ValuesFrom: []meta.ValuesReference{
{
Kind: "Secret",
Name: "missing",
@ -3526,12 +3526,12 @@ func Test_waitForHistoryCacheSync(t *testing.T) {
func TestValuesReferenceValidation(t *testing.T) {
tests := []struct {
name string
references []v2.ValuesReference
references []meta.ValuesReference
wantErr bool
}{
{
name: "valid ValuesKey",
references: []v2.ValuesReference{
references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@ -3542,7 +3542,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "valid ValuesKey: empty",
references: []v2.ValuesReference{
references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@ -3553,7 +3553,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "valid ValuesKey: long",
references: []v2.ValuesReference{
references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@ -3564,7 +3564,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "invalid ValuesKey",
references: []v2.ValuesReference{
references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@ -3575,7 +3575,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "invalid ValuesKey: too long",
references: []v2.ValuesReference{
references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@ -3586,7 +3586,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "valid target path: empty",
references: []v2.ValuesReference{
references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@ -3597,7 +3597,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "valid target path",
references: []v2.ValuesReference{
references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@ -3608,7 +3608,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "valid target path: long",
references: []v2.ValuesReference{
references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@ -3619,7 +3619,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "invalid target path: too long",
references: []v2.ValuesReference{
references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@ -3630,7 +3630,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "invalid target path: opened index",
references: []v2.ValuesReference{
references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@ -3642,7 +3642,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "invalid target path: incorrect index syntax",
references: []v2.ValuesReference{
references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",

View File

@ -30,8 +30,8 @@ import (
v2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/helm-controller/internal/action"
"github.com/fluxcd/helm-controller/internal/chartutil"
"github.com/fluxcd/helm-controller/internal/digest"
"github.com/fluxcd/pkg/chartutil"
)
// Install is an ActionReconciler which attempts to install a Helm release

View File

@ -40,11 +40,11 @@ import (
v2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/helm-controller/internal/action"
"github.com/fluxcd/helm-controller/internal/chartutil"
"github.com/fluxcd/helm-controller/internal/digest"
"github.com/fluxcd/helm-controller/internal/release"
"github.com/fluxcd/helm-controller/internal/storage"
"github.com/fluxcd/helm-controller/internal/testutil"
"github.com/fluxcd/pkg/chartutil"
)
func TestInstall_Reconcile(t *testing.T) {

View File

@ -31,10 +31,10 @@ import (
v2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/helm-controller/internal/action"
"github.com/fluxcd/helm-controller/internal/chartutil"
"github.com/fluxcd/helm-controller/internal/digest"
"github.com/fluxcd/helm-controller/internal/release"
"github.com/fluxcd/helm-controller/internal/storage"
"github.com/fluxcd/pkg/chartutil"
)
// RollbackRemediation is an ActionReconciler which attempts to roll back

View File

@ -39,11 +39,11 @@ import (
v2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/helm-controller/internal/action"
"github.com/fluxcd/helm-controller/internal/chartutil"
"github.com/fluxcd/helm-controller/internal/digest"
"github.com/fluxcd/helm-controller/internal/release"
"github.com/fluxcd/helm-controller/internal/storage"
"github.com/fluxcd/helm-controller/internal/testutil"
"github.com/fluxcd/pkg/chartutil"
)
func TestRollbackRemediation_Reconcile(t *testing.T) {

View File

@ -38,10 +38,10 @@ import (
v2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/helm-controller/internal/action"
"github.com/fluxcd/helm-controller/internal/chartutil"
"github.com/fluxcd/helm-controller/internal/digest"
"github.com/fluxcd/helm-controller/internal/release"
"github.com/fluxcd/helm-controller/internal/testutil"
"github.com/fluxcd/pkg/chartutil"
)
// testHookFixtures is a list of release.Hook in every possible LastRun state.

View File

@ -37,11 +37,11 @@ import (
v2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/helm-controller/internal/action"
"github.com/fluxcd/helm-controller/internal/chartutil"
"github.com/fluxcd/helm-controller/internal/digest"
"github.com/fluxcd/helm-controller/internal/release"
"github.com/fluxcd/helm-controller/internal/storage"
"github.com/fluxcd/helm-controller/internal/testutil"
"github.com/fluxcd/pkg/chartutil"
)
func TestUninstallRemediation_Reconcile(t *testing.T) {

View File

@ -38,11 +38,11 @@ import (
v2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/helm-controller/internal/action"
"github.com/fluxcd/helm-controller/internal/chartutil"
"github.com/fluxcd/helm-controller/internal/digest"
"github.com/fluxcd/helm-controller/internal/release"
"github.com/fluxcd/helm-controller/internal/storage"
"github.com/fluxcd/helm-controller/internal/testutil"
"github.com/fluxcd/pkg/chartutil"
)
func TestUninstall_Reconcile(t *testing.T) {

View File

@ -38,11 +38,11 @@ import (
v2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/helm-controller/internal/action"
"github.com/fluxcd/helm-controller/internal/chartutil"
"github.com/fluxcd/helm-controller/internal/digest"
"github.com/fluxcd/helm-controller/internal/release"
"github.com/fluxcd/helm-controller/internal/storage"
"github.com/fluxcd/helm-controller/internal/testutil"
"github.com/fluxcd/pkg/chartutil"
)
func TestUnlock_Reconcile(t *testing.T) {

View File

@ -30,8 +30,8 @@ import (
v2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/helm-controller/internal/action"
"github.com/fluxcd/helm-controller/internal/chartutil"
"github.com/fluxcd/helm-controller/internal/digest"
"github.com/fluxcd/pkg/chartutil"
)
// Upgrade is an ActionReconciler which attempts to upgrade a Helm release

View File

@ -40,11 +40,11 @@ import (
v2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/helm-controller/internal/action"
"github.com/fluxcd/helm-controller/internal/chartutil"
"github.com/fluxcd/helm-controller/internal/digest"
"github.com/fluxcd/helm-controller/internal/release"
"github.com/fluxcd/helm-controller/internal/storage"
"github.com/fluxcd/helm-controller/internal/testutil"
"github.com/fluxcd/pkg/chartutil"
)
func TestUpgrade_Reconcile(t *testing.T) {

View File

@ -26,8 +26,8 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/helm-controller/internal/chartutil"
"github.com/fluxcd/helm-controller/internal/digest"
"github.com/fluxcd/pkg/chartutil"
)
var (

View File

@ -1,43 +0,0 @@
/*
Copyright 2023 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 yaml
import (
"io"
"sigs.k8s.io/yaml"
goyaml "sigs.k8s.io/yaml/goyaml.v2"
)
// PreEncoder allows for pre-processing of the YAML data before encoding.
type PreEncoder func(goyaml.MapSlice)
// Encode encodes the given data to YAML format and writes it to the provided
// io.Write, without going through a byte representation (unlike
// sigs.k8s.io/yaml#Unmarshal).
//
// It optionally takes one or more PreEncoder functions that allow
// for pre-processing of the data before encoding, such as sorting the data.
//
// It returns an error if the data cannot be encoded.
func Encode(w io.Writer, data map[string]interface{}, pe ...PreEncoder) error {
ms := yaml.JSONObjectToYAMLObject(data)
for _, m := range pe {
m(ms)
}
return goyaml.NewEncoder(w).Encode(ms)
}

View File

@ -1,106 +0,0 @@
/*
Copyright 2023 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 yaml
import (
"bytes"
"os"
"testing"
"sigs.k8s.io/yaml"
)
func TestEncode(t *testing.T) {
tests := []struct {
name string
input map[string]interface{}
preEncoders []PreEncoder
want []byte
}{
{
name: "empty map",
input: map[string]interface{}{},
want: []byte(`{}
`),
},
{
name: "simple values",
input: map[string]interface{}{
"replicaCount": 3,
},
want: []byte(`replicaCount: 3
`),
},
{
name: "with pre-encoder",
input: map[string]interface{}{
"replicaCount": 3,
"image": map[string]interface{}{
"repository": "nginx",
"tag": "latest",
},
"port": 8080,
},
preEncoders: []PreEncoder{SortMapSlice},
want: []byte(`image:
repository: nginx
tag: latest
port: 8080
replicaCount: 3
`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var actual bytes.Buffer
err := Encode(&actual, tt.input, tt.preEncoders...)
if err != nil {
t.Fatalf("error encoding: %v", err)
}
if !bytes.Equal(actual.Bytes(), tt.want) {
t.Errorf("Encode() = %v, want: %s", actual.String(), tt.want)
}
})
}
}
func BenchmarkEncode(b *testing.B) {
// Test against the values.yaml from the kube-prometheus-stack chart, which
// is a fairly large file.
v, err := os.ReadFile("testdata/values.yaml")
if err != nil {
b.Fatalf("error reading testdata: %v", err)
}
var data map[string]interface{}
if err = yaml.Unmarshal(v, &data); err != nil {
b.Fatalf("error unmarshalling testdata: %v", err)
}
b.Run("EncodeWithSort", func(b *testing.B) {
for i := 0; i < b.N; i++ {
Encode(bytes.NewBuffer(nil), data, SortMapSlice)
}
})
b.Run("SigYAMLMarshal", func(b *testing.B) {
for i := 0; i < b.N; i++ {
yaml.Marshal(data)
}
})
}

View File

@ -1,44 +0,0 @@
/*
Copyright 2023 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 yaml
import (
"sort"
goyaml "sigs.k8s.io/yaml/goyaml.v2"
)
// SortMapSlice recursively sorts the given goyaml.MapSlice by key.
// It can be used in combination with Encode to sort YAML by key
// before encoding it.
func SortMapSlice(ms goyaml.MapSlice) {
sort.Slice(ms, func(i, j int) bool {
return ms[i].Key.(string) < ms[j].Key.(string)
})
for _, item := range ms {
if nestedMS, ok := item.Value.(goyaml.MapSlice); ok {
SortMapSlice(nestedMS)
} else if nestedSlice, ok := item.Value.([]interface{}); ok {
for _, vItem := range nestedSlice {
if nestedMS, ok := vItem.(goyaml.MapSlice); ok {
SortMapSlice(nestedMS)
}
}
}
}
}

View File

@ -1,183 +0,0 @@
/*
Copyright 2023 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 yaml
import (
"bytes"
"testing"
"sigs.k8s.io/yaml"
goyaml "sigs.k8s.io/yaml/goyaml.v2"
)
func TestSortMapSlice(t *testing.T) {
tests := []struct {
name string
input map[string]interface{}
want map[string]interface{}
}{
{
name: "empty map",
input: map[string]interface{}{},
want: map[string]interface{}{},
},
{
name: "flat map",
input: map[string]interface{}{
"b": "value-b",
"a": "value-a",
"c": "value-c",
},
want: map[string]interface{}{
"a": "value-a",
"b": "value-b",
"c": "value-c",
},
},
{
name: "nested map",
input: map[string]interface{}{
"b": "value-b",
"a": "value-a",
"c": map[string]interface{}{
"z": "value-z",
"y": "value-y",
},
},
want: map[string]interface{}{
"a": "value-a",
"b": "value-b",
"c": map[string]interface{}{
"y": "value-y",
"z": "value-z",
},
},
},
{
name: "map with slices",
input: map[string]interface{}{
"b": []interface{}{"apple", "banana", "cherry"},
"a": []interface{}{"orange", "grape"},
"c": []interface{}{"strawberry"},
},
want: map[string]interface{}{
"a": []interface{}{"orange", "grape"},
"b": []interface{}{"apple", "banana", "cherry"},
"c": []interface{}{"strawberry"},
},
},
{
name: "map with mixed data types",
input: map[string]interface{}{
"b": 50,
"a": "value-a",
"c": []interface{}{"strawberry", "banana"},
"d": map[string]interface{}{
"x": true,
"y": 123,
},
},
want: map[string]interface{}{
"a": "value-a",
"b": 50,
"c": []interface{}{"strawberry", "banana"},
"d": map[string]interface{}{
"x": true,
"y": 123,
},
},
},
{
name: "map with complex structure",
input: map[string]interface{}{
"a": map[string]interface{}{
"c": "value-c",
"b": "value-b",
"a": "value-a",
},
"b": "value-b",
"c": map[string]interface{}{
"z": map[string]interface{}{
"a": "value-a",
"b": "value-b",
"c": "value-c",
},
"y": "value-y",
},
"d": map[string]interface{}{
"q": "value-q",
"p": "value-p",
"r": "value-r",
},
"e": []interface{}{"strawberry", "banana"},
},
want: map[string]interface{}{
"a": map[string]interface{}{
"a": "value-a",
"b": "value-b",
"c": "value-c",
},
"b": "value-b",
"c": map[string]interface{}{
"y": "value-y",
"z": map[string]interface{}{
"a": "value-a",
"b": "value-b",
"c": "value-c",
},
},
"d": map[string]interface{}{
"p": "value-p",
"q": "value-q",
"r": "value-r",
},
"e": []interface{}{"strawberry", "banana"},
},
},
{
name: "map with empty slices and maps",
input: map[string]interface{}{
"b": []interface{}{},
"a": map[string]interface{}{},
},
want: map[string]interface{}{
"a": map[string]interface{}{},
"b": []interface{}{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
input := yaml.JSONObjectToYAMLObject(tt.input)
SortMapSlice(input)
expect, err := goyaml.Marshal(input)
if err != nil {
t.Fatalf("error marshalling output: %v", err)
}
actual, err := goyaml.Marshal(tt.want)
if err != nil {
t.Fatalf("error marshalling want: %v", err)
}
if !bytes.Equal(expect, actual) {
t.Errorf("SortMapSlice() = %s, want %s", expect, actual)
}
})
}
}

File diff suppressed because it is too large Load Diff