argo-cd/util/db/repository_secrets_test.go
Ville Vesilehto e20828f869
Merge commit from fork
Fixed a race condition in repository credentials handling by
implementing deep copying of secrets before modification.
This prevents concurrent map read/write panics when multiple
goroutines access the same secret.

The fix ensures thread-safe operations by always operating on
copies rather than shared objects.

Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>
2025-09-30 10:45:59 -04:00

1132 lines
41 KiB
Go

package db
import (
"fmt"
"strconv"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes/fake"
k8stesting "k8s.io/client-go/testing"
"github.com/argoproj/argo-cd/v3/common"
appsv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/settings"
)
func TestSecretsRepositoryBackend_CreateRepository(t *testing.T) {
t.Parallel()
type fixture struct {
clientSet *fake.Clientset
repoBackend *secretsRepositoryBackend
}
repo := &appsv1.Repository{
Name: "ArgoCD",
Repo: "git@github.com:argoproj/argo-cd.git",
Username: "someUsername",
Password: "somePassword",
InsecureIgnoreHostKey: false,
EnableLFS: true,
}
setupWithK8sObjects := func(objects ...runtime.Object) *fixture {
clientset := getClientset(objects...)
settingsMgr := settings.NewSettingsManager(t.Context(), clientset, testNamespace)
repoBackend := &secretsRepositoryBackend{db: &db{
ns: testNamespace,
kubeclientset: clientset,
settingsMgr: settingsMgr,
}}
return &fixture{
clientSet: clientset,
repoBackend: repoBackend,
}
}
t.Run("will create repository successfully", func(t *testing.T) {
// given
t.Parallel()
f := setupWithK8sObjects()
// when
output, err := f.repoBackend.CreateRepository(t.Context(), repo)
// then
require.NoError(t, err)
assert.Same(t, repo, output)
secret, err := f.clientSet.CoreV1().Secrets(testNamespace).Get(
t.Context(),
RepoURLToSecretName(repoSecretPrefix, repo.Repo, ""),
metav1.GetOptions{},
)
assert.NotNil(t, secret)
require.NoError(t, err)
assert.Equal(t, common.AnnotationValueManagedByArgoCD, secret.Annotations[common.AnnotationKeyManagedBy])
assert.Equal(t, common.LabelValueSecretTypeRepository, secret.Labels[common.LabelKeySecretType])
assert.Equal(t, repo.Name, string(secret.Data["name"]))
assert.Equal(t, repo.Repo, string(secret.Data["url"]))
assert.Equal(t, repo.Username, string(secret.Data["username"]))
assert.Equal(t, repo.Password, string(secret.Data["password"]))
assert.Empty(t, string(secret.Data["insecureIgnoreHostKey"]))
assert.Equal(t, strconv.FormatBool(repo.EnableLFS), string(secret.Data["enableLfs"]))
})
t.Run("will return proper error if secret does not have expected label", func(t *testing.T) {
// given
t.Parallel()
secret := &corev1.Secret{}
s := secretsRepositoryBackend{}
updatedSecret := s.repositoryToSecret(repo, secret)
delete(updatedSecret.Labels, common.LabelKeySecretType)
f := setupWithK8sObjects(updatedSecret)
f.clientSet.ReactionChain = nil
f.clientSet.AddReactor("create", "secrets", func(_ k8stesting.Action) (handled bool, ret runtime.Object, err error) {
gr := schema.GroupResource{
Group: "v1",
Resource: "secrets",
}
return true, nil, apierrors.NewAlreadyExists(gr, "already exists")
})
// when
output, err := f.repoBackend.CreateRepository(t.Context(), repo)
// then
require.Error(t, err)
assert.Nil(t, output)
status, ok := status.FromError(err)
assert.True(t, ok)
assert.Equal(t, codes.InvalidArgument, status.Code())
})
t.Run("will return proper error if secret already exists", func(t *testing.T) {
// given
t.Parallel()
secName := RepoURLToSecretName(repoSecretPrefix, repo.Repo, "")
secret := &corev1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: secName,
Namespace: "default",
},
}
s := secretsRepositoryBackend{}
updatedSecret := s.repositoryToSecret(repo, secret)
f := setupWithK8sObjects(updatedSecret)
f.clientSet.ReactionChain = nil
f.clientSet.WatchReactionChain = nil
f.clientSet.AddReactor("create", "secrets", func(_ k8stesting.Action) (handled bool, ret runtime.Object, err error) {
gr := schema.GroupResource{
Group: "v1",
Resource: "secrets",
}
return true, nil, apierrors.NewAlreadyExists(gr, "already exists")
})
watcher := watch.NewFakeWithChanSize(1, true)
watcher.Add(updatedSecret)
f.clientSet.AddWatchReactor("secrets", func(_ k8stesting.Action) (handled bool, ret watch.Interface, err error) {
return true, watcher, nil
})
// when
output, err := f.repoBackend.CreateRepository(t.Context(), repo)
// then
require.Error(t, err)
assert.Nil(t, output)
status, ok := status.FromError(err)
assert.True(t, ok)
assert.Equal(t, codes.AlreadyExists, status.Code())
})
}
func TestSecretsRepositoryBackend_GetRepository(t *testing.T) {
repoSecrets := []runtime.Object{
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: RepoURLToSecretName(repoSecretPrefix, "git@github.com:argoproj/argo-cd.git", ""),
Annotations: map[string]string{common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD},
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepository},
},
Data: map[string][]byte{
"name": []byte("ArgoCD"),
"url": []byte("git@github.com:argoproj/argo-cd.git"),
"username": []byte("someUsername"),
"password": []byte("somePassword"),
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: RepoURLToSecretName(repoSecretPrefix, "git@github.com:argoproj/argo-cd.git", "testProject"),
Annotations: map[string]string{common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD},
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepository},
},
Data: map[string][]byte{
"name": []byte("Scoped ArgoCD"),
"url": []byte("git@github.com:argoproj/argo-cd.git"),
"username": []byte("someScopedUsername"),
"password": []byte("someScopedPassword"),
"project": []byte("testProject"),
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "user-managed",
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepository},
},
Data: map[string][]byte{
"name": []byte("UserManagedRepo"),
"url": []byte("git@github.com:argoproj/argoproj.git"),
"username": []byte("someOtherUsername"),
"password": []byte("someOtherPassword"),
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "other-user-managed",
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepository},
},
Data: map[string][]byte{
"name": []byte("Scoped UserManagedRepo"),
"url": []byte("git@github.com:argoproj/argoproj.git"),
"username": []byte("someOtherUsername"),
"password": []byte("someOtherPassword"),
"project": []byte("testProject"),
},
},
}
clientset := getClientset(repoSecrets...)
testee := &secretsRepositoryBackend{db: &db{
ns: testNamespace,
kubeclientset: clientset,
settingsMgr: settings.NewSettingsManager(t.Context(), clientset, testNamespace),
}}
repository, err := testee.GetRepository(t.Context(), "git@github.com:argoproj/argo-cd.git", "")
require.NoError(t, err)
assert.NotNil(t, repository)
assert.Equal(t, "ArgoCD", repository.Name)
assert.Equal(t, "git@github.com:argoproj/argo-cd.git", repository.Repo)
assert.Equal(t, "someUsername", repository.Username)
assert.Equal(t, "somePassword", repository.Password)
repository, err = testee.GetRepository(t.Context(), "git@github.com:argoproj/argoproj.git", "")
require.NoError(t, err)
assert.NotNil(t, repository)
assert.Equal(t, "UserManagedRepo", repository.Name)
assert.Equal(t, "git@github.com:argoproj/argoproj.git", repository.Repo)
assert.Equal(t, "someOtherUsername", repository.Username)
assert.Equal(t, "someOtherPassword", repository.Password)
repository, err = testee.GetRepository(t.Context(), "git@github.com:argoproj/argo-cd.git", "testProject")
require.NoError(t, err)
assert.NotNil(t, repository)
assert.Equal(t, "Scoped ArgoCD", repository.Name)
assert.Equal(t, "git@github.com:argoproj/argo-cd.git", repository.Repo)
assert.Equal(t, "someScopedUsername", repository.Username)
assert.Equal(t, "someScopedPassword", repository.Password)
assert.Equal(t, "testProject", repository.Project)
repository, err = testee.GetRepository(t.Context(), "git@github.com:argoproj/argoproj.git", "testProject")
require.NoError(t, err)
assert.NotNil(t, repository)
assert.Equal(t, "Scoped UserManagedRepo", repository.Name)
assert.Equal(t, "git@github.com:argoproj/argoproj.git", repository.Repo)
assert.Equal(t, "someOtherUsername", repository.Username)
assert.Equal(t, "someOtherPassword", repository.Password)
assert.Equal(t, "testProject", repository.Project)
}
func TestSecretsRepositoryBackend_ListRepositories(t *testing.T) {
repoSecrets := []runtime.Object{
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: RepoURLToSecretName(repoSecretPrefix, "git@github.com:argoproj/argo-cd.git", ""),
Annotations: map[string]string{common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD},
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepository},
},
Data: map[string][]byte{
"name": []byte("ArgoCD"),
"url": []byte("git@github.com:argoproj/argo-cd.git"),
"username": []byte("someUsername"),
"password": []byte("somePassword"),
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "user-managed",
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepository},
},
Data: map[string][]byte{
"name": []byte("UserManagedRepo"),
"url": []byte("git@github.com:argoproj/argoproj.git"),
"username": []byte("someOtherUsername"),
"password": []byte("someOtherPassword"),
},
},
}
clientset := getClientset(repoSecrets...)
testee := &secretsRepositoryBackend{db: &db{
ns: testNamespace,
kubeclientset: clientset,
settingsMgr: settings.NewSettingsManager(t.Context(), clientset, testNamespace),
}}
repositories, err := testee.ListRepositories(t.Context(), nil)
require.NoError(t, err)
assert.Len(t, repositories, 2)
for _, repository := range repositories {
switch repository.Name {
case "ArgoCD":
assert.Equal(t, "git@github.com:argoproj/argo-cd.git", repository.Repo)
assert.Equal(t, "someUsername", repository.Username)
assert.Equal(t, "somePassword", repository.Password)
case "UserManagedRepo":
assert.Equal(t, "git@github.com:argoproj/argoproj.git", repository.Repo)
assert.Equal(t, "someOtherUsername", repository.Username)
assert.Equal(t, "someOtherPassword", repository.Password)
default:
assert.Fail(t, "unexpected repository found in list")
}
}
}
func TestSecretsRepositoryBackend_UpdateRepository(t *testing.T) {
managedRepository := &appsv1.Repository{
Name: "Managed",
Repo: "git@github.com:argoproj/argo-cd.git",
Username: "someUsername",
Password: "somePassword",
}
managedProjectRepository := &appsv1.Repository{
Name: "Managed",
Repo: "git@github.com:argoproj/argo-cd.git",
Username: "someUsername",
Password: "somePassword",
Project: "someProject",
}
userProvidedRepository := &appsv1.Repository{
Name: "User Provided",
Repo: "git@github.com:argoproj/argoproj.git",
Username: "someOtherUsername",
Password: "someOtherPassword",
}
userProvidedProjectRepository := &appsv1.Repository{
Name: "User Provided",
Repo: "git@github.com:argoproj/argoproj.git",
Username: "someOtherUsername",
Password: "someOtherPassword",
Project: "someProject",
}
newRepository := &appsv1.Repository{
Name: "New",
Repo: "git@github.com:argoproj/argo-events.git",
Username: "foo",
Password: "bar",
}
managedSecretName := RepoURLToSecretName(repoSecretPrefix, managedRepository.Repo, "")
managedProjectSecretName := RepoURLToSecretName(repoSecretPrefix, managedProjectRepository.Repo, "someProject")
newSecretName := RepoURLToSecretName(repoSecretPrefix, newRepository.Repo, "")
repoSecrets := []runtime.Object{
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: managedSecretName,
Annotations: map[string]string{common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD},
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepository},
},
Data: map[string][]byte{
"name": []byte(managedRepository.Name),
"url": []byte(managedRepository.Repo),
"username": []byte(managedRepository.Username),
"password": []byte(managedRepository.Password),
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: managedProjectSecretName,
Annotations: map[string]string{common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD},
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepository},
},
Data: map[string][]byte{
"name": []byte(managedProjectRepository.Name),
"url": []byte(managedProjectRepository.Repo),
"username": []byte(managedProjectRepository.Username),
"password": []byte(managedProjectRepository.Password),
"project": []byte(managedProjectRepository.Project),
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "user-managed",
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepository},
},
Data: map[string][]byte{
"name": []byte(userProvidedRepository.Name),
"url": []byte(userProvidedRepository.Repo),
"username": []byte(userProvidedRepository.Username),
"password": []byte(userProvidedRepository.Password),
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "user-managed-scoped",
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepository},
},
Data: map[string][]byte{
"name": []byte(userProvidedProjectRepository.Name),
"url": []byte(userProvidedProjectRepository.Repo),
"username": []byte(userProvidedProjectRepository.Username),
"password": []byte(userProvidedProjectRepository.Password),
"project": []byte(userProvidedProjectRepository.Project),
},
},
}
clientset := getClientset(repoSecrets...)
testee := &secretsRepositoryBackend{db: &db{
ns: testNamespace,
kubeclientset: clientset,
settingsMgr: settings.NewSettingsManager(t.Context(), clientset, testNamespace),
}}
managedRepository.Username = "newUsername"
updateRepository, err := testee.UpdateRepository(t.Context(), managedRepository)
require.NoError(t, err)
assert.Same(t, managedRepository, updateRepository)
assert.Equal(t, managedRepository.Username, updateRepository.Username)
secret, err := clientset.CoreV1().Secrets(testNamespace).Get(t.Context(), managedSecretName, metav1.GetOptions{})
require.NoError(t, err)
assert.NotNil(t, secret)
assert.Equal(t, "newUsername", string(secret.Data["username"]))
userProvidedRepository.Username = "newOtherUsername"
updateRepository, err = testee.UpdateRepository(t.Context(), userProvidedRepository)
require.NoError(t, err)
assert.Same(t, userProvidedRepository, updateRepository)
assert.Equal(t, userProvidedRepository.Username, updateRepository.Username)
secret, err = clientset.CoreV1().Secrets(testNamespace).Get(t.Context(), "user-managed", metav1.GetOptions{})
require.NoError(t, err)
assert.NotNil(t, secret)
assert.Equal(t, "newOtherUsername", string(secret.Data["username"]))
updateRepository, err = testee.UpdateRepository(t.Context(), newRepository)
require.NoError(t, err)
assert.Same(t, newRepository, updateRepository)
secret, err = clientset.CoreV1().Secrets(testNamespace).Get(t.Context(), newSecretName, metav1.GetOptions{})
require.NoError(t, err)
assert.NotNil(t, secret)
assert.Equal(t, "foo", string(secret.Data["username"]))
managedProjectRepository.Username = "newUsername"
updateRepository, err = testee.UpdateRepository(t.Context(), managedProjectRepository)
require.NoError(t, err)
assert.Same(t, managedProjectRepository, updateRepository)
assert.Equal(t, managedProjectRepository.Username, updateRepository.Username)
secret, err = clientset.CoreV1().Secrets(testNamespace).Get(t.Context(), managedProjectSecretName, metav1.GetOptions{})
require.NoError(t, err)
assert.NotNil(t, secret)
assert.Equal(t, "newUsername", string(secret.Data["username"]))
userProvidedProjectRepository.Username = "newUsernameScoped"
updateRepository, err = testee.UpdateRepository(t.Context(), userProvidedProjectRepository)
require.NoError(t, err)
assert.Same(t, userProvidedProjectRepository, updateRepository)
assert.Equal(t, userProvidedProjectRepository.Username, updateRepository.Username)
secret, err = clientset.CoreV1().Secrets(testNamespace).Get(t.Context(), "user-managed-scoped", metav1.GetOptions{})
require.NoError(t, err)
assert.NotNil(t, secret)
assert.Equal(t, "newUsernameScoped", string(secret.Data["username"]))
}
func TestSecretsRepositoryBackend_DeleteRepository(t *testing.T) {
managedSecretName := RepoURLToSecretName(repoSecretPrefix, "git@github.com:argoproj/argo-cd.git", "")
managedScopedSecretName := RepoURLToSecretName(repoSecretPrefix, "git@github.com:argoproj/argo-cd.git", "someProject")
repoSecrets := []runtime.Object{
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: managedSecretName,
Annotations: map[string]string{common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD},
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepository},
},
Data: map[string][]byte{
"name": []byte("ArgoCD"),
"url": []byte("git@github.com:argoproj/argo-cd.git"),
"username": []byte("someUsername"),
"password": []byte("somePassword"),
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: managedScopedSecretName,
Annotations: map[string]string{common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD},
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepository},
},
Data: map[string][]byte{
"name": []byte("ArgoCD"),
"url": []byte("git@github.com:argoproj/argo-cd.git"),
"username": []byte("someUsername"),
"password": []byte("somePassword"),
"project": []byte("someProject"),
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "user-managed",
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepository},
},
Data: map[string][]byte{
"name": []byte("UserManagedRepo"),
"url": []byte("git@github.com:argoproj/argoproj.git"),
"username": []byte("someOtherUsername"),
"password": []byte("someOtherPassword"),
},
},
}
clientset := getClientset(repoSecrets...)
testee := &secretsRepositoryBackend{db: &db{
ns: testNamespace,
kubeclientset: clientset,
settingsMgr: settings.NewSettingsManager(t.Context(), clientset, testNamespace),
}}
err := testee.DeleteRepository(t.Context(), "git@github.com:argoproj/argo-cd.git", "")
require.NoError(t, err)
_, err = clientset.CoreV1().Secrets(testNamespace).Get(t.Context(), managedSecretName, metav1.GetOptions{})
require.Error(t, err)
_, err = clientset.CoreV1().Secrets(testNamespace).Get(t.Context(), managedScopedSecretName, metav1.GetOptions{})
require.NoError(t, err)
err = testee.DeleteRepository(t.Context(), "git@github.com:argoproj/argo-cd.git", "someProject")
require.NoError(t, err)
_, err = clientset.CoreV1().Secrets(testNamespace).Get(t.Context(), managedScopedSecretName, metav1.GetOptions{})
require.Error(t, err)
err = testee.DeleteRepository(t.Context(), "git@github.com:argoproj/argoproj.git", "")
require.NoError(t, err)
secret, err := clientset.CoreV1().Secrets(testNamespace).Get(t.Context(), "user-managed", metav1.GetOptions{})
require.NoError(t, err)
assert.NotNil(t, secret)
assert.Empty(t, secret.Labels[common.LabelValueSecretTypeRepository])
}
func TestSecretsRepositoryBackend_CreateRepoCreds(t *testing.T) {
clientset := getClientset()
testee := &secretsRepositoryBackend{db: &db{
ns: testNamespace,
kubeclientset: clientset,
settingsMgr: settings.NewSettingsManager(t.Context(), clientset, testNamespace),
}}
testCases := []struct {
name string
repoCreds appsv1.RepoCreds
// Note: URL needs to be a different one for every testCase
// otherwise we would need to use the DeleteRepoCreds method to clean up the secret after each test
// which results in an unwanted dependency in a unit test
}{
{
name: "minimal_https_fields",
repoCreds: appsv1.RepoCreds{
URL: "git@github.com:argoproj",
Username: "someUsername",
Password: "somePassword",
EnableOCI: true,
},
},
{
name: "with_proxy",
repoCreds: appsv1.RepoCreds{
URL: "git@github.com:kubernetes",
Username: "anotherUsername",
Password: "anotherPassword",
Proxy: "https://proxy.argoproj.io:3128",
},
},
{
name: "with_noProxy",
repoCreds: appsv1.RepoCreds{
URL: "git@github.com:proxy",
Username: "anotherUsername",
Password: "anotherPassword",
NoProxy: ".example.com,127.0.0.1",
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
output, err := testee.CreateRepoCreds(t.Context(), &testCase.repoCreds)
require.NoError(t, err)
assert.Same(t, &testCase.repoCreds, output)
secret, err := clientset.CoreV1().Secrets(testNamespace).Get(
t.Context(),
RepoURLToSecretName(credSecretPrefix, testCase.repoCreds.URL, ""),
metav1.GetOptions{},
)
assert.NotNil(t, secret)
require.NoError(t, err)
assert.Equal(t, common.AnnotationValueManagedByArgoCD, secret.Annotations[common.AnnotationKeyManagedBy])
assert.Equal(t, common.LabelValueSecretTypeRepoCreds, secret.Labels[common.LabelKeySecretType])
// check every possible field of the secret if it has the same (default) value as the repoCred struct
// non-string fields must be parsed so that their default value matches the one of the corresponding type
assert.Equal(t, testCase.repoCreds.URL, string(secret.Data["url"]))
assert.Equal(t, testCase.repoCreds.Username, string(secret.Data["username"]))
assert.Equal(t, testCase.repoCreds.Password, string(secret.Data["password"]))
if enableOCI, err := strconv.ParseBool(string(secret.Data["githubAppPrivateKey"])); err == nil {
assert.Equal(t, strconv.FormatBool(testCase.repoCreds.EnableOCI), enableOCI)
}
assert.Equal(t, testCase.repoCreds.SSHPrivateKey, string(secret.Data["sshPrivateKey"]))
assert.Equal(t, testCase.repoCreds.TLSClientCertData, string(secret.Data["tlsClientCertData"]))
assert.Equal(t, testCase.repoCreds.TLSClientCertKey, string(secret.Data["tlsClientCertKey"]))
assert.Equal(t, testCase.repoCreds.Type, string(secret.Data["type"]))
assert.Equal(t, testCase.repoCreds.GithubAppPrivateKey, string(secret.Data["githubAppPrivateKey"]))
if githubAppPrivateKey, err := strconv.ParseInt(string(secret.Data["githubAppPrivateKey"]), 10, 64); err == nil {
assert.Equal(t, testCase.repoCreds.GithubAppId, githubAppPrivateKey)
}
if githubAppID, err := strconv.ParseInt(string(secret.Data["githubAppId"]), 10, 64); err == nil {
assert.Equal(t, testCase.repoCreds.GithubAppInstallationId, githubAppID)
}
assert.Equal(t, testCase.repoCreds.GitHubAppEnterpriseBaseURL, string(secret.Data["githubAppEnterpriseUrl"]))
assert.Equal(t, testCase.repoCreds.Proxy, string(secret.Data["proxy"]))
assert.Equal(t, testCase.repoCreds.NoProxy, string(secret.Data["noProxy"]))
})
}
}
func TestSecretsRepositoryBackend_GetRepoCreds(t *testing.T) {
repoCredSecrets := []runtime.Object{
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: RepoURLToSecretName(repoSecretPrefix, "git@github.com:argoproj", ""),
Annotations: map[string]string{common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD},
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepoCreds},
},
Data: map[string][]byte{
"url": []byte("git@github.com:argoproj"),
"username": []byte("someUsername"),
"password": []byte("somePassword"),
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "user-managed",
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepoCreds},
},
Data: map[string][]byte{
"url": []byte("git@gitlab.com"),
"username": []byte("someOtherUsername"),
"password": []byte("someOtherPassword"),
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "proxy-repo",
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepoCreds},
},
Data: map[string][]byte{
"url": []byte("git@gitlab.com"),
"username": []byte("someOtherUsername"),
"password": []byte("someOtherPassword"),
"proxy": []byte("https://proxy.argoproj.io:3128"),
"noProxy": []byte(".example.com,127.0.0.1"),
},
},
}
clientset := getClientset(repoCredSecrets...)
testee := &secretsRepositoryBackend{db: &db{
ns: testNamespace,
kubeclientset: clientset,
settingsMgr: settings.NewSettingsManager(t.Context(), clientset, testNamespace),
}}
repoCred, err := testee.GetRepoCreds(t.Context(), "git@github.com:argoproj")
require.NoError(t, err)
require.NotNil(t, repoCred)
assert.Equal(t, "git@github.com:argoproj", repoCred.URL)
assert.Equal(t, "someUsername", repoCred.Username)
assert.Equal(t, "somePassword", repoCred.Password)
repoCred, err = testee.GetRepoCreds(t.Context(), "git@gitlab.com")
require.NoError(t, err)
assert.NotNil(t, repoCred)
assert.Equal(t, "git@gitlab.com", repoCred.URL)
assert.Equal(t, "someOtherUsername", repoCred.Username)
assert.Equal(t, "someOtherPassword", repoCred.Password)
if repoCred.Proxy != "" {
assert.Equal(t, "https://proxy.argoproj.io:3128", repoCred.Proxy)
}
if repoCred.NoProxy != "" {
assert.Equal(t, ".example.com,127.0.0.1", repoCred.NoProxy)
}
}
func TestSecretsRepositoryBackend_ListRepoCreds(t *testing.T) {
repoCredSecrets := []runtime.Object{
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: RepoURLToSecretName(repoSecretPrefix, "git@github.com:argoproj", ""),
Annotations: map[string]string{common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD},
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepoCreds},
},
Data: map[string][]byte{
"url": []byte("git@github.com:argoproj"),
"username": []byte("someUsername"),
"password": []byte("somePassword"),
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "user-managed",
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepoCreds},
},
Data: map[string][]byte{
"url": []byte("git@gitlab.com"),
"username": []byte("someOtherUsername"),
"password": []byte("someOtherPassword"),
},
},
}
clientset := getClientset(repoCredSecrets...)
testee := &secretsRepositoryBackend{db: &db{
ns: testNamespace,
kubeclientset: clientset,
settingsMgr: settings.NewSettingsManager(t.Context(), clientset, testNamespace),
}}
repoCreds, err := testee.ListRepoCreds(t.Context())
require.NoError(t, err)
assert.Len(t, repoCreds, 2)
assert.Contains(t, repoCreds, "git@github.com:argoproj")
assert.Contains(t, repoCreds, "git@gitlab.com")
}
func TestSecretsRepositoryBackend_UpdateRepoCreds(t *testing.T) {
managedCreds := &appsv1.RepoCreds{
URL: "git@github.com:argoproj",
Username: "someUsername",
Password: "somePassword",
}
userProvidedCreds := &appsv1.RepoCreds{
URL: "git@gitlab.com",
Username: "someOtherUsername",
Password: "someOtherPassword",
}
newCreds := &appsv1.RepoCreds{
URL: "git@github.com:foobar",
Username: "foo",
Password: "bar",
}
managedCredsName := RepoURLToSecretName(credSecretPrefix, managedCreds.URL, "")
newCredsName := RepoURLToSecretName(credSecretPrefix, newCreds.URL, "")
repoCredSecrets := []runtime.Object{
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: managedCredsName,
Annotations: map[string]string{common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD},
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepoCreds},
},
Data: map[string][]byte{
"url": []byte(managedCreds.URL),
"username": []byte(managedCreds.Username),
"password": []byte(managedCreds.Password),
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "user-managed",
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepoCreds},
},
Data: map[string][]byte{
"url": []byte(userProvidedCreds.URL),
"username": []byte(userProvidedCreds.Username),
"password": []byte(userProvidedCreds.Password),
},
},
}
clientset := getClientset(repoCredSecrets...)
testee := &secretsRepositoryBackend{db: &db{
ns: testNamespace,
kubeclientset: clientset,
settingsMgr: settings.NewSettingsManager(t.Context(), clientset, testNamespace),
}}
managedCreds.Username = "newUsername"
updateRepoCreds, err := testee.UpdateRepoCreds(t.Context(), managedCreds)
require.NoError(t, err)
assert.NotSame(t, managedCreds, updateRepoCreds)
assert.Equal(t, managedCreds.Username, updateRepoCreds.Username)
secret, err := clientset.CoreV1().Secrets(testNamespace).Get(t.Context(), managedCredsName, metav1.GetOptions{})
require.NoError(t, err)
assert.NotNil(t, secret)
assert.Equal(t, "newUsername", string(secret.Data["username"]))
userProvidedCreds.Username = "newOtherUsername"
updateRepoCreds, err = testee.UpdateRepoCreds(t.Context(), userProvidedCreds)
require.NoError(t, err)
assert.NotSame(t, userProvidedCreds, updateRepoCreds)
assert.Equal(t, userProvidedCreds.Username, updateRepoCreds.Username)
secret, err = clientset.CoreV1().Secrets(testNamespace).Get(t.Context(), "user-managed", metav1.GetOptions{})
require.NoError(t, err)
assert.NotNil(t, secret)
assert.Equal(t, "newOtherUsername", string(secret.Data["username"]))
updateRepoCreds, err = testee.UpdateRepoCreds(t.Context(), newCreds)
require.NoError(t, err)
assert.Same(t, newCreds, updateRepoCreds)
secret, err = clientset.CoreV1().Secrets(testNamespace).Get(t.Context(), newCredsName, metav1.GetOptions{})
require.NoError(t, err)
assert.NotNil(t, secret)
assert.Equal(t, "foo", string(secret.Data["username"]))
}
func TestSecretsRepositoryBackend_DeleteRepoCreds(t *testing.T) {
managedSecretName := RepoURLToSecretName(repoSecretPrefix, "git@github.com:argoproj/argo-cd.git", "")
repoSecrets := []runtime.Object{
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: managedSecretName,
Annotations: map[string]string{common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD},
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepoCreds},
},
Data: map[string][]byte{
"url": []byte("git@github.com:argoproj"),
"username": []byte("someUsername"),
"password": []byte("somePassword"),
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "user-managed",
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepoCreds},
},
Data: map[string][]byte{
"url": []byte("git@gitlab.com"),
"username": []byte("someOtherUsername"),
"password": []byte("someOtherPassword"),
},
},
}
clientset := getClientset(repoSecrets...)
testee := &secretsRepositoryBackend{db: &db{
ns: testNamespace,
kubeclientset: clientset,
settingsMgr: settings.NewSettingsManager(t.Context(), clientset, testNamespace),
}}
err := testee.DeleteRepoCreds(t.Context(), "git@github.com:argoproj")
require.NoError(t, err)
_, err = clientset.CoreV1().Secrets(testNamespace).Get(t.Context(), managedSecretName, metav1.GetOptions{})
require.Error(t, err)
err = testee.DeleteRepoCreds(t.Context(), "git@gitlab.com")
require.NoError(t, err)
secret, err := clientset.CoreV1().Secrets(testNamespace).Get(t.Context(), "user-managed", metav1.GetOptions{})
require.NoError(t, err)
assert.NotNil(t, secret)
assert.Empty(t, secret.Labels[common.LabelValueSecretTypeRepoCreds])
}
func TestSecretsRepositoryBackend_GetAllHelmRepoCreds(t *testing.T) {
repoCredSecrets := []runtime.Object{
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: RepoURLToSecretName(repoSecretPrefix, "git@github.com:argoproj", ""),
Annotations: map[string]string{common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD},
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepoCreds},
},
Data: map[string][]byte{
"url": []byte("git@github.com:argoproj"),
"username": []byte("someUsername"),
"password": []byte("somePassword"),
"type": []byte("helm"),
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: RepoURLToSecretName(repoSecretPrefix, "git@gitlab.com", ""),
Annotations: map[string]string{common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD},
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepoCreds},
},
Data: map[string][]byte{
"url": []byte("git@gitlab.com"),
"username": []byte("someOtherUsername"),
"password": []byte("someOtherPassword"),
"type": []byte("git"),
},
},
}
clientset := getClientset(repoCredSecrets...)
testee := &secretsRepositoryBackend{db: &db{
ns: testNamespace,
kubeclientset: clientset,
settingsMgr: settings.NewSettingsManager(t.Context(), clientset, testNamespace),
}}
repoCreds, err := testee.GetAllHelmRepoCreds(t.Context())
require.NoError(t, err)
assert.Len(t, repoCreds, 1)
}
func TestRepoCredsToSecret(t *testing.T) {
s := &corev1.Secret{}
creds := &appsv1.RepoCreds{
URL: "URL",
Username: "Username",
Password: "Password",
SSHPrivateKey: "SSHPrivateKey",
EnableOCI: true,
TLSClientCertData: "TLSClientCertData",
TLSClientCertKey: "TLSClientCertKey",
Type: "Type",
GithubAppPrivateKey: "GithubAppPrivateKey",
GithubAppId: 123,
GithubAppInstallationId: 456,
GitHubAppEnterpriseBaseURL: "GitHubAppEnterpriseBaseURL",
}
s = repoCredsToSecret(creds, s)
assert.Equal(t, []byte(creds.URL), s.Data["url"])
assert.Equal(t, []byte(creds.Username), s.Data["username"])
assert.Equal(t, []byte(creds.Password), s.Data["password"])
assert.Equal(t, []byte(creds.SSHPrivateKey), s.Data["sshPrivateKey"])
assert.Equal(t, []byte(strconv.FormatBool(creds.EnableOCI)), s.Data["enableOCI"])
assert.Equal(t, []byte(creds.TLSClientCertData), s.Data["tlsClientCertData"])
assert.Equal(t, []byte(creds.TLSClientCertKey), s.Data["tlsClientCertKey"])
assert.Equal(t, []byte(creds.Type), s.Data["type"])
assert.Equal(t, []byte(creds.GithubAppPrivateKey), s.Data["githubAppPrivateKey"])
assert.Equal(t, []byte(strconv.FormatInt(creds.GithubAppId, 10)), s.Data["githubAppID"])
assert.Equal(t, []byte(strconv.FormatInt(creds.GithubAppInstallationId, 10)), s.Data["githubAppInstallationID"])
assert.Equal(t, []byte(creds.GitHubAppEnterpriseBaseURL), s.Data["githubAppEnterpriseBaseUrl"])
assert.Equal(t, map[string]string{common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD}, s.Annotations)
assert.Equal(t, map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepoCreds}, s.Labels)
}
func TestRaceConditionInRepoCredsOperations(t *testing.T) {
// Create a single shared secret that will be accessed concurrently
sharedSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: RepoURLToSecretName(repoSecretPrefix, "git@github.com:argoproj/argo-cd.git", ""),
Namespace: testNamespace,
Labels: map[string]string{
common.LabelKeySecretType: common.LabelValueSecretTypeRepoCreds,
},
},
Data: map[string][]byte{
"url": []byte("git@github.com:argoproj/argo-cd.git"),
"username": []byte("test-user"),
"password": []byte("test-pass"),
},
}
// Create test credentials that we'll use for conversion
repoCreds := &appsv1.RepoCreds{
URL: "git@github.com:argoproj/argo-cd.git",
Username: "test-user",
Password: "test-pass",
}
backend := &secretsRepositoryBackend{}
var wg sync.WaitGroup
concurrentOps := 50
errChan := make(chan error, concurrentOps*2) // Channel to collect errors
// Launch goroutines that perform concurrent operations
for i := 0; i < concurrentOps; i++ {
wg.Add(2)
// One goroutine converts from RepoCreds to Secret
go func() {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
errChan <- fmt.Errorf("panic in repoCredsToSecret: %v", r)
}
}()
_ = repoCredsToSecret(repoCreds, sharedSecret)
}()
// Another goroutine converts from Secret to RepoCreds
go func() {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
errChan <- fmt.Errorf("panic in secretToRepoCred: %v", r)
}
}()
creds, err := backend.secretToRepoCred(sharedSecret)
if err != nil {
errChan <- fmt.Errorf("error in secretToRepoCred: %w", err)
return
}
// Verify data integrity
if creds.URL != repoCreds.URL || creds.Username != repoCreds.Username || creds.Password != repoCreds.Password {
errChan <- fmt.Errorf("data mismatch in conversion: expected %v, got %v", repoCreds, creds)
}
}()
}
wg.Wait()
close(errChan)
// Check for any errors that occurred during concurrent operations
for err := range errChan {
t.Errorf("concurrent operation error: %v", err)
}
// Verify final state
finalCreds, err := backend.secretToRepoCred(sharedSecret)
require.NoError(t, err)
assert.Equal(t, repoCreds.URL, finalCreds.URL)
assert.Equal(t, repoCreds.Username, finalCreds.Username)
assert.Equal(t, repoCreds.Password, finalCreds.Password)
}
func TestRaceConditionInRepositoryOperations(t *testing.T) {
// Create a single shared secret that will be accessed concurrently
sharedSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: RepoURLToSecretName(repoSecretPrefix, "git@github.com:argoproj/argo-cd.git", ""),
Namespace: testNamespace,
Labels: map[string]string{
common.LabelKeySecretType: common.LabelValueSecretTypeRepository,
},
},
Data: map[string][]byte{
"url": []byte("git@github.com:argoproj/argo-cd.git"),
"name": []byte("test-repo"),
"username": []byte("test-user"),
"password": []byte("test-pass"),
},
}
// Create test repository that we'll use for conversion
repo := &appsv1.Repository{
Name: "test-repo",
Repo: "git@github.com:argoproj/argo-cd.git",
Username: "test-user",
Password: "test-pass",
}
backend := &secretsRepositoryBackend{}
var wg sync.WaitGroup
concurrentOps := 50
errChan := make(chan error, concurrentOps*2) // Channel to collect errors
// Launch goroutines that perform concurrent operations
for i := 0; i < concurrentOps; i++ {
wg.Add(2)
// One goroutine converts from Repository to Secret
go func() {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
errChan <- fmt.Errorf("panic in repositoryToSecret: %v", r)
}
}()
_ = backend.repositoryToSecret(repo, sharedSecret)
}()
// Another goroutine converts from Secret to Repository
go func() {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
errChan <- fmt.Errorf("panic in secretToRepository: %v", r)
}
}()
repository, err := secretToRepository(sharedSecret)
if err != nil {
errChan <- fmt.Errorf("error in secretToRepository: %w", err)
return
}
// Verify data integrity
if repository.Name != repo.Name || repository.Repo != repo.Repo ||
repository.Username != repo.Username || repository.Password != repo.Password {
errChan <- fmt.Errorf("data mismatch in conversion: expected %v, got %v", repo, repository)
}
}()
}
wg.Wait()
close(errChan)
// Check for any errors that occurred during concurrent operations
for err := range errChan {
t.Errorf("concurrent operation error: %v", err)
}
// Verify final state
finalRepo, err := secretToRepository(sharedSecret)
require.NoError(t, err)
assert.Equal(t, repo.Name, finalRepo.Name)
assert.Equal(t, repo.Repo, finalRepo.Repo)
assert.Equal(t, repo.Username, finalRepo.Username)
assert.Equal(t, repo.Password, finalRepo.Password)
}