Skip to content

Commit b62f393

Browse files
committed
feat: Add support for token auth in oci-sync
1 parent 5a81da7 commit b62f393

21 files changed

+622
-71
lines changed

cmd/oci-sync/main.go

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ var flOneTime = flag.Bool("one-time", util.EnvBool("OCI_SYNC_ONE_TIME", false),
5252
"exit after the first sync")
5353
var flMaxSyncFailures = flag.Int("max-sync-failures", util.EnvInt("OCI_SYNC_MAX_SYNC_FAILURES", 0),
5454
"the number of consecutive failures allowed before aborting (the first sync must succeed, -1 will retry forever after the initial sync)")
55+
var flUsername = flag.String("username", util.EnvString("OCI_SYNC_USERNAME", ""),
56+
"the username to use for oci authentication")
57+
var flPassword = flag.String("password", util.EnvString("OCI_SYNC_PASSWORD", ""),
58+
"the password or personal access token to use for oci authentication")
5559

5660
func main() {
5761
utillog.Setup()
@@ -89,25 +93,9 @@ func main() {
8993
pollPeriod := util.WaitTime(*flWait)
9094
backoff := util.SyncContainerBackoff(pollPeriod)
9195

92-
var authenticator authn.Authenticator
93-
switch configsync.AuthType(*flAuth) {
94-
case configsync.AuthNone:
95-
authenticator = authn.Anonymous
96-
case configsync.AuthGCPServiceAccount, configsync.AuthK8sServiceAccount, configsync.AuthGCENode:
97-
authenticator = &oci.CredentialAuthenticator{
98-
CredentialProvider: &auth.CachingCredentialProvider{
99-
Scopes: auth.OCISourceScopes(),
100-
},
101-
}
102-
default:
103-
utillog.HandleError(log, true, "ERROR: --auth type must be one of %#v, but found %q",
104-
[]configsync.AuthType{
105-
configsync.AuthNone,
106-
configsync.AuthGCPServiceAccount,
107-
configsync.AuthK8sServiceAccount,
108-
configsync.AuthGCENode,
109-
},
110-
*flAuth)
96+
authenticator, err := getAuthenticator(configsync.AuthType(*flAuth))
97+
if err != nil {
98+
utillog.HandleError(log, true, "failed to create authenticator: %v", err)
11199
}
112100

113101
fetcher := &oci.Fetcher{
@@ -170,3 +158,30 @@ func sleepForever() {
170158
<-c
171159
os.Exit(0)
172160
}
161+
162+
func getAuthenticator(authType configsync.AuthType) (authn.Authenticator, error) {
163+
switch authType {
164+
case configsync.AuthNone:
165+
return authn.Anonymous, nil
166+
case configsync.AuthToken:
167+
return &authn.Basic{
168+
Username: *flUsername,
169+
Password: *flPassword,
170+
}, nil
171+
case configsync.AuthGCPServiceAccount, configsync.AuthK8sServiceAccount, configsync.AuthGCENode:
172+
return &oci.CredentialAuthenticator{
173+
CredentialProvider: &auth.CachingCredentialProvider{
174+
Scopes: auth.OCISourceScopes(),
175+
},
176+
}, nil
177+
default:
178+
return nil, fmt.Errorf("--auth type must be one of %#v, but found %q",
179+
[]configsync.AuthType{
180+
configsync.AuthNone,
181+
configsync.AuthGCPServiceAccount,
182+
configsync.AuthK8sServiceAccount,
183+
configsync.AuthGCENode,
184+
configsync.AuthToken,
185+
}, authType)
186+
}
187+
}

cmd/oci-sync/main_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"testing"
19+
20+
"github.com/GoogleContainerTools/config-sync/pkg/api/configsync"
21+
"github.com/GoogleContainerTools/config-sync/pkg/auth"
22+
"github.com/GoogleContainerTools/config-sync/pkg/oci"
23+
"github.com/google/go-containerregistry/pkg/authn"
24+
"github.com/stretchr/testify/assert"
25+
)
26+
27+
func TestGetAuthenticator(t *testing.T) {
28+
username := "username"
29+
password := "password"
30+
flUsername = &username
31+
flPassword = &password
32+
33+
testCases := []struct {
34+
name string
35+
auth configsync.AuthType
36+
expected authn.Authenticator
37+
hasError bool
38+
}{
39+
{
40+
name: "none",
41+
auth: configsync.AuthNone,
42+
expected: authn.Anonymous,
43+
},
44+
{
45+
name: "token",
46+
auth: configsync.AuthToken,
47+
expected: &authn.Basic{
48+
Username: username,
49+
Password: password,
50+
},
51+
},
52+
{
53+
name: "k8sserviceaccount",
54+
auth: configsync.AuthK8sServiceAccount,
55+
expected: &oci.CredentialAuthenticator{
56+
CredentialProvider: &auth.CachingCredentialProvider{
57+
Scopes: auth.OCISourceScopes(),
58+
},
59+
},
60+
},
61+
{
62+
name: "gcenode",
63+
auth: configsync.AuthGCENode,
64+
expected: &oci.CredentialAuthenticator{
65+
CredentialProvider: &auth.CachingCredentialProvider{
66+
Scopes: auth.OCISourceScopes(),
67+
},
68+
},
69+
},
70+
{
71+
name: "gcpserviceaccount",
72+
auth: configsync.AuthGCPServiceAccount,
73+
expected: &oci.CredentialAuthenticator{
74+
CredentialProvider: &auth.CachingCredentialProvider{
75+
Scopes: auth.OCISourceScopes(),
76+
},
77+
},
78+
},
79+
{
80+
name: "invalid auth",
81+
auth: "invalid",
82+
hasError: true,
83+
},
84+
}
85+
86+
for _, tc := range testCases {
87+
t.Run(tc.name, func(t *testing.T) {
88+
auth, err := getAuthenticator(tc.auth)
89+
assert.Equal(t, tc.hasError, err != nil)
90+
assert.Equal(t, tc.expected, auth)
91+
})
92+
}
93+
}

e2e/testcases/oci_sync_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,74 @@ func TestPublicOCI(t *testing.T) {
111111
kustomizecomponents.ValidateAllTenants(nt, string(declared.RootScope), "../base", tenant)
112112
}
113113

114+
// TestOCIARTokenAuth verifies Config Sync can pull Helm chart from private
115+
// Artifact Registry with Token auth type.
116+
//
117+
// Test pre-requisites:
118+
// - Google service account
119+
// `e2e-test-ar-reader@${GCP_PROJECT}.iam.gserviceaccount.com` is created
120+
// with `roles/artifactregistry.reader` for accessing images in Artifact
121+
// Registry.
122+
// - A JSON key file is generated for this service account and stored in
123+
// Secret Manager
124+
//
125+
// Test handles service account key rotation.
126+
func TestOCIARTokenAuth(t *testing.T) {
127+
nt := nomostest.New(t,
128+
nomostesting.SyncSourceOCI,
129+
ntopts.SyncWithGitSource(nomostest.DefaultRootSyncID, ntopts.Unstructured),
130+
ntopts.RequireGKE(t),
131+
ntopts.RequireOCIArtifactRegistry(t),
132+
)
133+
rootSyncID := nomostest.DefaultRootSyncID
134+
rootSyncKey := rootSyncID.ObjectKey
135+
136+
gsaKeySecretID := "config-sync-ci-ar-key"
137+
gsaEmail := registryproviders.ArtifactRegistryReaderEmail()
138+
gsaName := registryproviders.ArtifactRegistryReaderName
139+
gsaKeyFilePath, err := fetchServiceAccountKeyFile(nt, *e2e.GCPProject, gsaKeySecretID, gsaEmail, gsaName)
140+
if err != nil {
141+
nt.T.Fatal(err)
142+
}
143+
144+
nt.T.Log("Creating kubernetes secret for authentication")
145+
_, err = nt.Shell.Kubectl("create", "secret", "generic", "foo",
146+
"--namespace", configsync.ControllerNamespace,
147+
"--from-literal", "username=_json_key",
148+
"--from-file", fmt.Sprintf("password=%s", gsaKeyFilePath))
149+
if err != nil {
150+
nt.T.Fatalf("failed to create secret, err: %v", err)
151+
}
152+
nt.T.Cleanup(func() {
153+
nt.MustKubectl("delete", "secret", "foo", "-n", configsync.ControllerNamespace, "--ignore-not-found")
154+
})
155+
156+
// OCI image will only contain the bookinfo-admin role
157+
bookinfoRole := k8sobjects.RoleObject(core.Name("bookinfo-admin"))
158+
image, err := nt.BuildAndPushOCIImage(rootSyncKey, registryproviders.ImageInputObjects(nt.Scheme, bookinfoRole))
159+
if err != nil {
160+
nt.T.Fatalf("failed to push oci image: %v", err)
161+
}
162+
163+
nt.T.Log("Update RootSync to sync from a private Artifact Registry")
164+
rs := nt.RootSyncObjectOCI(configsync.RootSyncName, image.OCIImageID().WithoutDigest(), "", image.Digest)
165+
rs.Spec.Oci = &v1beta1.Oci{
166+
Image: rs.Spec.Oci.Image,
167+
Auth: configsync.AuthToken,
168+
SecretRef: &v1beta1.SecretReference{
169+
Name: "foo",
170+
},
171+
Period: metav1.Duration{Duration: 5 * time.Second},
172+
}
173+
nt.Must(nt.KubeClient.Apply(rs))
174+
175+
nt.T.Log("Wait for RootSync to sync from an oci image chart")
176+
nt.Must(nt.WatchForAllSyncs())
177+
178+
nt.T.Log("Validate Role from OCI image exists")
179+
nt.Must(nt.Validate(bookinfoRole.Name, "default", &rbacv1.Role{}))
180+
}
181+
114182
func TestSwitchFromGitToOciCentralized(t *testing.T) {
115183
namespace := testNs
116184
rootSyncID := nomostest.DefaultRootSyncID

manifests/reposync-crd.yaml

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,12 +292,13 @@ spec:
292292
auth:
293293
description: |-
294294
auth is the type of secret configured for access to the OCI package.
295-
Must be one of gcenode, gcpserviceaccount, k8sserviceaccount, or none.
295+
Must be one of gcenode, gcpserviceaccount, k8sserviceaccount, token, or none.
296296
The validation of this is case-sensitive. Required.
297297
enum:
298298
- gcenode
299299
- gcpserviceaccount
300300
- k8sserviceaccount
301+
- token
301302
- none
302303
type: string
303304
caCertSecretRef:
@@ -343,6 +344,16 @@ spec:
343344
granularity, and it is easy to introduce a bug where it looks like the
344345
code is dealing with seconds but its actually nanoseconds (or vice versa).
345346
type: string
347+
secretRef:
348+
description: |-
349+
secretRef holds the authentication secret for accessing
350+
the OCI repository.
351+
nullable: true
352+
properties:
353+
name:
354+
description: name represents the secret name.
355+
type: string
356+
type: object
346357
required:
347358
- auth
348359
- image
@@ -1405,12 +1416,13 @@ spec:
14051416
auth:
14061417
description: |-
14071418
auth is the type of secret configured for access to the OCI package.
1408-
Must be one of gcenode, gcpserviceaccount, k8sserviceaccount, or none.
1419+
Must be one of gcenode, gcpserviceaccount, k8sserviceaccount, token, or none.
14091420
The validation of this is case-sensitive. Required.
14101421
enum:
14111422
- gcenode
14121423
- gcpserviceaccount
14131424
- k8sserviceaccount
1425+
- token
14141426
- none
14151427
type: string
14161428
caCertSecretRef:
@@ -1456,6 +1468,16 @@ spec:
14561468
granularity, and it is easy to introduce a bug where it looks like the
14571469
code is dealing with seconds but its actually nanoseconds (or vice versa).
14581470
type: string
1471+
secretRef:
1472+
description: |-
1473+
secretRef holds the authentication secret for accessing
1474+
the OCI repository.
1475+
nullable: true
1476+
properties:
1477+
name:
1478+
description: name represents the secret name.
1479+
type: string
1480+
type: object
14591481
required:
14601482
- auth
14611483
- image

manifests/rootsync-crd.yaml

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,13 @@ spec:
304304
auth:
305305
description: |-
306306
auth is the type of secret configured for access to the OCI package.
307-
Must be one of gcenode, gcpserviceaccount, k8sserviceaccount, or none.
307+
Must be one of gcenode, gcpserviceaccount, k8sserviceaccount, token, or none.
308308
The validation of this is case-sensitive. Required.
309309
enum:
310310
- gcenode
311311
- gcpserviceaccount
312312
- k8sserviceaccount
313+
- token
313314
- none
314315
type: string
315316
caCertSecretRef:
@@ -355,6 +356,16 @@ spec:
355356
granularity, and it is easy to introduce a bug where it looks like the
356357
code is dealing with seconds but its actually nanoseconds (or vice versa).
357358
type: string
359+
secretRef:
360+
description: |-
361+
secretRef holds the authentication secret for accessing
362+
the OCI repository.
363+
nullable: true
364+
properties:
365+
name:
366+
description: name represents the secret name.
367+
type: string
368+
type: object
358369
required:
359370
- auth
360371
- image
@@ -1478,12 +1489,13 @@ spec:
14781489
auth:
14791490
description: |-
14801491
auth is the type of secret configured for access to the OCI package.
1481-
Must be one of gcenode, gcpserviceaccount, k8sserviceaccount, or none.
1492+
Must be one of gcenode, gcpserviceaccount, k8sserviceaccount, token, or none.
14821493
The validation of this is case-sensitive. Required.
14831494
enum:
14841495
- gcenode
14851496
- gcpserviceaccount
14861497
- k8sserviceaccount
1498+
- token
14871499
- none
14881500
type: string
14891501
caCertSecretRef:
@@ -1529,6 +1541,16 @@ spec:
15291541
granularity, and it is easy to introduce a bug where it looks like the
15301542
code is dealing with seconds but its actually nanoseconds (or vice versa).
15311543
type: string
1544+
secretRef:
1545+
description: |-
1546+
secretRef holds the authentication secret for accessing
1547+
the OCI repository.
1548+
nullable: true
1549+
properties:
1550+
name:
1551+
description: name represents the secret name.
1552+
type: string
1553+
type: object
15321554
required:
15331555
- auth
15341556
- image

pkg/api/configsync/v1alpha1/ociconfig.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ type Oci struct {
4545
Period metav1.Duration `json:"period,omitempty"`
4646

4747
// auth is the type of secret configured for access to the OCI package.
48-
// Must be one of gcenode, gcpserviceaccount, k8sserviceaccount, or none.
48+
// Must be one of gcenode, gcpserviceaccount, k8sserviceaccount, token, or none.
4949
// The validation of this is case-sensitive. Required.
5050
//
51-
// +kubebuilder:validation:Enum=gcenode;gcpserviceaccount;k8sserviceaccount;none
51+
// +kubebuilder:validation:Enum=gcenode;gcpserviceaccount;k8sserviceaccount;token;none
5252
Auth configsync.AuthType `json:"auth"`
5353

5454
// gcpServiceAccountEmail specifies the GCP service account used to annotate
@@ -64,4 +64,10 @@ type Oci struct {
6464
// +nullable
6565
// +optional
6666
CACertSecretRef *SecretReference `json:"caCertSecretRef,omitempty"`
67+
68+
// secretRef holds the authentication secret for accessing
69+
// the OCI repository.
70+
// +nullable
71+
// +optional
72+
SecretRef *SecretReference `json:"secretRef,omitempty"`
6773
}

0 commit comments

Comments
 (0)