Skip to content

Commit 49514c9

Browse files
fix: minor oci fixes (#23434)
Signed-off-by: Blake Pettersson <[email protected]>
1 parent f7590fa commit 49514c9

File tree

9 files changed

+541
-37
lines changed

9 files changed

+541
-37
lines changed

.mockery.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ packages:
6666
github.com/argoproj/argo-cd/v3/util/helm:
6767
interfaces:
6868
Client: {}
69+
github.com/argoproj/argo-cd/v3/util/oci:
70+
interfaces:
71+
Client: {}
6972
github.com/argoproj/argo-cd/v3/util/io:
7073
interfaces:
7174
TempPaths: {}

controller/appcontroller_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,10 @@ func (m *MockKubectl) DeleteResource(ctx context.Context, config *rest.Config, g
9595
}
9696

9797
func newFakeController(data *fakeData, repoErr error) *ApplicationController {
98-
return newFakeControllerWithResync(data, time.Minute, repoErr)
98+
return newFakeControllerWithResync(data, time.Minute, repoErr, nil)
9999
}
100100

101-
func newFakeControllerWithResync(data *fakeData, appResyncPeriod time.Duration, repoErr error) *ApplicationController {
101+
func newFakeControllerWithResync(data *fakeData, appResyncPeriod time.Duration, repoErr, revisionPathsErr error) *ApplicationController {
102102
var clust corev1.Secret
103103
err := yaml.Unmarshal([]byte(fakeCluster), &clust)
104104
if err != nil {
@@ -124,7 +124,11 @@ func newFakeControllerWithResync(data *fakeData, appResyncPeriod time.Duration,
124124
}
125125
}
126126

127-
mockRepoClient.On("UpdateRevisionForPaths", mock.Anything, mock.Anything).Return(data.updateRevisionForPathsResponse, nil)
127+
if revisionPathsErr != nil {
128+
mockRepoClient.On("UpdateRevisionForPaths", mock.Anything, mock.Anything).Return(nil, revisionPathsErr)
129+
} else {
130+
mockRepoClient.On("UpdateRevisionForPaths", mock.Anything, mock.Anything).Return(data.updateRevisionForPathsResponse, nil)
131+
}
128132

129133
mockRepoClientset := mockrepoclient.Clientset{RepoServerServiceClient: &mockRepoClient}
130134

@@ -1898,7 +1902,7 @@ apps/Deployment:
18981902
{},
18991903
{},
19001904
},
1901-
}, time.Millisecond*10, nil)
1905+
}, time.Millisecond*10, nil, nil)
19021906

19031907
testCases := []struct {
19041908
name string

controller/state.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alp
260260
appNamespace = ""
261261
}
262262

