Skip to content

Commit 0f8d447

Browse files
committed
test: Add SSM gitprovider to E2E tests
* Adds SSM gitprovider * Adds tests to test basic flow of fetching from a SSM repo
1 parent 1e2369d commit 0f8d447

File tree

14 files changed

+261
-27
lines changed

14 files changed

+261
-27
lines changed

e2e/flags.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,14 @@ var TestCluster = flag.String("test-cluster", Kind,
133133
var ShareTestEnv = flag.Bool("share-test-env", false,
134134
"Specify that the test is using a shared test environment instead of fresh installation per test case.")
135135

136+
// SSMInstanceRegion is the region of the Secure Source Manager instance to be used by SSM tests
137+
var SSMInstanceRegion = flag.String("ssm-instance-region", util.EnvString("E2E_SSM_INSTANCE_REGION", "us-central1"),
138+
"The region of the Secure Source Manager instance to be used by SSM tests. Defaults to E2E_SSM_INSTANCE_REGION env var")
139+
136140
// GitProvider is the provider that hosts the Git repositories.
137141
var GitProvider = newStringEnum("git-provider", util.EnvString("E2E_GIT_PROVIDER", Local),
138142
"The git provider that hosts the Git repositories. Defaults to Local.",
139-
[]string{Local, Bitbucket, GitLab, CSR})
143+
[]string{Local, Bitbucket, GitLab, CSR, SSM})
140144

