mirror of
https://github.com/argoproj/argo-cd
synced 2026-04-21 08:57:17 +00:00
5704 lines
213 KiB
Go
5704 lines
213 KiB
Go
package repository
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
goio "io"
|
|
"io/fs"
|
|
"net/mail"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
"k8s.io/apimachinery/pkg/util/intstr"
|
|
|
|
"github.com/argoproj/argo-cd/v3/util/oci"
|
|
|
|
cacheutil "github.com/argoproj/argo-cd/v3/util/cache"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"sigs.k8s.io/yaml"
|
|
|
|
"github.com/argoproj/argo-cd/v3/common"
|
|
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
|
"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
|
|
"github.com/argoproj/argo-cd/v3/reposerver/cache"
|
|
repositorymocks "github.com/argoproj/argo-cd/v3/reposerver/cache/mocks"
|
|
"github.com/argoproj/argo-cd/v3/reposerver/metrics"
|
|
fileutil "github.com/argoproj/argo-cd/v3/test/fixture/path"
|
|
"github.com/argoproj/argo-cd/v3/util/argo"
|
|
"github.com/argoproj/argo-cd/v3/util/git"
|
|
gitmocks "github.com/argoproj/argo-cd/v3/util/git/mocks"
|
|
"github.com/argoproj/argo-cd/v3/util/helm"
|
|
helmmocks "github.com/argoproj/argo-cd/v3/util/helm/mocks"
|
|
utilio "github.com/argoproj/argo-cd/v3/util/io"
|
|
iomocks "github.com/argoproj/argo-cd/v3/util/io/mocks"
|
|
ocimocks "github.com/argoproj/argo-cd/v3/util/oci/mocks"
|
|
"github.com/argoproj/argo-cd/v3/util/settings"
|
|
)
|
|
|
|
const testSignature = `gpg: Signature made Wed Feb 26 23:22:34 2020 CET
|
|
gpg: using RSA key 4AEE18F83AFDEB23
|
|
gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>" [ultimate]
|
|
`
|
|
|
|
type clientFunc func(*gitmocks.Client, *helmmocks.Client, *ocimocks.Client, *iomocks.TempPaths)
|
|
|
|
type repoCacheMocks struct {
|
|
mock.Mock
|
|
cacheutilCache *cacheutil.Cache
|
|
cache *cache.Cache
|
|
mockCache *repositorymocks.MockRepoCache
|
|
}
|
|
|
|
type newGitRepoHelmChartOptions struct {
|
|
chartName string
|
|
// valuesFiles is a map of the values file name to the key/value pairs to be written to the file
|
|
valuesFiles map[string]map[string]string
|
|
}
|
|
|
|
type newGitRepoOptions struct {
|
|
path string
|
|
createPath bool
|
|
remote string
|
|
addEmptyCommit bool
|
|
helmChartOptions newGitRepoHelmChartOptions
|
|
}
|
|
|
|
func newCacheMocks() *repoCacheMocks {
|
|
return newCacheMocksWithOpts(1*time.Minute, 1*time.Minute, 10*time.Second)
|
|
}
|
|
|
|
func newCacheMocksWithOpts(repoCacheExpiration, revisionCacheExpiration, revisionCacheLockTimeout time.Duration) *repoCacheMocks {
|
|
mockRepoCache := repositorymocks.NewMockRepoCache(&repositorymocks.MockCacheOptions{
|
|
RepoCacheExpiration: 1 * time.Minute,
|
|
RevisionCacheExpiration: 1 * time.Minute,
|
|
ReadDelay: 0,
|
|
WriteDelay: 0,
|
|
})
|
|
cacheutilCache := cacheutil.NewCache(mockRepoCache.RedisClient)
|
|
return &repoCacheMocks{
|
|
cacheutilCache: cacheutilCache,
|
|
cache: cache.NewCache(cacheutilCache, repoCacheExpiration, revisionCacheExpiration, revisionCacheLockTimeout),
|
|
mockCache: mockRepoCache,
|
|
}
|
|
}
|
|
|
|
func newServiceWithMocks(t *testing.T, root string, signed bool) (*Service, *gitmocks.Client, *repoCacheMocks) {
|
|
t.Helper()
|
|
root, err := filepath.Abs(root)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, ociClient *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().Init().Return(nil)
|
|
gitClient.EXPECT().IsRevisionPresent(mock.Anything).Return(false)
|
|
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Return(nil)
|
|
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
|
|
gitClient.EXPECT().LsRemote(mock.Anything).Return(mock.Anything, nil)
|
|
gitClient.EXPECT().CommitSHA().Return(mock.Anything, nil)
|
|
gitClient.EXPECT().Root().Return(root)
|
|
gitClient.EXPECT().IsAnnotatedTag(mock.Anything).Return(false)
|
|
if signed {
|
|
gitClient.EXPECT().VerifyCommitSignature(mock.Anything).Return(testSignature, nil)
|
|
} else {
|
|
gitClient.EXPECT().VerifyCommitSignature(mock.Anything).Return("", nil)
|
|
}
|
|
|
|
chart := "my-chart"
|
|
oobChart := "out-of-bounds-chart"
|
|
version := "1.1.0"
|
|
helmClient.EXPECT().GetIndex(mock.AnythingOfType("bool"), mock.Anything).Return(&helm.Index{Entries: map[string]helm.Entries{
|
|
chart: {{Version: "1.0.0"}, {Version: version}},
|
|
oobChart: {{Version: "1.0.0"}, {Version: version}},
|
|
}}, nil)
|
|
helmClient.EXPECT().GetTags(mock.Anything, mock.Anything).Return(nil, nil)
|
|
helmClient.EXPECT().ExtractChart(chart, version, false, int64(0), false).Return("./testdata/my-chart", utilio.NopCloser, nil)
|
|
helmClient.EXPECT().ExtractChart(oobChart, version, false, int64(0), false).Return("./testdata2/out-of-bounds-chart", utilio.NopCloser, nil)
|
|
helmClient.EXPECT().CleanChartCache(chart, version).Return(nil)
|
|
helmClient.EXPECT().CleanChartCache(oobChart, version).Return(nil)
|
|
|
|
ociClient.EXPECT().GetTags(mock.Anything, mock.Anything).Return(nil, nil)
|
|
ociClient.EXPECT().ResolveRevision(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
|
|
ociClient.EXPECT().Extract(mock.Anything, mock.Anything).Return("./testdata/my-chart", utilio.NopCloser, nil)
|
|
|
|
paths.EXPECT().Add(mock.Anything, mock.Anything).Return()
|
|
paths.EXPECT().GetPath(mock.Anything).Return(root, nil)
|
|
paths.EXPECT().GetPathIfExists(mock.Anything).Return(root)
|
|
paths.EXPECT().GetPaths().Return(map[string]string{"fake-nonce": root})
|
|
}, root)
|
|
}
|
|
|
|
func newServiceWithOpt(t *testing.T, cf clientFunc, root string) (*Service, *gitmocks.Client, *repoCacheMocks) {
|
|
t.Helper()
|
|
helmClient := &helmmocks.Client{}
|
|
gitClient := &gitmocks.Client{}
|
|
ociClient := &ocimocks.Client{}
|
|
paths := &iomocks.TempPaths{}
|
|
cf(gitClient, helmClient, ociClient, paths)
|
|
cacheMocks := newCacheMocks()
|
|
t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
|
|
service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, &git.NoopCredsStore{}, root)
|
|
|
|
service.newGitClient = func(_ string, _ string, _ git.Creds, _ bool, _ bool, _ string, _ string, _ ...git.ClientOpts) (client git.Client, e error) {
|
|
return gitClient, nil
|
|
}
|
|
service.newHelmClient = func(_ string, _ helm.Creds, _ bool, _ string, _ string, _ ...helm.ClientOpts) helm.Client {
|
|
return helmClient
|
|
}
|
|
service.newOCIClient = func(_ string, _ oci.Creds, _ string, _ string, _ []string, _ ...oci.ClientOpts) (oci.Client, error) {
|
|
return ociClient, nil
|
|
}
|
|
service.gitRepoInitializer = func(_ string) goio.Closer {
|
|
return utilio.NopCloser
|
|
}
|
|
service.gitRepoPaths = paths
|
|
return service, gitClient, cacheMocks
|
|
}
|
|
|
|
func newService(t *testing.T, root string) *Service {
|
|
t.Helper()
|
|
service, _, _ := newServiceWithMocks(t, root, false)
|
|
return service
|
|
}
|
|
|
|
func newServiceWithSignature(t *testing.T, root string) *Service {
|
|
t.Helper()
|
|
service, _, _ := newServiceWithMocks(t, root, true)
|
|
return service
|
|
}
|
|
|
|
func newServiceWithCommitSHA(t *testing.T, root, revision string) *Service {
|
|
t.Helper()
|
|
var revisionErr error
|
|
|
|
commitSHARegex := regexp.MustCompile("^[0-9A-Fa-f]{40}$")
|
|
if !commitSHARegex.MatchString(revision) {
|
|
revisionErr = errors.New("not a commit SHA")
|
|
}
|
|
|
|
service, gitClient, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().Init().Return(nil)
|
|
gitClient.EXPECT().IsRevisionPresent(mock.Anything).Return(false)
|
|
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Return(nil)
|
|
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
|
|
gitClient.EXPECT().LsRemote(revision).Return(revision, revisionErr)
|
|
gitClient.EXPECT().CommitSHA().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
|
gitClient.EXPECT().Root().Return(root)
|
|
paths.EXPECT().GetPath(mock.Anything).Return(root, nil)
|
|
paths.EXPECT().GetPathIfExists(mock.Anything).Return(root)
|
|
}, root)
|
|
|
|
service.newGitClient = func(_ string, _ string, _ git.Creds, _ bool, _ bool, _ string, _ string, _ ...git.ClientOpts) (client git.Client, e error) {
|
|
return gitClient, nil
|
|
}
|
|
|
|
return service
|
|
}
|
|
|
|
func TestGenerateYamlManifestInDir(t *testing.T) {
|
|
service := newService(t, "../../manifests/base")
|
|
|
|
src := v1alpha1.ApplicationSource{Path: "."}
|
|
q := apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &src,
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
|
|
// update this value if we add/remove manifests
|
|
const countOfManifests = 50
|
|
|
|
res1, err := service.GenerateManifest(t.Context(), &q)
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, res1.Manifests, countOfManifests)
|
|
|
|
// this will test concatenated manifests to verify we split YAMLs correctly
|
|
res2, err := GenerateManifests(t.Context(), "./testdata/concatenated", "/", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil)
|
|
require.NoError(t, err)
|
|
assert.Len(t, res2.Manifests, 3)
|
|
}
|
|
|
|
func Test_GenerateManifest_KustomizeWithVersionOverride(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
service := newService(t, "../../util/kustomize/testdata/kustomize-with-version-override")
|
|
|
|
src := v1alpha1.ApplicationSource{Path: "."}
|
|
q := apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &src,
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
KustomizeOptions: &v1alpha1.KustomizeOptions{
|
|
Versions: []v1alpha1.KustomizeVersion{},
|
|
},
|
|
}
|
|
|
|
_, err := service.GenerateManifest(t.Context(), &q)
|
|
require.ErrorAs(t, err, &settings.KustomizeVersionNotRegisteredError{Version: "v1.2.3"})
|
|
|
|
q.KustomizeOptions.Versions = []v1alpha1.KustomizeVersion{
|
|
{
|
|
Name: "v1.2.3",
|
|
Path: "kustomize",
|
|
},
|
|
}
|
|
|
|
res, err := service.GenerateManifest(t.Context(), &q)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, res)
|
|
}
|
|
|
|
func Test_GenerateManifests_NoOutOfBoundsAccess(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
outOfBoundsFilename string
|
|
outOfBoundsFileContents string
|
|
mustNotContain string // Optional string that must not appear in error or manifest output. If empty, use outOfBoundsFileContents.
|
|
}{
|
|
{
|
|
name: "out of bounds JSON file should not appear in error output",
|
|
outOfBoundsFilename: "test.json",
|
|
outOfBoundsFileContents: `{"some": "json"}`,
|
|
},
|
|
{
|
|
name: "malformed JSON file contents should not appear in error output",
|
|
outOfBoundsFilename: "test.json",
|
|
outOfBoundsFileContents: "$",
|
|
},
|
|
{
|
|
name: "out of bounds JSON manifest should not appear in manifest output",
|
|
outOfBoundsFilename: "test.json",
|
|
// JSON marshalling is deterministic. So if there's a leak, exactly this should appear in the manifests.
|
|
outOfBoundsFileContents: `{"apiVersion":"v1","kind":"Secret","metadata":{"name":"test","namespace":"default"},"type":"Opaque"}`,
|
|
},
|
|
{
|
|
name: "out of bounds YAML manifest should not appear in manifest output",
|
|
outOfBoundsFilename: "test.yaml",
|
|
outOfBoundsFileContents: "apiVersion: v1\nkind: Secret\nmetadata:\n name: test\n namespace: default\ntype: Opaque",
|
|
mustNotContain: `{"apiVersion":"v1","kind":"Secret","metadata":{"name":"test","namespace":"default"},"type":"Opaque"}`,
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
testCaseCopy := testCase
|
|
t.Run(testCaseCopy.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
outOfBoundsDir := t.TempDir()
|
|
outOfBoundsFile := path.Join(outOfBoundsDir, testCaseCopy.outOfBoundsFilename)
|
|
err := os.WriteFile(outOfBoundsFile, []byte(testCaseCopy.outOfBoundsFileContents), os.FileMode(0o444))
|
|
require.NoError(t, err)
|
|
|
|
repoDir := t.TempDir()
|
|
err = os.Symlink(outOfBoundsFile, path.Join(repoDir, testCaseCopy.outOfBoundsFilename))
|
|
require.NoError(t, err)
|
|
|
|
mustNotContain := testCaseCopy.outOfBoundsFileContents
|
|
if testCaseCopy.mustNotContain != "" {
|
|
mustNotContain = testCaseCopy.mustNotContain
|
|
}
|
|
|
|
q := apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{}, ApplicationSource: &v1alpha1.ApplicationSource{}, ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
res, err := GenerateManifests(t.Context(), repoDir, "", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil)
|
|
require.Error(t, err)
|
|
assert.NotContains(t, err.Error(), mustNotContain)
|
|
require.ErrorContains(t, err, "illegal filepath")
|
|
assert.Nil(t, res)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGenerateManifests_MissingSymlinkDestination(t *testing.T) {
|
|
repoDir := t.TempDir()
|
|
err := os.Symlink("/obviously/does/not/exist", path.Join(repoDir, "test.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
q := apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{}, ApplicationSource: &v1alpha1.ApplicationSource{}, ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
_, err = GenerateManifests(t.Context(), repoDir, "", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestGenerateManifests_K8SAPIResetCache(t *testing.T) {
|
|
service := newService(t, "../../manifests/base")
|
|
|
|
src := v1alpha1.ApplicationSource{Path: "."}
|
|
q := apiclient.ManifestRequest{
|
|
KubeVersion: "v1.16.0",
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &src,
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
|
|
cachedFakeResponse := &apiclient.ManifestResponse{Manifests: []string{"Fake"}, Revision: mock.Anything}
|
|
|
|
err := service.cache.SetManifests(mock.Anything, &src, q.RefSources, &q, "", "", "", "", &cache.CachedManifestResponse{ManifestResponse: cachedFakeResponse}, nil, "")
|
|
require.NoError(t, err)
|
|
|
|
res, err := service.GenerateManifest(t.Context(), &q)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, cachedFakeResponse, res)
|
|
|
|
q.KubeVersion = "v1.17.0"
|
|
res, err = service.GenerateManifest(t.Context(), &q)
|
|
require.NoError(t, err)
|
|
assert.NotEqual(t, cachedFakeResponse, res)
|
|
assert.Greater(t, len(res.Manifests), 1)
|
|
}
|
|
|
|
func TestGenerateManifests_EmptyCache(t *testing.T) {
|
|
service, gitMocks, mockCache := newServiceWithMocks(t, "../../manifests/base", false)
|
|
|
|
src := v1alpha1.ApplicationSource{Path: "."}
|
|
q := apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &src,
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
|
|
err := service.cache.SetManifests(mock.Anything, &src, q.RefSources, &q, "", "", "", "", &cache.CachedManifestResponse{ManifestResponse: nil}, nil, "")
|
|
require.NoError(t, err)
|
|
|
|
res, err := service.GenerateManifest(t.Context(), &q)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, res.Manifests)
|
|
mockCache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
|
|
ExternalSets: 2,
|
|
ExternalGets: 2,
|
|
ExternalDeletes: 1,
|
|
})
|
|
gitMocks.AssertCalled(t, "LsRemote", mock.Anything)
|
|
gitMocks.AssertCalled(t, "Fetch", mock.Anything, mock.Anything)
|
|
}
|
|
|
|
// Test that when Generate manifest is called with a source that is ref only it does not try to generate manifests or hit the manifest cache
|
|
// but it does resolve and cache the revision
|
|
func TestGenerateManifest_RefOnlyShortCircuit(t *testing.T) {
|
|
lsremoteCalled := false
|
|
dir := t.TempDir()
|
|
repopath := dir + "/tmprepo"
|
|
repoRemote := "file://" + repopath
|
|
cacheMocks := newCacheMocks()
|
|
t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
|
|
service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, &git.NoopCredsStore{}, repopath)
|
|
service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, noProxy string, opts ...git.ClientOpts) (client git.Client, e error) {
|
|
opts = append(opts, git.WithEventHandlers(git.EventHandlers{
|
|
// Primary check, we want to make sure ls-remote is not called when the item is in cache
|
|
OnLsRemote: func(_ string) func() {
|
|
return func() {
|
|
lsremoteCalled = true
|
|
}
|
|
},
|
|
OnFetch: func(_ string) func() {
|
|
return func() {
|
|
assert.Fail(t, "Fetch should not be called from GenerateManifest when the source is ref only")
|
|
}
|
|
},
|
|
}))
|
|
gitClient, err := git.NewClientExt(rawRepoURL, root, creds, insecure, enableLfs, proxy, noProxy, opts...)
|
|
return gitClient, err
|
|
}
|
|
revision := initGitRepo(t, newGitRepoOptions{
|
|
path: repopath,
|
|
createPath: true,
|
|
remote: repoRemote,
|
|
addEmptyCommit: true,
|
|
})
|
|
src := v1alpha1.ApplicationSource{RepoURL: repoRemote, TargetRevision: "HEAD", Ref: "test-ref"}
|
|
repo := &v1alpha1.Repository{
|
|
Repo: repoRemote,
|
|
}
|
|
q := apiclient.ManifestRequest{
|
|
Repo: repo,
|
|
Revision: "HEAD",
|
|
HasMultipleSources: true,
|
|
ApplicationSource: &src,
|
|
ProjectName: "default",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
_, err := service.GenerateManifest(t.Context(), &q)
|
|
require.NoError(t, err)
|
|
cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
|
|
ExternalSets: 2,
|
|
ExternalGets: 2,
|
|
})
|
|
assert.True(t, lsremoteCalled, "ls-remote should be called when the source is ref only")
|
|
var revisions [][2]string
|
|
require.NoError(t, cacheMocks.cacheutilCache.GetItem("git-refs|"+repoRemote, &revisions))
|
|
assert.ElementsMatch(t, [][2]string{{"refs/heads/main", revision}, {"HEAD", "ref: refs/heads/main"}}, revisions)
|
|
}
|
|
|
|
// Test that calling manifest generation on source helm reference helm files that when the revision is cached it does not call ls-remote
|
|
func TestGenerateManifestsHelmWithRefs_CachedNoLsRemote(t *testing.T) {
|
|
// Use os.MkdirTemp instead of t.TempDir() because the async goroutine in
|
|
// runManifestGenAsync sets directory permissions to 0o000 (via
|
|
// directoryPermissionInitializer) after GenerateManifest returns, racing
|
|
// with t.TempDir()'s implicit RemoveAll cleanup.
|
|
dir, mkErr := os.MkdirTemp("", "TestGenerateManifestsHelmWithRefs_CachedNoLsRemote")
|
|
require.NoError(t, mkErr)
|
|
repopath := dir + "/tmprepo"
|
|
cacheMocks := newCacheMocks()
|
|
t.Cleanup(func() {
|
|
cacheMocks.mockCache.StopRedisCallback()
|
|
})
|
|
service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, &git.NoopCredsStore{}, repopath)
|
|
var gitClient git.Client
|
|
var err error
|
|
service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, noProxy string, opts ...git.ClientOpts) (client git.Client, e error) {
|
|
opts = append(opts, git.WithEventHandlers(git.EventHandlers{
|
|
// Primary check, we want to make sure ls-remote is not called when the item is in cache
|
|
OnLsRemote: func(_ string) func() {
|
|
return func() {
|
|
assert.Fail(t, "LsRemote should not be called when the item is in cache")
|
|
}
|
|
},
|
|
}))
|
|
gitClient, err = git.NewClientExt(rawRepoURL, root, creds, insecure, enableLfs, proxy, noProxy, opts...)
|
|
return gitClient, err
|
|
}
|
|
repoRemote := "file://" + repopath
|
|
revision := initGitRepo(t, newGitRepoOptions{
|
|
path: repopath,
|
|
createPath: true,
|
|
remote: repoRemote,
|
|
helmChartOptions: newGitRepoHelmChartOptions{
|
|
chartName: "my-chart",
|
|
valuesFiles: map[string]map[string]string{"test.yaml": {"testval": "test"}},
|
|
},
|
|
})
|
|
src := v1alpha1.ApplicationSource{RepoURL: repoRemote, Path: ".", TargetRevision: "HEAD", Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"$ref/test.yaml"},
|
|
}}
|
|
repo := &v1alpha1.Repository{
|
|
Repo: repoRemote,
|
|
}
|
|
q := apiclient.ManifestRequest{
|
|
Repo: repo,
|
|
Revision: "HEAD",
|
|
HasMultipleSources: true,
|
|
ApplicationSource: &src,
|
|
ProjectName: "default",
|
|
ProjectSourceRepos: []string{"*"},
|
|
RefSources: map[string]*v1alpha1.RefTarget{"$ref": {TargetRevision: "HEAD", Repo: *repo}},
|
|
}
|
|
err = cacheMocks.cacheutilCache.SetItem("git-refs|"+repoRemote, [][2]string{{"HEAD", revision}}, nil)
|
|
require.NoError(t, err)
|
|
_, err = service.GenerateManifest(t.Context(), &q)
|
|
require.NoError(t, err)
|
|
cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
|
|
ExternalSets: 2,
|
|
ExternalGets: 4,
|
|
})
|
|
}
|
|
|
|
// ensure we can use a semver constraint range (>= 1.0.0) and get back the correct chart (1.0.0)
|
|
func TestHelmManifestFromChartRepo(t *testing.T) {
|
|
root := t.TempDir()
|
|
service, gitMocks, mockCache := newServiceWithMocks(t, root, false)
|
|
source := &v1alpha1.ApplicationSource{Chart: "my-chart", TargetRevision: ">= 1.0.0"}
|
|
request := &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{}, ApplicationSource: source, NoCache: true, ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
response, err := service.GenerateManifest(t.Context(), request)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, response)
|
|
assert.Equal(t, &apiclient.ManifestResponse{
|
|
Manifests: []string{"{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"},
|
|
Namespace: "",
|
|
Server: "",
|
|
Revision: "1.1.0",
|
|
SourceType: "Helm",
|
|
Commands: []string{`helm template . --name-template "" --include-crds`},
|
|
}, response)
|
|
mockCache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
|
|
ExternalSets: 1,
|
|
ExternalGets: 0,
|
|
})
|
|
gitMocks.AssertNotCalled(t, "LsRemote", mock.Anything)
|
|
}
|
|
|
|
func TestHelmChartReferencingExternalValues(t *testing.T) {
|
|
service := newService(t, ".")
|
|
spec := v1alpha1.ApplicationSpec{
|
|
Sources: []v1alpha1.ApplicationSource{
|
|
{RepoURL: "https://helm.example.com", Chart: "my-chart", TargetRevision: ">= 1.0.0", Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"$ref/testdata/my-chart/my-chart-values.yaml"},
|
|
}},
|
|
{Ref: "ref", RepoURL: "https://git.example.com/test/repo"},
|
|
},
|
|
}
|
|
refSources, err := argo.GetRefSources(t.Context(), spec.Sources, spec.Project, func(_ context.Context, _ string, _ string) (*v1alpha1.Repository, error) {
|
|
return &v1alpha1.Repository{
|
|
Repo: "https://git.example.com/test/repo",
|
|
}, nil
|
|
}, []string{})
|
|
require.NoError(t, err)
|
|
request := &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true, ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
response, err := service.GenerateManifest(t.Context(), request)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, response)
|
|
assert.Equal(t, &apiclient.ManifestResponse{
|
|
Manifests: []string{"{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"},
|
|
Namespace: "",
|
|
Server: "",
|
|
Revision: "1.1.0",
|
|
SourceType: "Helm",
|
|
Commands: []string{`helm template . --name-template "" --values ./testdata/my-chart/my-chart-values.yaml --include-crds`},
|
|
}, response)
|
|
}
|
|
|
|
func TestHelmChartReferencingExternalValues_InvalidRefs(t *testing.T) {
|
|
spec := v1alpha1.ApplicationSpec{
|
|
Sources: []v1alpha1.ApplicationSource{
|
|
{RepoURL: "https://helm.example.com", Chart: "my-chart", TargetRevision: ">= 1.0.0", Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"$ref/testdata/my-chart/my-chart-values.yaml"},
|
|
}},
|
|
{RepoURL: "https://git.example.com/test/repo"},
|
|
},
|
|
}
|
|
|
|
// Empty refsource
|
|
service := newService(t, ".")
|
|
|
|
getRepository := func(_ context.Context, _ string, _ string) (*v1alpha1.Repository, error) {
|
|
return &v1alpha1.Repository{
|
|
Repo: "https://git.example.com/test/repo",
|
|
}, nil
|
|
}
|
|
|
|
refSources, err := argo.GetRefSources(t.Context(), spec.Sources, spec.Project, getRepository, []string{})
|
|
require.NoError(t, err)
|
|
|
|
request := &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true, ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
response, err := service.GenerateManifest(t.Context(), request)
|
|
require.Error(t, err)
|
|
assert.Nil(t, response)
|
|
|
|
// Invalid ref
|
|
service = newService(t, ".")
|
|
|
|
spec.Sources[1].Ref = "Invalid"
|
|
refSources, err = argo.GetRefSources(t.Context(), spec.Sources, spec.Project, getRepository, []string{})
|
|
require.NoError(t, err)
|
|
|
|
request = &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true, ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
response, err = service.GenerateManifest(t.Context(), request)
|
|
require.Error(t, err)
|
|
assert.Nil(t, response)
|
|
|
|
// Helm chart as ref (unsupported)
|
|
service = newService(t, ".")
|
|
|
|
spec.Sources[1].Ref = "ref"
|
|
spec.Sources[1].Chart = "helm-chart"
|
|
refSources, err = argo.GetRefSources(t.Context(), spec.Sources, spec.Project, getRepository, []string{})
|
|
require.NoError(t, err)
|
|
|
|
request = &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true, ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
response, err = service.GenerateManifest(t.Context(), request)
|
|
require.Error(t, err)
|
|
assert.Nil(t, response)
|
|
}
|
|
|
|
func TestHelmChartReferencingExternalValues_OutOfBounds_Symlink(t *testing.T) {
|
|
service := newService(t, ".")
|
|
err := os.Mkdir("testdata/oob-symlink", 0o755)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err = os.RemoveAll("testdata/oob-symlink")
|
|
require.NoError(t, err)
|
|
})
|
|
// Create a symlink to a file outside the repo
|
|
err = os.Symlink("../../../values.yaml", "./testdata/oob-symlink/oob-symlink.yaml")
|
|
// Create a regular file to reference from another source
|
|
err = os.WriteFile("./testdata/oob-symlink/values.yaml", []byte("foo: bar"), 0o644)
|
|
require.NoError(t, err)
|
|
spec := v1alpha1.ApplicationSpec{
|
|
Project: "default",
|
|
Sources: []v1alpha1.ApplicationSource{
|
|
{RepoURL: "https://helm.example.com", Chart: "my-chart", TargetRevision: ">= 1.0.0", Helm: &v1alpha1.ApplicationSourceHelm{
|
|
// Reference `ref` but do not use the oob symlink. The mere existence of the link should be enough to
|
|
// cause an error.
|
|
ValueFiles: []string{"$ref/testdata/oob-symlink/values.yaml"},
|
|
}},
|
|
{Ref: "ref", RepoURL: "https://git.example.com/test/repo"},
|
|
},
|
|
}
|
|
refSources, err := argo.GetRefSources(t.Context(), spec.Sources, spec.Project, func(_ context.Context, _ string, _ string) (*v1alpha1.Repository, error) {
|
|
return &v1alpha1.Repository{
|
|
Repo: "https://git.example.com/test/repo",
|
|
}, nil
|
|
}, []string{})
|
|
require.NoError(t, err)
|
|
request := &apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true}
|
|
_, err = service.GenerateManifest(t.Context(), request)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestGenerateManifestsUseExactRevision(t *testing.T) {
|
|
service, gitClient, _ := newServiceWithMocks(t, ".", false)
|
|
|
|
src := v1alpha1.ApplicationSource{Path: "./testdata/recurse", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}}
|
|
|
|
q := apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{}, ApplicationSource: &src, Revision: "abc", ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
|
|
res1, err := service.GenerateManifest(t.Context(), &q)
|
|
require.NoError(t, err)
|
|
assert.Len(t, res1.Manifests, 2)
|
|
assert.Equal(t, "abc", gitClient.Calls[0].Arguments[0])
|
|
}
|
|
|
|
func TestRecurseManifestsInDir(t *testing.T) {
|
|
service := newService(t, ".")
|
|
|
|
src := v1alpha1.ApplicationSource{Path: "./testdata/recurse", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}}
|
|
|
|
q := apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{}, ApplicationSource: &src, ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
|
|
res1, err := service.GenerateManifest(t.Context(), &q)
|
|
require.NoError(t, err)
|
|
assert.Len(t, res1.Manifests, 2)
|
|
}
|
|
|
|
func TestInvalidManifestsInDir(t *testing.T) {
|
|
service := newService(t, ".")
|
|
|
|
src := v1alpha1.ApplicationSource{Path: "./testdata/invalid-manifests", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}}
|
|
|
|
q := apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: &src}
|
|
|
|
_, err := service.GenerateManifest(t.Context(), &q)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestSkippedInvalidManifestsInDir(t *testing.T) {
|
|
service := newService(t, ".")
|
|
|
|
src := v1alpha1.ApplicationSource{Path: "./testdata/invalid-manifests-skipped", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}}
|
|
|
|
q := apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: &src}
|
|
|
|
_, err := service.GenerateManifest(t.Context(), &q)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestInvalidMetadata(t *testing.T) {
|
|
service := newService(t, ".")
|
|
|
|
src := v1alpha1.ApplicationSource{Path: "./testdata/invalid-metadata", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}}
|
|
q := apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: &src, AppLabelKey: "test", AppName: "invalid-metadata", TrackingMethod: "annotation+label"}
|
|
_, err := service.GenerateManifest(t.Context(), &q)
|
|
assert.ErrorContains(t, err, "contains non-string value in the map under key \"invalid\"")
|
|
}
|
|
|
|
func TestNilMetadataAccessors(t *testing.T) {
|
|
service := newService(t, ".")
|
|
expected := "{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{\"argocd.argoproj.io/tracking-id\":\"nil-metadata-accessors:/ConfigMap:/my-map\"},\"labels\":{\"test\":\"nil-metadata-accessors\"},\"name\":\"my-map\"},\"stringData\":{\"foo\":\"bar\"}}"
|
|
|
|
src := v1alpha1.ApplicationSource{Path: "./testdata/nil-metadata-accessors", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}}
|
|
q := apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: &src, AppLabelKey: "test", AppName: "nil-metadata-accessors", TrackingMethod: "annotation+label"}
|
|
res, err := service.GenerateManifest(t.Context(), &q)
|
|
require.NoError(t, err)
|
|
assert.Len(t, res.Manifests, 1)
|
|
assert.Equal(t, expected, res.Manifests[0])
|
|
}
|
|
|
|
func TestGenerateJsonnetManifestInDir(t *testing.T) {
|
|
service := newService(t, ".")
|
|
|
|
q := apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "./testdata/jsonnet",
|
|
Directory: &v1alpha1.ApplicationSourceDirectory{
|
|
Jsonnet: v1alpha1.ApplicationSourceJsonnet{
|
|
ExtVars: []v1alpha1.JsonnetVar{{Name: "extVarString", Value: "extVarString"}, {Name: "extVarCode", Value: "\"extVarCode\"", Code: true}},
|
|
TLAs: []v1alpha1.JsonnetVar{{Name: "tlaString", Value: "tlaString"}, {Name: "tlaCode", Value: "\"tlaCode\"", Code: true}},
|
|
Libs: []string{"testdata/jsonnet/vendor"},
|
|
},
|
|
},
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
res1, err := service.GenerateManifest(t.Context(), &q)
|
|
require.NoError(t, err)
|
|
assert.Len(t, res1.Manifests, 2)
|
|
}
|
|
|
|
func TestGenerateJsonnetManifestInRootDir(t *testing.T) {
|
|
service := newService(t, "testdata/jsonnet-1")
|
|
|
|
q := apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: ".",
|
|
Directory: &v1alpha1.ApplicationSourceDirectory{
|
|
Jsonnet: v1alpha1.ApplicationSourceJsonnet{
|
|
ExtVars: []v1alpha1.JsonnetVar{{Name: "extVarString", Value: "extVarString"}, {Name: "extVarCode", Value: "\"extVarCode\"", Code: true}},
|
|
TLAs: []v1alpha1.JsonnetVar{{Name: "tlaString", Value: "tlaString"}, {Name: "tlaCode", Value: "\"tlaCode\"", Code: true}},
|
|
Libs: []string{"."},
|
|
},
|
|
},
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
res1, err := service.GenerateManifest(t.Context(), &q)
|
|
require.NoError(t, err)
|
|
assert.Len(t, res1.Manifests, 2)
|
|
}
|
|
|
|
func TestGenerateJsonnetLibOutside(t *testing.T) {
|
|
service := newService(t, ".")
|
|
|
|
q := apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "./testdata/jsonnet",
|
|
Directory: &v1alpha1.ApplicationSourceDirectory{
|
|
Jsonnet: v1alpha1.ApplicationSourceJsonnet{
|
|
Libs: []string{"../../../testdata/jsonnet/vendor"},
|
|
},
|
|
},
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
_, err := service.GenerateManifest(t.Context(), &q)
|
|
require.ErrorContains(t, err, "file '../../../testdata/jsonnet/vendor' resolved to outside repository root")
|
|
}
|
|
|
|
func TestManifestGenErrorCacheByNumRequests(t *testing.T) {
|
|
// Returns the state of the manifest generation cache, by querying the cache for the previously set result
|
|
getRecentCachedEntry := func(service *Service, manifestRequest *apiclient.ManifestRequest) *cache.CachedManifestResponse {
|
|
assert.NotNil(t, service)
|
|
assert.NotNil(t, manifestRequest)
|
|
|
|
cachedManifestResponse := &cache.CachedManifestResponse{}
|
|
err := service.cache.GetManifests(mock.Anything, manifestRequest.ApplicationSource, manifestRequest.RefSources, manifestRequest, manifestRequest.Namespace, "", manifestRequest.AppLabelKey, manifestRequest.AppName, cachedManifestResponse, nil, "")
|
|
require.NoError(t, err)
|
|
return cachedManifestResponse
|
|
}
|
|
|
|
// Example:
|
|
// With repo server (test) parameters:
|
|
// - PauseGenerationAfterFailedGenerationAttempts: 2
|
|
// - PauseGenerationOnFailureForRequests: 4
|
|
// - TotalCacheInvocations: 10
|
|
//
|
|
// After 2 manifest generation failures in a row, the next 4 manifest generation requests should be cached,
|
|
// with the next 2 after that being uncached. Here's how it looks...
|
|
//
|
|
// request count) result
|
|
// --------------------------
|
|
// 1) Attempt to generate manifest, fails.
|
|
// 2) Second attempt to generate manifest, fails.
|
|
// 3) Return cached error attempt from #2
|
|
// 4) Return cached error attempt from #2
|
|
// 5) Return cached error attempt from #2
|
|
// 6) Return cached error attempt from #2. Max response limit hit, so reset cache entry.
|
|
// 7) Attempt to generate manifest, fails.
|
|
// 8) Attempt to generate manifest, fails.
|
|
// 9) Return cached error attempt from #8
|
|
// 10) Return cached error attempt from #8
|
|
|
|
// The same pattern PauseGenerationAfterFailedGenerationAttempts generation attempts, followed by
|
|
// PauseGenerationOnFailureForRequests cached responses, should apply for various combinations of
|
|
// both parameters.
|
|
|
|
tests := []struct {
|
|
PauseGenerationAfterFailedGenerationAttempts int
|
|
PauseGenerationOnFailureForRequests int
|
|
TotalCacheInvocations int
|
|
}{
|
|
{2, 4, 10},
|
|
{3, 5, 10},
|
|
{1, 2, 5},
|
|
}
|
|
for _, tt := range tests {
|
|
testName := fmt.Sprintf("gen-attempts-%d-pause-%d-total-%d", tt.PauseGenerationAfterFailedGenerationAttempts, tt.PauseGenerationOnFailureForRequests, tt.TotalCacheInvocations)
|
|
t.Run(testName, func(t *testing.T) {
|
|
service := newService(t, ".")
|
|
|
|
service.initConstants = RepoServerInitConstants{
|
|
ParallelismLimit: 1,
|
|
PauseGenerationAfterFailedGenerationAttempts: tt.PauseGenerationAfterFailedGenerationAttempts,
|
|
PauseGenerationOnFailureForMinutes: 0,
|
|
PauseGenerationOnFailureForRequests: tt.PauseGenerationOnFailureForRequests,
|
|
}
|
|
|
|
totalAttempts := service.initConstants.PauseGenerationAfterFailedGenerationAttempts + service.initConstants.PauseGenerationOnFailureForRequests
|
|
|
|
for invocationCount := 0; invocationCount < tt.TotalCacheInvocations; invocationCount++ {
|
|
adjustedInvocation := invocationCount % totalAttempts
|
|
|
|
fmt.Printf("%d )-------------------------------------------\n", invocationCount)
|
|
|
|
manifestRequest := &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "./testdata/invalid-helm",
|
|
},
|
|
}
|
|
|
|
res, err := service.GenerateManifest(t.Context(), manifestRequest)
|
|
|
|
// Verify invariant: res != nil xor err != nil
|
|
if err != nil {
|
|
assert.Nil(t, res, "both err and res are non-nil res: %v err: %v", res, err)
|
|
} else {
|
|
assert.NotNil(t, res, "both err and res are nil")
|
|
}
|
|
|
|
cachedManifestResponse := getRecentCachedEntry(service, manifestRequest)
|
|
|
|
isCachedError := err != nil && strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)
|
|
|
|
if adjustedInvocation < service.initConstants.PauseGenerationAfterFailedGenerationAttempts {
|
|
// GenerateManifest should not return cached errors for the first X responses, where X is the FailGenAttempts constants
|
|
require.False(t, isCachedError)
|
|
|
|
require.NotNil(t, cachedManifestResponse)
|
|
assert.Nil(t, cachedManifestResponse.ManifestResponse)
|
|
assert.NotEqual(t, 0, cachedManifestResponse.FirstFailureTimestamp)
|
|
|
|
// Internal cache consec failures value should increase with invocations, cached response should stay the same,
|
|
assert.Equal(t, cachedManifestResponse.NumberOfConsecutiveFailures, adjustedInvocation+1)
|
|
assert.Equal(t, 0, cachedManifestResponse.NumberOfCachedResponsesReturned)
|
|
} else {
|
|
// GenerateManifest SHOULD return cached errors for the next X responses, where X is the
|
|
// PauseGenerationOnFailureForRequests constant
|
|
assert.True(t, isCachedError)
|
|
require.NotNil(t, cachedManifestResponse)
|
|
assert.Nil(t, cachedManifestResponse.ManifestResponse)
|
|
assert.NotEqual(t, 0, cachedManifestResponse.FirstFailureTimestamp)
|
|
|
|
// Internal cache values should update correctly based on number of return cache entries, consecutive failures should stay the same
|
|
assert.Equal(t, cachedManifestResponse.NumberOfConsecutiveFailures, service.initConstants.PauseGenerationAfterFailedGenerationAttempts)
|
|
assert.Equal(t, cachedManifestResponse.NumberOfCachedResponsesReturned, (adjustedInvocation - service.initConstants.PauseGenerationAfterFailedGenerationAttempts + 1))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestManifestGenErrorCacheFileContentsChange(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
service := newService(t, tmpDir)
|
|
|
|
service.initConstants = RepoServerInitConstants{
|
|
ParallelismLimit: 1,
|
|
PauseGenerationAfterFailedGenerationAttempts: 2,
|
|
PauseGenerationOnFailureForMinutes: 0,
|
|
PauseGenerationOnFailureForRequests: 4,
|
|
}
|
|
|
|
for step := range 3 {
|
|
// step 1) Attempt to generate manifests against invalid helm chart (should return uncached error)
|
|
// step 2) Attempt to generate manifest against valid helm chart (should succeed and return valid response)
|
|
// step 3) Attempt to generate manifest against invalid helm chart (should return cached value from step 2)
|
|
|
|
errorExpected := step%2 == 0
|
|
|
|
// Ensure that the target directory will succeed or fail, so we can verify the cache correctly handles it
|
|
err := os.RemoveAll(tmpDir)
|
|
require.NoError(t, err)
|
|
err = os.MkdirAll(tmpDir, 0o777)
|
|
require.NoError(t, err)
|
|
if errorExpected {
|
|
// Copy invalid helm chart into temporary directory, ensuring manifest generation will fail
|
|
err = fileutil.CopyDir("./testdata/invalid-helm", tmpDir)
|
|
require.NoError(t, err)
|
|
} else {
|
|
// Copy valid helm chart into temporary directory, ensuring generation will succeed
|
|
err = fileutil.CopyDir("./testdata/my-chart", tmpDir)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: ".",
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
|
|
fmt.Println("-", step, "-", res != nil, err != nil, errorExpected)
|
|
fmt.Println(" err: ", err)
|
|
fmt.Println(" res: ", res)
|
|
|
|
if step < 2 {
|
|
if errorExpected {
|
|
require.Error(t, err, "error return value and error expected did not match")
|
|
assert.Nil(t, res, "GenerateManifest return value and expected value did not match")
|
|
} else {
|
|
require.NoError(t, err, "error return value and error expected did not match")
|
|
assert.NotNil(t, res, "GenerateManifest return value and expected value did not match")
|
|
}
|
|
}
|
|
|
|
if step == 2 {
|
|
require.NoError(t, err, "error ret val was non-nil on step 3")
|
|
assert.NotNil(t, res, "GenerateManifest ret val was nil on step 3")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestManifestGenErrorCacheByMinutesElapsed(t *testing.T) {
|
|
tests := []struct {
|
|
// Test with a range of pause expiration thresholds
|
|
PauseGenerationOnFailureForMinutes int
|
|
}{
|
|
{1}, {2}, {10}, {24 * 60},
|
|
}
|
|
for _, tt := range tests {
|
|
testName := fmt.Sprintf("pause-time-%d", tt.PauseGenerationOnFailureForMinutes)
|
|
t.Run(testName, func(t *testing.T) {
|
|
service := newService(t, ".")
|
|
|
|
// Here we simulate the passage of time by overriding the now() function of Service
|
|
currentTime := time.Now()
|
|
service.now = func() time.Time {
|
|
return currentTime
|
|
}
|
|
|
|
service.initConstants = RepoServerInitConstants{
|
|
ParallelismLimit: 1,
|
|
PauseGenerationAfterFailedGenerationAttempts: 1,
|
|
PauseGenerationOnFailureForMinutes: tt.PauseGenerationOnFailureForMinutes,
|
|
PauseGenerationOnFailureForRequests: 0,
|
|
}
|
|
|
|
// 1) Put the cache into the failure state
|
|
for x := range 2 {
|
|
res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "./testdata/invalid-helm",
|
|
},
|
|
})
|
|
|
|
assert.True(t, err != nil && res == nil)
|
|
|
|
// Ensure that the second invocation triggers the cached error state
|
|
if x == 1 {
|
|
assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix))
|
|
}
|
|
}
|
|
|
|
// 2) Jump forward X-1 minutes in time, where X is the expiration boundary
|
|
currentTime = currentTime.Add(time.Duration(tt.PauseGenerationOnFailureForMinutes-1) * time.Minute)
|
|
res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "./testdata/invalid-helm",
|
|
},
|
|
})
|
|
|
|
// 3) Ensure that the cache still returns a cached copy of the last error
|
|
assert.True(t, err != nil && res == nil)
|
|
assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix))
|
|
|
|
// 4) Jump forward 2 minutes in time, such that the pause generation time has elapsed and we should return to normal state
|
|
currentTime = currentTime.Add(2 * time.Minute)
|
|
|
|
res, err = service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "./testdata/invalid-helm",
|
|
},
|
|
})
|
|
|
|
// 5) Ensure that the service no longer returns a cached copy of the last error
|
|
assert.True(t, err != nil && res == nil)
|
|
assert.False(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestManifestGenErrorCacheRespectsNoCache(t *testing.T) {
|
|
service := newService(t, ".")
|
|
|
|
service.initConstants = RepoServerInitConstants{
|
|
ParallelismLimit: 1,
|
|
PauseGenerationAfterFailedGenerationAttempts: 1,
|
|
PauseGenerationOnFailureForMinutes: 0,
|
|
PauseGenerationOnFailureForRequests: 4,
|
|
}
|
|
|
|
// 1) Put the cache into the failure state
|
|
for x := range 2 {
|
|
res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "./testdata/invalid-helm",
|
|
},
|
|
})
|
|
|
|
assert.True(t, err != nil && res == nil)
|
|
|
|
// Ensure that the second invocation is cached
|
|
if x == 1 {
|
|
assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix))
|
|
}
|
|
}
|
|
|
|
// 2) Call generateManifest with NoCache enabled
|
|
res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "./testdata/invalid-helm",
|
|
},
|
|
NoCache: true,
|
|
})
|
|
|
|
// 3) Ensure that the cache returns a new generation attempt, rather than a previous cached error
|
|
assert.True(t, err != nil && res == nil)
|
|
assert.False(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix))
|
|
|
|
// 4) Call generateManifest
|
|
res, err = service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "./testdata/invalid-helm",
|
|
},
|
|
})
|
|
|
|
// 5) Ensure that the subsequent invocation, after nocache, is cached
|
|
assert.True(t, err != nil && res == nil)
|
|
assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix))
|
|
}
|
|
|
|
func TestGenerateHelmKubeVersion(t *testing.T) {
|
|
service := newService(t, "../../util/helm/testdata/redis")
|
|
|
|
res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: ".",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
KubeVersion: "1.30.11+IKS",
|
|
},
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, res.Commands, 1)
|
|
assert.Contains(t, res.Commands[0], "--kube-version 1.30.11")
|
|
}
|
|
|
|
func TestGenerateHelmWithValues(t *testing.T) {
|
|
service := newService(t, "../../util/helm/testdata/redis")
|
|
|
|
res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: ".",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"values-production.yaml"},
|
|
ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
|
|
},
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
replicasVerified := false
|
|
for _, src := range res.Manifests {
|
|
obj := unstructured.Unstructured{}
|
|
err = json.Unmarshal([]byte(src), &obj)
|
|
require.NoError(t, err)
|
|
|
|
if obj.GetKind() == "Deployment" && obj.GetName() == "test-redis-slave" {
|
|
var dep appsv1.Deployment
|
|
err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &dep)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int32(2), *dep.Spec.Replicas)
|
|
replicasVerified = true
|
|
}
|
|
}
|
|
assert.True(t, replicasVerified)
|
|
}
|
|
|
|
func TestHelmWithMissingValueFiles(t *testing.T) {
|
|
service := newService(t, "../../util/helm/testdata/redis")
|
|
missingValuesFile := "values-prod-overrides.yaml"
|
|
|
|
req := &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: ".",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"values-production.yaml", missingValuesFile},
|
|
},
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
|
|
// Should fail since we're passing a non-existent values file, and error should indicate that
|
|
_, err := service.GenerateManifest(t.Context(), req)
|
|
require.ErrorContains(t, err, missingValuesFile+": no such file or directory")
|
|
|
|
// Should template without error even if defining a non-existent values file
|
|
req.ApplicationSource.Helm.IgnoreMissingValueFiles = true
|
|
_, err = service.GenerateManifest(t.Context(), req)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestGenerateHelmWithEnvVars(t *testing.T) {
|
|
service := newService(t, "../../util/helm/testdata/redis")
|
|
|
|
res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "production",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: ".",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"values-$ARGOCD_APP_NAME.yaml"},
|
|
},
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
replicasVerified := false
|
|
for _, src := range res.Manifests {
|
|
obj := unstructured.Unstructured{}
|
|
err = json.Unmarshal([]byte(src), &obj)
|
|
require.NoError(t, err)
|
|
|
|
if obj.GetKind() == "Deployment" && obj.GetName() == "production-redis-slave" {
|
|
var dep appsv1.Deployment
|
|
err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &dep)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int32(3), *dep.Spec.Replicas)
|
|
replicasVerified = true
|
|
}
|
|
}
|
|
assert.True(t, replicasVerified)
|
|
}
|
|
|
|
// The requested value file (`../minio/values.yaml`) is outside the app path (`./util/helm/testdata/redis`), however
|
|
// since the requested value is still under the repo directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed
|
|
func TestGenerateHelmWithValuesDirectoryTraversal(t *testing.T) {
|
|
service := newService(t, "../../util/helm/testdata")
|
|
_, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "./redis",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"../minio/values.yaml"},
|
|
ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
|
|
},
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Test the case where the path is "."
|
|
service = newService(t, "./testdata")
|
|
_, err = service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "./my-chart",
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestChartRepoWithOutOfBoundsSymlink(t *testing.T) {
|
|
service := newService(t, ".")
|
|
source := &v1alpha1.ApplicationSource{Chart: "out-of-bounds-chart", TargetRevision: ">= 1.0.0"}
|
|
request := &apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: source, NoCache: true}
|
|
_, err := service.GenerateManifest(t.Context(), request)
|
|
assert.ErrorContains(t, err, "chart contains out-of-bounds symlinks")
|
|
}
|
|
|
|
// This is a Helm first-class app with a values file inside the repo directory
|
|
// (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is allowed
|
|
func TestHelmManifestFromChartRepoWithValueFile(t *testing.T) {
|
|
service := newService(t, ".")
|
|
source := &v1alpha1.ApplicationSource{
|
|
Chart: "my-chart",
|
|
TargetRevision: ">= 1.0.0",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"./my-chart-values.yaml"},
|
|
},
|
|
}
|
|
request := &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: source,
|
|
NoCache: true,
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
response, err := service.GenerateManifest(t.Context(), request)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, response)
|
|
assert.Equal(t, &apiclient.ManifestResponse{
|
|
Manifests: []string{"{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"},
|
|
Namespace: "",
|
|
Server: "",
|
|
Revision: "1.1.0",
|
|
SourceType: "Helm",
|
|
Commands: []string{`helm template . --name-template "" --values ./testdata/my-chart/my-chart-values.yaml --include-crds`},
|
|
}, response)
|
|
}
|
|
|
|
// This is a Helm first-class app with a values file outside the repo directory
|
|
// (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is not allowed
|
|
func TestHelmManifestFromChartRepoWithValueFileOutsideRepo(t *testing.T) {
|
|
service := newService(t, ".")
|
|
source := &v1alpha1.ApplicationSource{
|
|
Chart: "my-chart",
|
|
TargetRevision: ">= 1.0.0",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"../my-chart-2/my-chart-2-values.yaml"},
|
|
},
|
|
}
|
|
request := &apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: source, NoCache: true}
|
|
_, err := service.GenerateManifest(t.Context(), request)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestHelmManifestFromChartRepoWithValueFileLinks(t *testing.T) {
|
|
t.Run("Valid symlink", func(t *testing.T) {
|
|
service := newService(t, ".")
|
|
source := &v1alpha1.ApplicationSource{
|
|
Chart: "my-chart",
|
|
TargetRevision: ">= 1.0.0",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"my-chart-link.yaml"},
|
|
},
|
|
}
|
|
request := &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{}, ApplicationSource: source, NoCache: true, ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
_, err := service.GenerateManifest(t.Context(), request)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestGenerateHelmWithURL(t *testing.T) {
|
|
service := newService(t, "../../util/helm/testdata/redis")
|
|
|
|
_, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: ".",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"https://raw.githubusercontent.com/argoproj/argocd-example-apps/master/helm-guestbook/values.yaml"},
|
|
ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
|
|
},
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
HelmOptions: &v1alpha1.HelmOptions{ValuesFileSchemes: []string{"https"}},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// The requested value file (`../minio/values.yaml`) is outside the repo directory
|
|
// (`~/go/src/github.com/argoproj/argo-cd/util/helm/testdata/redis`), so it is blocked
|
|
func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
|
|
t.Run("Values file with relative path pointing outside repo root", func(t *testing.T) {
|
|
service := newService(t, "../../util/helm/testdata/redis")
|
|
_, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: ".",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"../minio/values.yaml"},
|
|
ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
|
|
},
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
assert.ErrorContains(t, err, "outside repository root")
|
|
})
|
|
|
|
t.Run("Values file with relative path pointing inside repo root", func(t *testing.T) {
|
|
service := newService(t, "./testdata")
|
|
_, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "./my-chart",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"../my-chart/my-chart-values.yaml"},
|
|
ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
|
|
},
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("Values file with absolute path stays within repo root", func(t *testing.T) {
|
|
service := newService(t, "./testdata")
|
|
_, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "./my-chart",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"/my-chart/my-chart-values.yaml"},
|
|
ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
|
|
},
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("Values file with absolute path using back-references outside repo root", func(t *testing.T) {
|
|
service := newService(t, "./testdata")
|
|
_, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "./my-chart",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"/../../../my-chart-values.yaml"},
|
|
ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
|
|
},
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
assert.ErrorContains(t, err, "outside repository root")
|
|
})
|
|
|
|
t.Run("Remote values file from forbidden protocol", func(t *testing.T) {
|
|
service := newService(t, "./testdata")
|
|
_, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "./my-chart",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"file://../../../../my-chart-values.yaml"},
|
|
ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
|
|
},
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
assert.ErrorContains(t, err, "is not allowed")
|
|
})
|
|
|
|
t.Run("Remote values file from custom allowed protocol", func(t *testing.T) {
|
|
service := newService(t, "./testdata")
|
|
_, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "./my-chart",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"s3://my-bucket/my-chart-values.yaml"},
|
|
},
|
|
},
|
|
HelmOptions: &v1alpha1.HelmOptions{ValuesFileSchemes: []string{"s3"}},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
assert.ErrorContains(t, err, "s3://my-bucket/my-chart-values.yaml: no such file or directory")
|
|
})
|
|
}
|
|
|
|
// File parameter should not allow traversal outside of the repository root
|
|
func TestGenerateHelmWithAbsoluteFileParameter(t *testing.T) {
|
|
service := newService(t, "../..")
|
|
|
|
file, err := os.CreateTemp(t.TempDir(), "external-secret.txt")
|
|
require.NoError(t, err)
|
|
externalSecretPath := file.Name()
|
|
defer func() { _ = os.RemoveAll(externalSecretPath) }()
|
|
expectedFileContent, err := os.ReadFile("../../util/helm/testdata/external/external-secret.txt")
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(externalSecretPath, expectedFileContent, 0o644)
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
if err = file.Close(); err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
|
|
_, err = service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "./util/helm/testdata/redis",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"values-production.yaml"},
|
|
ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
|
|
FileParameters: []v1alpha1.HelmFileParameter{{
|
|
Name: "passwordContent",
|
|
Path: externalSecretPath,
|
|
}},
|
|
},
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// The requested file parameter (`../external/external-secret.txt`) is outside the app path
|
|
// (`./util/helm/testdata/redis`), however since the requested value is still under the repo
|
|
// directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed. It is used as a means of
|
|
// providing direct content to a helm chart via a specific key.
|
|
func TestGenerateHelmWithFileParameter(t *testing.T) {
|
|
service := newService(t, "../../util/helm/testdata")
|
|
|
|
res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
AppName: "test",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "./redis",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"values-production.yaml"},
|
|
Values: `cluster: {slaveCount: 10}`,
|
|
ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
|
|
FileParameters: []v1alpha1.HelmFileParameter{{
|
|
Name: "passwordContent",
|
|
Path: "../external/external-secret.txt",
|
|
}},
|
|
},
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Contains(t, res.Manifests[6], `"replicas":2`, "ValuesObject should override Values")
|
|
}
|
|
|
|
func TestGenerateNullList(t *testing.T) {
|
|
service := newService(t, ".")
|
|
|
|
t.Run("null list", func(t *testing.T) {
|
|
res1, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{Path: "./testdata/null-list"},
|
|
NoCache: true,
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Len(t, res1.Manifests, 1)
|
|
assert.Contains(t, res1.Manifests[0], "prometheus-operator-operator")
|
|
})
|
|
|
|
t.Run("empty list", func(t *testing.T) {
|
|
res1, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{Path: "./testdata/empty-list"},
|
|
NoCache: true,
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Len(t, res1.Manifests, 1)
|
|
assert.Contains(t, res1.Manifests[0], "prometheus-operator-operator")
|
|
})
|
|
|
|
t.Run("weird list", func(t *testing.T) {
|
|
res1, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{Path: "./testdata/weird-list"},
|
|
NoCache: true,
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Len(t, res1.Manifests, 2)
|
|
})
|
|
}
|
|
|
|
func TestIdentifyAppSourceTypeByAppDirWithKustomizations(t *testing.T) {
|
|
sourceType, err := GetAppSourceType(t.Context(), &v1alpha1.ApplicationSource{}, "./testdata/kustomization_yaml", "./testdata", "testapp", map[string]bool{}, []string{}, []string{})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, v1alpha1.ApplicationSourceTypeKustomize, sourceType)
|
|
|
|
sourceType, err = GetAppSourceType(t.Context(), &v1alpha1.ApplicationSource{}, "./testdata/kustomization_yml", "./testdata", "testapp", map[string]bool{}, []string{}, []string{})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, v1alpha1.ApplicationSourceTypeKustomize, sourceType)
|
|
|
|
sourceType, err = GetAppSourceType(t.Context(), &v1alpha1.ApplicationSource{}, "./testdata/Kustomization", "./testdata", "testapp", map[string]bool{}, []string{}, []string{})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, v1alpha1.ApplicationSourceTypeKustomize, sourceType)
|
|
}
|
|
|
|
func TestGenerateFromUTF16(t *testing.T) {
|
|
q := apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
res1, err := GenerateManifests(t.Context(), "./testdata/utf-16", "/", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil)
|
|
require.NoError(t, err)
|
|
assert.Len(t, res1.Manifests, 2)
|
|
}
|
|
|
|
func TestListApps(t *testing.T) {
|
|
service := newService(t, "./testdata")
|
|
|
|
res, err := service.ListApps(t.Context(), &apiclient.ListAppsRequest{Repo: &v1alpha1.Repository{}})
|
|
require.NoError(t, err)
|
|
|
|
expectedApps := map[string]string{
|
|
"Kustomization": "Kustomize",
|
|
"app-parameters/multi": "Kustomize",
|
|
"app-parameters/single-app-only": "Kustomize",
|
|
"app-parameters/single-global": "Kustomize",
|
|
"app-parameters/single-global-helm": "Helm",
|
|
"in-bounds-values-file-link": "Helm",
|
|
"invalid-helm": "Helm",
|
|
"invalid-kustomize": "Kustomize",
|
|
"kustomization_yaml": "Kustomize",
|
|
"kustomization_yml": "Kustomize",
|
|
"my-chart": "Helm",
|
|
"my-chart-2": "Helm",
|
|
"oci-dependencies": "Helm",
|
|
"out-of-bounds-values-file-link": "Helm",
|
|
"values-files": "Helm",
|
|
"helm-with-dependencies": "Helm",
|
|
"helm-with-dependencies-alias": "Helm",
|
|
"helm-with-local-dependency": "Helm",
|
|
"simple-chart": "Helm",
|
|
"broken-schema-verification": "Helm",
|
|
}
|
|
assert.Equal(t, expectedApps, res.Apps)
|
|
}
|
|
|
|
func TestGetAppDetailsHelm(t *testing.T) {
|
|
service := newService(t, "../../util/helm/testdata/dependency")
|
|
|
|
res, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
|
|
Repo: &v1alpha1.Repository{},
|
|
Source: &v1alpha1.ApplicationSource{
|
|
Path: ".",
|
|
},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, res.Helm)
|
|
|
|
assert.Equal(t, "Helm", res.Type)
|
|
assert.Equal(t, []string{"values-production.yaml", "values.yaml"}, res.Helm.ValueFiles)
|
|
}
|
|
|
|
func TestGetAppDetailsHelmUsesCache(t *testing.T) {
|
|
service := newService(t, "../../util/helm/testdata/dependency")
|
|
|
|
res, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
|
|
Repo: &v1alpha1.Repository{},
|
|
Source: &v1alpha1.ApplicationSource{
|
|
Path: ".",
|
|
},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, res.Helm)
|
|
|
|
assert.Equal(t, "Helm", res.Type)
|
|
assert.Equal(t, []string{"values-production.yaml", "values.yaml"}, res.Helm.ValueFiles)
|
|
}
|
|
|
|
func TestGetAppDetailsHelm_WithNoValuesFile(t *testing.T) {
|
|
service := newService(t, "../../util/helm/testdata/api-versions")
|
|
|
|
res, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
|
|
Repo: &v1alpha1.Repository{},
|
|
Source: &v1alpha1.ApplicationSource{
|
|
Path: ".",
|
|
},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, res.Helm)
|
|
|
|
assert.Equal(t, "Helm", res.Type)
|
|
assert.Empty(t, res.Helm.ValueFiles)
|
|
assert.Empty(t, res.Helm.Values)
|
|
}
|
|
|
|
func TestGetAppDetailsKustomize(t *testing.T) {
|
|
service := newService(t, "../../util/kustomize/testdata/kustomization_yaml")
|
|
|
|
res, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
|
|
Repo: &v1alpha1.Repository{},
|
|
Source: &v1alpha1.ApplicationSource{
|
|
Path: ".",
|
|
},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "Kustomize", res.Type)
|
|
assert.NotNil(t, res.Kustomize)
|
|
assert.Equal(t, []string{"nginx:1.15.4", "registry.k8s.io/nginx-slim:0.8"}, res.Kustomize.Images)
|
|
}
|
|
|
|
func TestGetAppDetailsKustomize_CustomVersion(t *testing.T) {
|
|
service := newService(t, "../../util/kustomize/testdata/kustomize-with-version-override")
|
|
|
|
q := &apiclient.RepoServerAppDetailsQuery{
|
|
Repo: &v1alpha1.Repository{},
|
|
Source: &v1alpha1.ApplicationSource{
|
|
Path: ".",
|
|
},
|
|
KustomizeOptions: &v1alpha1.KustomizeOptions{},
|
|
}
|
|
|
|
_, err := service.GetAppDetails(t.Context(), q)
|
|
require.ErrorAs(t, err, &settings.KustomizeVersionNotRegisteredError{Version: "v1.2.3"})
|
|
|
|
q.KustomizeOptions.Versions = []v1alpha1.KustomizeVersion{
|
|
{
|
|
Name: "v1.2.3",
|
|
Path: "kustomize",
|
|
},
|
|
}
|
|
|
|
res, err := service.GetAppDetails(t.Context(), q)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Kustomize", res.Type)
|
|
}
|
|
|
|
func TestGetHelmCharts(t *testing.T) {
|
|
service := newService(t, "../..")
|
|
res, err := service.GetHelmCharts(t.Context(), &apiclient.HelmChartsRequest{Repo: &v1alpha1.Repository{}})
|
|
|
|
// fix flakiness
|
|
sort.Slice(res.Items, func(i, j int) bool {
|
|
return res.Items[i].Name < res.Items[j].Name
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, res.Items, 2)
|
|
|
|
item := res.Items[0]
|
|
assert.Equal(t, "my-chart", item.Name)
|
|
assert.Equal(t, []string{"1.0.0", "1.1.0"}, item.Versions)
|
|
|
|
item2 := res.Items[1]
|
|
assert.Equal(t, "out-of-bounds-chart", item2.Name)
|
|
assert.Equal(t, []string{"1.0.0", "1.1.0"}, item2.Versions)
|
|
}
|
|
|
|
func TestGetRevisionMetadata(t *testing.T) {
|
|
service, gitClient, _ := newServiceWithMocks(t, "../..", false)
|
|
now := time.Now()
|
|
|
|
gitClient.EXPECT().RevisionMetadata(mock.Anything).Return(&git.RevisionMetadata{
|
|
Message: "test",
|
|
Author: "author",
|
|
Date: now,
|
|
Tags: []string{"tag1", "tag2"},
|
|
References: []git.RevisionReference{
|
|
{
|
|
Commit: &git.CommitMetadata{
|
|
Author: mail.Address{
|
|
Name: "test-name",
|
|
Address: "test-email@example.com",
|
|
},
|
|
Date: now.Format(time.RFC3339),
|
|
Subject: "test-subject",
|
|
SHA: "test-sha",
|
|
RepoURL: "test-repo-url",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
|
|
res, err := service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
Revision: "c0b400fc458875d925171398f9ba9eabd5529923",
|
|
CheckSignature: true,
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "test", res.Message)
|
|
assert.Equal(t, now, res.Date.Time)
|
|
assert.Equal(t, "author", res.Author)
|
|
assert.Equal(t, []string{"tag1", "tag2"}, res.Tags)
|
|
assert.NotEmpty(t, res.SignatureInfo)
|
|
require.Len(t, res.References, 1)
|
|
require.NotNil(t, res.References[0].Commit)
|
|
assert.Equal(t, "test-sha", res.References[0].Commit.SHA)
|
|
|
|
// Check for truncated revision value
|
|
res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
Revision: "c0b400f",
|
|
CheckSignature: true,
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "test", res.Message)
|
|
assert.Equal(t, now, res.Date.Time)
|
|
assert.Equal(t, "author", res.Author)
|
|
assert.Equal(t, []string{"tag1", "tag2"}, res.Tags)
|
|
assert.NotEmpty(t, res.SignatureInfo)
|
|
|
|
// Cache hit - signature info should not be in result
|
|
res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
Revision: "c0b400fc458875d925171398f9ba9eabd5529923",
|
|
CheckSignature: false,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, res.SignatureInfo)
|
|
|
|
// Enforce cache miss - signature info should not be in result
|
|
res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
Revision: "da52afd3b2df1ec49470603d8bbb46954dab1091",
|
|
CheckSignature: false,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, res.SignatureInfo)
|
|
|
|
// Cache hit on previous entry that did not have signature info
|
|
res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
Revision: "da52afd3b2df1ec49470603d8bbb46954dab1091",
|
|
CheckSignature: true,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, res.SignatureInfo)
|
|
}
|
|
|
|
func TestGetSignatureVerificationResult(t *testing.T) {
|
|
// Commit with signature and verification requested
|
|
{
|
|
service := newServiceWithSignature(t, "../../manifests/base")
|
|
|
|
src := v1alpha1.ApplicationSource{Path: "."}
|
|
q := apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &src,
|
|
VerifySignature: true,
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
|
|
res, err := service.GenerateManifest(t.Context(), &q)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, testSignature, res.VerifyResult)
|
|
}
|
|
// Commit with signature and verification not requested
|
|
{
|
|
service := newServiceWithSignature(t, "../../manifests/base")
|
|
|
|
src := v1alpha1.ApplicationSource{Path: "."}
|
|
q := apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{}, ApplicationSource: &src, ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
|
|
res, err := service.GenerateManifest(t.Context(), &q)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, res.VerifyResult)
|
|
}
|
|
// Commit without signature and verification requested
|
|
{
|
|
service := newService(t, "../../manifests/base")
|
|
|
|
src := v1alpha1.ApplicationSource{Path: "."}
|
|
q := apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
|
|
res, err := service.GenerateManifest(t.Context(), &q)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, res.VerifyResult)
|
|
}
|
|
// Commit without signature and verification not requested
|
|
{
|
|
service := newService(t, "../../manifests/base")
|
|
|
|
src := v1alpha1.ApplicationSource{Path: "."}
|
|
q := apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
|
|
res, err := service.GenerateManifest(t.Context(), &q)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, res.VerifyResult)
|
|
}
|
|
}
|
|
|
|
func Test_newEnv(t *testing.T) {
|
|
assert.Equal(t, &v1alpha1.Env{
|
|
&v1alpha1.EnvEntry{Name: "ARGOCD_APP_NAME", Value: "my-app-name"},
|
|
&v1alpha1.EnvEntry{Name: "ARGOCD_APP_NAMESPACE", Value: "my-namespace"},
|
|
&v1alpha1.EnvEntry{Name: "ARGOCD_APP_PROJECT_NAME", Value: "my-project-name"},
|
|
&v1alpha1.EnvEntry{Name: "ARGOCD_APP_REVISION", Value: "my-revision"},
|
|
&v1alpha1.EnvEntry{Name: "ARGOCD_APP_REVISION_SHORT", Value: "my-revi"},
|
|
&v1alpha1.EnvEntry{Name: "ARGOCD_APP_REVISION_SHORT_8", Value: "my-revis"},
|
|
&v1alpha1.EnvEntry{Name: "ARGOCD_APP_SOURCE_REPO_URL", Value: "https://github.com/my-org/my-repo"},
|
|
&v1alpha1.EnvEntry{Name: "ARGOCD_APP_SOURCE_PATH", Value: "my-path"},
|
|
&v1alpha1.EnvEntry{Name: "ARGOCD_APP_SOURCE_TARGET_REVISION", Value: "my-target-revision"},
|
|
}, newEnv(&apiclient.ManifestRequest{
|
|
AppName: "my-app-name",
|
|
Namespace: "my-namespace",
|
|
ProjectName: "my-project-name",
|
|
Repo: &v1alpha1.Repository{Repo: "https://github.com/my-org/my-repo"},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "my-path",
|
|
TargetRevision: "my-target-revision",
|
|
},
|
|
}, "my-revision"))
|
|
}
|
|
|
|
func TestService_newHelmClientResolveRevision(t *testing.T) {
|
|
service := newService(t, ".")
|
|
|
|
t.Run("EmptyRevision", func(t *testing.T) {
|
|
_, _, err := service.newHelmClientResolveRevision(&v1alpha1.Repository{}, "", "my-chart", true)
|
|
assert.EqualError(t, err, "invalid revision: failed to determine semver constraint: improper constraint: ")
|
|
})
|
|
t.Run("InvalidRevision", func(t *testing.T) {
|
|
_, _, err := service.newHelmClientResolveRevision(&v1alpha1.Repository{}, "???", "my-chart", true)
|
|
assert.EqualError(t, err, "invalid revision: failed to determine semver constraint: improper constraint: ???")
|
|
})
|
|
}
|
|
|
|
func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
|
|
t.Run("No app name set and app specific file exists", func(t *testing.T) {
|
|
service := newService(t, ".")
|
|
runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
|
|
t.Helper()
|
|
details, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
|
|
Repo: &v1alpha1.Repository{},
|
|
Source: &v1alpha1.ApplicationSource{
|
|
Path: path,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"quay.io/argoprojlabs/argocd-e2e-container:0.2"}, details.Kustomize.Images)
|
|
})
|
|
})
|
|
t.Run("No app specific override", func(t *testing.T) {
|
|
service := newService(t, ".")
|
|
runWithTempTestdata(t, "single-global", func(t *testing.T, path string) {
|
|
t.Helper()
|
|
details, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
|
|
Repo: &v1alpha1.Repository{},
|
|
Source: &v1alpha1.ApplicationSource{
|
|
Path: path,
|
|
},
|
|
AppName: "testapp",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"quay.io/argoprojlabs/argocd-e2e-container:0.2"}, details.Kustomize.Images)
|
|
})
|
|
})
|
|
t.Run("Only app specific override", func(t *testing.T) {
|
|
service := newService(t, ".")
|
|
runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
|
|
t.Helper()
|
|
details, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
|
|
Repo: &v1alpha1.Repository{},
|
|
Source: &v1alpha1.ApplicationSource{
|
|
Path: path,
|
|
},
|
|
AppName: "testapp",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"quay.io/argoprojlabs/argocd-e2e-container:0.3"}, details.Kustomize.Images)
|
|
})
|
|
})
|
|
t.Run("App specific override", func(t *testing.T) {
|
|
service := newService(t, ".")
|
|
runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
|
|
t.Helper()
|
|
details, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
|
|
Repo: &v1alpha1.Repository{},
|
|
Source: &v1alpha1.ApplicationSource{
|
|
Path: path,
|
|
},
|
|
AppName: "testapp",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"quay.io/argoprojlabs/argocd-e2e-container:0.3"}, details.Kustomize.Images)
|
|
})
|
|
})
|
|
t.Run("App specific overrides containing non-mergeable field", func(t *testing.T) {
|
|
service := newService(t, ".")
|
|
runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
|
|
t.Helper()
|
|
details, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
|
|
Repo: &v1alpha1.Repository{},
|
|
Source: &v1alpha1.ApplicationSource{
|
|
Path: path,
|
|
},
|
|
AppName: "unmergeable",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"quay.io/argoprojlabs/argocd-e2e-container:0.3"}, details.Kustomize.Images)
|
|
})
|
|
})
|
|
t.Run("Broken app-specific overrides", func(t *testing.T) {
|
|
service := newService(t, ".")
|
|
runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
|
|
t.Helper()
|
|
_, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
|
|
Repo: &v1alpha1.Repository{},
|
|
Source: &v1alpha1.ApplicationSource{
|
|
Path: path,
|
|
},
|
|
AppName: "broken",
|
|
})
|
|
require.Error(t, err)
|
|
})
|
|
})
|
|
}
|
|
|
|
// There are unit test that will use kustomize set and by that modify the
|
|
// kustomization.yaml. For proper testing, we need to copy the testdata to a
|
|
// temporary path, run the tests, and then throw the copy away again.
|
|
func mkTempParameters(ctx context.Context, source string) string {
|
|
tempDir, err := os.MkdirTemp("./testdata", "app-parameters")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
cmd := exec.CommandContext(ctx, "cp", "-R", source, tempDir)
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
os.RemoveAll(tempDir)
|
|
panic(err)
|
|
}
|
|
return tempDir
|
|
}
|
|
|
|
// Simple wrapper run a test with a temporary copy of the testdata, because
|
|
// the test would modify the data when run.
|
|
func runWithTempTestdata(t *testing.T, path string, runner func(t *testing.T, path string)) {
|
|
t.Helper()
|
|
tempDir := mkTempParameters(t.Context(), "./testdata/app-parameters")
|
|
runner(t, filepath.Join(tempDir, "app-parameters", path))
|
|
os.RemoveAll(tempDir)
|
|
}
|
|
|
|
func TestGenerateManifestsWithAppParameterFile(t *testing.T) {
|
|
t.Run("Single global override", func(t *testing.T) {
|
|
runWithTempTestdata(t, "single-global", func(t *testing.T, path string) {
|
|
t.Helper()
|
|
service := newService(t, ".")
|
|
manifests, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: path,
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
require.NoError(t, err)
|
|
resourceByKindName := make(map[string]*unstructured.Unstructured)
|
|
for _, manifest := range manifests.Manifests {
|
|
var un unstructured.Unstructured
|
|
err := yaml.Unmarshal([]byte(manifest), &un)
|
|
require.NoError(t, err)
|
|
resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un
|
|
}
|
|
deployment, ok := resourceByKindName["Deployment/guestbook-ui"]
|
|
require.True(t, ok)
|
|
containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
|
|
require.True(t, ok)
|
|
image, ok, _ := unstructured.NestedString(containers[0].(map[string]any), "image")
|
|
require.True(t, ok)
|
|
assert.Equal(t, "quay.io/argoprojlabs/argocd-e2e-container:0.2", image)
|
|
})
|
|
})
|
|
|
|
t.Run("Single global override Helm", func(t *testing.T) {
|
|
runWithTempTestdata(t, "single-global-helm", func(t *testing.T, path string) {
|
|
t.Helper()
|
|
service := newService(t, ".")
|
|
manifests, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: path,
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
require.NoError(t, err)
|
|
resourceByKindName := make(map[string]*unstructured.Unstructured)
|
|
for _, manifest := range manifests.Manifests {
|
|
var un unstructured.Unstructured
|
|
err := yaml.Unmarshal([]byte(manifest), &un)
|
|
require.NoError(t, err)
|
|
resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un
|
|
}
|
|
deployment, ok := resourceByKindName["Deployment/guestbook-ui"]
|
|
require.True(t, ok)
|
|
containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
|
|
require.True(t, ok)
|
|
image, ok, _ := unstructured.NestedString(containers[0].(map[string]any), "image")
|
|
require.True(t, ok)
|
|
assert.Equal(t, "quay.io/argoprojlabs/argocd-e2e-container:0.2", image)
|
|
})
|
|
})
|
|
|
|
t.Run("Application specific override", func(t *testing.T) {
|
|
service := newService(t, ".")
|
|
runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
|
|
t.Helper()
|
|
manifests, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: path,
|
|
},
|
|
AppName: "testapp",
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
require.NoError(t, err)
|
|
resourceByKindName := make(map[string]*unstructured.Unstructured)
|
|
for _, manifest := range manifests.Manifests {
|
|
var un unstructured.Unstructured
|
|
err := yaml.Unmarshal([]byte(manifest), &un)
|
|
require.NoError(t, err)
|
|
resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un
|
|
}
|
|
deployment, ok := resourceByKindName["Deployment/guestbook-ui"]
|
|
require.True(t, ok)
|
|
containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
|
|
require.True(t, ok)
|
|
image, ok, _ := unstructured.NestedString(containers[0].(map[string]any), "image")
|
|
require.True(t, ok)
|
|
assert.Equal(t, "quay.io/argoprojlabs/argocd-e2e-container:0.3", image)
|
|
})
|
|
})
|
|
|
|
t.Run("Multi-source with source as ref only does not generate manifests", func(t *testing.T) {
|
|
service := newService(t, ".")
|
|
runWithTempTestdata(t, "single-app-only", func(t *testing.T, _ string) {
|
|
t.Helper()
|
|
manifests, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "",
|
|
Chart: "",
|
|
Ref: "test",
|
|
},
|
|
AppName: "testapp-multi-ref-only",
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
HasMultipleSources: true,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, manifests.Manifests)
|
|
assert.NotEmpty(t, manifests.Revision)
|
|
})
|
|
})
|
|
|
|
t.Run("Application specific override for other app", func(t *testing.T) {
|
|
service := newService(t, ".")
|
|
runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
|
|
t.Helper()
|
|
manifests, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: path,
|
|
},
|
|
AppName: "testapp2",
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
require.NoError(t, err)
|
|
resourceByKindName := make(map[string]*unstructured.Unstructured)
|
|
for _, manifest := range manifests.Manifests {
|
|
var un unstructured.Unstructured
|
|
err := yaml.Unmarshal([]byte(manifest), &un)
|
|
require.NoError(t, err)
|
|
resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un
|
|
}
|
|
deployment, ok := resourceByKindName["Deployment/guestbook-ui"]
|
|
require.True(t, ok)
|
|
containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
|
|
require.True(t, ok)
|
|
image, ok, _ := unstructured.NestedString(containers[0].(map[string]any), "image")
|
|
require.True(t, ok)
|
|
assert.Equal(t, "quay.io/argoprojlabs/argocd-e2e-container:0.1", image)
|
|
})
|
|
})
|
|
|
|
t.Run("Override info does not appear in cache key", func(t *testing.T) {
|
|
service := newService(t, ".")
|
|
runWithTempTestdata(t, "single-global", func(t *testing.T, path string) {
|
|
t.Helper()
|
|
source := &v1alpha1.ApplicationSource{
|
|
Path: path,
|
|
}
|
|
sourceCopy := source.DeepCopy() // make a copy in case GenerateManifest mutates it.
|
|
_, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: sourceCopy,
|
|
AppName: "test",
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
})
|
|
require.NoError(t, err)
|
|
res := &cache.CachedManifestResponse{}
|
|
// Try to pull from the cache with a `source` that does not include any overrides. Overrides should not be
|
|
// part of the cache key, because you can't get the overrides without a repo operation. And avoiding repo
|
|
// operations is the point of the cache.
|
|
err = service.cache.GetManifests(mock.Anything, source, v1alpha1.RefTargetRevisionMapping{}, &v1alpha1.ClusterInfo{}, "", "", "", "test", res, nil, "")
|
|
require.NoError(t, err)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) {
|
|
regularGitTagHash := "632039659e542ed7de0c170a4fcc1c571b288fc0"
|
|
annotatedGitTaghash := "95249be61b028d566c29d47b19e65c5603388a41"
|
|
invalidGitTaghash := "invalid-tag"
|
|
actualCommitSHA := "632039659e542ed7de0c170a4fcc1c571b288fc0"
|
|
|
|
tests := []struct {
|
|
name string
|
|
ctx context.Context
|
|
manifestRequest *apiclient.ManifestRequest
|
|
wantError bool
|
|
service *Service
|
|
}{
|
|
{
|
|
name: "Case: Git tag hash matches latest commit SHA (regular tag)",
|
|
ctx: t.Context(),
|
|
manifestRequest: &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
TargetRevision: regularGitTagHash,
|
|
},
|
|
NoCache: true,
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
},
|
|
wantError: false,
|
|
service: newServiceWithCommitSHA(t, ".", regularGitTagHash),
|
|
},
|
|
|
|
{
|
|
name: "Case: Git tag hash does not match latest commit SHA (annotated tag)",
|
|
ctx: t.Context(),
|
|
manifestRequest: &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
TargetRevision: annotatedGitTaghash,
|
|
},
|
|
NoCache: true,
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
},
|
|
wantError: false,
|
|
service: newServiceWithCommitSHA(t, ".", annotatedGitTaghash),
|
|
},
|
|
|
|
{
|
|
name: "Case: Git tag hash is invalid",
|
|
ctx: t.Context(),
|
|
manifestRequest: &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
TargetRevision: invalidGitTaghash,
|
|
},
|
|
NoCache: true,
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
},
|
|
wantError: true,
|
|
service: newServiceWithCommitSHA(t, ".", invalidGitTaghash),
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
manifestResponse, err := tt.service.GenerateManifest(tt.ctx, tt.manifestRequest)
|
|
if !tt.wantError {
|
|
require.NoError(t, err)
|
|
assert.Equal(t, manifestResponse.Revision, actualCommitSHA)
|
|
} else {
|
|
assert.Errorf(t, err, "expected an error but did not throw one")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGenerateManifestWithAnnotatedTagsAndMultiSourceApp(t *testing.T) {
|
|
annotatedGitTaghash := "95249be61b028d566c29d47b19e65c5603388a41"
|
|
|
|
service := newServiceWithCommitSHA(t, ".", annotatedGitTaghash)
|
|
|
|
refSources := map[string]*v1alpha1.RefTarget{}
|
|
|
|
refSources["$global"] = &v1alpha1.RefTarget{
|
|
TargetRevision: annotatedGitTaghash,
|
|
}
|
|
|
|
refSources["$default"] = &v1alpha1.RefTarget{
|
|
TargetRevision: annotatedGitTaghash,
|
|
}
|
|
|
|
manifestRequest := &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
TargetRevision: annotatedGitTaghash,
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"$global/values.yaml", "$default/secrets.yaml"},
|
|
},
|
|
},
|
|
HasMultipleSources: true,
|
|
NoCache: true,
|
|
RefSources: refSources,
|
|
}
|
|
|
|
response, err := service.GenerateManifest(t.Context(), manifestRequest)
|
|
require.NoError(t, err)
|
|
assert.Equalf(t, response.Revision, annotatedGitTaghash, "returned SHA %s is different from expected annotated tag %s", response.Revision, annotatedGitTaghash)
|
|
}
|
|
|
|
func TestGenerateMultiSourceHelmWithFileParameter(t *testing.T) {
|
|
expectedFileContent, err := os.ReadFile("../../util/helm/testdata/external/external-secret.txt")
|
|
require.NoError(t, err)
|
|
|
|
service := newService(t, "../../util/helm/testdata")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
refSources map[string]*v1alpha1.RefTarget
|
|
expectedContent string
|
|
expectedErr bool
|
|
}{{
|
|
name: "Successfully resolve multi-source ref for helm set-file",
|
|
refSources: map[string]*v1alpha1.RefTarget{
|
|
"$global": {
|
|
TargetRevision: "HEAD",
|
|
},
|
|
},
|
|
expectedContent: string(expectedFileContent),
|
|
expectedErr: false,
|
|
}, {
|
|
name: "Failed to resolve multi-source ref for helm set-file",
|
|
refSources: map[string]*v1alpha1.RefTarget{},
|
|
expectedContent: "DOES-NOT-EXIST",
|
|
expectedErr: true,
|
|
}}
|
|
|
|
for i := range testCases {
|
|
tc := testCases[i]
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
manifestRequest := &apiclient.ManifestRequest{
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Ref: "$global",
|
|
Path: "./redis",
|
|
TargetRevision: "HEAD",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
ValueFiles: []string{"$global/redis/values-production.yaml"},
|
|
FileParameters: []v1alpha1.HelmFileParameter{{
|
|
Name: "passwordContent",
|
|
Path: "$global/external/external-secret.txt",
|
|
}},
|
|
},
|
|
},
|
|
HasMultipleSources: true,
|
|
NoCache: true,
|
|
RefSources: tc.refSources,
|
|
}
|
|
|
|
res, err := service.GenerateManifest(t.Context(), manifestRequest)
|
|
|
|
if !tc.expectedErr {
|
|
require.NoError(t, err)
|
|
|
|
// Check that any of the manifests contains the secret
|
|
idx := slices.IndexFunc(res.Manifests, func(content string) bool {
|
|
return strings.Contains(content, tc.expectedContent)
|
|
})
|
|
assert.GreaterOrEqual(t, idx, 0, "No manifest contains the value set with the helm fileParameters")
|
|
} else {
|
|
assert.Error(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFindResources(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
include string
|
|
exclude string
|
|
expectedNames []string
|
|
}{{
|
|
name: "Include One Match",
|
|
include: "subdir/deploymentSub.yaml",
|
|
expectedNames: []string{"nginx-deployment-sub"},
|
|
}, {
|
|
name: "Include Everything",
|
|
include: "*.yaml",
|
|
expectedNames: []string{"nginx-deployment", "nginx-deployment-sub"},
|
|
}, {
|
|
name: "Include Subdirectory",
|
|
include: "**/*.yaml",
|
|
expectedNames: []string{"nginx-deployment-sub"},
|
|
}, {
|
|
name: "Include No Matches",
|
|
include: "nothing.yaml",
|
|
expectedNames: []string{},
|
|
}, {
|
|
name: "Exclude - One Match",
|
|
exclude: "subdir/deploymentSub.yaml",
|
|
expectedNames: []string{"nginx-deployment"},
|
|
}, {
|
|
name: "Exclude - Everything",
|
|
exclude: "*.yaml",
|
|
expectedNames: []string{},
|
|
}}
|
|
for i := range testCases {
|
|
tc := testCases[i]
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, v1alpha1.ApplicationSourceDirectory{
|
|
Recurse: true,
|
|
Include: tc.include,
|
|
Exclude: tc.exclude,
|
|
}, map[string]bool{}, resource.MustParse("0"))
|
|
require.NoError(t, err)
|
|
var names []string
|
|
for i := range objs {
|
|
names = append(names, objs[i].GetName())
|
|
}
|
|
assert.ElementsMatch(t, tc.expectedNames, names)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFindManifests_Exclude(t *testing.T) {
|
|
objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, v1alpha1.ApplicationSourceDirectory{
|
|
Recurse: true,
|
|
Exclude: "subdir/deploymentSub.yaml",
|
|
}, map[string]bool{}, resource.MustParse("0"))
|
|
|
|
require.NoError(t, err)
|
|
require.Len(t, objs, 1)
|
|
|
|
assert.Equal(t, "nginx-deployment", objs[0].GetName())
|
|
}
|
|
|
|
func TestFindManifests_Exclude_NothingMatches(t *testing.T) {
|
|
objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, v1alpha1.ApplicationSourceDirectory{
|
|
Recurse: true,
|
|
Exclude: "nothing.yaml",
|
|
}, map[string]bool{}, resource.MustParse("0"))
|
|
|
|
require.NoError(t, err)
|
|
require.Len(t, objs, 2)
|
|
|
|
assert.ElementsMatch(t,
|
|
[]string{"nginx-deployment", "nginx-deployment-sub"}, []string{objs[0].GetName(), objs[1].GetName()})
|
|
}
|
|
|
|
func tempDir(t *testing.T) string {
|
|
t.Helper()
|
|
dir, err := os.MkdirTemp(".", "")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err = os.RemoveAll(dir)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
})
|
|
absDir, err := filepath.Abs(dir)
|
|
require.NoError(t, err)
|
|
return absDir
|
|
}
|
|
|
|
func walkFor(t *testing.T, root string, testPath string, run func(info fs.FileInfo)) {
|
|
t.Helper()
|
|
hitExpectedPath := false
|
|
err := filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
|
|
if path == testPath {
|
|
require.NoError(t, err)
|
|
hitExpectedPath = true
|
|
run(info)
|
|
}
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, hitExpectedPath, "did not hit expected path when walking directory")
|
|
}
|
|
|
|
func Test_getPotentiallyValidManifestFile(t *testing.T) {
|
|
// These tests use filepath.Walk instead of os.Stat to get file info, because FileInfo from os.Stat does not return
|
|
// true for IsSymlink like os.Walk does.
|
|
|
|
// These tests do not use t.TempDir() because those directories can contain symlinks which cause test to fail
|
|
// InBound checks.
|
|
|
|
t.Run("non-JSON/YAML is skipped with an empty ignore message", func(t *testing.T) {
|
|
appDir := tempDir(t)
|
|
filePath := filepath.Join(appDir, "not-json-or-yaml")
|
|
file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0o644)
|
|
require.NoError(t, err)
|
|
err = file.Close()
|
|
require.NoError(t, err)
|
|
|
|
walkFor(t, appDir, filePath, func(info fs.FileInfo) {
|
|
realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "", "")
|
|
assert.Nil(t, realFileInfo)
|
|
assert.Empty(t, ignoreMessage)
|
|
require.NoError(t, err)
|
|
})
|
|
})
|
|
|
|
t.Run("circular link should throw an error", func(t *testing.T) {
|
|
appDir := tempDir(t)
|
|
|
|
aPath := filepath.Join(appDir, "a.json")
|
|
bPath := filepath.Join(appDir, "b.json")
|
|
err := os.Symlink(bPath, aPath)
|
|
require.NoError(t, err)
|
|
err = os.Symlink(aPath, bPath)
|
|
require.NoError(t, err)
|
|
|
|
walkFor(t, appDir, aPath, func(info fs.FileInfo) {
|
|
realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(aPath, info, appDir, appDir, "", "")
|
|
assert.Nil(t, realFileInfo)
|
|
assert.Empty(t, ignoreMessage)
|
|
assert.ErrorContains(t, err, "too many links")
|
|
})
|
|
})
|
|
|
|
t.Run("symlink with missing destination should throw an error", func(t *testing.T) {
|
|
appDir := tempDir(t)
|
|
|
|
aPath := filepath.Join(appDir, "a.json")
|
|
bPath := filepath.Join(appDir, "b.json")
|
|
err := os.Symlink(bPath, aPath)
|
|
require.NoError(t, err)
|
|
|
|
walkFor(t, appDir, aPath, func(info fs.FileInfo) {
|
|
realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(aPath, info, appDir, appDir, "", "")
|
|
assert.Nil(t, realFileInfo)
|
|
assert.NotEmpty(t, ignoreMessage)
|
|
require.NoError(t, err)
|
|
})
|
|
})
|
|
|
|
t.Run("out-of-bounds symlink should throw an error", func(t *testing.T) {
|
|
appDir := tempDir(t)
|
|
|
|
linkPath := filepath.Join(appDir, "a.json")
|
|
err := os.Symlink("..", linkPath)
|
|
require.NoError(t, err)
|
|
|
|
walkFor(t, appDir, linkPath, func(info fs.FileInfo) {
|
|
realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "")
|
|
assert.Nil(t, realFileInfo)
|
|
assert.Empty(t, ignoreMessage)
|
|
assert.ErrorContains(t, err, "illegal filepath in symlink")
|
|
})
|
|
})
|
|
|
|
t.Run("symlink to a non-regular file should be skipped with warning", func(t *testing.T) {
|
|
appDir := tempDir(t)
|
|
|
|
dirPath := filepath.Join(appDir, "test.dir")
|
|
err := os.MkdirAll(dirPath, 0o644)
|
|
require.NoError(t, err)
|
|
linkPath := filepath.Join(appDir, "test.json")
|
|
err = os.Symlink(dirPath, linkPath)
|
|
require.NoError(t, err)
|
|
|
|
walkFor(t, appDir, linkPath, func(info fs.FileInfo) {
|
|
realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "")
|
|
assert.Nil(t, realFileInfo)
|
|
assert.Contains(t, ignoreMessage, "non-regular file")
|
|
require.NoError(t, err)
|
|
})
|
|
})
|
|
|
|
t.Run("non-included file should be skipped with no message", func(t *testing.T) {
|
|
appDir := tempDir(t)
|
|
|
|
filePath := filepath.Join(appDir, "not-included.yaml")
|
|
file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0o644)
|
|
require.NoError(t, err)
|
|
err = file.Close()
|
|
require.NoError(t, err)
|
|
|
|
walkFor(t, appDir, filePath, func(info fs.FileInfo) {
|
|
realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "*.json", "")
|
|
assert.Nil(t, realFileInfo)
|
|
assert.Empty(t, ignoreMessage)
|
|
require.NoError(t, err)
|
|
})
|
|
})
|
|
|
|
t.Run("excluded file should be skipped with no message", func(t *testing.T) {
|
|
appDir := tempDir(t)
|
|
|
|
filePath := filepath.Join(appDir, "excluded.json")
|
|
file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0o644)
|
|
require.NoError(t, err)
|
|
err = file.Close()
|
|
require.NoError(t, err)
|
|
|
|
walkFor(t, appDir, filePath, func(info fs.FileInfo) {
|
|
realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "", "excluded.*")
|
|
assert.Nil(t, realFileInfo)
|
|
assert.Empty(t, ignoreMessage)
|
|
require.NoError(t, err)
|
|
})
|
|
})
|
|
|
|
t.Run("symlink to a regular file is potentially valid", func(t *testing.T) {
|
|
appDir := tempDir(t)
|
|
|
|
filePath := filepath.Join(appDir, "regular-file")
|
|
file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0o644)
|
|
require.NoError(t, err)
|
|
err = file.Close()
|
|
require.NoError(t, err)
|
|
|
|
linkPath := filepath.Join(appDir, "link.json")
|
|
err = os.Symlink(filePath, linkPath)
|
|
require.NoError(t, err)
|
|
|
|
walkFor(t, appDir, linkPath, func(info fs.FileInfo) {
|
|
realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "")
|
|
assert.NotNil(t, realFileInfo)
|
|
assert.Empty(t, ignoreMessage)
|
|
require.NoError(t, err)
|
|
})
|
|
})
|
|
|
|
t.Run("a regular file is potentially valid", func(t *testing.T) {
|
|
appDir := tempDir(t)
|
|
|
|
filePath := filepath.Join(appDir, "regular-file.json")
|
|
file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0o644)
|
|
require.NoError(t, err)
|
|
err = file.Close()
|
|
require.NoError(t, err)
|
|
|
|
walkFor(t, appDir, filePath, func(info fs.FileInfo) {
|
|
realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "", "")
|
|
assert.NotNil(t, realFileInfo)
|
|
assert.Empty(t, ignoreMessage)
|
|
require.NoError(t, err)
|
|
})
|
|
})
|
|
|
|
t.Run("realFileInfo is for the destination rather than the symlink", func(t *testing.T) {
|
|
appDir := tempDir(t)
|
|
|
|
filePath := filepath.Join(appDir, "regular-file")
|
|
file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0o644)
|
|
require.NoError(t, err)
|
|
err = file.Close()
|
|
require.NoError(t, err)
|
|
|
|
linkPath := filepath.Join(appDir, "link.json")
|
|
err = os.Symlink(filePath, linkPath)
|
|
require.NoError(t, err)
|
|
|
|
walkFor(t, appDir, linkPath, func(info fs.FileInfo) {
|
|
realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "")
|
|
assert.NotNil(t, realFileInfo)
|
|
assert.Equal(t, filepath.Base(filePath), realFileInfo.Name())
|
|
assert.Empty(t, ignoreMessage)
|
|
require.NoError(t, err)
|
|
})
|
|
})
|
|
}
|
|
|
|
func Test_getPotentiallyValidManifests(t *testing.T) {
|
|
// Tests which return no manifests and an error check to make sure the directory exists before running. A missing
|
|
// directory would produce those same results.
|
|
|
|
logCtx := log.WithField("test", "test")
|
|
|
|
t.Run("unreadable file throws error", func(t *testing.T) {
|
|
appDir := t.TempDir()
|
|
unreadablePath := filepath.Join(appDir, "unreadable.json")
|
|
err := os.WriteFile(unreadablePath, []byte{}, 0o666)
|
|
require.NoError(t, err)
|
|
err = os.Chmod(appDir, 0o000)
|
|
require.NoError(t, err)
|
|
|
|
manifests, err := getPotentiallyValidManifests(logCtx, appDir, appDir, false, "", "", resource.MustParse("0"))
|
|
assert.Empty(t, manifests)
|
|
require.Error(t, err)
|
|
|
|
// allow cleanup
|
|
err = os.Chmod(appDir, 0o777)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
})
|
|
|
|
t.Run("no recursion when recursion is disabled", func(t *testing.T) {
|
|
manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/recurse", "./testdata/recurse", false, "", "", resource.MustParse("0"))
|
|
assert.Len(t, manifests, 1)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("recursion when recursion is enabled", func(t *testing.T) {
|
|
manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/recurse", "./testdata/recurse", true, "", "", resource.MustParse("0"))
|
|
assert.Len(t, manifests, 2)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("non-JSON/YAML is skipped", func(t *testing.T) {
|
|
manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/non-manifest-file", "./testdata/non-manifest-file", false, "", "", resource.MustParse("0"))
|
|
assert.Empty(t, manifests)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("circular link should throw an error", func(t *testing.T) {
|
|
const testDir = "./testdata/circular-link"
|
|
require.DirExists(t, testDir)
|
|
t.Cleanup(func() {
|
|
os.Remove(path.Join(testDir, "a.json"))
|
|
os.Remove(path.Join(testDir, "b.json"))
|
|
})
|
|
t.Chdir(testDir)
|
|
require.NoError(t, fileutil.CreateSymlink(t, "a.json", "b.json"))
|
|
require.NoError(t, fileutil.CreateSymlink(t, "b.json", "a.json"))
|
|
manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/circular-link", "./testdata/circular-link", false, "", "", resource.MustParse("0"))
|
|
assert.Empty(t, manifests)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("out-of-bounds symlink should throw an error", func(t *testing.T) {
|
|
require.DirExists(t, "./testdata/out-of-bounds-link")
|
|
manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/out-of-bounds-link", "./testdata/out-of-bounds-link", false, "", "", resource.MustParse("0"))
|
|
assert.Empty(t, manifests)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("symlink to a regular file works", func(t *testing.T) {
|
|
repoRoot, err := filepath.Abs("./testdata/in-bounds-link")
|
|
require.NoError(t, err)
|
|
appPath, err := filepath.Abs("./testdata/in-bounds-link/app")
|
|
require.NoError(t, err)
|
|
manifests, err := getPotentiallyValidManifests(logCtx, appPath, repoRoot, false, "", "", resource.MustParse("0"))
|
|
assert.Len(t, manifests, 1)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("symlink to nowhere should be ignored", func(t *testing.T) {
|
|
manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/link-to-nowhere", "./testdata/link-to-nowhere", false, "", "", resource.MustParse("0"))
|
|
assert.Empty(t, manifests)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("link to over-sized manifest fails", func(t *testing.T) {
|
|
repoRoot, err := filepath.Abs("./testdata/in-bounds-link")
|
|
require.NoError(t, err)
|
|
appPath, err := filepath.Abs("./testdata/in-bounds-link/app")
|
|
require.NoError(t, err)
|
|
// The file is 35 bytes.
|
|
manifests, err := getPotentiallyValidManifests(logCtx, appPath, repoRoot, false, "", "", resource.MustParse("34"))
|
|
assert.Empty(t, manifests)
|
|
assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize)
|
|
})
|
|
|
|
t.Run("group of files should be limited at precisely the sum of their size", func(t *testing.T) {
|
|
// There is a total of 10 files, ech file being 10 bytes.
|
|
manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/several-files", "./testdata/several-files", false, "", "", resource.MustParse("365"))
|
|
assert.Len(t, manifests, 10)
|
|
require.NoError(t, err)
|
|
|
|
manifests, err = getPotentiallyValidManifests(logCtx, "./testdata/several-files", "./testdata/several-files", false, "", "", resource.MustParse("100"))
|
|
assert.Empty(t, manifests)
|
|
assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize)
|
|
})
|
|
}
|
|
|
|
func Test_findManifests(t *testing.T) {
|
|
logCtx := log.WithField("test", "test")
|
|
noRecurse := v1alpha1.ApplicationSourceDirectory{Recurse: false}
|
|
|
|
t.Run("unreadable file throws error", func(t *testing.T) {
|
|
appDir := t.TempDir()
|
|
unreadablePath := filepath.Join(appDir, "unreadable.json")
|
|
err := os.WriteFile(unreadablePath, []byte{}, 0o666)
|
|
require.NoError(t, err)
|
|
err = os.Chmod(appDir, 0o000)
|
|
require.NoError(t, err)
|
|
|
|
manifests, err := findManifests(logCtx, appDir, appDir, nil, noRecurse, nil, resource.MustParse("0"))
|
|
assert.Empty(t, manifests)
|
|
require.Error(t, err)
|
|
|
|
// allow cleanup
|
|
err = os.Chmod(appDir, 0o777)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
})
|
|
|
|
t.Run("no recursion when recursion is disabled", func(t *testing.T) {
|
|
manifests, err := findManifests(logCtx, "./testdata/recurse", "./testdata/recurse", nil, noRecurse, nil, resource.MustParse("0"))
|
|
assert.Len(t, manifests, 2)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("recursion when recursion is enabled", func(t *testing.T) {
|
|
recurse := v1alpha1.ApplicationSourceDirectory{Recurse: true}
|
|
manifests, err := findManifests(logCtx, "./testdata/recurse", "./testdata/recurse", nil, recurse, nil, resource.MustParse("0"))
|
|
assert.Len(t, manifests, 4)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("non-JSON/YAML is skipped", func(t *testing.T) {
|
|
manifests, err := findManifests(logCtx, "./testdata/non-manifest-file", "./testdata/non-manifest-file", nil, noRecurse, nil, resource.MustParse("0"))
|
|
assert.Empty(t, manifests)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("circular link should throw an error", func(t *testing.T) {
|
|
const testDir = "./testdata/circular-link"
|
|
require.DirExists(t, testDir)
|
|
t.Cleanup(func() {
|
|
os.Remove(path.Join(testDir, "a.json"))
|
|
os.Remove(path.Join(testDir, "b.json"))
|
|
})
|
|
t.Chdir(testDir)
|
|
require.NoError(t, fileutil.CreateSymlink(t, "a.json", "b.json"))
|
|
require.NoError(t, fileutil.CreateSymlink(t, "b.json", "a.json"))
|
|
manifests, err := findManifests(logCtx, "./testdata/circular-link", "./testdata/circular-link", nil, noRecurse, nil, resource.MustParse("0"))
|
|
assert.Empty(t, manifests)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("out-of-bounds symlink should throw an error", func(t *testing.T) {
|
|
require.DirExists(t, "./testdata/out-of-bounds-link")
|
|
manifests, err := findManifests(logCtx, "./testdata/out-of-bounds-link", "./testdata/out-of-bounds-link", nil, noRecurse, nil, resource.MustParse("0"))
|
|
assert.Empty(t, manifests)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("symlink to a regular file works", func(t *testing.T) {
|
|
repoRoot, err := filepath.Abs("./testdata/in-bounds-link")
|
|
require.NoError(t, err)
|
|
appPath, err := filepath.Abs("./testdata/in-bounds-link/app")
|
|
require.NoError(t, err)
|
|
manifests, err := findManifests(logCtx, appPath, repoRoot, nil, noRecurse, nil, resource.MustParse("0"))
|
|
assert.Len(t, manifests, 1)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("symlink to nowhere should be ignored", func(t *testing.T) {
|
|
manifests, err := findManifests(logCtx, "./testdata/link-to-nowhere", "./testdata/link-to-nowhere", nil, noRecurse, nil, resource.MustParse("0"))
|
|
assert.Empty(t, manifests)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("link to over-sized manifest fails", func(t *testing.T) {
|
|
repoRoot, err := filepath.Abs("./testdata/in-bounds-link")
|
|
require.NoError(t, err)
|
|
appPath, err := filepath.Abs("./testdata/in-bounds-link/app")
|
|
require.NoError(t, err)
|
|
// The file is 35 bytes.
|
|
manifests, err := findManifests(logCtx, appPath, repoRoot, nil, noRecurse, nil, resource.MustParse("34"))
|
|
assert.Empty(t, manifests)
|
|
assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize)
|
|
})
|
|
|
|
t.Run("group of files should be limited at precisely the sum of their size", func(t *testing.T) {
|
|
// There is a total of 10 files, each file being 10 bytes.
|
|
manifests, err := findManifests(logCtx, "./testdata/several-files", "./testdata/several-files", nil, noRecurse, nil, resource.MustParse("365"))
|
|
assert.Len(t, manifests, 10)
|
|
require.NoError(t, err)
|
|
|
|
manifests, err = findManifests(logCtx, "./testdata/several-files", "./testdata/several-files", nil, noRecurse, nil, resource.MustParse("364"))
|
|
assert.Empty(t, manifests)
|
|
assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize)
|
|
})
|
|
|
|
t.Run("jsonnet isn't counted against size limit", func(t *testing.T) {
|
|
// Each file is 36 bytes. Only the 36-byte json file should be counted against the limit.
|
|
manifests, err := findManifests(logCtx, "./testdata/jsonnet-and-json", "./testdata/jsonnet-and-json", nil, noRecurse, nil, resource.MustParse("36"))
|
|
assert.Len(t, manifests, 2)
|
|
require.NoError(t, err)
|
|
|
|
manifests, err = findManifests(logCtx, "./testdata/jsonnet-and-json", "./testdata/jsonnet-and-json", nil, noRecurse, nil, resource.MustParse("35"))
|
|
assert.Empty(t, manifests)
|
|
assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize)
|
|
})
|
|
|
|
t.Run("partially valid YAML file throws an error", func(t *testing.T) {
|
|
require.DirExists(t, "./testdata/partially-valid-yaml")
|
|
manifests, err := findManifests(logCtx, "./testdata/partially-valid-yaml", "./testdata/partially-valid-yaml", nil, noRecurse, nil, resource.MustParse("0"))
|
|
assert.Empty(t, manifests)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("invalid manifest throws an error", func(t *testing.T) {
|
|
require.DirExists(t, "./testdata/invalid-manifests")
|
|
manifests, err := findManifests(logCtx, "./testdata/invalid-manifests", "./testdata/invalid-manifests", nil, noRecurse, nil, resource.MustParse("0"))
|
|
assert.Empty(t, manifests)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("invalid manifest containing '+argocd:skip-file-rendering' doesn't throw an error", func(t *testing.T) {
|
|
require.DirExists(t, "./testdata/invalid-manifests-skipped")
|
|
manifests, err := findManifests(logCtx, "./testdata/invalid-manifests-skipped", "./testdata/invalid-manifests-skipped", nil, noRecurse, nil, resource.MustParse("0"))
|
|
assert.Empty(t, manifests)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("irrelevant YAML gets skipped, relevant YAML gets parsed", func(t *testing.T) {
|
|
manifests, err := findManifests(logCtx, "./testdata/irrelevant-yaml", "./testdata/irrelevant-yaml", nil, noRecurse, nil, resource.MustParse("0"))
|
|
assert.Len(t, manifests, 1)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("multiple JSON objects in one file throws an error", func(t *testing.T) {
|
|
require.DirExists(t, "./testdata/json-list")
|
|
manifests, err := findManifests(logCtx, "./testdata/json-list", "./testdata/json-list", nil, noRecurse, nil, resource.MustParse("0"))
|
|
assert.Empty(t, manifests)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("invalid JSON throws an error", func(t *testing.T) {
|
|
require.DirExists(t, "./testdata/invalid-json")
|
|
manifests, err := findManifests(logCtx, "./testdata/invalid-json", "./testdata/invalid-json", nil, noRecurse, nil, resource.MustParse("0"))
|
|
assert.Empty(t, manifests)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("valid JSON returns manifest and no error", func(t *testing.T) {
|
|
manifests, err := findManifests(logCtx, "./testdata/valid-json", "./testdata/valid-json", nil, noRecurse, nil, resource.MustParse("0"))
|
|
assert.Len(t, manifests, 1)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("YAML with an empty document doesn't throw an error", func(t *testing.T) {
|
|
manifests, err := findManifests(logCtx, "./testdata/yaml-with-empty-document", "./testdata/yaml-with-empty-document", nil, noRecurse, nil, resource.MustParse("0"))
|
|
assert.Len(t, manifests, 1)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestTestRepoHelmOCI(t *testing.T) {
|
|
service := newService(t, ".")
|
|
_, err := service.TestRepository(t.Context(), &apiclient.TestRepositoryRequest{
|
|
Repo: &v1alpha1.Repository{
|
|
Repo: "https://demo.goharbor.io",
|
|
Type: "helm",
|
|
EnableOCI: true,
|
|
},
|
|
})
|
|
assert.ErrorContains(t, err, "OCI Helm repository URL should include hostname and port only")
|
|
}
|
|
|
|
func Test_getHelmDependencyRepos(t *testing.T) {
|
|
repo1 := "https://charts.bitnami.com/bitnami"
|
|
repo2 := "https://eventstore.github.io/EventStore.Charts"
|
|
|
|
repos, err := getHelmDependencyRepos("../../util/helm/testdata/dependency")
|
|
require.NoError(t, err)
|
|
assert.Len(t, repos, 2)
|
|
assert.Equal(t, repos[0].Repo, repo1)
|
|
assert.Equal(t, repos[1].Repo, repo2)
|
|
}
|
|
|
|
func TestResolveRevision(t *testing.T) {
|
|
expectedRevision := "03b17e0233e64787ffb5fcf65c740cc2a20822ba"
|
|
service, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().LsRemote("v2.2.2").Return(expectedRevision, nil)
|
|
gitClient.EXPECT().Root().Return(".")
|
|
paths.EXPECT().GetPath(mock.Anything).Return(".", nil)
|
|
}, ".")
|
|
repo := &v1alpha1.Repository{Repo: "https://github.com/argoproj/argo-cd"}
|
|
app := &v1alpha1.Application{Spec: v1alpha1.ApplicationSpec{Source: &v1alpha1.ApplicationSource{}}}
|
|
resolveRevisionResponse, err := service.ResolveRevision(t.Context(), &apiclient.ResolveRevisionRequest{
|
|
Repo: repo,
|
|
App: app,
|
|
AmbiguousRevision: "v2.2.2",
|
|
})
|
|
|
|
expectedResolveRevisionResponse := &apiclient.ResolveRevisionResponse{
|
|
Revision: expectedRevision,
|
|
AmbiguousRevision: fmt.Sprintf("v2.2.2 (%s)", expectedRevision),
|
|
}
|
|
|
|
assert.NotNil(t, resolveRevisionResponse.Revision)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedResolveRevisionResponse, resolveRevisionResponse)
|
|
}
|
|
|
|
func TestResolveRevisionNegativeScenarios(t *testing.T) {
|
|
service, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().LsRemote("v2.a.2").Return("", fmt.Errorf("unable to resolve '%s' to a commit SHA", "v2.a.2"))
|
|
gitClient.EXPECT().Root().Return(".")
|
|
paths.EXPECT().GetPath(mock.Anything).Return(".", nil)
|
|
}, ".")
|
|
repo := &v1alpha1.Repository{Repo: "https://github.com/argoproj/argo-cd"}
|
|
app := &v1alpha1.Application{Spec: v1alpha1.ApplicationSpec{Source: &v1alpha1.ApplicationSource{}}}
|
|
resolveRevisionResponse, err := service.ResolveRevision(t.Context(), &apiclient.ResolveRevisionRequest{
|
|
Repo: repo,
|
|
App: app,
|
|
AmbiguousRevision: "v2.a.2",
|
|
})
|
|
|
|
expectedResolveRevisionResponse := &apiclient.ResolveRevisionResponse{
|
|
Revision: "",
|
|
AmbiguousRevision: "",
|
|
}
|
|
|
|
assert.NotNil(t, resolveRevisionResponse.Revision)
|
|
require.Error(t, err)
|
|
assert.Equal(t, expectedResolveRevisionResponse, resolveRevisionResponse)
|
|
}
|
|
|
|
func TestDirectoryPermissionInitializer(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
file, err := os.CreateTemp(dir, "")
|
|
require.NoError(t, err)
|
|
utilio.Close(file)
|
|
|
|
// remove read permissions
|
|
require.NoError(t, os.Chmod(dir, 0o000))
|
|
|
|
// Remember to restore permissions when the test finishes so dir can
|
|
// be removed properly.
|
|
t.Cleanup(func() {
|
|
require.NoError(t, os.Chmod(dir, 0o777))
|
|
})
|
|
|
|
// make sure permission are restored
|
|
closer := directoryPermissionInitializer(dir)
|
|
_, err = os.ReadFile(file.Name())
|
|
require.NoError(t, err)
|
|
|
|
// make sure permission are removed by closer
|
|
utilio.Close(closer)
|
|
_, err = os.ReadFile(file.Name())
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func addHelmToGitRepo(t *testing.T, options newGitRepoOptions) {
|
|
t.Helper()
|
|
ctx := t.Context()
|
|
err := os.WriteFile(filepath.Join(options.path, "Chart.yaml"), []byte("name: test\nversion: v1.0.0"), 0o777)
|
|
require.NoError(t, err)
|
|
for valuesFileName, values := range options.helmChartOptions.valuesFiles {
|
|
valuesFileContents, err := yaml.Marshal(values)
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(filepath.Join(options.path, valuesFileName), valuesFileContents, 0o777)
|
|
require.NoError(t, err)
|
|
}
|
|
require.NoError(t, err)
|
|
cmd := exec.CommandContext(ctx, "git", "add", "-A")
|
|
cmd.Dir = options.path
|
|
require.NoError(t, cmd.Run())
|
|
cmd = exec.CommandContext(ctx, "git", "commit", "-m", "Initial commit")
|
|
cmd.Dir = options.path
|
|
require.NoError(t, cmd.Run())
|
|
}
|
|
|
|
func initGitRepo(t *testing.T, options newGitRepoOptions) (revision string) {
|
|
t.Helper()
|
|
if options.createPath {
|
|
require.NoError(t, os.Mkdir(options.path, 0o755))
|
|
}
|
|
ctx := t.Context()
|
|
|
|
cmd := exec.CommandContext(ctx, "git", "init", "-b", "main", options.path)
|
|
cmd.Dir = options.path
|
|
require.NoError(t, cmd.Run())
|
|
|
|
if options.remote != "" {
|
|
cmd = exec.CommandContext(ctx, "git", "remote", "add", "origin", options.path)
|
|
cmd.Dir = options.path
|
|
require.NoError(t, cmd.Run())
|
|
}
|
|
|
|
commitAdded := options.addEmptyCommit || options.helmChartOptions.chartName != ""
|
|
if options.addEmptyCommit {
|
|
cmd = exec.CommandContext(ctx, "git", "commit", "-m", "Initial commit", "--allow-empty")
|
|
cmd.Dir = options.path
|
|
require.NoError(t, cmd.Run())
|
|
} else if options.helmChartOptions.chartName != "" {
|
|
addHelmToGitRepo(t, options)
|
|
}
|
|
|
|
if commitAdded {
|
|
var revB bytes.Buffer
|
|
cmd = exec.CommandContext(ctx, "git", "rev-parse", "HEAD", options.path)
|
|
cmd.Dir = options.path
|
|
cmd.Stdout = &revB
|
|
require.NoError(t, cmd.Run())
|
|
revision = strings.Split(revB.String(), "\n")[0]
|
|
}
|
|
return revision
|
|
}
|
|
|
|
func TestInit(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
// service.Init sets permission to 0300. Restore permissions when the test
|
|
// finishes so dir can be removed properly.
|
|
t.Cleanup(func() {
|
|
require.NoError(t, os.Chmod(dir, 0o777))
|
|
})
|
|
|
|
repoPath := path.Join(dir, "repo1")
|
|
initGitRepo(t, newGitRepoOptions{path: repoPath, remote: "https://github.com/argo-cd/test-repo1", createPath: true, addEmptyCommit: false})
|
|
|
|
service := newService(t, ".")
|
|
service.rootDir = dir
|
|
|
|
require.NoError(t, service.Init())
|
|
|
|
_, err := os.ReadDir(dir)
|
|
require.Error(t, err)
|
|
initGitRepo(t, newGitRepoOptions{path: path.Join(dir, "repo2"), remote: "https://github.com/argo-cd/test-repo2", createPath: true, addEmptyCommit: false})
|
|
}
|
|
|
|
// TestCheckoutRevisionCanGetNonstandardRefs shows that we can fetch a revision that points to a non-standard ref. In
|
|
// other words, we haven't regressed and caused this issue again: https://github.com/argoproj/argo-cd/issues/4935
|
|
func TestCheckoutRevisionCanGetNonstandardRefs(t *testing.T) {
|
|
rootPath := t.TempDir()
|
|
|
|
sourceRepoPath, err := os.MkdirTemp(rootPath, "")
|
|
require.NoError(t, err)
|
|
|
|
// Create a repo such that one commit is on a non-standard ref _and nowhere else_. This is meant to simulate, for
|
|
// example, a GitHub ref for a pull into one repo from a fork of that repo.
|
|
runGit(t, sourceRepoPath, "init")
|
|
runGit(t, sourceRepoPath, "checkout", "-b", "main") // make sure there's a main branch to switch back to
|
|
runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty")
|
|
runGit(t, sourceRepoPath, "checkout", "-b", "branch")
|
|
runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty")
|
|
sha := runGit(t, sourceRepoPath, "rev-parse", "HEAD")
|
|
runGit(t, sourceRepoPath, "update-ref", "refs/pull/123/head", strings.TrimSuffix(sha, "\n"))
|
|
runGit(t, sourceRepoPath, "checkout", "main")
|
|
runGit(t, sourceRepoPath, "branch", "-D", "branch")
|
|
|
|
destRepoPath, err := os.MkdirTemp(rootPath, "")
|
|
require.NoError(t, err)
|
|
|
|
gitClient, err := git.NewClientExt("file://"+sourceRepoPath, destRepoPath, &git.NopCreds{}, true, false, "", "")
|
|
require.NoError(t, err)
|
|
|
|
pullSha, err := gitClient.LsRemote("refs/pull/123/head")
|
|
require.NoError(t, err)
|
|
|
|
err = checkoutRevision(gitClient, "does-not-exist", false, 0, true)
|
|
require.Error(t, err)
|
|
|
|
err = checkoutRevision(gitClient, pullSha, false, 0, true)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestCheckoutRevisionPresentSkipFetch(t *testing.T) {
|
|
revision := "0123456789012345678901234567890123456789"
|
|
|
|
gitClient := &gitmocks.Client{}
|
|
gitClient.EXPECT().Init().Return(nil)
|
|
gitClient.EXPECT().IsRevisionPresent(revision).Return(true)
|
|
gitClient.EXPECT().Checkout(revision, mock.Anything, mock.Anything).Return("", nil)
|
|
|
|
err := checkoutRevision(gitClient, revision, false, 0, true)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestCheckoutRevisionNotPresentCallFetch(t *testing.T) {
|
|
revision := "0123456789012345678901234567890123456789"
|
|
|
|
gitClient := &gitmocks.Client{}
|
|
gitClient.EXPECT().Init().Return(nil)
|
|
gitClient.EXPECT().IsRevisionPresent(revision).Return(false)
|
|
gitClient.EXPECT().Fetch("", mock.Anything).Return(nil)
|
|
gitClient.EXPECT().Checkout(revision, mock.Anything, mock.Anything).Return("", nil)
|
|
|
|
err := checkoutRevision(gitClient, revision, false, 0, true)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestFetch(t *testing.T) {
|
|
revision1 := "0123456789012345678901234567890123456789"
|
|
revision2 := "abcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
|
|
|
gitClient := &gitmocks.Client{}
|
|
gitClient.EXPECT().Init().Return(nil)
|
|
gitClient.EXPECT().IsRevisionPresent(revision1).Once().Return(true)
|
|
gitClient.EXPECT().IsRevisionPresent(revision2).Once().Return(false)
|
|
gitClient.EXPECT().Fetch("", mock.Anything).Return(nil)
|
|
gitClient.EXPECT().IsRevisionPresent(revision1).Once().Return(true)
|
|
gitClient.EXPECT().IsRevisionPresent(revision2).Once().Return(true)
|
|
|
|
err := fetch(gitClient, []string{revision1, revision2})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestFetchRevisionCanGetNonstandardRefs shows that we can fetch a revision that points to a non-standard ref. In
|
|
func TestFetchRevisionCanGetNonstandardRefs(t *testing.T) {
|
|
rootPath := t.TempDir()
|
|
|
|
sourceRepoPath, err := os.MkdirTemp(rootPath, "")
|
|
require.NoError(t, err)
|
|
|
|
// Create a repo such that one commit is on a non-standard ref _and nowhere else_. This is meant to simulate, for
|
|
// example, a GitHub ref for a pull into one repo from a fork of that repo.
|
|
runGit(t, sourceRepoPath, "init")
|
|
runGit(t, sourceRepoPath, "checkout", "-b", "main") // make sure there's a main branch to switch back to
|
|
runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty")
|
|
runGit(t, sourceRepoPath, "checkout", "-b", "branch")
|
|
runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty")
|
|
sha := runGit(t, sourceRepoPath, "rev-parse", "HEAD")
|
|
runGit(t, sourceRepoPath, "update-ref", "refs/pull/123/head", strings.TrimSuffix(sha, "\n"))
|
|
runGit(t, sourceRepoPath, "checkout", "main")
|
|
runGit(t, sourceRepoPath, "branch", "-D", "branch")
|
|
|
|
destRepoPath, err := os.MkdirTemp(rootPath, "")
|
|
require.NoError(t, err)
|
|
|
|
gitClient, err := git.NewClientExt("file://"+sourceRepoPath, destRepoPath, &git.NopCreds{}, true, false, "", "")
|
|
require.NoError(t, err)
|
|
|
|
// We should initialize repository
|
|
err = gitClient.Init()
|
|
require.NoError(t, err)
|
|
|
|
pullSha, err := gitClient.LsRemote("refs/pull/123/head")
|
|
require.NoError(t, err)
|
|
|
|
err = fetch(gitClient, []string{"does-not-exist"})
|
|
require.Error(t, err)
|
|
|
|
err = fetch(gitClient, []string{pullSha})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// runGit runs a git command in the given working directory. If the command succeeds, it returns the combined standard
|
|
// and error output. If it fails, it stops the test with a failure message.
|
|
func runGit(t *testing.T, workDir string, args ...string) string {
|
|
t.Helper()
|
|
cmd := exec.CommandContext(t.Context(), "git", args...)
|
|
cmd.Dir = workDir
|
|
out, err := cmd.CombinedOutput()
|
|
stringOut := string(out)
|
|
require.NoError(t, err, stringOut)
|
|
return stringOut
|
|
}
|
|
|
|
func Test_walkHelmValueFilesInPath(t *testing.T) {
|
|
t.Run("does not exist", func(t *testing.T) {
|
|
var files []string
|
|
root := "/obviously/does/not/exist"
|
|
err := filepath.Walk(root, walkHelmValueFilesInPath(root, &files))
|
|
require.Error(t, err)
|
|
assert.Empty(t, files)
|
|
})
|
|
t.Run("values files", func(t *testing.T) {
|
|
var files []string
|
|
root := "./testdata/values-files"
|
|
err := filepath.Walk(root, walkHelmValueFilesInPath(root, &files))
|
|
require.NoError(t, err)
|
|
assert.Len(t, files, 5)
|
|
})
|
|
t.Run("unrelated root", func(t *testing.T) {
|
|
var files []string
|
|
root := "./testdata/values-files"
|
|
unrelatedRoot := "/different/root/path"
|
|
err := filepath.Walk(root, walkHelmValueFilesInPath(unrelatedRoot, &files))
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func Test_populateHelmAppDetails(t *testing.T) {
|
|
sha := "632039659e542ed7de0c170a4fcc1c571b288fc0"
|
|
service := newService(t, ".")
|
|
emptyTempPaths := utilio.NewRandomizedTempPaths(t.TempDir())
|
|
res := apiclient.RepoAppDetailsResponse{}
|
|
q := apiclient.RepoServerAppDetailsQuery{
|
|
Repo: &v1alpha1.Repository{},
|
|
Source: &v1alpha1.ApplicationSource{
|
|
Helm: &v1alpha1.ApplicationSourceHelm{ValueFiles: []string{"exclude.yaml", "has-the-word-values.yaml"}},
|
|
},
|
|
}
|
|
appPath, err := filepath.Abs("./testdata/values-files/")
|
|
require.NoError(t, err)
|
|
err = service.populateHelmAppDetails(&res, appPath, appPath, sha, "main", &q, emptyTempPaths)
|
|
require.NoError(t, err)
|
|
assert.Len(t, res.Helm.Parameters, 3)
|
|
assert.Len(t, res.Helm.ValueFiles, 5)
|
|
}
|
|
|
|
func Test_populateHelmAppDetailsWithRef(t *testing.T) {
|
|
dummyErrMsg := "dummy error"
|
|
repoURL := "https://github.com/foo/bar"
|
|
refRepoURL := "https://github.com/foo/baz"
|
|
unusedRefRepoURL := "https://github.com/unused/baz"
|
|
ociRepoURL := "oci://foocr.io"
|
|
repoRoot := "./testdata/my-chart/"
|
|
refRoot := "./testdata/values-files/"
|
|
refName := "$values"
|
|
refNameA := "$valuesA"
|
|
refNameB := "$valuesB"
|
|
refNameUnused := "$valuesU"
|
|
targetRevision := "main"
|
|
sha := "888839659e542ed7de0c170a4fcc1c571b288888"
|
|
refTargetRevision := targetRevision
|
|
refTargetRevision2 := "dev"
|
|
refSha := "999932039659e542ed7de0c170a4fcc1c5799999"
|
|
refSha2 := "777732039659e542ed7de0c170a4fcc1c5777777"
|
|
queryTemplate := apiclient.RepoServerAppDetailsQuery{
|
|
Repo: &v1alpha1.Repository{
|
|
Repo: repoURL,
|
|
Type: "git",
|
|
},
|
|
Source: &v1alpha1.ApplicationSource{
|
|
Helm: &v1alpha1.ApplicationSourceHelm{ValueFiles: []string{"$values/dir/values.yaml"}},
|
|
},
|
|
RefSources: map[string]*v1alpha1.RefTarget{
|
|
refName: {
|
|
Repo: v1alpha1.Repository{
|
|
Type: "git",
|
|
Repo: refRepoURL,
|
|
},
|
|
TargetRevision: refTargetRevision,
|
|
},
|
|
},
|
|
}
|
|
var err error
|
|
var appPath string
|
|
var res apiclient.RepoAppDetailsResponse
|
|
|
|
testCases := []struct {
|
|
name string
|
|
makeQuery func() apiclient.RepoServerAppDetailsQuery
|
|
testResults func(t *testing.T)
|
|
mockOpts func(_ *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths)
|
|
// make new client for accessing the referenced repository
|
|
newGitClient func(_ string, _ string, _ git.Creds, _ bool, _ bool, _ string, _ string, _ ...git.ClientOpts) (client git.Client, e error)
|
|
}{
|
|
{
|
|
name: "success",
|
|
makeQuery: func() apiclient.RepoServerAppDetailsQuery {
|
|
return queryTemplate
|
|
},
|
|
mockOpts: func(_ *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
paths.EXPECT().GetPath(refRepoURL).Return(refRoot, nil)
|
|
paths.EXPECT().GetPathIfExists(refRepoURL).Return(refRoot)
|
|
},
|
|
newGitClient: func(_ string, _ string, _ git.Creds, _ bool, _ bool, _ string, _ string, _ ...git.ClientOpts) (gitClient git.Client, e error) {
|
|
client := gitmocks.Client{}
|
|
client.EXPECT().LsRemote(refTargetRevision).Return(refSha, nil)
|
|
client.EXPECT().Root().Return(refRoot)
|
|
client.EXPECT().Init().Return(nil)
|
|
client.EXPECT().IsRevisionPresent(refSha).Return(true)
|
|
client.EXPECT().Checkout(refSha, false, true).Return("", nil)
|
|
return &client, nil
|
|
},
|
|
|
|
testResults: func(t *testing.T) {
|
|
t.Helper()
|
|
require.NoError(t, err)
|
|
assert.Len(t, res.Helm.Parameters, 1)
|
|
// The values must come from the referenced values file ./testdata/values-files/dir/values.yaml
|
|
for _, v := range res.Helm.Parameters {
|
|
require.NotNil(t, v)
|
|
assert.Equal(t, v1alpha1.HelmParameter{Name: "values", Value: "yaml", ForceString: false}, *v)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "ref_checkout_error",
|
|
makeQuery: func() apiclient.RepoServerAppDetailsQuery {
|
|
return queryTemplate
|
|
},
|
|
mockOpts: func(_ *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
paths.EXPECT().GetPath(refRepoURL).Return(refRoot, nil)
|
|
},
|
|
newGitClient: func(_ string, _ string, _ git.Creds, _ bool, _ bool, _ string, _ string, _ ...git.ClientOpts) (gitClient git.Client, e error) {
|
|
client := gitmocks.Client{}
|
|
client.EXPECT().LsRemote("main").Return(refSha, nil)
|
|
client.EXPECT().Root().Return(refRoot)
|
|
client.EXPECT().Init().Return(nil)
|
|
client.EXPECT().IsRevisionPresent(refSha).Return(true)
|
|
client.EXPECT().Checkout(refSha, false, true).Return("", fmt.Errorf("%s", dummyErrMsg))
|
|
// one error is not enough: checkout falls back to fetch specific revision
|
|
client.EXPECT().Fetch(refSha, int64(0)).Return(fmt.Errorf("%s", dummyErrMsg))
|
|
return &client, nil
|
|
},
|
|
|
|
testResults: func(t *testing.T) {
|
|
t.Helper()
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, fmt.Sprintf("failed to acquire lock for referenced repo %q:", refRepoURL))
|
|
assert.ErrorContains(t, err, dummyErrMsg)
|
|
},
|
|
},
|
|
{
|
|
name: "same_repo_diff_revision_error",
|
|
makeQuery: func() apiclient.RepoServerAppDetailsQuery {
|
|
query := queryTemplate
|
|
query.RefSources = map[string]*v1alpha1.RefTarget{
|
|
refName: {
|
|
Repo: v1alpha1.Repository{
|
|
Type: "git",
|
|
Repo: repoURL,
|
|
},
|
|
TargetRevision: refTargetRevision2,
|
|
},
|
|
}
|
|
return query
|
|
},
|
|
|
|
mockOpts: func(_ *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
paths.EXPECT().GetPath(repoURL).Return(repoRoot, nil)
|
|
},
|
|
newGitClient: func(_ string, _ string, _ git.Creds, _ bool, _ bool, _ string, _ string, _ ...git.ClientOpts) (gitClient git.Client, e error) {
|
|
client := gitmocks.Client{}
|
|
client.EXPECT().LsRemote(refTargetRevision2).Return(refSha, nil)
|
|
return &client, nil
|
|
},
|
|
|
|
testResults: func(t *testing.T) {
|
|
t.Helper()
|
|
expMsg := fmt.Sprintf("cannot reference a different revision of the same repository (%s references %q which resolves to %q while the application references %q which resolves to %q", refName, refTargetRevision2, refSha, targetRevision, sha)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, expMsg)
|
|
},
|
|
},
|
|
{
|
|
name: "same_ref_repo_diff_revision_error",
|
|
makeQuery: func() apiclient.RepoServerAppDetailsQuery {
|
|
query := queryTemplate
|
|
delete(query.RefSources, "$values")
|
|
query.RefSources["$valuesA"] = &v1alpha1.RefTarget{
|
|
Repo: v1alpha1.Repository{
|
|
Type: "git",
|
|
Repo: refRepoURL,
|
|
},
|
|
TargetRevision: refTargetRevision,
|
|
}
|
|
query.RefSources["$valuesB"] = &v1alpha1.RefTarget{
|
|
Repo: v1alpha1.Repository{
|
|
Type: "git",
|
|
Repo: refRepoURL,
|
|
},
|
|
TargetRevision: refTargetRevision2,
|
|
}
|
|
query.Source.Helm.ValueFiles = []string{"$valuesA/dir/values.yaml", "$valuesB/dir/values.yaml"}
|
|
return query
|
|
},
|
|
|
|
mockOpts: func(_ *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
paths.EXPECT().GetPath(repoURL).Return(repoRoot, nil)
|
|
paths.EXPECT().GetPath(refRepoURL).Return(refRoot, nil)
|
|
paths.EXPECT().GetPath(refRepoURL).Return(refRoot, nil)
|
|
},
|
|
newGitClient: func(_ string, _ string, _ git.Creds, _ bool, _ bool, _ string, _ string, _ ...git.ClientOpts) (gitClient git.Client, e error) {
|
|
client := gitmocks.Client{}
|
|
client.EXPECT().LsRemote("main").Return(refSha, nil)
|
|
client.EXPECT().LsRemote("dev").Return(refSha2, nil)
|
|
client.EXPECT().Root().Return(refRoot)
|
|
client.EXPECT().Init().Return(nil)
|
|
client.EXPECT().IsRevisionPresent(refSha).Return(true)
|
|
client.EXPECT().IsRevisionPresent(refSha2).Return(true)
|
|
client.EXPECT().Checkout(refSha, false, true).Return("", nil)
|
|
client.EXPECT().Checkout(refSha2, false, true).Return("", nil)
|
|
return &client, nil
|
|
},
|
|
|
|
testResults: func(t *testing.T) {
|
|
t.Helper()
|
|
expMsg := fmt.Sprintf("cannot reference multiple revisions for the same repository (%s references %q which resolves to %q while %s references %q which resolves to %q", refNameB, refTargetRevision2, refSha2, refNameA, refTargetRevision, refSha)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, expMsg)
|
|
},
|
|
},
|
|
{
|
|
name: "ref_revision_resolution_error",
|
|
makeQuery: func() apiclient.RepoServerAppDetailsQuery {
|
|
return queryTemplate
|
|
},
|
|
mockOpts: func(_ *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
// paths.EXPECT().GetPath(repoURL).Return(repoRoot, nil)
|
|
paths.EXPECT().GetPath(refRepoURL).Return(refRoot, nil)
|
|
},
|
|
newGitClient: func(_ string, _ string, _ git.Creds, _ bool, _ bool, _ string, _ string, _ ...git.ClientOpts) (gitClient git.Client, e error) {
|
|
client := gitmocks.Client{}
|
|
client.EXPECT().LsRemote("main").Return("", fmt.Errorf("%s", dummyErrMsg))
|
|
client.EXPECT().Root().Return(refRoot)
|
|
return &client, nil
|
|
},
|
|
testResults: func(t *testing.T) {
|
|
t.Helper()
|
|
expMsg := fmt.Sprintf("error setting up git client for %s and resolving revision %s: %s", refRepoURL, "main", dummyErrMsg)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, expMsg)
|
|
},
|
|
},
|
|
{
|
|
name: "not_a_git_referenced_repo",
|
|
makeQuery: func() apiclient.RepoServerAppDetailsQuery {
|
|
query := queryTemplate
|
|
query.RefSources = map[string]*v1alpha1.RefTarget{
|
|
refName: {
|
|
Repo: v1alpha1.Repository{
|
|
Type: "oci",
|
|
Repo: ociRepoURL,
|
|
},
|
|
TargetRevision: refTargetRevision2,
|
|
},
|
|
}
|
|
return query
|
|
},
|
|
mockOpts: func(_ *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
paths.EXPECT().GetPath(repoURL).Return(repoRoot, nil)
|
|
paths.EXPECT().GetPathIfExists(ociRepoURL).Return("")
|
|
},
|
|
newGitClient: func(_ string, _ string, _ git.Creds, _ bool, _ bool, _ string, _ string, _ ...git.ClientOpts) (gitClient git.Client, e error) {
|
|
client := gitmocks.Client{}
|
|
return &client, nil
|
|
},
|
|
testResults: func(t *testing.T) {
|
|
t.Helper()
|
|
require.Error(t, fmt.Errorf("failed to find repo %q", ociRepoURL))
|
|
},
|
|
},
|
|
{
|
|
name: "unused_refsource_is_not_checked_out",
|
|
makeQuery: func() apiclient.RepoServerAppDetailsQuery {
|
|
q := queryTemplate
|
|
// Add a second ref source but do NOT reference it in ValueFiles.
|
|
q.RefSources = map[string]*v1alpha1.RefTarget{
|
|
refName: {
|
|
Repo: v1alpha1.Repository{
|
|
Type: "git",
|
|
Repo: refRepoURL,
|
|
},
|
|
TargetRevision: refTargetRevision,
|
|
},
|
|
refNameUnused: {
|
|
Repo: v1alpha1.Repository{
|
|
Type: "git",
|
|
Repo: unusedRefRepoURL,
|
|
},
|
|
TargetRevision: "main",
|
|
},
|
|
}
|
|
// Keep ValueFiles referencing only $values
|
|
q.Source.Helm.ValueFiles = []string{"$values/dir/values.yaml"}
|
|
return q
|
|
},
|
|
mockOpts: func(_ *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
paths.EXPECT().GetPath(refRepoURL).Return(refRoot, nil)
|
|
paths.EXPECT().GetPathIfExists(refRepoURL).Return(refRoot)
|
|
// No expectations for "https://github.com/foo/unused" on purpose: it should not be used.
|
|
},
|
|
newGitClient: func(repo string, _ string, _ git.Creds, _ bool, _ bool, _ string, _ string, _ ...git.ClientOpts) (git.Client, error) {
|
|
if repo == unusedRefRepoURL {
|
|
return nil, fmt.Errorf("newGitClient should not be called for unused ref source: %s", repo)
|
|
}
|
|
|
|
client := gitmocks.Client{}
|
|
client.EXPECT().LsRemote(refTargetRevision).Return(refSha, nil)
|
|
client.EXPECT().Root().Return(refRoot)
|
|
client.EXPECT().Init().Return(nil)
|
|
client.EXPECT().IsRevisionPresent(refSha).Return(true)
|
|
client.EXPECT().Checkout(refSha, false, true).Return("", nil)
|
|
return &client, nil
|
|
},
|
|
testResults: func(t *testing.T) {
|
|
t.Helper()
|
|
require.NoError(t, err)
|
|
assert.Len(t, res.Helm.Parameters, 1)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
query := tc.makeQuery()
|
|
service, _, _ := newServiceWithOpt(t, tc.mockOpts, ".")
|
|
service.newGitClient = tc.newGitClient
|
|
appPath, err = filepath.Abs(repoRoot)
|
|
require.NoError(t, err)
|
|
res = apiclient.RepoAppDetailsResponse{}
|
|
err = service.populateHelmAppDetails(&res, appPath, appPath, sha, "main", &query, service.gitRepoPaths)
|
|
tc.testResults(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_populateHelmAppDetails_values_symlinks(t *testing.T) {
|
|
service := newService(t, ".")
|
|
sha := "632039659e542ed7de0c170a4fcc1c571b288fc0"
|
|
emptyTempPaths := utilio.NewRandomizedTempPaths(t.TempDir())
|
|
t.Run("inbound", func(t *testing.T) {
|
|
res := apiclient.RepoAppDetailsResponse{}
|
|
q := apiclient.RepoServerAppDetailsQuery{Repo: &v1alpha1.Repository{}, Source: &v1alpha1.ApplicationSource{}}
|
|
err := service.populateHelmAppDetails(&res, "./testdata/in-bounds-values-file-link/", "./testdata/in-bounds-values-file-link/", "dummy_sha", "main", &q, emptyTempPaths)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, res.Helm.Values)
|
|
assert.NotEmpty(t, res.Helm.Parameters)
|
|
})
|
|
|
|
t.Run("out of bounds", func(t *testing.T) {
|
|
res := apiclient.RepoAppDetailsResponse{}
|
|
q := apiclient.RepoServerAppDetailsQuery{Repo: &v1alpha1.Repository{}, Source: &v1alpha1.ApplicationSource{}}
|
|
err := service.populateHelmAppDetails(&res, "./testdata/out-of-bounds-values-file-link/", "./testdata/out-of-bounds-values-file-link/", sha, "main", &q, emptyTempPaths)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, res.Helm.Values)
|
|
assert.Empty(t, res.Helm.Parameters)
|
|
})
|
|
}
|
|
|
|
func TestGetHelmRepos_OCIHelmDependenciesWithHelmRepo(t *testing.T) {
|
|
q := apiclient.ManifestRequest{Repos: []*v1alpha1.Repository{}, HelmRepoCreds: []*v1alpha1.RepoCreds{
|
|
{URL: "example.com", Username: "test", Password: "test", EnableOCI: true},
|
|
}}
|
|
|
|
helmRepos, err := getHelmRepos("./testdata/oci-dependencies", q.Repos, q.HelmRepoCreds)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, helmRepos, 1)
|
|
assert.Equal(t, "test", helmRepos[0].GetUsername())
|
|
assert.True(t, helmRepos[0].EnableOci)
|
|
assert.Equal(t, "example.com/myrepo", helmRepos[0].Repo)
|
|
}
|
|
|
|
func TestGetHelmRepos_OCIHelmDependenciesWithRepo(t *testing.T) {
|
|
q := apiclient.ManifestRequest{Repos: []*v1alpha1.Repository{{Repo: "example.com", Username: "test", Password: "test", EnableOCI: true}}, HelmRepoCreds: []*v1alpha1.RepoCreds{}}
|
|
|
|
helmRepos, err := getHelmRepos("./testdata/oci-dependencies", q.Repos, q.HelmRepoCreds)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, helmRepos, 1)
|
|
assert.Equal(t, "test", helmRepos[0].GetUsername())
|
|
assert.True(t, helmRepos[0].EnableOci)
|
|
assert.Equal(t, "example.com/myrepo", helmRepos[0].Repo)
|
|
}
|
|
|
|
func TestGetHelmRepos_OCIDependenciesWithHelmRepo(t *testing.T) {
|
|
q := apiclient.ManifestRequest{Repos: []*v1alpha1.Repository{}, HelmRepoCreds: []*v1alpha1.RepoCreds{
|
|
{URL: "oci://example.com", Username: "test", Password: "test", Type: "oci"},
|
|
}}
|
|
|
|
helmRepos, err := getHelmRepos("./testdata/oci-dependencies", q.Repos, q.HelmRepoCreds)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, helmRepos, 1)
|
|
assert.Equal(t, "test", helmRepos[0].GetUsername())
|
|
assert.True(t, helmRepos[0].EnableOci)
|
|
assert.Equal(t, "example.com/myrepo", helmRepos[0].Repo)
|
|
}
|
|
|
|
func TestGetHelmRepos_OCIDependenciesWithRepo(t *testing.T) {
|
|
q := apiclient.ManifestRequest{Repos: []*v1alpha1.Repository{{Repo: "oci://example.com", Username: "test", Password: "test", Type: "oci"}}, HelmRepoCreds: []*v1alpha1.RepoCreds{}}
|
|
|
|
helmRepos, err := getHelmRepos("./testdata/oci-dependencies", q.Repos, q.HelmRepoCreds)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, helmRepos, 1)
|
|
assert.Equal(t, "test", helmRepos[0].GetUsername())
|
|
assert.True(t, helmRepos[0].EnableOci)
|
|
assert.Equal(t, "example.com/myrepo", helmRepos[0].Repo)
|
|
}
|
|
|
|
func TestGetHelmRepo_NamedRepos(t *testing.T) {
|
|
q := apiclient.ManifestRequest{
|
|
Repos: []*v1alpha1.Repository{{
|
|
Name: "custom-repo",
|
|
Repo: "https://example.com",
|
|
Username: "test",
|
|
}},
|
|
}
|
|
|
|
helmRepos, err := getHelmRepos("./testdata/helm-with-dependencies", q.Repos, q.HelmRepoCreds)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, helmRepos, 1)
|
|
assert.Equal(t, "test", helmRepos[0].GetUsername())
|
|
assert.Equal(t, "https://example.com", helmRepos[0].Repo)
|
|
}
|
|
|
|
func TestGetHelmRepo_NamedReposAlias(t *testing.T) {
|
|
q := apiclient.ManifestRequest{
|
|
Repos: []*v1alpha1.Repository{{
|
|
Name: "custom-repo-alias",
|
|
Repo: "https://example.com",
|
|
Username: "test-alias",
|
|
}},
|
|
}
|
|
|
|
helmRepos, err := getHelmRepos("./testdata/helm-with-dependencies-alias", q.Repos, q.HelmRepoCreds)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, helmRepos, 1)
|
|
assert.Equal(t, "test-alias", helmRepos[0].GetUsername())
|
|
assert.Equal(t, "https://example.com", helmRepos[0].Repo)
|
|
}
|
|
|
|
func Test_getResolvedValueFiles(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tempDir := t.TempDir()
|
|
paths := utilio.NewRandomizedTempPaths(tempDir)
|
|
|
|
paths.Add(git.NormalizeGitURL("https://github.com/org/repo1"), path.Join(tempDir, "repo1"))
|
|
|
|
testCases := []struct {
|
|
name string
|
|
rawPath string
|
|
env *v1alpha1.Env
|
|
refSources map[string]*v1alpha1.RefTarget
|
|
expectedPath string
|
|
expectedErr bool
|
|
}{
|
|
{
|
|
name: "simple path",
|
|
rawPath: "values.yaml",
|
|
env: &v1alpha1.Env{},
|
|
refSources: map[string]*v1alpha1.RefTarget{},
|
|
expectedPath: path.Join(tempDir, "main-repo", "values.yaml"),
|
|
},
|
|
{
|
|
name: "simple ref",
|
|
rawPath: "$ref/values.yaml",
|
|
env: &v1alpha1.Env{},
|
|
refSources: map[string]*v1alpha1.RefTarget{
|
|
"$ref": {
|
|
Repo: v1alpha1.Repository{
|
|
Repo: "https://github.com/org/repo1",
|
|
},
|
|
},
|
|
},
|
|
expectedPath: path.Join(tempDir, "repo1", "values.yaml"),
|
|
},
|
|
{
|
|
name: "only ref",
|
|
rawPath: "$ref",
|
|
env: &v1alpha1.Env{},
|
|
refSources: map[string]*v1alpha1.RefTarget{
|
|
"$ref": {
|
|
Repo: v1alpha1.Repository{
|
|
Repo: "https://github.com/org/repo1",
|
|
},
|
|
},
|
|
},
|
|
expectedErr: true,
|
|
},
|
|
{
|
|
name: "attempted traversal",
|
|
rawPath: "$ref/../values.yaml",
|
|
env: &v1alpha1.Env{},
|
|
refSources: map[string]*v1alpha1.RefTarget{
|
|
"$ref": {
|
|
Repo: v1alpha1.Repository{
|
|
Repo: "https://github.com/org/repo1",
|
|
},
|
|
},
|
|
},
|
|
expectedErr: true,
|
|
},
|
|
{
|
|
// Since $ref doesn't resolve to a ref target, we assume it's an env var. Since the env var isn't specified,
|
|
// it's replaced with an empty string. This is necessary for backwards compatibility with behavior before
|
|
// ref targets were introduced.
|
|
name: "ref doesn't exist",
|
|
rawPath: "$ref/values.yaml",
|
|
env: &v1alpha1.Env{},
|
|
refSources: map[string]*v1alpha1.RefTarget{},
|
|
expectedPath: path.Join(tempDir, "main-repo", "values.yaml"),
|
|
},
|
|
{
|
|
name: "repo doesn't exist",
|
|
rawPath: "$ref/values.yaml",
|
|
env: &v1alpha1.Env{},
|
|
refSources: map[string]*v1alpha1.RefTarget{
|
|
"$ref": {
|
|
Repo: v1alpha1.Repository{
|
|
Repo: "https://github.com/org/repo2",
|
|
},
|
|
},
|
|
},
|
|
expectedErr: true,
|
|
},
|
|
{
|
|
name: "env var is resolved",
|
|
rawPath: "$ref/$APP_PATH/values.yaml",
|
|
env: &v1alpha1.Env{
|
|
&v1alpha1.EnvEntry{
|
|
Name: "APP_PATH",
|
|
Value: "app-path",
|
|
},
|
|
},
|
|
refSources: map[string]*v1alpha1.RefTarget{
|
|
"$ref": {
|
|
Repo: v1alpha1.Repository{
|
|
Repo: "https://github.com/org/repo1",
|
|
},
|
|
},
|
|
},
|
|
expectedPath: path.Join(tempDir, "repo1", "app-path", "values.yaml"),
|
|
},
|
|
{
|
|
name: "traversal in env var is blocked",
|
|
rawPath: "$ref/$APP_PATH/values.yaml",
|
|
env: &v1alpha1.Env{
|
|
&v1alpha1.EnvEntry{
|
|
Name: "APP_PATH",
|
|
Value: "..",
|
|
},
|
|
},
|
|
refSources: map[string]*v1alpha1.RefTarget{
|
|
"$ref": {
|
|
Repo: v1alpha1.Repository{
|
|
Repo: "https://github.com/org/repo1",
|
|
},
|
|
},
|
|
},
|
|
expectedErr: true,
|
|
},
|
|
{
|
|
name: "env var prefix",
|
|
rawPath: "$APP_PATH/values.yaml",
|
|
env: &v1alpha1.Env{
|
|
&v1alpha1.EnvEntry{
|
|
Name: "APP_PATH",
|
|
Value: "app-path",
|
|
},
|
|
},
|
|
refSources: map[string]*v1alpha1.RefTarget{},
|
|
expectedPath: path.Join(tempDir, "main-repo", "app-path", "values.yaml"),
|
|
},
|
|
{
|
|
name: "unresolved env var",
|
|
rawPath: "$APP_PATH/values.yaml",
|
|
env: &v1alpha1.Env{},
|
|
refSources: map[string]*v1alpha1.RefTarget{},
|
|
expectedPath: path.Join(tempDir, "main-repo", "values.yaml"),
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tcc := tc
|
|
t.Run(tcc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
resolvedPaths, err := getResolvedValueFiles(path.Join(tempDir, "main-repo"), path.Join(tempDir, "main-repo"), tcc.env, []string{}, []string{tcc.rawPath}, tcc.refSources, paths, false)
|
|
if !tcc.expectedErr {
|
|
require.NoError(t, err)
|
|
require.Len(t, resolvedPaths, 1)
|
|
assert.Equal(t, tcc.expectedPath, string(resolvedPaths[0]))
|
|
} else {
|
|
require.Error(t, err)
|
|
assert.Empty(t, resolvedPaths)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_getResolvedValueFiles_glob(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tempDir := t.TempDir()
|
|
paths := utilio.NewRandomizedTempPaths(tempDir)
|
|
paths.Add(git.NormalizeGitURL("https://github.com/org/repo1"), path.Join(tempDir, "repo1"))
|
|
|
|
// main-repo files
|
|
require.NoError(t, os.MkdirAll(path.Join(tempDir, "main-repo", "prod", "nested"), 0o755))
|
|
require.NoError(t, os.MkdirAll(path.Join(tempDir, "main-repo", "staging"), 0o755))
|
|
require.NoError(t, os.WriteFile(path.Join(tempDir, "main-repo", "prod", "a.yaml"), []byte{}, 0o644))
|
|
require.NoError(t, os.WriteFile(path.Join(tempDir, "main-repo", "prod", "b.yaml"), []byte{}, 0o644))
|
|
require.NoError(t, os.WriteFile(path.Join(tempDir, "main-repo", "prod", "nested", "c.yaml"), []byte{}, 0o644))
|
|
require.NoError(t, os.WriteFile(path.Join(tempDir, "main-repo", "prod", "nested", "d.yaml"), []byte{}, 0o644))
|
|
require.NoError(t, os.WriteFile(path.Join(tempDir, "main-repo", "staging", "e.yaml"), []byte{}, 0o644))
|
|
|
|
// main-repo envs: used to verify depth-order with ** (z.yaml sorts after nested/ alphabetically
|
|
// but is still returned before nested/c.yaml because doublestar matches depth-0 files first).
|
|
require.NoError(t, os.MkdirAll(path.Join(tempDir, "main-repo", "envs", "nested"), 0o755))
|
|
require.NoError(t, os.WriteFile(path.Join(tempDir, "main-repo", "envs", "a.yaml"), []byte{}, 0o644))
|
|
require.NoError(t, os.WriteFile(path.Join(tempDir, "main-repo", "envs", "z.yaml"), []byte{}, 0o644))
|
|
require.NoError(t, os.WriteFile(path.Join(tempDir, "main-repo", "envs", "nested", "c.yaml"), []byte{}, 0o644))
|
|
|
|
// repo1 files
|
|
require.NoError(t, os.MkdirAll(path.Join(tempDir, "repo1", "prod", "nested"), 0o755))
|
|
require.NoError(t, os.WriteFile(path.Join(tempDir, "repo1", "prod", "x.yaml"), []byte{}, 0o644))
|
|
require.NoError(t, os.WriteFile(path.Join(tempDir, "repo1", "prod", "y.yaml"), []byte{}, 0o644))
|
|
require.NoError(t, os.WriteFile(path.Join(tempDir, "repo1", "prod", "nested", "z.yaml"), []byte{}, 0o644))
|
|
|
|
tests := []struct {
|
|
name string
|
|
rawPath string
|
|
env *v1alpha1.Env
|
|
refSources map[string]*v1alpha1.RefTarget
|
|
expectedPaths []string
|
|
ignoreMissingValueFiles bool
|
|
expectedErr bool
|
|
}{
|
|
{
|
|
name: "local glob matches multiple files",
|
|
rawPath: "prod/*.yaml",
|
|
env: &v1alpha1.Env{},
|
|
refSources: map[string]*v1alpha1.RefTarget{},
|
|
expectedPaths: []string{
|
|
// the order is a.yaml before b.yaml
|
|
// since doublestar.FilepathGlob returns lexical order
|
|
path.Join(tempDir, "main-repo", "prod", "a.yaml"),
|
|
path.Join(tempDir, "main-repo", "prod", "b.yaml"),
|
|
},
|
|
},
|
|
{
|
|
name: "local glob matches no files returns error",
|
|
rawPath: "dev/*.yaml",
|
|
env: &v1alpha1.Env{},
|
|
refSources: map[string]*v1alpha1.RefTarget{},
|
|
expectedPaths: nil,
|
|
expectedErr: true,
|
|
},
|
|
{
|
|
name: "local glob matches no files with ignoreMissingValueFiles set to true",
|
|
rawPath: "dev/*.yaml",
|
|
env: &v1alpha1.Env{},
|
|
refSources: map[string]*v1alpha1.RefTarget{},
|
|
ignoreMissingValueFiles: true,
|
|
expectedPaths: nil,
|
|
},
|
|
{
|
|
name: "referenced glob matches multiple files in external repo",
|
|
rawPath: "$ref/prod/*.yaml",
|
|
env: &v1alpha1.Env{},
|
|
refSources: map[string]*v1alpha1.RefTarget{
|
|
"$ref": {
|
|
Repo: v1alpha1.Repository{
|
|
Repo: "https://github.com/org/repo1",
|
|
},
|
|
},
|
|
},
|
|
expectedPaths: []string{
|
|
path.Join(tempDir, "repo1", "prod", "x.yaml"),
|
|
path.Join(tempDir, "repo1", "prod", "y.yaml"),
|
|
},
|
|
},
|
|
{
|
|
name: "ref glob with env var in path",
|
|
rawPath: "$ref/$ENV/*.yaml",
|
|
env: &v1alpha1.Env{
|
|
&v1alpha1.EnvEntry{
|
|
Name: "ENV",
|
|
Value: "prod",
|
|
},
|
|
},
|
|
refSources: map[string]*v1alpha1.RefTarget{
|
|
"$ref": {
|
|
Repo: v1alpha1.Repository{
|
|
Repo: "https://github.com/org/repo1",
|
|
},
|
|
},
|
|
},
|
|
expectedPaths: []string{
|
|
path.Join(tempDir, "repo1", "prod", "x.yaml"),
|
|
path.Join(tempDir, "repo1", "prod", "y.yaml"),
|
|
},
|
|
},
|
|
{
|
|
name: "local glob single match",
|
|
rawPath: "prod/a*.yaml",
|
|
env: &v1alpha1.Env{},
|
|
refSources: map[string]*v1alpha1.RefTarget{},
|
|
expectedPaths: []string{path.Join(tempDir, "main-repo", "prod", "a.yaml")},
|
|
},
|
|
{
|
|
name: "recursive glob matches files at all depths under a subdirectory",
|
|
// ** matches zero or more path segments, so prod/**/*.yaml covers both
|
|
// prod/*.yaml (zero intermediate segments) and prod/nested/*.yaml (one segment), etc.
|
|
rawPath: "prod/**/*.yaml",
|
|
env: &v1alpha1.Env{},
|
|
refSources: map[string]*v1alpha1.RefTarget{},
|
|
// lexical order: prod/a.yaml, prod/b.yaml, prod/nested/c.yaml, prod/nested/d.yaml
|
|
expectedPaths: []string{
|
|
path.Join(tempDir, "main-repo", "prod", "a.yaml"),
|
|
path.Join(tempDir, "main-repo", "prod", "b.yaml"),
|
|
path.Join(tempDir, "main-repo", "prod", "nested", "c.yaml"),
|
|
path.Join(tempDir, "main-repo", "prod", "nested", "d.yaml"),
|
|
},
|
|
},
|
|
{
|
|
name: "recursive glob from repo root matches yaml files across all directories",
|
|
rawPath: "**/*.yaml",
|
|
env: &v1alpha1.Env{},
|
|
refSources: map[string]*v1alpha1.RefTarget{},
|
|
// doublestar traverses directories in lexical order, processing each directory's
|
|
// own files before its subdirectories. So the order is:
|
|
// envs/ flat files → envs/nested/ files → prod/ flat files → prod/nested/ files → staging/ files
|
|
expectedPaths: []string{
|
|
path.Join(tempDir, "main-repo", "envs", "a.yaml"),
|
|
path.Join(tempDir, "main-repo", "envs", "z.yaml"),
|
|
path.Join(tempDir, "main-repo", "envs", "nested", "c.yaml"),
|
|
path.Join(tempDir, "main-repo", "prod", "a.yaml"),
|
|
path.Join(tempDir, "main-repo", "prod", "b.yaml"),
|
|
path.Join(tempDir, "main-repo", "prod", "nested", "c.yaml"),
|
|
path.Join(tempDir, "main-repo", "prod", "nested", "d.yaml"),
|
|
path.Join(tempDir, "main-repo", "staging", "e.yaml"),
|
|
},
|
|
},
|
|
{
|
|
name: "recursive glob anchored to a named subdirectory matches at any depth",
|
|
rawPath: "**/nested/*.yaml",
|
|
env: &v1alpha1.Env{},
|
|
refSources: map[string]*v1alpha1.RefTarget{},
|
|
expectedPaths: []string{
|
|
path.Join(tempDir, "main-repo", "envs", "nested", "c.yaml"),
|
|
path.Join(tempDir, "main-repo", "prod", "nested", "c.yaml"),
|
|
path.Join(tempDir, "main-repo", "prod", "nested", "d.yaml"),
|
|
},
|
|
},
|
|
{
|
|
name: "recursive glob with no matches and ignoreMissingValueFiles skips silently",
|
|
rawPath: "**/nonexistent/*.yaml",
|
|
env: &v1alpha1.Env{},
|
|
refSources: map[string]*v1alpha1.RefTarget{},
|
|
ignoreMissingValueFiles: true,
|
|
expectedPaths: nil,
|
|
},
|
|
{
|
|
name: "recursive glob with no matches returns error",
|
|
rawPath: "**/nonexistent/*.yaml",
|
|
env: &v1alpha1.Env{},
|
|
refSources: map[string]*v1alpha1.RefTarget{},
|
|
expectedPaths: nil,
|
|
expectedErr: true,
|
|
},
|
|
{
|
|
// z.yaml sorts after "nested/" alphabetically by full path, but doublestar processes
|
|
// each directory's own files before descending into subdirectories. So for envs/**/*.yaml:
|
|
// envs/ flat files (a, z) come before envs/nested/ files (c), giving:
|
|
// a.yaml, z.yaml, nested/c.yaml — not a.yaml, nested/c.yaml, z.yaml.
|
|
name: "** depth-order: flat files before nested even when flat file sorts after nested/ alphabetically",
|
|
rawPath: "envs/**/*.yaml",
|
|
env: &v1alpha1.Env{},
|
|
refSources: map[string]*v1alpha1.RefTarget{},
|
|
expectedPaths: []string{
|
|
path.Join(tempDir, "main-repo", "envs", "a.yaml"),
|
|
path.Join(tempDir, "main-repo", "envs", "z.yaml"),
|
|
path.Join(tempDir, "main-repo", "envs", "nested", "c.yaml"),
|
|
},
|
|
},
|
|
{
|
|
name: "recursive glob in external ref repo",
|
|
rawPath: "$ref/prod/**/*.yaml",
|
|
env: &v1alpha1.Env{},
|
|
refSources: map[string]*v1alpha1.RefTarget{
|
|
"$ref": {
|
|
Repo: v1alpha1.Repository{
|
|
Repo: "https://github.com/org/repo1",
|
|
},
|
|
},
|
|
},
|
|
expectedPaths: []string{
|
|
// doublestar matches zero path segments before recursing into subdirectories,
|
|
// so flat files (x, y) come before nested ones (nested/z).
|
|
path.Join(tempDir, "repo1", "prod", "x.yaml"),
|
|
path.Join(tempDir, "repo1", "prod", "y.yaml"),
|
|
path.Join(tempDir, "repo1", "prod", "nested", "z.yaml"),
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
repoPath := path.Join(tempDir, "main-repo")
|
|
resolvedPaths, err := getResolvedValueFiles(repoPath, repoPath, tt.env, []string{}, []string{tt.rawPath}, tt.refSources, paths, tt.ignoreMissingValueFiles)
|
|
if tt.expectedErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.Len(t, resolvedPaths, len(tt.expectedPaths))
|
|
|
|
for i, p := range tt.expectedPaths {
|
|
assert.Equal(t, p, string(resolvedPaths[i]))
|
|
}
|
|
})
|
|
}
|
|
|
|
// Deduplication: first occurrence of a resolved path wins. Subsequent references to the
|
|
// same file, whether explicit or via glob are silently dropped. This preserves the
|
|
// merge-precedence position set by the first mention of each file.
|
|
t.Run("glob then explicit: explicit entry placed at end, giving it highest Helm precedence", func(t *testing.T) {
|
|
t.Parallel()
|
|
repoPath := path.Join(tempDir, "main-repo")
|
|
resolvedPaths, err := getResolvedValueFiles(
|
|
repoPath, repoPath,
|
|
&v1alpha1.Env{}, []string{},
|
|
[]string{
|
|
"envs/*.yaml", // glob - z.yaml is explicit so skipped; only a.yaml added
|
|
"envs/z.yaml", // explicit - placed last, highest precedence
|
|
},
|
|
map[string]*v1alpha1.RefTarget{}, paths, false,
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, resolvedPaths, 2)
|
|
assert.Equal(t, path.Join(tempDir, "main-repo", "envs", "a.yaml"), string(resolvedPaths[0]))
|
|
assert.Equal(t, path.Join(tempDir, "main-repo", "envs", "z.yaml"), string(resolvedPaths[1]))
|
|
})
|
|
|
|
t.Run("explicit path before glob: explicit position is kept, glob re-match is dropped", func(t *testing.T) {
|
|
t.Parallel()
|
|
repoPath := path.Join(tempDir, "main-repo")
|
|
resolvedPaths, err := getResolvedValueFiles(
|
|
repoPath, repoPath,
|
|
&v1alpha1.Env{}, []string{},
|
|
[]string{
|
|
"prod/a.yaml", // explicit locks in position 0
|
|
"prod/*.yaml", // glob - a.yaml already seen, only b.yaml is new
|
|
},
|
|
map[string]*v1alpha1.RefTarget{}, paths, false,
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, resolvedPaths, 2)
|
|
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "a.yaml"), string(resolvedPaths[0]))
|
|
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "b.yaml"), string(resolvedPaths[1]))
|
|
})
|
|
|
|
t.Run("glob before explicit path: explicit position wins, glob skips the explicitly listed file", func(t *testing.T) {
|
|
t.Parallel()
|
|
repoPath := path.Join(tempDir, "main-repo")
|
|
resolvedPaths, err := getResolvedValueFiles(
|
|
repoPath, repoPath,
|
|
&v1alpha1.Env{}, []string{},
|
|
[]string{
|
|
"prod/*.yaml", // glob - a.yaml is explicit so skipped; only b.yaml added (pos 0)
|
|
"prod/a.yaml", // explicit - placed here at pos 1 (highest precedence)
|
|
},
|
|
map[string]*v1alpha1.RefTarget{}, paths, false,
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, resolvedPaths, 2)
|
|
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "b.yaml"), string(resolvedPaths[0]))
|
|
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "a.yaml"), string(resolvedPaths[1]))
|
|
})
|
|
|
|
t.Run("two overlapping globs: second glob only adds files not matched by first", func(t *testing.T) {
|
|
t.Parallel()
|
|
repoPath := path.Join(tempDir, "main-repo")
|
|
resolvedPaths, err := getResolvedValueFiles(
|
|
repoPath, repoPath,
|
|
&v1alpha1.Env{}, []string{},
|
|
[]string{
|
|
"prod/*.yaml", // adds a.yaml, b.yaml
|
|
"prod/**/*.yaml", // a.yaml, b.yaml already seen; adds nested/c.yaml, nested/d.yaml
|
|
},
|
|
map[string]*v1alpha1.RefTarget{}, paths, false,
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, resolvedPaths, 4)
|
|
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "a.yaml"), string(resolvedPaths[0]))
|
|
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "b.yaml"), string(resolvedPaths[1]))
|
|
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "nested", "c.yaml"), string(resolvedPaths[2]))
|
|
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "nested", "d.yaml"), string(resolvedPaths[3]))
|
|
})
|
|
|
|
t.Run("explicit paths take priority: globs skip explicitly listed files, which are placed at their explicit positions", func(t *testing.T) {
|
|
t.Parallel()
|
|
repoPath := path.Join(tempDir, "main-repo")
|
|
resolvedPaths, err := getResolvedValueFiles(
|
|
repoPath, repoPath,
|
|
&v1alpha1.Env{}, []string{},
|
|
[]string{
|
|
"prod/a.yaml", // explicit - pos 0
|
|
"prod/*.yaml", // a.yaml and b.yaml are both explicit, skipped entirely
|
|
"prod/b.yaml", // explicit - pos 1
|
|
"prod/**/*.yaml", // a.yaml, b.yaml, nested/c.yaml all explicit and skipped; nested/d.yaml added - pos 2
|
|
"prod/nested/c.yaml", // explicit - pos 3
|
|
},
|
|
map[string]*v1alpha1.RefTarget{}, paths, false,
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, resolvedPaths, 4)
|
|
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "a.yaml"), string(resolvedPaths[0]))
|
|
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "b.yaml"), string(resolvedPaths[1]))
|
|
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "nested", "d.yaml"), string(resolvedPaths[2]))
|
|
assert.Equal(t, path.Join(tempDir, "main-repo", "prod", "nested", "c.yaml"), string(resolvedPaths[3]))
|
|
})
|
|
}
|
|
|
|
func Test_verifyGlobMatchesWithinRoot(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tempDir := t.TempDir()
|
|
repoDir := filepath.Join(tempDir, "repo")
|
|
outsideDir := filepath.Join(tempDir, "outside")
|
|
|
|
require.NoError(t, os.MkdirAll(filepath.Join(repoDir, "values", "sub"), 0o755))
|
|
require.NoError(t, os.MkdirAll(outsideDir, 0o755))
|
|
|
|
// Files used as symlink targets
|
|
inRepoFile := filepath.Join(repoDir, "values", "real.yaml")
|
|
outsideFile := filepath.Join(outsideDir, "secret.yaml")
|
|
require.NoError(t, os.WriteFile(inRepoFile, []byte{}, 0o644))
|
|
require.NoError(t, os.WriteFile(outsideFile, []byte("password: hunter2"), 0o644))
|
|
|
|
// Symlink inside repo → file inside repo (safe)
|
|
inRepoLink := filepath.Join(repoDir, "values", "inrepo-link.yaml")
|
|
require.NoError(t, os.Symlink(inRepoFile, inRepoLink))
|
|
|
|
// Symlink inside repo → file outside repo (escape)
|
|
escapeLink := filepath.Join(repoDir, "values", "escape-link.yaml")
|
|
require.NoError(t, os.Symlink(outsideFile, escapeLink))
|
|
|
|
// Two-hop symlink: inside repo → another symlink (still inside) → file inside repo
|
|
hop1 := filepath.Join(repoDir, "values", "hop1.yaml")
|
|
require.NoError(t, os.Symlink(inRepoLink, hop1)) // hop1 → inRepoLink → real.yaml
|
|
|
|
// Two-hop symlink: inside repo → another symlink (inside repo) → file outside repo
|
|
hop2 := filepath.Join(repoDir, "values", "hop2.yaml")
|
|
require.NoError(t, os.Symlink(escapeLink, hop2)) // hop2 → escape-link → secret.yaml
|
|
|
|
tests := []struct {
|
|
name string
|
|
matches []string
|
|
expectErr bool
|
|
errContains string
|
|
}{
|
|
{
|
|
name: "regular file inside root passes",
|
|
matches: []string{inRepoFile},
|
|
},
|
|
{
|
|
name: "symlink inside root pointing to file inside root passes",
|
|
matches: []string{inRepoLink},
|
|
},
|
|
{
|
|
name: "two-hop chain that stays within root passes",
|
|
matches: []string{hop1},
|
|
},
|
|
{
|
|
name: "symlink pointing directly outside root is rejected",
|
|
matches: []string{escapeLink},
|
|
expectErr: true,
|
|
errContains: "resolved to outside repository root",
|
|
},
|
|
{
|
|
name: "two-hop chain that escapes root is rejected",
|
|
matches: []string{hop2},
|
|
expectErr: true,
|
|
errContains: "resolved to outside repository root",
|
|
},
|
|
{
|
|
name: "multiple matches all inside root pass",
|
|
matches: []string{inRepoFile, inRepoLink, hop1},
|
|
},
|
|
{
|
|
name: "one bad match in a list fails the whole call",
|
|
matches: []string{inRepoFile, escapeLink},
|
|
expectErr: true,
|
|
errContains: "resolved to outside repository root",
|
|
},
|
|
{
|
|
name: "empty matches list is a no-op",
|
|
matches: []string{},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
err := verifyGlobMatchesWithinRoot(tt.matches, repoDir)
|
|
if tt.expectErr {
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.errContains)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test_getResolvedValueFiles_glob_symlink_escape is an integration-level check
|
|
// that verifyGlobMatchesWithinRoot is wired into glob expansion correctly: a
|
|
// symlink inside the repo pointing outside must cause getResolvedValueFiles to
|
|
// return an error rather than silently including the external file.
|
|
func Test_getResolvedValueFiles_glob_symlink_escape(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tempDir := t.TempDir()
|
|
paths := utilio.NewRandomizedTempPaths(tempDir)
|
|
|
|
repoDir := filepath.Join(tempDir, "repo")
|
|
outsideDir := filepath.Join(tempDir, "outside")
|
|
|
|
require.NoError(t, os.MkdirAll(filepath.Join(repoDir, "values"), 0o755))
|
|
require.NoError(t, os.MkdirAll(outsideDir, 0o755))
|
|
|
|
require.NoError(t, os.WriteFile(filepath.Join(repoDir, "values", "base.yaml"), []byte{}, 0o644))
|
|
require.NoError(t, os.WriteFile(filepath.Join(outsideDir, "secret.yaml"), []byte("password: hunter2"), 0o644))
|
|
require.NoError(t, os.Symlink(filepath.Join(outsideDir, "secret.yaml"), filepath.Join(repoDir, "values", "escape.yaml")))
|
|
|
|
_, err := getResolvedValueFiles(repoDir, repoDir, &v1alpha1.Env{}, []string{}, []string{"values/*.yaml"}, map[string]*v1alpha1.RefTarget{}, paths, false)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "resolved to outside repository root")
|
|
}
|
|
|
|
func Test_isGlobPath(t *testing.T) {
|
|
tests := []struct {
|
|
path string
|
|
expected bool
|
|
}{
|
|
{
|
|
path: "prod/*.yaml",
|
|
expected: true,
|
|
},
|
|
{
|
|
path: "prod/?.yaml",
|
|
expected: true,
|
|
},
|
|
{
|
|
path: "prod[ab].yaml",
|
|
expected: true,
|
|
},
|
|
{
|
|
path: "prod/**/*.yaml",
|
|
expected: true,
|
|
},
|
|
{
|
|
path: "prod/values.yaml",
|
|
},
|
|
{
|
|
path: "values.yaml",
|
|
},
|
|
{
|
|
path: "",
|
|
},
|
|
{
|
|
path: "/absolute/path/to/*.yaml",
|
|
expected: true,
|
|
},
|
|
{
|
|
path: "/absolute/path/to/values.yaml",
|
|
},
|
|
{
|
|
path: "*",
|
|
expected: true,
|
|
},
|
|
{
|
|
path: "?",
|
|
expected: true,
|
|
},
|
|
{
|
|
path: "[",
|
|
expected: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.path, func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, isGlobPath(tt.path))
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_getReferencedSource(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
refTarget := &v1alpha1.RefTarget{
|
|
Repo: v1alpha1.Repository{
|
|
Repo: "https://github.com/org/repo1",
|
|
},
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
rawValueFile string
|
|
refSources map[string]*v1alpha1.RefTarget
|
|
expected *v1alpha1.RefTarget
|
|
}{
|
|
{
|
|
name: "ref with file path found in map",
|
|
rawValueFile: "$ref/values.yaml",
|
|
refSources: map[string]*v1alpha1.RefTarget{
|
|
"$ref": refTarget,
|
|
},
|
|
expected: refTarget,
|
|
},
|
|
{
|
|
name: "ref with file path not in map",
|
|
rawValueFile: "$ref/values.yaml",
|
|
refSources: map[string]*v1alpha1.RefTarget{},
|
|
expected: nil,
|
|
},
|
|
{
|
|
name: "bare ref without file path found in map",
|
|
rawValueFile: "$ref",
|
|
refSources: map[string]*v1alpha1.RefTarget{
|
|
"$ref": refTarget,
|
|
},
|
|
expected: refTarget,
|
|
},
|
|
{
|
|
name: "empty string returns nil",
|
|
rawValueFile: "",
|
|
refSources: map[string]*v1alpha1.RefTarget{
|
|
"$ref": refTarget,
|
|
},
|
|
expected: nil,
|
|
},
|
|
{
|
|
name: "no $ prefix returns nil",
|
|
rawValueFile: "values.yaml",
|
|
refSources: map[string]*v1alpha1.RefTarget{
|
|
"$ref": refTarget,
|
|
},
|
|
expected: nil,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
result := getReferencedSource(tt.rawValueFile, tt.refSources)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestErrorGetGitDirectories(t *testing.T) {
|
|
// test not using the cache
|
|
root := "./testdata/git-files-dirs"
|
|
|
|
type fields struct {
|
|
service *Service
|
|
}
|
|
type args struct {
|
|
ctx context.Context
|
|
request *apiclient.GitDirectoriesRequest
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
want *apiclient.GitDirectoriesResponse
|
|
wantErr assert.ErrorAssertionFunc
|
|
}{
|
|
{name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{
|
|
ctx: t.Context(),
|
|
request: &apiclient.GitDirectoriesRequest{
|
|
Repo: nil,
|
|
SubmoduleEnabled: false,
|
|
Revision: "HEAD",
|
|
},
|
|
}, want: nil, wantErr: assert.Error},
|
|
{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
|
|
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
|
|
gitClient.EXPECT().LsRemote(mock.Anything).Return("", errors.New("ah error"))
|
|
gitClient.EXPECT().Root().Return(root)
|
|
paths.EXPECT().GetPath(mock.Anything).Return(".", nil)
|
|
paths.EXPECT().GetPathIfExists(mock.Anything).Return(".")
|
|
}, ".")
|
|
return s
|
|
}()}, args: args{
|
|
ctx: t.Context(),
|
|
request: &apiclient.GitDirectoriesRequest{
|
|
Repo: &v1alpha1.Repository{Repo: "not-a-valid-url"},
|
|
SubmoduleEnabled: false,
|
|
Revision: "sadfsadf",
|
|
},
|
|
}, want: nil, wantErr: assert.Error},
|
|
{name: "ErrorVerifyCommit", fields: fields{service: func() *Service {
|
|
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
|
|
gitClient.EXPECT().LsRemote(mock.Anything).Return("", errors.New("ah error"))
|
|
gitClient.EXPECT().VerifyCommitSignature(mock.Anything).Return("", fmt.Errorf("revision %s is not signed", "sadfsadf"))
|
|
gitClient.EXPECT().Root().Return(root)
|
|
paths.EXPECT().GetPath(mock.Anything).Return(".", nil)
|
|
paths.EXPECT().GetPathIfExists(mock.Anything).Return(".")
|
|
}, ".")
|
|
return s
|
|
}()}, args: args{
|
|
ctx: t.Context(),
|
|
request: &apiclient.GitDirectoriesRequest{
|
|
Repo: &v1alpha1.Repository{Repo: "not-a-valid-url"},
|
|
SubmoduleEnabled: false,
|
|
Revision: "sadfsadf",
|
|
VerifyCommit: true,
|
|
},
|
|
}, want: nil, wantErr: assert.Error},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
s := tt.fields.service
|
|
got, err := s.GetGitDirectories(tt.args.ctx, tt.args.request)
|
|
if !tt.wantErr(t, err, fmt.Sprintf("GetGitDirectories(%v, %v)", tt.args.ctx, tt.args.request)) {
|
|
return
|
|
}
|
|
assert.Equalf(t, tt.want, got, "GetGitDirectories(%v, %v)", tt.args.ctx, tt.args.request)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetGitDirectories(t *testing.T) {
|
|
// test not using the cache
|
|
root := "./testdata/git-files-dirs"
|
|
s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().Init().Return(nil)
|
|
gitClient.EXPECT().IsRevisionPresent(mock.Anything).Return(false)
|
|
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Return(nil)
|
|
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Once().Return("", nil)
|
|
gitClient.EXPECT().LsRemote("HEAD").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
|
gitClient.EXPECT().Root().Return(root)
|
|
paths.EXPECT().GetPath(mock.Anything).Return(root, nil)
|
|
paths.EXPECT().GetPathIfExists(mock.Anything).Return(root)
|
|
}, root)
|
|
dirRequest := &apiclient.GitDirectoriesRequest{
|
|
Repo: &v1alpha1.Repository{Repo: "a-url.com"},
|
|
SubmoduleEnabled: false,
|
|
Revision: "HEAD",
|
|
}
|
|
directories, err := s.GetGitDirectories(t.Context(), dirRequest)
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, directories.GetPaths(), []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo"})
|
|
|
|
// do the same request again to use the cache
|
|
// we only allow CheckOut to be called once in the mock
|
|
directories, err = s.GetGitDirectories(t.Context(), dirRequest)
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo"}, directories.GetPaths())
|
|
cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
|
|
ExternalSets: 1,
|
|
ExternalGets: 2,
|
|
})
|
|
}
|
|
|
|
func TestGetGitDirectoriesWithHiddenDirSupported(t *testing.T) {
|
|
// test not using the cache
|
|
root := "./testdata/git-files-dirs"
|
|
s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().Init().Return(nil)
|
|
gitClient.EXPECT().IsRevisionPresent(mock.Anything).Return(false)
|
|
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Return(nil)
|
|
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Once().Return("", nil)
|
|
gitClient.EXPECT().LsRemote("HEAD").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
|
gitClient.EXPECT().Root().Return(root)
|
|
paths.EXPECT().GetPath(mock.Anything).Return(root, nil)
|
|
paths.EXPECT().GetPathIfExists(mock.Anything).Return(root)
|
|
}, root)
|
|
s.initConstants.IncludeHiddenDirectories = true
|
|
dirRequest := &apiclient.GitDirectoriesRequest{
|
|
Repo: &v1alpha1.Repository{Repo: "a-url.com"},
|
|
SubmoduleEnabled: false,
|
|
Revision: "HEAD",
|
|
}
|
|
directories, err := s.GetGitDirectories(t.Context(), dirRequest)
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, directories.GetPaths(), []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo", "app/bar/.hidden"})
|
|
|
|
// do the same request again to use the cache
|
|
// we only allow CheckOut to be called once in the mock
|
|
directories, err = s.GetGitDirectories(t.Context(), dirRequest)
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo", "app/bar/.hidden"}, directories.GetPaths())
|
|
cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
|
|
ExternalSets: 1,
|
|
ExternalGets: 2,
|
|
})
|
|
}
|
|
|
|
func TestErrorGetGitFiles(t *testing.T) {
|
|
// test not using the cache
|
|
root := ""
|
|
|
|
type fields struct {
|
|
service *Service
|
|
}
|
|
type args struct {
|
|
ctx context.Context
|
|
request *apiclient.GitFilesRequest
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
want *apiclient.GitFilesResponse
|
|
wantErr assert.ErrorAssertionFunc
|
|
}{
|
|
{name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{
|
|
ctx: t.Context(),
|
|
request: &apiclient.GitFilesRequest{
|
|
Repo: nil,
|
|
SubmoduleEnabled: false,
|
|
Revision: "HEAD",
|
|
},
|
|
}, want: nil, wantErr: assert.Error},
|
|
{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
|
|
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
|
|
gitClient.EXPECT().LsRemote(mock.Anything).Return("", errors.New("ah error"))
|
|
gitClient.EXPECT().Root().Return(root)
|
|
paths.EXPECT().GetPath(mock.Anything).Return(".", nil)
|
|
paths.EXPECT().GetPathIfExists(mock.Anything).Return(".")
|
|
}, ".")
|
|
return s
|
|
}()}, args: args{
|
|
ctx: t.Context(),
|
|
request: &apiclient.GitFilesRequest{
|
|
Repo: &v1alpha1.Repository{Repo: "not-a-valid-url"},
|
|
SubmoduleEnabled: false,
|
|
Revision: "sadfsadf",
|
|
},
|
|
}, want: nil, wantErr: assert.Error},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
s := tt.fields.service
|
|
got, err := s.GetGitFiles(tt.args.ctx, tt.args.request)
|
|
if !tt.wantErr(t, err, fmt.Sprintf("GetGitFiles(%v, %v)", tt.args.ctx, tt.args.request)) {
|
|
return
|
|
}
|
|
assert.Equalf(t, tt.want, got, "GetGitFiles(%v, %v)", tt.args.ctx, tt.args.request)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetGitFiles(t *testing.T) {
|
|
// test not using the cache
|
|
files := []string{
|
|
"./testdata/git-files-dirs/somedir/config.yaml",
|
|
"./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/app/foo/bar/config.yaml",
|
|
}
|
|
root := ""
|
|
s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().Init().Return(nil)
|
|
gitClient.EXPECT().IsRevisionPresent(mock.Anything).Return(false)
|
|
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Return(nil)
|
|
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Once().Return("", nil)
|
|
gitClient.EXPECT().LsRemote("HEAD").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
|
gitClient.EXPECT().Root().Return(root)
|
|
gitClient.EXPECT().LsFiles(mock.Anything, mock.Anything).Once().Return(files, nil)
|
|
paths.EXPECT().GetPath(mock.Anything).Return(root, nil)
|
|
paths.EXPECT().GetPathIfExists(mock.Anything).Return(root)
|
|
}, root)
|
|
filesRequest := &apiclient.GitFilesRequest{
|
|
Repo: &v1alpha1.Repository{Repo: "a-url.com"},
|
|
SubmoduleEnabled: false,
|
|
Revision: "HEAD",
|
|
}
|
|
|
|
// expected map
|
|
expected := make(map[string][]byte)
|
|
for _, filePath := range files {
|
|
fileContents, err := os.ReadFile(filePath)
|
|
require.NoError(t, err)
|
|
expected[filePath] = fileContents
|
|
}
|
|
|
|
fileResponse, err := s.GetGitFiles(t.Context(), filesRequest)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expected, fileResponse.GetMap())
|
|
|
|
// do the same request again to use the cache
|
|
// we only allow LsFiles to be called once in the mock
|
|
fileResponse, err = s.GetGitFiles(t.Context(), filesRequest)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expected, fileResponse.GetMap())
|
|
cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
|
|
ExternalSets: 1,
|
|
ExternalGets: 2,
|
|
})
|
|
}
|
|
|
|
func TestErrorUpdateRevisionForPaths(t *testing.T) {
|
|
// test not using the cache
|
|
root := ""
|
|
|
|
type fields struct {
|
|
service *Service
|
|
}
|
|
type args struct {
|
|
ctx context.Context
|
|
request *apiclient.UpdateRevisionForPathsRequest
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
want *apiclient.UpdateRevisionForPathsResponse
|
|
wantErr assert.ErrorAssertionFunc
|
|
}{
|
|
{name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{
|
|
ctx: t.Context(),
|
|
request: &apiclient.UpdateRevisionForPathsRequest{
|
|
Repo: nil,
|
|
Revision: "HEAD",
|
|
SyncedRevision: "sadfsadf",
|
|
Paths: []string{"."},
|
|
},
|
|
}, want: nil, wantErr: assert.Error},
|
|
{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
|
|
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
|
|
gitClient.EXPECT().LsRemote(mock.Anything).Return("", errors.New("ah error"))
|
|
gitClient.EXPECT().Root().Return(root)
|
|
paths.EXPECT().GetPath(mock.Anything).Return(".", nil)
|
|
paths.EXPECT().GetPathIfExists(mock.Anything).Return(".")
|
|
}, ".")
|
|
return s
|
|
}()}, args: args{
|
|
ctx: t.Context(),
|
|
request: &apiclient.UpdateRevisionForPathsRequest{
|
|
Repo: &v1alpha1.Repository{Repo: "not-a-valid-url", Type: "git"},
|
|
Revision: "sadfsadf",
|
|
SyncedRevision: "HEAD",
|
|
Paths: []string{"."},
|
|
},
|
|
}, want: nil, wantErr: assert.Error},
|
|
{name: "InvalidResolveSyncedRevision", fields: fields{service: func() *Service {
|
|
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
|
|
gitClient.EXPECT().LsRemote("HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
|
gitClient.EXPECT().LsRemote(mock.Anything).Return("", errors.New("ah error"))
|
|
gitClient.EXPECT().Root().Return(root)
|
|
paths.EXPECT().GetPath(mock.Anything).Return(".", nil)
|
|
paths.EXPECT().GetPathIfExists(mock.Anything).Return(".")
|
|
}, ".")
|
|
return s
|
|
}()}, args: args{
|
|
ctx: t.Context(),
|
|
request: &apiclient.UpdateRevisionForPathsRequest{
|
|
Repo: &v1alpha1.Repository{Repo: "not-a-valid-url", Type: "git"},
|
|
Revision: "HEAD",
|
|
SyncedRevision: "sadfsadf",
|
|
Paths: []string{"."},
|
|
},
|
|
}, want: nil, wantErr: assert.Error},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
s := tt.fields.service
|
|
got, err := s.UpdateRevisionForPaths(tt.args.ctx, tt.args.request)
|
|
if !tt.wantErr(t, err, fmt.Sprintf("UpdateRevisionForPaths(%v, %v)", tt.args.ctx, tt.args.request)) {
|
|
return
|
|
}
|
|
assert.Equalf(t, tt.want, got, "UpdateRevisionForPaths(%v, %v)", tt.args.ctx, tt.args.request)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpdateRevisionForPaths(t *testing.T) {
|
|
type fields struct {
|
|
service *Service
|
|
cache *repoCacheMocks
|
|
}
|
|
type args struct {
|
|
ctx context.Context
|
|
request *apiclient.UpdateRevisionForPathsRequest
|
|
}
|
|
type cacheHit struct {
|
|
revision string
|
|
previousRevision string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
want *apiclient.UpdateRevisionForPathsResponse
|
|
wantErr assert.ErrorAssertionFunc
|
|
cacheHit *cacheHit
|
|
cacheCallCount *repositorymocks.CacheCallCounts
|
|
}{
|
|
{name: "NoPathAbort", fields: func() fields {
|
|
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, _ *iomocks.TempPaths) {
|
|
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
|
|
}, ".")
|
|
return fields{
|
|
service: s,
|
|
cache: c,
|
|
}
|
|
}(), args: args{
|
|
ctx: t.Context(),
|
|
request: &apiclient.UpdateRevisionForPathsRequest{
|
|
Repo: &v1alpha1.Repository{Repo: "a-url.com", Type: "git"},
|
|
Revision: "",
|
|
SyncedRevision: "",
|
|
Paths: []string{},
|
|
},
|
|
}, want: &apiclient.UpdateRevisionForPathsResponse{Changes: true}, wantErr: assert.NoError},
|
|
{name: "SameResolvedRevisionAbort", fields: func() fields {
|
|
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
|
|
gitClient.EXPECT().LsRemote("HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
|
gitClient.EXPECT().LsRemote("SYNCEDHEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
|
paths.EXPECT().GetPath(mock.Anything).Return(".", nil)
|
|
paths.EXPECT().GetPathIfExists(mock.Anything).Return(".")
|
|
}, ".")
|
|
return fields{
|
|
service: s,
|
|
cache: c,
|
|
}
|
|
}(), args: args{
|
|
ctx: t.Context(),
|
|
request: &apiclient.UpdateRevisionForPathsRequest{
|
|
Repo: &v1alpha1.Repository{Repo: "a-url.com", Type: "git"},
|
|
Revision: "HEAD",
|
|
SyncedRevision: "SYNCEDHEAD",
|
|
Paths: []string{"."},
|
|
},
|
|
}, want: &apiclient.UpdateRevisionForPathsResponse{
|
|
Revision: "632039659e542ed7de0c170a4fcc1c571b288fc0",
|
|
}, wantErr: assert.NoError, cacheCallCount: &repositorymocks.CacheCallCounts{
|
|
ExternalRenames: 0,
|
|
ExternalGets: 0,
|
|
ExternalSets: 0,
|
|
}},
|
|
{name: "ChangedFilesDoNothing", fields: func() fields {
|
|
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().Init().Return(nil)
|
|
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Once().Return(nil)
|
|
gitClient.EXPECT().IsRevisionPresent("632039659e542ed7de0c170a4fcc1c571b288fc0").Once().Return(false)
|
|
gitClient.EXPECT().Checkout("632039659e542ed7de0c170a4fcc1c571b288fc0", mock.Anything, mock.Anything).Once().Return("", nil)
|
|
// fetch
|
|
gitClient.EXPECT().IsRevisionPresent("1e67a504d03def3a6a1125d934cb511680f72555").Once().Return(false)
|
|
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Once().Return(nil)
|
|
gitClient.EXPECT().IsRevisionPresent("1e67a504d03def3a6a1125d934cb511680f72555").Once().Return(true)
|
|
gitClient.EXPECT().LsRemote("HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
|
gitClient.EXPECT().LsRemote("SYNCEDHEAD").Once().Return("1e67a504d03def3a6a1125d934cb511680f72555", nil)
|
|
paths.EXPECT().GetPath(mock.Anything).Return(".", nil)
|
|
paths.EXPECT().GetPathIfExists(mock.Anything).Return(".")
|
|
gitClient.EXPECT().Root().Return("")
|
|
gitClient.EXPECT().ChangedFiles(mock.Anything, mock.Anything).Return([]string{"app.yaml"}, nil)
|
|
}, ".")
|
|
return fields{
|
|
service: s,
|
|
cache: c,
|
|
}
|
|
}(), args: args{
|
|
ctx: t.Context(),
|
|
request: &apiclient.UpdateRevisionForPathsRequest{
|
|
Repo: &v1alpha1.Repository{Repo: "a-url.com", Type: "git"},
|
|
Revision: "HEAD",
|
|
SyncedRevision: "SYNCEDHEAD",
|
|
Paths: []string{"."},
|
|
},
|
|
}, want: &apiclient.UpdateRevisionForPathsResponse{
|
|
Revision: "632039659e542ed7de0c170a4fcc1c571b288fc0",
|
|
Changes: true,
|
|
}, wantErr: assert.NoError, cacheCallCount: &repositorymocks.CacheCallCounts{
|
|
ExternalRenames: 0,
|
|
ExternalGets: 1,
|
|
ExternalSets: 1,
|
|
}},
|
|
{name: "NoChangesUpdateCache", fields: func() fields {
|
|
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().Init().Return(nil)
|
|
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Once().Return(nil)
|
|
gitClient.EXPECT().IsRevisionPresent("632039659e542ed7de0c170a4fcc1c571b288fc0").Once().Return(false)
|
|
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
|
|
gitClient.EXPECT().IsRevisionPresent("1e67a504d03def3a6a1125d934cb511680f72555").Once().Return(false)
|
|
// fetch
|
|
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Once().Return(nil)
|
|
gitClient.EXPECT().IsRevisionPresent("1e67a504d03def3a6a1125d934cb511680f72555").Once().Return(true)
|
|
gitClient.EXPECT().LsRemote("HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
|
gitClient.EXPECT().LsRemote("SYNCEDHEAD").Once().Return("1e67a504d03def3a6a1125d934cb511680f72555", nil)
|
|
paths.EXPECT().GetPath(mock.Anything).Return(".", nil)
|
|
paths.EXPECT().GetPathIfExists(mock.Anything).Return(".")
|
|
gitClient.EXPECT().Root().Return("")
|
|
gitClient.EXPECT().ChangedFiles(mock.Anything, mock.Anything).Return([]string{}, nil)
|
|
}, ".")
|
|
return fields{
|
|
service: s,
|
|
cache: c,
|
|
}
|
|
}(), args: args{
|
|
ctx: t.Context(),
|
|
request: &apiclient.UpdateRevisionForPathsRequest{
|
|
Repo: &v1alpha1.Repository{Repo: "a-url.com", Type: "git"},
|
|
Revision: "HEAD",
|
|
SyncedRevision: "SYNCEDHEAD",
|
|
Paths: []string{"."},
|
|
AppLabelKey: "app.kubernetes.io/name",
|
|
AppName: "no-change-update-cache",
|
|
Namespace: "default",
|
|
TrackingMethod: "annotation+label",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{Path: "."},
|
|
KubeVersion: "v1.16.0",
|
|
},
|
|
}, want: &apiclient.UpdateRevisionForPathsResponse{
|
|
Revision: "632039659e542ed7de0c170a4fcc1c571b288fc0", Changes: true, // FIXME: need to fix changes=true, because now test can't mock Rename cache
|
|
|
|
}, wantErr: assert.NoError, cacheHit: &cacheHit{
|
|
previousRevision: "1e67a504d03def3a6a1125d934cb511680f72555",
|
|
revision: "632039659e542ed7de0c170a4fcc1c571b288fc0",
|
|
}, cacheCallCount: &repositorymocks.CacheCallCounts{
|
|
ExternalRenames: 1,
|
|
ExternalGets: 1,
|
|
ExternalSets: 1,
|
|
}},
|
|
{name: "NoChangesHelmMultiSourceUpdateCache", fields: func() fields {
|
|
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().Init().Return(nil)
|
|
gitClient.EXPECT().IsRevisionPresent("632039659e542ed7de0c170a4fcc1c571b288fc0").Once().Return(false)
|
|
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Once().Return(nil)
|
|
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
|
|
// fetch
|
|
gitClient.EXPECT().IsRevisionPresent("1e67a504d03def3a6a1125d934cb511680f72555").Once().Return(true)
|
|
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Once().Return(nil)
|
|
gitClient.EXPECT().LsRemote("HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
|
gitClient.EXPECT().LsRemote("SYNCEDHEAD").Once().Return("1e67a504d03def3a6a1125d934cb511680f72555", nil)
|
|
paths.EXPECT().GetPath(mock.Anything).Return(".", nil)
|
|
paths.EXPECT().GetPathIfExists(mock.Anything).Return(".")
|
|
gitClient.EXPECT().Root().Return("")
|
|
gitClient.EXPECT().ChangedFiles(mock.Anything, mock.Anything).Return([]string{}, nil)
|
|
}, ".")
|
|
return fields{
|
|
service: s,
|
|
cache: c,
|
|
}
|
|
}(), args: args{
|
|
ctx: t.Context(),
|
|
request: &apiclient.UpdateRevisionForPathsRequest{
|
|
Repo: &v1alpha1.Repository{Repo: "a-url.com", Type: "git"},
|
|
Revision: "HEAD",
|
|
SyncedRevision: "SYNCEDHEAD",
|
|
Paths: []string{"."},
|
|
AppLabelKey: "app.kubernetes.io/name",
|
|
AppName: "no-change-update-cache",
|
|
Namespace: "default",
|
|
TrackingMethod: "annotation+label",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{Path: ".", Helm: &v1alpha1.ApplicationSourceHelm{ReleaseName: "test"}},
|
|
KubeVersion: "v1.16.0",
|
|
|
|
HasMultipleSources: true,
|
|
},
|
|
}, want: &apiclient.UpdateRevisionForPathsResponse{
|
|
Revision: "632039659e542ed7de0c170a4fcc1c571b288fc0", Changes: true, // FIXME: need to fix changes=true, because now test can't mock Rename cache
|
|
}, wantErr: assert.NoError, cacheHit: &cacheHit{
|
|
previousRevision: "1e67a504d03def3a6a1125d934cb511680f72555",
|
|
revision: "632039659e542ed7de0c170a4fcc1c571b288fc0",
|
|
}, cacheCallCount: &repositorymocks.CacheCallCounts{
|
|
ExternalRenames: 1,
|
|
ExternalGets: 1,
|
|
ExternalSets: 1,
|
|
}},
|
|
{name: "NoChangesHelmWithRefMultiSourceUpdateCache", fields: func() fields {
|
|
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().Init().Return(nil)
|
|
gitClient.EXPECT().IsRevisionPresent("632039659e542ed7de0c170a4fcc1c571b288fc0").Once().Return(false)
|
|
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Once().Return(nil)
|
|
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
|
|
// fetch
|
|
gitClient.EXPECT().IsRevisionPresent("1e67a504d03def3a6a1125d934cb511680f72555").Once().Return(true)
|
|
gitClient.EXPECT().IsRevisionPresent("732039659e542ed7de0c170a4fcc1c571b288fc1").Once().Return(true)
|
|
gitClient.EXPECT().IsRevisionPresent("2e67a504d03def3a6a1125d934cb511680f72554").Once().Return(true)
|
|
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Once().Return(nil)
|
|
gitClient.EXPECT().LsRemote("HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
|
gitClient.EXPECT().LsRemote("SYNCEDHEAD").Once().Return("1e67a504d03def3a6a1125d934cb511680f72555", nil)
|
|
gitClient.EXPECT().LsRemote("HEAD-1").Once().Return("732039659e542ed7de0c170a4fcc1c571b288fc1", nil)
|
|
gitClient.EXPECT().LsRemote("SYNCEDHEAD-1").Once().Return("2e67a504d03def3a6a1125d934cb511680f72554", nil)
|
|
paths.EXPECT().GetPath(mock.Anything).Return(".", nil)
|
|
paths.EXPECT().GetPathIfExists(mock.Anything).Return(".")
|
|
gitClient.EXPECT().Root().Return("")
|
|
gitClient.EXPECT().ChangedFiles(mock.Anything, mock.Anything).Return([]string{}, nil)
|
|
}, ".")
|
|
return fields{
|
|
service: s,
|
|
cache: c,
|
|
}
|
|
}(), args: args{
|
|
ctx: t.Context(),
|
|
request: &apiclient.UpdateRevisionForPathsRequest{
|
|
Repo: &v1alpha1.Repository{Repo: "url.com", Type: "helm"},
|
|
RefSources: v1alpha1.RefTargetRevisionMapping{
|
|
"$values": {Repo: v1alpha1.Repository{Repo: "a-url.com"}, Chart: "test", TargetRevision: "HEAD"},
|
|
"$values_2": {Repo: v1alpha1.Repository{Repo: "a-url.com"}, Chart: "test", TargetRevision: "HEAD-1"},
|
|
},
|
|
SyncedRefSources: v1alpha1.RefTargetRevisionMapping{
|
|
"$values": {Repo: v1alpha1.Repository{Repo: "a-url.com"}, Chart: "test", TargetRevision: "SYNCEDHEAD"},
|
|
"$values_2": {Repo: v1alpha1.Repository{Repo: "a-url.com"}, Chart: "test", TargetRevision: "SYNCEDHEAD-1"},
|
|
},
|
|
Revision: "0.0.1",
|
|
SyncedRevision: "0.0.1",
|
|
Paths: []string{"."},
|
|
AppLabelKey: "app.kubernetes.io/name",
|
|
AppName: "no-change-update-cache",
|
|
Namespace: "default",
|
|
TrackingMethod: "annotation+label",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{Path: ".", Helm: &v1alpha1.ApplicationSourceHelm{ReleaseName: "test", ValueFiles: []string{"$values/path", "$values_2/path"}}},
|
|
KubeVersion: "v1.16.0",
|
|
HasMultipleSources: true,
|
|
},
|
|
}, want: &apiclient.UpdateRevisionForPathsResponse{
|
|
Revision: "0.0.1", Changes: true, // FIXME: need to fix changes=true, because now test can't mock Rename cache
|
|
}, wantErr: assert.NoError, cacheHit: &cacheHit{
|
|
previousRevision: "0.0.1",
|
|
revision: "0.0.1",
|
|
}, cacheCallCount: &repositorymocks.CacheCallCounts{
|
|
ExternalRenames: 1,
|
|
ExternalGets: 2,
|
|
ExternalSets: 2,
|
|
}},
|
|
{name: "NoChangesHelmWithRefMultiSource_IgnoreUnusedRef", fields: func() fields {
|
|
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().Init().Return(nil)
|
|
gitClient.EXPECT().IsRevisionPresent("632039659e542ed7de0c170a4fcc1c571b288fc0").Once().Return(false)
|
|
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Once().Return(nil)
|
|
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
|
|
// fetch
|
|
gitClient.EXPECT().IsRevisionPresent("1e67a504d03def3a6a1125d934cb511680f72555").Once().Return(true)
|
|
gitClient.EXPECT().IsRevisionPresent("732039659e542ed7de0c170a4fcc1c571b288fc1").Once().Return(true)
|
|
gitClient.EXPECT().IsRevisionPresent("2e67a504d03def3a6a1125d934cb511680f72554").Once().Return(true)
|
|
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Once().Return(nil)
|
|
gitClient.EXPECT().LsRemote("HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
|
gitClient.EXPECT().LsRemote("SYNCEDHEAD").Once().Return("1e67a504d03def3a6a1125d934cb511680f72555", nil)
|
|
paths.EXPECT().GetPath(mock.Anything).Return(".", nil)
|
|
paths.EXPECT().GetPathIfExists(mock.Anything).Return(".")
|
|
gitClient.EXPECT().Root().Return("")
|
|
gitClient.EXPECT().ChangedFiles(mock.Anything, mock.Anything).Return([]string{}, nil)
|
|
}, ".")
|
|
return fields{
|
|
service: s,
|
|
cache: c,
|
|
}
|
|
}(), args: args{
|
|
ctx: t.Context(),
|
|
request: &apiclient.UpdateRevisionForPathsRequest{
|
|
Repo: &v1alpha1.Repository{Repo: "url.com", Type: "helm"},
|
|
RefSources: v1alpha1.RefTargetRevisionMapping{
|
|
"$values": {Repo: v1alpha1.Repository{Repo: "a-url.com"}, Chart: "test", TargetRevision: "HEAD"},
|
|
"$values_2": {Repo: v1alpha1.Repository{Repo: "a-url.com"}, Chart: "test", TargetRevision: "HEAD-1"},
|
|
},
|
|
SyncedRefSources: v1alpha1.RefTargetRevisionMapping{
|
|
"$values": {Repo: v1alpha1.Repository{Repo: "a-url.com"}, Chart: "test", TargetRevision: "SYNCEDHEAD"},
|
|
"$values_2": {Repo: v1alpha1.Repository{Repo: "a-url.com"}, Chart: "test", TargetRevision: "SYNCEDHEAD-1"},
|
|
},
|
|
Revision: "0.0.1",
|
|
SyncedRevision: "0.0.1",
|
|
Paths: []string{"."},
|
|
AppLabelKey: "app.kubernetes.io/name",
|
|
AppName: "no-change-update-cache",
|
|
Namespace: "default",
|
|
TrackingMethod: "annotation+label",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{Path: ".", Helm: &v1alpha1.ApplicationSourceHelm{ReleaseName: "test", ValueFiles: []string{"$values/path"}}},
|
|
KubeVersion: "v1.16.0",
|
|
HasMultipleSources: true,
|
|
},
|
|
}, want: &apiclient.UpdateRevisionForPathsResponse{
|
|
Revision: "0.0.1", Changes: true, // FIXME: need to fix changes=true, because now test can't mock Rename cache
|
|
}, wantErr: assert.NoError, cacheHit: &cacheHit{
|
|
previousRevision: "0.0.1",
|
|
revision: "0.0.1",
|
|
}, cacheCallCount: &repositorymocks.CacheCallCounts{
|
|
ExternalRenames: 1,
|
|
ExternalGets: 1,
|
|
ExternalSets: 1,
|
|
}},
|
|
{name: "NoChangesHelmWithRefMultiSource_UndefinedRef", fields: func() fields {
|
|
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().Init().Return(nil)
|
|
gitClient.EXPECT().IsRevisionPresent("632039659e542ed7de0c170a4fcc1c571b288fc0").Once().Return(false)
|
|
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Once().Return(nil)
|
|
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
|
|
// fetch
|
|
gitClient.EXPECT().IsRevisionPresent("1e67a504d03def3a6a1125d934cb511680f72555").Once().Return(true)
|
|
gitClient.EXPECT().IsRevisionPresent("732039659e542ed7de0c170a4fcc1c571b288fc1").Once().Return(true)
|
|
gitClient.EXPECT().IsRevisionPresent("2e67a504d03def3a6a1125d934cb511680f72554").Once().Return(true)
|
|
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Once().Return(nil)
|
|
gitClient.EXPECT().LsRemote("HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
|
gitClient.EXPECT().LsRemote("SYNCEDHEAD").Once().Return("1e67a504d03def3a6a1125d934cb511680f72555", nil)
|
|
paths.EXPECT().GetPath(mock.Anything).Return(".", nil)
|
|
paths.EXPECT().GetPathIfExists(mock.Anything).Return(".")
|
|
gitClient.EXPECT().Root().Return("")
|
|
gitClient.EXPECT().ChangedFiles(mock.Anything, mock.Anything).Return([]string{}, nil)
|
|
}, ".")
|
|
return fields{
|
|
service: s,
|
|
cache: c,
|
|
}
|
|
}(), args: args{
|
|
ctx: t.Context(),
|
|
request: &apiclient.UpdateRevisionForPathsRequest{
|
|
Repo: &v1alpha1.Repository{Repo: "url.com", Type: "helm"},
|
|
RefSources: v1alpha1.RefTargetRevisionMapping{
|
|
"$values": {Repo: v1alpha1.Repository{Repo: "a-url.com"}, Chart: "test", TargetRevision: "HEAD"},
|
|
"$values_2": {Repo: v1alpha1.Repository{Repo: "a-url.com"}, Chart: "test", TargetRevision: "HEAD-1"},
|
|
},
|
|
SyncedRefSources: v1alpha1.RefTargetRevisionMapping{
|
|
"$values": {Repo: v1alpha1.Repository{Repo: "a-url.com"}, Chart: "test", TargetRevision: "SYNCEDHEAD"},
|
|
"$values_2": {Repo: v1alpha1.Repository{Repo: "a-url.com"}, Chart: "test", TargetRevision: "SYNCEDHEAD-1"},
|
|
},
|
|
Revision: "0.0.1",
|
|
SyncedRevision: "0.0.1",
|
|
Paths: []string{"."},
|
|
AppLabelKey: "app.kubernetes.io/name",
|
|
AppName: "no-change-update-cache",
|
|
Namespace: "default",
|
|
TrackingMethod: "annotation+label",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{Path: ".", Helm: &v1alpha1.ApplicationSourceHelm{ReleaseName: "test", ValueFiles: []string{"$values_3/path"}}},
|
|
KubeVersion: "v1.16.0",
|
|
HasMultipleSources: true,
|
|
},
|
|
}, want: &apiclient.UpdateRevisionForPathsResponse{
|
|
Revision: "0.0.1", Changes: true,
|
|
}, wantErr: assert.Error, cacheHit: nil},
|
|
{name: "IgnoreRefSourcesForGitSource", fields: func() fields {
|
|
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
|
gitClient.EXPECT().Init().Return(nil)
|
|
gitClient.EXPECT().IsRevisionPresent("632039659e542ed7de0c170a4fcc1c571b288fc0").Once().Return(false)
|
|
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Once().Return(nil)
|
|
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
|
|
// fetch
|
|
gitClient.EXPECT().IsRevisionPresent("1e67a504d03def3a6a1125d934cb511680f72555").Once().Return(true)
|
|
gitClient.EXPECT().IsRevisionPresent("732039659e542ed7de0c170a4fcc1c571b288fc1").Once().Return(true)
|
|
gitClient.EXPECT().IsRevisionPresent("2e67a504d03def3a6a1125d934cb511680f72554").Once().Return(true)
|
|
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Once().Return(nil)
|
|
gitClient.EXPECT().LsRemote("HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
|
gitClient.EXPECT().LsRemote("SYNCEDHEAD").Once().Return("1e67a504d03def3a6a1125d934cb511680f72555", nil)
|
|
paths.EXPECT().GetPath(mock.Anything).Return(".", nil)
|
|
paths.EXPECT().GetPathIfExists(mock.Anything).Return(".")
|
|
gitClient.EXPECT().Root().Return("")
|
|
gitClient.EXPECT().ChangedFiles(mock.Anything, mock.Anything).Return([]string{}, nil)
|
|
}, ".")
|
|
return fields{
|
|
service: s,
|
|
cache: c,
|
|
}
|
|
}(), args: args{
|
|
ctx: t.Context(),
|
|
request: &apiclient.UpdateRevisionForPathsRequest{
|
|
Repo: &v1alpha1.Repository{Repo: "https://github.com", Type: "git"},
|
|
RefSources: v1alpha1.RefTargetRevisionMapping{
|
|
"$values_2": {Repo: v1alpha1.Repository{Repo: "a-url.com"}, Chart: "test", TargetRevision: "HEAD-1"},
|
|
},
|
|
SyncedRefSources: v1alpha1.RefTargetRevisionMapping{
|
|
"$values_2": {Repo: v1alpha1.Repository{Repo: "a-url.com"}, Chart: "test", TargetRevision: "SYNCEDHEAD-1"},
|
|
},
|
|
Revision: "HEAD",
|
|
SyncedRevision: "SYNCEDHEAD",
|
|
Paths: []string{"."},
|
|
AppLabelKey: "app.kubernetes.io/name",
|
|
AppName: "no-change-update-cache",
|
|
Namespace: "default",
|
|
TrackingMethod: "annotation+label",
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: ".",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{ReleaseName: "test", ValueFiles: []string{"path"}},
|
|
RepoURL: "https://github.com",
|
|
TargetRevision: "HEAD",
|
|
},
|
|
KubeVersion: "v1.16.0",
|
|
HasMultipleSources: true,
|
|
},
|
|
}, want: &apiclient.UpdateRevisionForPathsResponse{
|
|
Revision: "632039659e542ed7de0c170a4fcc1c571b288fc0", Changes: true, // FIXME: need to fix changes=true, because now test can't mock Rename cache
|
|
}, wantErr: assert.NoError, cacheHit: &cacheHit{
|
|
previousRevision: "632039659e542ed7de0c170a4fcc1c571b288fc0",
|
|
revision: "1e67a504d03def3a6a1125d934cb511680f72555",
|
|
}, cacheCallCount: &repositorymocks.CacheCallCounts{
|
|
ExternalRenames: 1,
|
|
ExternalGets: 1,
|
|
ExternalSets: 1,
|
|
}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
s := tt.fields.service
|
|
cache := tt.fields.cache
|
|
|
|
if tt.cacheHit != nil {
|
|
cache.mockCache.On("Rename", tt.cacheHit.previousRevision, tt.cacheHit.revision, mock.Anything).Return(nil)
|
|
}
|
|
|
|
got, err := s.UpdateRevisionForPaths(tt.args.ctx, tt.args.request)
|
|
if !tt.wantErr(t, err, fmt.Sprintf("UpdateRevisionForPaths(%v, %v)", tt.args.ctx, tt.args.request)) {
|
|
return
|
|
}
|
|
assert.Equalf(t, tt.want, got, "UpdateRevisionForPaths(%v, %v)", tt.args.ctx, tt.args.request)
|
|
|
|
if tt.cacheCallCount != nil {
|
|
cache.mockCache.AssertCacheCalledTimes(t, tt.cacheCallCount)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_getRepoSanitizerRegex(t *testing.T) {
|
|
r := getRepoSanitizerRegex("/tmp/_argocd-repo")
|
|
msg := r.ReplaceAllString("error message containing /tmp/_argocd-repo/SENSITIVE and other stuff", "<path to cached source>")
|
|
assert.Equal(t, "error message containing <path to cached source> and other stuff", msg)
|
|
msg = r.ReplaceAllString("error message containing /tmp/_argocd-repo/SENSITIVE/with/trailing/path and other stuff", "<path to cached source>")
|
|
assert.Equal(t, "error message containing <path to cached source>/with/trailing/path and other stuff", msg)
|
|
}
|
|
|
|
func TestGetRefs_CacheWithLockDisabled(t *testing.T) {
|
|
// Test that when the lock is disabled the default behavior still works correctly
|
|
// Also shows the current issue with the git requests due to cache misses
|
|
dir := t.TempDir()
|
|
initGitRepo(t, newGitRepoOptions{
|
|
path: dir,
|
|
createPath: false,
|
|
remote: "",
|
|
addEmptyCommit: true,
|
|
})
|
|
// Test in-memory and redis
|
|
cacheMocks := newCacheMocksWithOpts(1*time.Minute, 1*time.Minute, 0)
|
|
t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
|
|
var wg sync.WaitGroup
|
|
numberOfCallers := 10
|
|
for range numberOfCallers {
|
|
wg.Go(func() {
|
|
client, err := git.NewClient("file://"+dir, git.NopCreds{}, true, false, "", "", git.WithCache(cacheMocks.cache, true))
|
|
require.NoError(t, err)
|
|
refs, err := client.LsRefs()
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, refs)
|
|
assert.NotEmpty(t, refs.Branches, "Expected branches to be populated")
|
|
assert.NotEmpty(t, refs.Branches[0])
|
|
})
|
|
}
|
|
wg.Wait()
|
|
// Unlock should not have been called
|
|
cacheMocks.mockCache.AssertNumberOfCalls(t, "UnlockGitReferences", 0)
|
|
// Lock should not have been called
|
|
cacheMocks.mockCache.AssertNumberOfCalls(t, "TryLockGitRefCache", 0)
|
|
}
|
|
|
|
func TestGetRefs_CacheDisabled(t *testing.T) {
|
|
// Test that default get refs with cache disabled does not call GetOrLockGitReferences
|
|
dir := t.TempDir()
|
|
initGitRepo(t, newGitRepoOptions{
|
|
path: dir,
|
|
createPath: false,
|
|
remote: "",
|
|
addEmptyCommit: true,
|
|
})
|
|
cacheMocks := newCacheMocks()
|
|
t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
|
|
client, err := git.NewClient("file://"+dir, git.NopCreds{}, true, false, "", "", git.WithCache(cacheMocks.cache, false))
|
|
require.NoError(t, err)
|
|
refs, err := client.LsRefs()
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, refs)
|
|
assert.NotEmpty(t, refs.Branches, "Expected branches to be populated")
|
|
assert.NotEmpty(t, refs.Branches[0])
|
|
// Unlock should not have been called
|
|
cacheMocks.mockCache.AssertNumberOfCalls(t, "UnlockGitReferences", 0)
|
|
cacheMocks.mockCache.AssertNumberOfCalls(t, "GetOrLockGitReferences", 0)
|
|
}
|
|
|
|
func TestGetRefs_CacheWithLock(t *testing.T) {
|
|
// Test that there is only one call to SetGitReferences for the same repo which is done after the ls-remote
|
|
dir := t.TempDir()
|
|
initGitRepo(t, newGitRepoOptions{
|
|
path: dir,
|
|
createPath: false,
|
|
remote: "",
|
|
addEmptyCommit: true,
|
|
})
|
|
cacheMocks := newCacheMocks()
|
|
t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
|
|
var wg sync.WaitGroup
|
|
numberOfCallers := 10
|
|
for range numberOfCallers {
|
|
wg.Go(func() {
|
|
client, err := git.NewClient("file://"+dir, git.NopCreds{}, true, false, "", "", git.WithCache(cacheMocks.cache, true))
|
|
require.NoError(t, err)
|
|
refs, err := client.LsRefs()
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, refs)
|
|
assert.NotEmpty(t, refs.Branches, "Expected branches to be populated")
|
|
assert.NotEmpty(t, refs.Branches[0])
|
|
})
|
|
}
|
|
wg.Wait()
|
|
// Unlock should not have been called
|
|
cacheMocks.mockCache.AssertNumberOfCalls(t, "UnlockGitReferences", 0)
|
|
cacheMocks.mockCache.AssertNumberOfCalls(t, "GetOrLockGitReferences", 0)
|
|
}
|
|
|
|
func TestGetRefs_CacheUnlockedOnUpdateFailed(t *testing.T) {
|
|
// Worst case the ttl on the lock expires and the lock is removed
|
|
// however if the holder of the lock fails to update the cache the caller should remove the lock
|
|
// to allow other callers to attempt to update the cache as quickly as possible
|
|
dir := t.TempDir()
|
|
initGitRepo(t, newGitRepoOptions{
|
|
path: dir,
|
|
createPath: false,
|
|
remote: "",
|
|
addEmptyCommit: true,
|
|
})
|
|
cacheMocks := newCacheMocks()
|
|
t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
|
|
repoURL := "file://" + dir
|
|
client, err := git.NewClient(repoURL, git.NopCreds{}, true, false, "", "", git.WithCache(cacheMocks.cache, true))
|
|
require.NoError(t, err)
|
|
refs, err := client.LsRefs()
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, refs)
|
|
assert.NotEmpty(t, refs.Branches, "Expected branches to be populated")
|
|
assert.NotEmpty(t, refs.Branches[0])
|
|
var output [][2]string
|
|
err = cacheMocks.cacheutilCache.GetItem(fmt.Sprintf("git-refs|%s|%s", repoURL, common.CacheVersion), &output)
|
|
require.Error(t, err, "Should be a cache miss")
|
|
assert.Empty(t, output, "Expected cache to be empty for key")
|
|
cacheMocks.mockCache.AssertNumberOfCalls(t, "UnlockGitReferences", 0)
|
|
cacheMocks.mockCache.AssertNumberOfCalls(t, "GetOrLockGitReferences", 0)
|
|
}
|
|
|
|
func TestGetRefs_CacheLockTryLockGitRefCacheError(t *testing.T) {
|
|
// Worst case the ttl on the lock expires and the lock is removed
|
|
// however if the holder of the lock fails to update the cache the caller should remove the lock
|
|
// to allow other callers to attempt to update the cache as quickly as possible
|
|
dir := t.TempDir()
|
|
initGitRepo(t, newGitRepoOptions{
|
|
path: dir,
|
|
createPath: false,
|
|
remote: "",
|
|
addEmptyCommit: true,
|
|
})
|
|
cacheMocks := newCacheMocks()
|
|
t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
|
|
repoURL := "file://" + dir
|
|
// buf := bytes.Buffer{}
|
|
// log.SetOutput(&buf)
|
|
client, err := git.NewClient(repoURL, git.NopCreds{}, true, false, "", "", git.WithCache(cacheMocks.cache, true))
|
|
require.NoError(t, err)
|
|
refs, err := client.LsRefs()
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, refs)
|
|
}
|
|
|
|
func TestGetRevisionChartDetails(t *testing.T) {
|
|
t.Run("Test revision semver", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
service := newService(t, root)
|
|
_, err := service.GetRevisionChartDetails(t.Context(), &apiclient.RepoServerRevisionChartDetailsRequest{
|
|
Repo: &v1alpha1.Repository{
|
|
Repo: "file://" + root,
|
|
Name: "test-repo-name",
|
|
Type: "helm",
|
|
},
|
|
Name: "test-name",
|
|
Revision: "test-revision",
|
|
})
|
|
assert.ErrorContains(t, err, "invalid revision")
|
|
})
|
|
|
|
t.Run("Test GetRevisionChartDetails", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
service := newService(t, root)
|
|
repoURL := "file://" + root
|
|
err := service.cache.SetRevisionChartDetails(repoURL, "my-chart", "1.1.0", &v1alpha1.ChartDetails{
|
|
Description: "test-description",
|
|
Home: "test-home",
|
|
Maintainers: []string{"test-maintainer"},
|
|
})
|
|
require.NoError(t, err)
|
|
chartDetails, err := service.GetRevisionChartDetails(t.Context(), &apiclient.RepoServerRevisionChartDetailsRequest{
|
|
Repo: &v1alpha1.Repository{
|
|
Repo: "file://" + root,
|
|
Name: "test-repo-name",
|
|
Type: "helm",
|
|
},
|
|
Name: "my-chart",
|
|
Revision: "1.1.0",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "test-description", chartDetails.Description)
|
|
assert.Equal(t, "test-home", chartDetails.Home)
|
|
assert.Equal(t, []string{"test-maintainer"}, chartDetails.Maintainers)
|
|
})
|
|
}
|
|
|
|
func TestVerifyCommitSignature(t *testing.T) {
|
|
repo := &v1alpha1.Repository{
|
|
Repo: "https://github.com/example/repo.git",
|
|
}
|
|
|
|
t.Run("VerifyCommitSignature with valid signature", func(t *testing.T) {
|
|
t.Setenv("ARGOCD_GPG_ENABLED", "true")
|
|
mockGitClient := &gitmocks.Client{}
|
|
mockGitClient.EXPECT().VerifyCommitSignature(mock.Anything).
|
|
Return(testSignature, nil)
|
|
err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("VerifyCommitSignature with invalid signature", func(t *testing.T) {
|
|
t.Setenv("ARGOCD_GPG_ENABLED", "true")
|
|
mockGitClient := &gitmocks.Client{}
|
|
mockGitClient.EXPECT().VerifyCommitSignature(mock.Anything).
|
|
Return("", nil)
|
|
err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo)
|
|
assert.EqualError(t, err, "revision abcd1234 is not signed")
|
|
})
|
|
|
|
t.Run("VerifyCommitSignature with unknown signature", func(t *testing.T) {
|
|
t.Setenv("ARGOCD_GPG_ENABLED", "true")
|
|
mockGitClient := &gitmocks.Client{}
|
|
mockGitClient.EXPECT().VerifyCommitSignature(mock.Anything).
|
|
Return("", errors.New("UNKNOWN signature: gpg: Unknown signature from ABCDEFGH"))
|
|
err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo)
|
|
assert.EqualError(t, err, "UNKNOWN signature: gpg: Unknown signature from ABCDEFGH")
|
|
})
|
|
|
|
t.Run("VerifyCommitSignature with error verifying signature", func(t *testing.T) {
|
|
t.Setenv("ARGOCD_GPG_ENABLED", "true")
|
|
mockGitClient := &gitmocks.Client{}
|
|
mockGitClient.EXPECT().VerifyCommitSignature(mock.Anything).
|
|
Return("", errors.New("error verifying signature of commit 'abcd1234' in repo 'https://github.com/example/repo.git': failed to verify signature"))
|
|
err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo)
|
|
assert.EqualError(t, err, "error verifying signature of commit 'abcd1234' in repo 'https://github.com/example/repo.git': failed to verify signature")
|
|
})
|
|
|
|
t.Run("VerifyCommitSignature with signature verification disabled", func(t *testing.T) {
|
|
t.Setenv("ARGOCD_GPG_ENABLED", "false")
|
|
mockGitClient := &gitmocks.Client{}
|
|
err := verifyCommitSignature(false, mockGitClient, "abcd1234", repo)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func Test_GenerateManifests_Commands(t *testing.T) {
|
|
t.Run("helm", func(t *testing.T) {
|
|
service := newService(t, "testdata/my-chart")
|
|
|
|
// Fill the manifest request with as many parameters affecting Helm commands as possible.
|
|
q := apiclient.ManifestRequest{
|
|
AppName: "test-app",
|
|
Namespace: "test-namespace",
|
|
KubeVersion: "1.2.3+something",
|
|
ApiVersions: []string{"v1/Test", "v2/Test"},
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: ".",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
FileParameters: []v1alpha1.HelmFileParameter{
|
|
{
|
|
Name: "test-file-param-name",
|
|
Path: "test-file-param.yaml",
|
|
},
|
|
},
|
|
Parameters: []v1alpha1.HelmParameter{
|
|
{
|
|
Name: "test-param-name",
|
|
// Use build env var to test substitution.
|
|
Value: "test-value-$ARGOCD_APP_NAME",
|
|
ForceString: true,
|
|
},
|
|
{
|
|
Name: "test-param-bool-name",
|
|
// Use build env var to test substitution.
|
|
Value: "false",
|
|
},
|
|
},
|
|
PassCredentials: true,
|
|
SkipCrds: true,
|
|
SkipSchemaValidation: false,
|
|
ValueFiles: []string{
|
|
"my-chart-values.yaml",
|
|
},
|
|
Values: "test: values",
|
|
},
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
|
|
res, err := service.GenerateManifest(t.Context(), &q)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"helm template . --name-template test-app --namespace test-namespace --kube-version 1.2.3 --set test-param-bool-name=false --set-string test-param-name=test-value-test-app --set-file test-file-param-name=./test-file-param.yaml --values ./my-chart-values.yaml --values <temp file with values from source.helm.values/valuesObject> --api-versions v1/Test --api-versions v2/Test"}, res.Commands)
|
|
|
|
t.Run("with overrides", func(t *testing.T) {
|
|
// These can be set explicitly instead of using inferred values. Make sure the overrides apply.
|
|
q.ApplicationSource.Helm.APIVersions = []string{"v3", "v4"}
|
|
q.ApplicationSource.Helm.KubeVersion = "5.6.7+something"
|
|
q.ApplicationSource.Helm.Namespace = "different-namespace"
|
|
q.ApplicationSource.Helm.ReleaseName = "different-release-name"
|
|
|
|
res, err = service.GenerateManifest(t.Context(), &q)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"helm template . --name-template different-release-name --namespace different-namespace --kube-version 5.6.7 --set test-param-bool-name=false --set-string test-param-name=test-value-test-app --set-file test-file-param-name=./test-file-param.yaml --values ./my-chart-values.yaml --values <temp file with values from source.helm.values/valuesObject> --api-versions v3 --api-versions v4"}, res.Commands)
|
|
})
|
|
})
|
|
|
|
t.Run("helm with dependencies", func(t *testing.T) {
|
|
// This test makes sure we still get commands, even if we hit the code path that has to run "helm dependency build."
|
|
// We don't actually return the "helm dependency build" command, because we expect that the user is able to read
|
|
// the "helm template" and figure out how to fix it.
|
|
t.Cleanup(func() {
|
|
err := os.Remove("testdata/helm-with-local-dependency/Chart.lock")
|
|
require.NoError(t, err)
|
|
err = os.RemoveAll("testdata/helm-with-local-dependency/charts")
|
|
require.NoError(t, err)
|
|
err = os.Remove(path.Join("testdata/helm-with-local-dependency", helmDepUpMarkerFile))
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
service := newService(t, "testdata/helm-with-local-dependency")
|
|
|
|
q := apiclient.ManifestRequest{
|
|
AppName: "test-app",
|
|
Namespace: "test-namespace",
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: ".",
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
|
|
res, err := service.GenerateManifest(t.Context(), &q)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"helm template . --name-template test-app --namespace test-namespace --include-crds"}, res.Commands)
|
|
})
|
|
|
|
t.Run("kustomize", func(t *testing.T) {
|
|
// Write test files to a temp dir, because the test mutates kustomization.yaml in place.
|
|
tempDir := t.TempDir()
|
|
err := os.WriteFile(path.Join(tempDir, "kustomization.yaml"), []byte(`
|
|
resources:
|
|
- guestbook.yaml
|
|
`), os.FileMode(0o600))
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(path.Join(tempDir, "guestbook.yaml"), []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: guestbook-ui
|
|
`), os.FileMode(0o400))
|
|
require.NoError(t, err)
|
|
err = os.Mkdir(path.Join(tempDir, "component"), os.FileMode(0o700))
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(path.Join(tempDir, "component", "kustomization.yaml"), []byte(`
|
|
apiVersion: kustomize.config.k8s.io/v1alpha1
|
|
kind: Component
|
|
images:
|
|
- name: old
|
|
newName: new
|
|
`), os.FileMode(0o400))
|
|
require.NoError(t, err)
|
|
|
|
service := newService(t, tempDir)
|
|
|
|
// Fill the manifest request with as many parameters affecting Kustomize commands as possible.
|
|
q := apiclient.ManifestRequest{
|
|
AppName: "test-app",
|
|
Namespace: "test-namespace",
|
|
KubeVersion: "1.2.3+something",
|
|
ApiVersions: []string{"v1/Test", "v2/Test"},
|
|
Repo: &v1alpha1.Repository{},
|
|
KustomizeOptions: &v1alpha1.KustomizeOptions{
|
|
BuildOptions: "--enable-helm",
|
|
},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: ".",
|
|
Kustomize: &v1alpha1.ApplicationSourceKustomize{
|
|
APIVersions: []string{"v1", "v2"},
|
|
CommonAnnotations: map[string]string{
|
|
// Use build env var to test substitution.
|
|
"test": "annotation-$ARGOCD_APP_NAME",
|
|
},
|
|
CommonAnnotationsEnvsubst: true,
|
|
CommonLabels: map[string]string{
|
|
"test": "label",
|
|
},
|
|
Components: []string{"component"},
|
|
ForceCommonAnnotations: true,
|
|
ForceCommonLabels: true,
|
|
Images: v1alpha1.KustomizeImages{
|
|
"image=override",
|
|
},
|
|
KubeVersion: "5.6.7+something",
|
|
LabelWithoutSelector: true,
|
|
LabelIncludeTemplates: true,
|
|
NamePrefix: "test-prefix",
|
|
NameSuffix: "test-suffix",
|
|
Namespace: "override-namespace",
|
|
Replicas: v1alpha1.KustomizeReplicas{
|
|
{
|
|
Name: "guestbook-ui",
|
|
Count: intstr.Parse("1337"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ProjectName: "something",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
|
|
res, err := service.GenerateManifest(t.Context(), &q)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{
|
|
"kustomize edit set nameprefix -- test-prefix",
|
|
"kustomize edit set namesuffix -- test-suffix",
|
|
"kustomize edit set image image=override",
|
|
"kustomize edit set replicas guestbook-ui=1337",
|
|
"kustomize edit add label --force --without-selector --include-templates test:label",
|
|
"kustomize edit add annotation --force test:annotation-test-app",
|
|
"kustomize edit set namespace -- override-namespace",
|
|
"kustomize edit add component component",
|
|
"kustomize build . --enable-helm --helm-kube-version 5.6.7 --helm-api-versions v1 --helm-api-versions v2",
|
|
}, res.Commands)
|
|
})
|
|
}
|
|
|
|
func Test_SkipSchemaValidation(t *testing.T) {
|
|
t.Run("helm", func(t *testing.T) {
|
|
service := newService(t, "testdata/broken-schema-verification")
|
|
|
|
q := apiclient.ManifestRequest{
|
|
AppName: "test-app",
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: ".",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
SkipSchemaValidation: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
res, err := service.GenerateManifest(t.Context(), &q)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"helm template . --name-template test-app --include-crds --skip-schema-validation"}, res.Commands)
|
|
})
|
|
t.Run("helm", func(t *testing.T) {
|
|
service := newService(t, "testdata/broken-schema-verification")
|
|
|
|
q := apiclient.ManifestRequest{
|
|
AppName: "test-app",
|
|
Repo: &v1alpha1.Repository{},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: ".",
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
SkipSchemaValidation: false,
|
|
},
|
|
},
|
|
}
|
|
|
|
_, err := service.GenerateManifest(t.Context(), &q)
|
|
|
|
require.ErrorContains(t, err, "values don't meet the specifications of the schema(s)")
|
|
})
|
|
}
|
|
|
|
func TestGenerateManifest_OCISourceSkipsGitClient(t *testing.T) {
|
|
svc := newService(t, t.TempDir())
|
|
|
|
gitCalled := false
|
|
svc.newGitClient = func(_, _ string, _ git.Creds, _, _ bool, _, _ string, _ ...git.ClientOpts) (git.Client, error) {
|
|
gitCalled = true
|
|
return nil, errors.New("git should not be called for OCI")
|
|
}
|
|
|
|
req := &apiclient.ManifestRequest{
|
|
HasMultipleSources: true,
|
|
Repo: &v1alpha1.Repository{
|
|
Repo: "oci://example.com/foo",
|
|
},
|
|
ApplicationSource: &v1alpha1.ApplicationSource{
|
|
Path: "",
|
|
TargetRevision: "v1",
|
|
Ref: "foo",
|
|
RepoURL: "oci://example.com/foo",
|
|
},
|
|
ProjectName: "foo-project",
|
|
ProjectSourceRepos: []string{"*"},
|
|
}
|
|
|
|
_, err := svc.GenerateManifest(t.Context(), req)
|
|
require.NoError(t, err)
|
|
|
|
// verify that newGitClient was never invoked
|
|
assert.False(t, gitCalled, "GenerateManifest should not invoke Git for OCI sources")
|
|
}
|