263-
if !source.IsHelm() && syncedRevision != "" && keyManifestGenerateAnnotationExists && keyManifestGenerateAnnotationVal != "" {
263+
if !source.IsHelm() && !source.IsOCI() && syncedRevision != "" && keyManifestGenerateAnnotationExists && keyManifestGenerateAnnotationVal != "" {
264264
// Validate the manifest-generate-path annotation to avoid generating manifests if it has not changed.
265265
updateRevisionResult, err := repoClient.UpdateRevisionForPaths(context.Background(), &apiclient.UpdateRevisionForPathsRequest{
266266
Repo: repo,

controller/state_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1825,3 +1825,31 @@ func Test_normalizeClusterScopeTracking(t *testing.T) {
18251825
require.NoError(t, err)
18261826
require.True(t, called, "normalization function should have called the callback function")
18271827
}
1828+
1829+
func TestCompareAppState_DoesNotCallUpdateRevisionForPaths_ForOCI(t *testing.T) {
1830+
app := newFakeApp()
1831+
// Enable the manifest-generate-paths annotation and set a synced revision
1832+
app.SetAnnotations(map[string]string{v1alpha1.AnnotationKeyManifestGeneratePaths: "."})
1833+
app.Status.Sync = v1alpha1.SyncStatus{
1834+
Revision: "abc123",
1835+
Status: v1alpha1.SyncStatusCodeSynced,
1836+
}
1837+
1838+
data := fakeData{
1839+
manifestResponse: &apiclient.ManifestResponse{
1840+
Manifests: []string{},
1841+
Namespace: test.FakeDestNamespace,
1842+
Server: test.FakeClusterURL,
1843+
Revision: "abc123",
1844+
},
1845+
}
1846+
ctrl := newFakeControllerWithResync(&data, time.Minute, nil, errors.New("this should not be called"))
1847+
1848+
source := app.Spec.GetSource()
1849+
source.RepoURL = "oci://example.com/argo/argo-cd"
1850+
sources := make([]v1alpha1.ApplicationSource, 0)
1851+
sources = append(sources, source)
1852+
1853+
_, _, _, err := ctrl.appStateManager.GetRepoObjs(app, sources, "abc123", []string{"123456"}, false, false, false, &defaultProj, false, false)
1854+
require.NoError(t, err)
1855+
}

reposerver/repository/repository.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,7 @@ func (s *Service) GenerateManifest(ctx context.Context, q *apiclient.ManifestReq
585585
var err error
586586

587587
// Skip this path for ref only sources
588-
if q.HasMultipleSources && q.ApplicationSource.Path == "" && !q.ApplicationSource.IsHelm() && q.ApplicationSource.IsRef() {
588+
if q.HasMultipleSources && q.ApplicationSource.Path == "" && !q.ApplicationSource.IsOCI() && !q.ApplicationSource.IsHelm() && q.ApplicationSource.IsRef() {
589589
log.Debugf("Skipping manifest generation for ref only source for application: %s and ref %s", q.AppName, q.ApplicationSource.Ref)
590590
_, revision, err := s.newClientResolveRevision(q.Repo, q.Revision, git.WithCache(s.cache, !q.NoRevisionCache && !q.NoCache))
591591
res = &apiclient.ManifestResponse{

reposerver/repository/repository_test.go

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import (
2525
"k8s.io/apimachinery/pkg/api/resource"
2626
"k8s.io/apimachinery/pkg/util/intstr"
2727

28+
"github.com/argoproj/argo-cd/v3/util/oci"
29+
2830
cacheutil "github.com/argoproj/argo-cd/v3/util/cache"
2931

3032
"github.com/stretchr/testify/assert"
@@ -49,14 +51,15 @@ import (
4951
helmmocks "github.com/argoproj/argo-cd/v3/util/helm/mocks"
5052
utilio "github.com/argoproj/argo-cd/v3/util/io"
5153
iomocks "github.com/argoproj/argo-cd/v3/util/io/mocks"
54+
ocimocks "github.com/argoproj/argo-cd/v3/util/oci/mocks"
5255
)
5356

5457
const testSignature = `gpg: Signature made Wed Feb 26 23:22:34 2020 CET
5558
gpg: using RSA key 4AEE18F83AFDEB23
5659
gpg: Good signature from "GitHub (web-flow commit signing) <[email protected]>" [ultimate]
5760
`
5861

59-
type clientFunc func(*gitmocks.Client, *helmmocks.Client, *iomocks.TempPaths)
62+
type clientFunc func(*gitmocks.Client, *helmmocks.Client, *ocimocks.Client, *iomocks.TempPaths)
6063

6164
type repoCacheMocks struct {
6265
mock.Mock
@@ -104,7 +107,7 @@ func newServiceWithMocks(t *testing.T, root string, signed bool) (*Service, *git
104107
if err != nil {
105108
panic(err)
106109
}
107-
return newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
110+
return newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, ociClient *ocimocks.Client, paths *iomocks.TempPaths) {
108111
gitClient.On("Init").Return(nil)
109112
gitClient.On("IsRevisionPresent", mock.Anything).Return(false)
110113
gitClient.On("Fetch", mock.Anything).Return(nil)
@@ -133,6 +136,10 @@ func newServiceWithMocks(t *testing.T, root string, signed bool) (*Service, *git
133136
helmClient.On("CleanChartCache", oobChart, version).Return(nil)
134137
helmClient.On("DependencyBuild").Return(nil)
135138

139+
ociClient.On("GetTags", mock.Anything, mock.Anything).Return(nil)
140+
ociClient.On("ResolveRevision", mock.Anything, mock.Anything, mock.Anything).Return("", nil)
141+
ociClient.On("Extract", mock.Anything, mock.Anything).Return("./testdata/my-chart", utilio.NopCloser, nil)
142+
136143
paths.On("Add", mock.Anything, mock.Anything).Return(root, nil)
137144
paths.On("GetPath", mock.Anything).Return(root, nil)
138145
paths.On("GetPathIfExists", mock.Anything).Return(root, nil)
@@ -144,8 +151,9 @@ func newServiceWithOpt(t *testing.T, cf clientFunc, root string) (*Service, *git
144151
t.Helper()
145152
helmClient := &helmmocks.Client{}
146153
gitClient := &gitmocks.Client{}
154+
ociClient := &ocimocks.Client{}
147155
paths := &iomocks.TempPaths{}
148-
cf(gitClient, helmClient, paths)
156+
cf(gitClient, helmClient, ociClient, paths)
149157
cacheMocks := newCacheMocks()
150158
t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
151159
service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, &git.NoopCredsStore{}, root)
@@ -156,6 +164,9 @@ func newServiceWithOpt(t *testing.T, cf clientFunc, root string) (*Service, *git
156164
service.newHelmClient = func(_ string, _ helm.Creds, _ bool, _ string, _ string, _ ...helm.ClientOpts) helm.Client {
157165
return helmClient
158166
}
167+
service.newOCIClient = func(_ string, _ oci.Creds, _ string, _ string, _ []string, _ ...oci.ClientOpts) (oci.Client, error) {
168+
return ociClient, nil
169+
}
159170
service.gitRepoInitializer = func(_ string) goio.Closer {
160171
return utilio.NopCloser
161172
}
@@ -184,7 +195,7 @@ func newServiceWithCommitSHA(t *testing.T, root, revision string) *Service {
184195
revisionErr = errors.New("not a commit SHA")
185196
}
186197

187-
service, gitClient, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, paths *iomocks.TempPaths) {
198+
service, gitClient, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
188199
gitClient.On("Init").Return(nil)
189200
gitClient.On("IsRevisionPresent", mock.Anything).Return(false)
190201
gitClient.On("Fetch", mock.Anything).Return(nil)
@@ -3515,7 +3526,7 @@ func TestErrorGetGitDirectories(t *testing.T) {
35153526
},
35163527
}, want: nil, wantErr: assert.Error},
35173528
{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
3518-
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, paths *iomocks.TempPaths) {
3529+
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
35193530
gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
35203531
gitClient.On("LsRemote", mock.Anything).Return("", errors.New("ah error"))
35213532
gitClient.On("Root").Return(root)
@@ -3532,7 +3543,7 @@ func TestErrorGetGitDirectories(t *testing.T) {
35323543
},
35333544
}, want: nil, wantErr: assert.Error},
35343545
{name: "ErrorVerifyCommit", fields: fields{service: func() *Service {
3535-
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, paths *iomocks.TempPaths) {
3546+
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
35363547
gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
35373548
gitClient.On("LsRemote", mock.Anything).Return("", errors.New("ah error"))
35383549
gitClient.On("VerifyCommitSignature", mock.Anything).Return("", fmt.Errorf("revision %s is not signed", "sadfsadf"))
@@ -3566,7 +3577,7 @@ func TestErrorGetGitDirectories(t *testing.T) {
35663577
func TestGetGitDirectories(t *testing.T) {
35673578
// test not using the cache
35683579
root := "./testdata/git-files-dirs"
3569-
s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, paths *iomocks.TempPaths) {
3580+
s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
35703581
gitClient.On("Init").Return(nil)
35713582
gitClient.On("IsRevisionPresent", mock.Anything).Return(false)
35723583
gitClient.On("Fetch", mock.Anything).Return(nil)
@@ -3599,7 +3610,7 @@ func TestGetGitDirectories(t *testing.T) {
35993610
func TestGetGitDirectoriesWithHiddenDirSupported(t *testing.T) {
36003611
// test not using the cache
36013612
root := "./testdata/git-files-dirs"
3602-
s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, paths *iomocks.TempPaths) {
3613+
s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
36033614
gitClient.On("Init").Return(nil)
36043615
gitClient.On("IsRevisionPresent", mock.Anything).Return(false)
36053616
gitClient.On("Fetch", mock.Anything).Return(nil)
@@ -3657,7 +3668,7 @@ func TestErrorGetGitFiles(t *testing.T) {
36573668
},
36583669
}, want: nil, wantErr: assert.Error},
36593670
{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
3660-
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, paths *iomocks.TempPaths) {
3671+
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
36613672
gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
36623673
gitClient.On("LsRemote", mock.Anything).Return("", errors.New("ah error"))
36633674
gitClient.On("Root").Return(root)
@@ -3693,7 +3704,7 @@ func TestGetGitFiles(t *testing.T) {
36933704
"./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/app/foo/bar/config.yaml",
36943705
}
36953706
root := ""
3696-
s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, paths *iomocks.TempPaths) {
3707+
s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
36973708
gitClient.On("Init").Return(nil)
36983709
gitClient.On("IsRevisionPresent", mock.Anything).Return(false)
36993710
gitClient.On("Fetch", mock.Anything).Return(nil)
@@ -3760,7 +3771,7 @@ func TestErrorUpdateRevisionForPaths(t *testing.T) {
37603771
},
37613772
}, want: nil, wantErr: assert.Error},
37623773
{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
3763-
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, paths *iomocks.TempPaths) {
3774+
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
37643775
gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
37653776
gitClient.On("LsRemote", mock.Anything).Return("", errors.New("ah error"))
37663777
gitClient.On("Root").Return(root)
@@ -3778,7 +3789,7 @@ func TestErrorUpdateRevisionForPaths(t *testing.T) {
37783789
},
37793790
}, want: nil, wantErr: assert.Error},
37803791
{name: "InvalidResolveSyncedRevision", fields: fields{service: func() *Service {
3781-
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, paths *iomocks.TempPaths) {
3792+
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
37823793
gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
37833794
gitClient.On("LsRemote", "HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
37843795
gitClient.On("LsRemote", mock.Anything).Return("", errors.New("ah error"))
@@ -3831,7 +3842,7 @@ func TestUpdateRevisionForPaths(t *testing.T) {
38313842
cacheHit *cacheHit
38323843
}{
38333844
{name: "NoPathAbort", fields: func() fields {
3834-
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *iomocks.TempPaths) {
3845+
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, _ *iomocks.TempPaths) {
38353846
gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
38363847
}, ".")
38373848
return fields{
@@ -3846,7 +3857,7 @@ func TestUpdateRevisionForPaths(t *testing.T) {
38463857
},
38473858
}, want: &apiclient.UpdateRevisionForPathsResponse{}, wantErr: assert.NoError},
38483859
{name: "SameResolvedRevisionAbort", fields: func() fields {
3849-
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, paths *iomocks.TempPaths) {
3860+
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
38503861
gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
38513862
gitClient.On("LsRemote", "HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
38523863
gitClient.On("LsRemote", "SYNCEDHEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
@@ -3869,7 +3880,7 @@ func TestUpdateRevisionForPaths(t *testing.T) {
38693880
Revision: "632039659e542ed7de0c170a4fcc1c571b288fc0",
38703881
}, wantErr: assert.NoError},
38713882
{name: "ChangedFilesDoNothing", fields: func() fields {
3872-
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, paths *iomocks.TempPaths) {
3883+
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
38733884
gitClient.On("Init").Return(nil)
38743885
gitClient.On("Fetch", mock.Anything).Once().Return(nil)
38753886
gitClient.On("IsRevisionPresent", "632039659e542ed7de0c170a4fcc1c571b288fc0").Once().Return(false)
@@ -3902,7 +3913,7 @@ func TestUpdateRevisionForPaths(t *testing.T) {
39023913
Changes: true,
39033914
}, wantErr: assert.NoError},
39043915
{name: "NoChangesUpdateCache", fields: func() fields {
3905-
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, paths *iomocks.TempPaths) {
3916+
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
39063917
gitClient.On("Init").Return(nil)
39073918
gitClient.On("Fetch", mock.Anything).Once().Return(nil)
39083919
gitClient.On("IsRevisionPresent", "632039659e542ed7de0c170a4fcc1c571b288fc0").Once().Return(false)
@@ -3944,7 +3955,7 @@ func TestUpdateRevisionForPaths(t *testing.T) {
39443955
revision: "632039659e542ed7de0c170a4fcc1c571b288fc0",
39453956
}},
39463957
{name: "NoChangesHelmMultiSourceUpdateCache", fields: func() fields {
3947-
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, paths *iomocks.TempPaths) {
3958+
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
39483959
gitClient.On("Init").Return(nil)
39493960
gitClient.On("IsRevisionPresent", "632039659e542ed7de0c170a4fcc1c571b288fc0").Once().Return(false)
39503961
gitClient.On("Fetch", mock.Anything).Once().Return(nil)
@@ -4480,3 +4491,34 @@ func Test_SkipSchemaValidation(t *testing.T) {
44804491
require.ErrorContains(t, err, "values don't meet the specifications of the schema(s)")
44814492
})
44824493
}
4494+
4495+
func TestGenerateManifest_OCISourceSkipsGitClient(t *testing.T) {
4496+
svc := newService(t, t.TempDir())
4497+
4498+
gitCalled := false
4499+
svc.newGitClient = func(_, _ string, _ git.Creds, _, _ bool, _, _ string, _ ...git.ClientOpts) (git.Client, error) {
4500+
gitCalled = true
4501+
return nil, errors.New("git should not be called for OCI")
4502+
}
4503+
4504+
req := &apiclient.ManifestRequest{
4505+
HasMultipleSources: true,
4506+
Repo: &v1alpha1.Repository{
4507+
Repo: "oci://example.com/foo",
4508+
},
4509+
ApplicationSource: &v1alpha1.ApplicationSource{
4510+
Path: "",
4511+
TargetRevision: "v1",
4512+
Ref: "foo",
4513+
RepoURL: "oci://example.com/foo",
4514+
},
4515+
ProjectName: "foo-project",
4516+
ProjectSourceRepos: []string{"*"},
4517+
}
4518+
4519+
_, err := svc.GenerateManifest(t.Context(), req)
4520+
require.NoError(t, err)
4521+
4522+
// verify that newGitClient was never invoked
4523+
assert.False(t, gitCalled, "GenerateManifest should not invoke Git for OCI sources")
4524+
}

0 commit comments

Comments
 (0)