141145
// OCIProvider is the provider that hosts the OCI repositories.
142146
var OCIProvider = newStringEnum("oci-provider", util.EnvString("E2E_OCI_PROVIDER", Local),
@@ -300,6 +304,8 @@ const (
300304
CSR = "csr"
301305
// ArtifactRegistry indicates using Google Artifact Registry to host the repositories.
302306
ArtifactRegistry = "gar"
307+
// SSM indicates using Secure Source Manager to host the repositories.
308+
SSM = "ssm"
303309
)
304310

305311
// NumParallel returns the number of parallel test threads

e2e/nomostest/config_sync.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,9 @@ func rootSyncObjectV1Alpha1Git(name, repoURL string, sourceFormat configsync.Sou
773773
rs.Spec.Git.Auth = configsync.AuthGCPServiceAccount
774774
rs.Spec.Git.GCPServiceAccountEmail = gitproviders.CSRReaderEmail()
775775
}
776+
case e2e.SSM:
777+
rs.Spec.Git.Auth = configsync.AuthGCPServiceAccount
778+
rs.Spec.Git.GCPServiceAccountEmail = gitproviders.SSMServiceAccountEmail()
776779
default:
777780
rs.Spec.Git.Auth = configsync.AuthSSH
778781
rs.Spec.Git.SecretRef = &v1alpha1.SecretReference{
@@ -821,6 +824,9 @@ func rootSyncObjectV1Beta1Git(name, repoURL string, branch, revision, syncPath s
821824
rs.Spec.Git.Auth = configsync.AuthGCPServiceAccount
822825
rs.Spec.Git.GCPServiceAccountEmail = gitproviders.CSRReaderEmail()
823826
}
827+
case e2e.SSM:
828+
rs.Spec.Git.Auth = configsync.AuthGCPServiceAccount
829+
rs.Spec.Git.GCPServiceAccountEmail = gitproviders.SSMServiceAccountEmail()
824830
default:
825831
rs.Spec.Git.Auth = configsync.AuthSSH
826832
rs.Spec.Git.SecretRef = &v1beta1.SecretReference{
@@ -889,6 +895,9 @@ func repoSyncObjectV1Alpha1Git(nn types.NamespacedName, repoURL string) *v1alpha
889895
rs.Spec.Git.Auth = configsync.AuthGCPServiceAccount
890896
rs.Spec.Git.GCPServiceAccountEmail = gitproviders.CSRReaderEmail()
891897
}
898+
case e2e.SSM:
899+
rs.Spec.Git.Auth = configsync.AuthGCPServiceAccount
900+
rs.Spec.Git.GCPServiceAccountEmail = gitproviders.SSMServiceAccountEmail()
892901
default:
893902
rs.Spec.Git.Auth = configsync.AuthSSH
894903
rs.Spec.Git.SecretRef = &v1alpha1.SecretReference{
@@ -939,6 +948,9 @@ func repoSyncObjectV1Beta1Git(nn types.NamespacedName, repoURL, branch, revision
939948
rs.Spec.Git.Auth = configsync.AuthGCPServiceAccount
940949
rs.Spec.Git.GCPServiceAccountEmail = gitproviders.CSRReaderEmail()
941950
}
951+
case e2e.SSM:
952+
rs.Spec.Git.Auth = configsync.AuthGCPServiceAccount
953+
rs.Spec.Git.GCPServiceAccountEmail = gitproviders.SSMServiceAccountEmail()
942954
default:
943955
rs.Spec.Git.Auth = configsync.AuthSSH
944956
rs.Spec.Git.SecretRef = &v1beta1.SecretReference{

e2e/nomostest/gitproviders/cloud_source_repository.go

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,15 @@
1515
package gitproviders
1616

1717
import (
18-
"crypto/sha1"
19-
"encoding/hex"
2018
"fmt"
2119
"strings"
2220

2321
"kpt.dev/configsync/e2e"
22+
"kpt.dev/configsync/e2e/nomostest/gitproviders/util"
2423
"kpt.dev/configsync/e2e/nomostest/testing"
2524
"kpt.dev/configsync/e2e/nomostest/testshell"
2625
)
2726

28-
const (
29-
repoNameMaxLen = 63
30-
repoNameHashLen = 8
31-
)
32-
3327
// CSRReaderEmail returns the email of the google service account with
3428
// permission to read from Cloud Source Registry.
3529
func CSRReaderEmail() string {
@@ -56,7 +50,7 @@ func newCSRClient(repoPrefix string, shell *testshell.TestShell) *CSRClient {
5650
}
5751

5852
func (c *CSRClient) fullName(name string) string {
59-
return sanitizeRepoName("cs-e2e-"+c.repoPrefix, name)
53+
return util.SanitizeRepoName("cs-e2e-"+c.repoPrefix, name)
6054
}
6155

6256
// Type returns the provider type.
@@ -128,16 +122,3 @@ func (c *CSRClient) DeleteRepositories(names ...string) error {
128122
func (c *CSRClient) DeleteObsoleteRepos() error {
129123
return nil
130124
}
131-
132-
// CSR repo name may contain between 3 and 63 lowercase letters, digits and hyphens.
133-
// sanitizeRepoName replaces all slashes with hyphens, and truncate the name.
134-
func sanitizeRepoName(repoPrefix, name string) string {
135-
fullName := "cs-e2e-" + repoPrefix + "-" + name
136-
hashBytes := sha1.Sum([]byte(fullName))
137-
hashStr := hex.EncodeToString(hashBytes[:])[:repoNameHashLen]
138-
139-
if len(fullName) > repoNameMaxLen-1-repoNameHashLen {
140-
fullName = fullName[:repoNameMaxLen-1-repoNameHashLen]
141-
}
142-
return fmt.Sprintf("%s-%s", strings.ReplaceAll(fullName, "/", "-"), hashStr)
143-
}

e2e/nomostest/gitproviders/git-provider.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
package gitproviders
1616

1717
import (
18+
"strings"
19+
1820
"kpt.dev/configsync/e2e"
1921
"kpt.dev/configsync/e2e/nomostest/testing"
2022
"kpt.dev/configsync/e2e/nomostest/testlogger"
@@ -62,6 +64,15 @@ func NewGitProvider(t testing.NTB, provider, clusterName string, logger *testlog
6264
return client
6365
case e2e.CSR:
6466
return newCSRClient(clusterName, shell)
67+
case e2e.SSM:
68+
out, err := shell.ExecWithDebug("gcloud", "projects", "describe", *e2e.GCPProject, "--format", "value(projectNumber)")
69+
if err != nil {
70+
t.Fatalf("getting project number: %w", err)
71+
}
72+
73+
projectNumber := strings.Split(string(out), "\n")[0]
74+
75+
return newSSMClient(clusterName, shell, projectNumber)
6576
default:
6677
return &LocalProvider{}
6778
}

e2e/nomostest/gitproviders/repository.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,12 @@ func (g *ReadWriteRepository) Init() error {
282282
// connect securely to CSR using Google Account credentials.
283283
[]string{"config", fmt.Sprintf("credential.%s.helper", testing.CSRHost), "gcloud.sh"})
284284
}
285+
if g.GitProvider.Type() == e2e.SSM {
286+
cmds = append(cmds,
287+
// Use credential helper script to provide information that Git needs to
288+
// connect securely to SSM using Google Account credentials.
289+
[]string{"config", "credential.https://*.*.sourcemanager.dev.helper", "gcloud.sh"})
290+
}
285291
return g.BulkGit(cmds...)
286292
}
287293

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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 gitproviders
16+
17+
import (
18+
"fmt"
19+
"strings"
20+
21+
"kpt.dev/configsync/e2e"
22+
"kpt.dev/configsync/e2e/nomostest/gitproviders/util"
23+
"kpt.dev/configsync/e2e/nomostest/testing"
24+
"kpt.dev/configsync/e2e/nomostest/testshell"
25+
)
26+
27+
// SSMServiceAccountEmail returns the email of the google service account with
28+
// permission to read from Secure Source Manager.
29+
func SSMServiceAccountEmail() string {
30+
return fmt.Sprintf("e2e-ssm-reader-sa@%s.iam.gserviceaccount.com", *e2e.GCPProject)
31+
}
32+
33+
// SSMClient is the client that interacts with Google Secure Source Manager.
34+
type SSMClient struct {
35+
// project in which to store the source repo
36+
project string
37+
// project number which is needed for the sync URL of the source repo
38+
projectNumber string
39+
// SSM instance ID where the source repo will be stored
40+
instanceID string
41+
// region of the SSM instance in which to store the source repo
42+
region string
43+
// repoPrefix is used to avoid overlap
44+
repoPrefix string
45+
// shell used for invoking CLI tools
46+
shell *testshell.TestShell
47+
}
48+
49+
var _ GitProvider = &SSMClient{}
50+
51+
// newSSMClient instantiates a new SSM client.
52+
func newSSMClient(repoPrefix string, shell *testshell.TestShell, projectNumber string) *SSMClient {
53+
return &SSMClient{
54+
project: *e2e.GCPProject,
55+
instanceID: testing.SSMInstanceID,
56+
region: *e2e.SSMInstanceRegion,
57+
repoPrefix: repoPrefix,
58+
shell: shell,
59+
projectNumber: projectNumber,
60+
}
61+
}
62+
63+
func (c *SSMClient) fullName(name string) string {
64+
return util.SanitizeRepoName(c.repoPrefix, name)
65+
}
66+
67+
// Type returns the provider type.
68+
func (c *SSMClient) Type() string {
69+
return e2e.SSM
70+
}
71+
72+
// RemoteURL returns the Git URL for the SSM repository.
73+
// name refers to the repo name in the format of <NAMESPACE>/<NAME> of RootSync|RepoSync.
74+
func (c *SSMClient) RemoteURL(name string) (string, error) {
75+
return c.SyncURL(name), nil
76+
}
77+
78+
// SyncURL returns a URL for Config Sync to sync from.
79+
func (c *SSMClient) SyncURL(name string) string {
80+
return fmt.Sprintf("https://%s-%s-git.%s.sourcemanager.dev/%s/%s", c.instanceID, c.projectNumber, c.region, c.project, name)
81+
}
82+
83+
func (c *SSMClient) login() error {
84+
_, err := c.shell.ExecWithDebug("gcloud", "init")
85+
if err != nil {
86+
return fmt.Errorf("authorizing gcloud: %w", err)
87+
}
88+
return nil
89+
}
90+
91+
// CreateRepository calls the gcloud SDK to create a remote repository on SSM.
92+
// It returns the full name with a prefix.
93+
func (c *SSMClient) CreateRepository(name string) (string, error) {
94+
fullName := c.fullName(name)
95+
if err := c.login(); err != nil {
96+
return fullName, err
97+
}
98+
99+
out, err := c.shell.ExecWithDebug("gcloud", "beta", "source-manager", "repos",
100+
"describe", fullName, "--region", c.region,
101+
"--project", c.project)
102+
if err == nil {
103+
return fullName, nil // repo already exists, skip creation
104+
}
105+
if !strings.Contains(string(out), "NOT_FOUND") {
106+
return fullName, fmt.Errorf("describing source repository: %w", err)
107+
}
108+
109+
_, err = c.shell.ExecWithDebug("gcloud", "beta", "source-manager", "repos",
110+
"create", fullName, "--region", c.region,
111+
"--instance", c.instanceID,
112+
"--project", c.project)
113+
if err != nil {
114+
return fullName, fmt.Errorf("creating source repository: %w", err)
115+
}
116+
117+
return fullName, nil
118+
}
119+
120+
// DeleteRepositories calls the gcloud SDK to delete the provided repositories from SSM.
121+
func (c *SSMClient) DeleteRepositories(names ...string) error {
122+
for _, name := range names {
123+
_, err := c.shell.ExecWithDebug("gcloud", "beta", "source-manager", "repos",
124+
"delete", name,
125+
"--region", c.region,
126+
"--project", c.project)
127+
if err != nil {
128+
return fmt.Errorf("deleting source repository: %w", err)
129+
}
130+
}
131+
return nil
132+
}
133+
134+
// DeleteObsoleteRepos is a no-op because SSM repo names are determined by the
135+
// test cluster name and RSync namespace and name, so it can be reused if it
136+
// failed to be deleted after the test.
137+
func (c *SSMClient) DeleteObsoleteRepos() error {
138+
return nil
139+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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 util
16+
17+
import (
18+
"crypto/sha1"
19+
"encoding/hex"
20+
"fmt"
21+
"strings"
22+
)
23+
24+
const (
25+
repoNameMaxLen = 63
26+
repoNameHashLen = 8
27+
)
28+
29+
// SanitizeRepoName replaces all slashes with hyphens, and truncate the name.
30+
// repo name may contain between 3 and 63 lowercase letters, digits and hyphens.
31+
func SanitizeRepoName(repoPrefix, name string) string {
32+
fullName := "cs-e2e-" + repoPrefix + "-" + name
33+
hashBytes := sha1.Sum([]byte(fullName))
34+
hashStr := hex.EncodeToString(hashBytes[:])[:repoNameHashLen]
35+
36+
if len(fullName) > repoNameMaxLen-1-repoNameHashLen {
37+
fullName = fullName[:repoNameMaxLen-1-repoNameHashLen]
38+
}
39+
return fmt.Sprintf("%s-%s", strings.ReplaceAll(fullName, "/", "-"), hashStr)
40+
}

e2e/nomostest/gitproviders/cloud_source_repository_test.go renamed to e2e/nomostest/gitproviders/util/reponame_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 Google LLC
1+
// Copyright 2025 Google LLC
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
package gitproviders
15+
package util
1616

1717
import (
1818
"testing"
@@ -55,7 +55,7 @@ func TestSanitizeRepoName(t *testing.T) {
5555

5656
for _, tc := range testCases {
5757
t.Run(tc.testName, func(t *testing.T) {
58-
gotName := sanitizeRepoName(tc.repoPrefix, tc.repoName)
58+
gotName := SanitizeRepoName(tc.repoPrefix, tc.repoName)
5959
assert.Equal(t, tc.expectedName, gotName)
6060
})
6161
}

e2e/nomostest/ntopts/new.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,14 @@ func RequireCloudSourceRepository(t testing.NTB) Opt {
159159
return func(_ *New) {}
160160
}
161161

162+
// RequireSecureSourceManagerRepository requires the --git-provider flag to be set to ssm
163+
func RequireSecureSourceManagerRepository(t testing.NTB) Opt {
164+
if *e2e.GitProvider != e2e.SSM {
165+
t.Skip("The --git-provider flag must be set to `ssm` to run this test.")
166+
}
167+
return func(_ *New) {}
168+
}
169+
162170
// RequireHelmArtifactRegistry requires the --helm-provider flag to be set to `gar`.
163171
// RequireHelmArtifactRegistry implies RequireHelmProvider.
164172
func RequireHelmArtifactRegistry(t testing.NTB) Opt {

e2e/nomostest/ssh.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ func downloadSSHKey(nt *NT) (string, error) {
376376
// It skips creating the Secret if the GitProvider is CSR because CSR uses either
377377
// 'gcenode' or 'gcpserviceaccount' for authentication, which doesn't require a Secret.
378378
func CreateNamespaceSecrets(nt *NT, ns string) error {
379-
if nt.GitProvider.Type() != e2e.CSR {
379+
if nt.GitProvider.Type() != e2e.CSR && nt.GitProvider.Type() != e2e.SSM {
380380
privateKeypath := nt.gitPrivateKeyPath
381381
if len(privateKeypath) == 0 {
382382
privateKeypath = privateKeyPath(nt)

0 commit comments

Comments
 (0)