source-controller/internal/helm/chart/builder_local_test.go

385 lines
11 KiB
Go

/*
Copyright 2021 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 chart
import (
"context"
"os"
"path/filepath"
"sync"
"testing"
. "github.com/onsi/gomega"
"github.com/otiai10/copy"
helmchart "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/repo"
"github.com/fluxcd/source-controller/internal/helm/repository"
)
func TestLocalBuilder_Build(t *testing.T) {
g := NewWithT(t)
// Prepare chart repositories to be used for charts with remote dependency.
chartB, err := os.ReadFile("./../testdata/charts/helmchart-0.1.0.tgz")
g.Expect(err).ToNot(HaveOccurred())
g.Expect(chartB).ToNot(BeEmpty())
mockRepo := func() *repository.ChartRepository {
return &repository.ChartRepository{
Client: &mockGetter{
Response: chartB,
},
Index: &repo.IndexFile{
Entries: map[string]repo.ChartVersions{
"grafana": {
&repo.ChartVersion{
Metadata: &helmchart.Metadata{
Name: "grafana",
Version: "6.17.4",
},
URLs: []string{"https://example.com/grafana.tgz"},
},
},
},
},
RWMutex: &sync.RWMutex{},
}
}
tests := []struct {
name string
reference Reference
buildOpts BuildOptions
valuesFiles []helmchart.File
repositories map[string]*repository.ChartRepository
dependentChartPaths []string
wantValues chartutil.Values
wantVersion string
wantPackaged bool
wantErr string
}{
{
name: "invalid reference",
reference: RemoteReference{},
wantErr: "expected local chart reference",
},
{
name: "invalid local reference - no path",
reference: LocalReference{},
wantErr: "no path set for local chart reference",
},
{
name: "invalid local reference - no file",
reference: LocalReference{Path: "/tmp/non-existent-path.xyz"},
wantErr: "no such file or directory",
},
{
name: "invalid version metadata",
reference: LocalReference{Path: "./../testdata/charts/helmchart"},
buildOpts: BuildOptions{VersionMetadata: "^"},
wantErr: "Invalid Metadata string",
},
{
name: "with version metadata",
reference: LocalReference{Path: "./../testdata/charts/helmchart"},
buildOpts: BuildOptions{VersionMetadata: "foo"},
wantVersion: "0.1.0+foo",
wantPackaged: true,
},
{
name: "already packaged chart",
reference: LocalReference{Path: "./../testdata/charts/helmchart-0.1.0.tgz"},
wantVersion: "0.1.0",
wantPackaged: false,
},
{
name: "default values",
reference: LocalReference{Path: "./../testdata/charts/helmchart"},
wantValues: chartutil.Values{
"replicaCount": float64(1),
},
wantVersion: "0.1.0",
wantPackaged: true,
},
{
name: "with values files",
reference: LocalReference{Path: "./../testdata/charts/helmchart"},
buildOpts: BuildOptions{
ValuesFiles: []string{"custom-values1.yaml", "custom-values2.yaml"},
},
valuesFiles: []helmchart.File{
{
Name: "custom-values1.yaml",
Data: []byte(`replicaCount: 11
nameOverride: "foo-name-override"`),
},
{
Name: "custom-values2.yaml",
Data: []byte(`replicaCount: 20
fullnameOverride: "full-foo-name-override"`),
},
},
wantValues: chartutil.Values{
"replicaCount": float64(20),
"nameOverride": "foo-name-override",
"fullnameOverride": "full-foo-name-override",
},
wantVersion: "0.1.0",
wantPackaged: true,
},
{
name: "chart with dependencies",
reference: LocalReference{Path: "./../testdata/charts/helmchartwithdeps"},
repositories: map[string]*repository.ChartRepository{
"https://grafana.github.io/helm-charts/": mockRepo(),
},
dependentChartPaths: []string{"./../testdata/charts/helmchart"},
wantVersion: "0.1.0",
wantPackaged: true,
},
{
name: "v1 chart",
reference: LocalReference{Path: "./../testdata/charts/helmchart-v1"},
wantValues: chartutil.Values{
"replicaCount": float64(1),
},
wantVersion: "0.2.0",
wantPackaged: true,
},
{
name: "v1 chart with dependencies",
reference: LocalReference{Path: "./../testdata/charts/helmchartwithdeps-v1"},
repositories: map[string]*repository.ChartRepository{
"https://grafana.github.io/helm-charts/": mockRepo(),
},
dependentChartPaths: []string{"./../testdata/charts/helmchart-v1"},
wantVersion: "0.3.0",
wantPackaged: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
workDir, err := os.MkdirTemp("", "local-builder-")
g.Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(workDir)
// Only if the reference is a LocalReference, set the WorkDir.
localRef, ok := tt.reference.(LocalReference)
if ok {
localRef.WorkDir = workDir
tt.reference = localRef
}
// Write value file in the base dir.
for _, f := range tt.valuesFiles {
vPath := filepath.Join(workDir, f.Name)
g.Expect(os.WriteFile(vPath, f.Data, 0644)).ToNot(HaveOccurred())
}
// Write chart dependencies in the base dir.
for _, dcp := range tt.dependentChartPaths {
// Construct the chart path relative to the testdata chart.
helmchartDir := filepath.Join(workDir, "testdata", "charts", filepath.Base(dcp))
g.Expect(copy.Copy(dcp, helmchartDir)).ToNot(HaveOccurred())
}
// Target path with name similar to the workDir.
targetPath := workDir + ".tgz"
defer os.RemoveAll(targetPath)
dm := NewDependencyManager(
WithRepositories(tt.repositories),
)
b := NewLocalBuilder(dm)
cb, err := b.Build(context.TODO(), tt.reference, targetPath, tt.buildOpts)
if tt.wantErr != "" {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
g.Expect(cb).To(BeZero())
return
}
g.Expect(err).ToNot(HaveOccurred())
g.Expect(cb.Packaged).To(Equal(tt.wantPackaged), "unexpected Build.Packaged value")
g.Expect(cb.Path).ToNot(BeEmpty(), "empty Build.Path")
// Load the resulting chart and verify the values.
resultChart, err := loader.Load(cb.Path)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(resultChart.Metadata.Version).To(Equal(tt.wantVersion))
for k, v := range tt.wantValues {
g.Expect(v).To(Equal(resultChart.Values[k]))
}
})
}
}
func TestLocalBuilder_Build_CachedChart(t *testing.T) {
g := NewWithT(t)
workDir, err := os.MkdirTemp("", "local-builder-")
g.Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(workDir)
reference := LocalReference{Path: "./../testdata/charts/helmchart"}
dm := NewDependencyManager()
b := NewLocalBuilder(dm)
tmpDir, err := os.MkdirTemp("", "local-chart-")
g.Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(tmpDir)
// Build first time.
targetPath := filepath.Join(tmpDir, "chart1.tgz")
buildOpts := BuildOptions{}
cb, err := b.Build(context.TODO(), reference, targetPath, buildOpts)
g.Expect(err).ToNot(HaveOccurred())
// Set the result as the CachedChart for second build.
buildOpts.CachedChart = cb.Path
targetPath2 := filepath.Join(tmpDir, "chart2.tgz")
defer os.RemoveAll(targetPath2)
cb, err = b.Build(context.TODO(), reference, targetPath2, buildOpts)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(cb.Path).To(Equal(targetPath))
// Rebuild with build option Force.
buildOpts.Force = true
cb, err = b.Build(context.TODO(), reference, targetPath2, buildOpts)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(cb.Path).To(Equal(targetPath2))
}
func Test_mergeFileValues(t *testing.T) {
tests := []struct {
name string
files []*helmchart.File
paths []string
want map[string]interface{}
wantErr string
}{
{
name: "merges values from files",
files: []*helmchart.File{
{Name: "a.yaml", Data: []byte("a: b")},
{Name: "b.yaml", Data: []byte("b: c")},
{Name: "c.yaml", Data: []byte("b: d")},
},
paths: []string{"a.yaml", "b.yaml", "c.yaml"},
want: map[string]interface{}{
"a": "b",
"b": "d",
},
},
{
name: "illegal traverse",
paths: []string{"../../../traversing/illegally/a/p/a/b"},
wantErr: "no values file found at path '/traversing/illegally/a/p/a/b'",
},
{
name: "unmarshal error",
files: []*helmchart.File{
{Name: "invalid", Data: []byte("abcd")},
},
paths: []string{"invalid"},
wantErr: "unmarshaling values from 'invalid' failed",
},
{
name: "error on invalid path",
paths: []string{"a.yaml"},
wantErr: "no values file found at path '/a.yaml'",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
baseDir, err := os.MkdirTemp("", "merge-file-values-*")
g.Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(baseDir)
for _, f := range tt.files {
g.Expect(os.WriteFile(filepath.Join(baseDir, f.Name), f.Data, 0644)).To(Succeed())
}
got, err := mergeFileValues(baseDir, tt.paths)
if tt.wantErr != "" {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
g.Expect(got).To(BeNil())
return
}
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).To(Equal(tt.want))
})
}
}
func Test_copyFileToPath(t *testing.T) {
tests := []struct {
name string
in string
wantErr string
}{
{
name: "copies input file",
in: "../testdata/local-index.yaml",
},
{
name: "invalid input file",
in: "../testdata/invalid.tgz",
wantErr: "failed to open file to copy from",
},
{
name: "invalid input directory",
in: "../testdata/charts",
wantErr: "failed to read from source during copy",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
out := tmpFile("copy-0.1.0", ".tgz")
defer os.RemoveAll(out)
err := copyFileToPath(tt.in, out)
if tt.wantErr != "" {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
return
}
g.Expect(err).ToNot(HaveOccurred())
g.Expect(out).To(BeARegularFile())
f1, err := os.ReadFile(tt.in)
g.Expect(err).ToNot(HaveOccurred())
f2, err := os.ReadFile(out)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(f2).To(Equal(f1))
})
}
}