Skip to content

Commit 643fe5b

Browse files
zmotsoMykolaMarusenko
authored andcommitted
feat: Add CloneRepositoryCredentials to Codebase spec (#234)
Add CloneRepositoryCredentials field to Codebase spec to support configurable repository credentials management. This replaces the hardcoded secret name pattern with a flexible reference-based approach. The new field includes: - secretRef: reference to credential secret - clearSecretAfterUse: flag to control secret cleanup behavior This change maintains backward compatibility by falling back to the legacy secret naming pattern when CloneRepositoryCredentials is not specified. The implementation includes comprehensive test coverage for various credential scenarios including missing secrets, empty credentials, and secret retention policies.
1 parent 1488661 commit 643fe5b

File tree

15 files changed

+801
-295
lines changed

15 files changed

+801
-295
lines changed

api/v1/codebase_types.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package v1
22

33
import (
4+
"fmt"
45
"strconv"
56
"strings"
67

8+
corev1 "k8s.io/api/core/v1"
79
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
810
)
911

@@ -129,6 +131,24 @@ type CodebaseSpec struct {
129131
// +optional
130132
// +kubebuilder:default:=true
131133
Private bool `json:"private"`
134+
135+
// CloneRepositoryCredentials contains reference to secret with credentials used to clone repository.
136+
// +nullable
137+
// +optional
138+
CloneRepositoryCredentials *CloneRepositoryCredentials `json:"cloneRepositoryCredentials,omitempty"`
139+
}
140+
141+
type CloneRepositoryCredentials struct {
142+
// SecretRef is a reference to secret that contains credentials for cloning repository.
143+
// The secret must contain "username" and "password" keys.
144+
// +required
145+
SecretRef corev1.LocalObjectReference `json:"secretRef"`
146+
147+
// ClearSecretAfterUse indicates whether the secret should be deleted after use.
148+
// For backward compatibility, the default value is true.
149+
// +optional
150+
// +kubebuilder:default:=true
151+
ClearSecretAfterUse bool `json:"clearSecretAfterUse"`
132152
}
133153

134154
// GetProjectID returns project id from GitUrlPath codebase spec. It removes the leading slash.
@@ -141,6 +161,16 @@ func (in *CodebaseSpec) IsVersionTypeSemver() bool {
141161
return in.Versioning.Type == VersioningTypeSemver || in.Versioning.Type == VersioningTypeEDP
142162
}
143163

164+
func (in *Codebase) GetCloneRepositoryCredentialSecret() string {
165+
if in.Spec.CloneRepositoryCredentials != nil && in.Spec.CloneRepositoryCredentials.SecretRef.Name != "" {
166+
return in.Spec.CloneRepositoryCredentials.SecretRef.Name
167+
}
168+
169+
// Fallback to generate secret name for backward compatibility.
170+
// Deprecated: Fallback for backward compatibility. Will be removed after new field is fully adopted.
171+
return fmt.Sprintf("repository-codebase-%s-temp", in.Name)
172+
}
173+
144174
type ActionType string
145175

146176
const (

api/v1/zz_generated.deepcopy.go

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/v2.edp.epam.com_codebases.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,36 @@ spec:
7474
default: tekton
7575
description: A name of tool which should be used as CI.
7676
type: string
77+
cloneRepositoryCredentials:
78+
description: CloneRepositoryCredentials contains reference to secret
79+
with credentials used to clone repository.
80+
nullable: true
81+
properties:
82+
clearSecretAfterUse:
83+
default: true
84+
description: |-
85+
ClearSecretAfterUse indicates whether the secret should be deleted after use.
86+
For backward compatibility, the default value is true.
87+
type: boolean
88+
secretRef:
89+
description: |-
90+
SecretRef is a reference to secret that contains credentials for cloning repository.
91+
The secret must contain "username" and "password" keys.
92+
properties:
93+
name:
94+
default: ""
95+
description: |-
96+
Name of the referent.
97+
This field is effectively required, but due to backwards compatibility is
98+
allowed to be empty. Instances of this type with an empty value here are
99+
almost certainly wrong.
100+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
101+
type: string
102+
type: object
103+
x-kubernetes-map-type: atomic
104+
required:
105+
- secretRef
106+
type: object
77107
commitMessagePattern:
78108
nullable: true
79109
type: string

controllers/codebase/service/chain/checkout_branch.go

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,14 @@ import (
44
"context"
55
"fmt"
66

7-
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
87
ctrl "sigs.k8s.io/controller-runtime"
98
"sigs.k8s.io/controller-runtime/pkg/client"
109

1110
codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1"
11+
codebaseutil "github.com/epam/edp-codebase-operator/v2/pkg/codebase"
1212
gitproviderv2 "github.com/epam/edp-codebase-operator/v2/pkg/git/v2"
13-
"github.com/epam/edp-codebase-operator/v2/pkg/util"
1413
)
1514

16-
func GetRepositoryCredentialsIfExists(cb *codebaseApi.Codebase, c client.Client) (userName, password *string, err error) {
17-
if cb.Spec.Repository == nil {
18-
return nil, nil, nil
19-
}
20-
21-
secret := fmt.Sprintf("repository-codebase-%v-temp", cb.Name)
22-
23-
repositoryUsername, repositoryPassword, err := util.GetVcsBasicAuthConfig(c, cb.Namespace, secret)
24-
if err != nil {
25-
return nil, nil, fmt.Errorf("failed to fetch VCS auth config: %w", err)
26-
}
27-
28-
userName = &repositoryUsername
29-
password = &repositoryPassword
30-
31-
return
32-
}
33-
3415
func CheckoutBranch(
3516
ctx context.Context,
3617
branchName string,
@@ -59,15 +40,14 @@ func CheckoutBranch(
5940
}
6041

6142
case "clone":
62-
user, password, err := GetRepositoryCredentialsIfExists(cb, c)
63-
if err != nil && !k8sErrors.IsNotFound(err) {
64-
return err
43+
user, password, _, err := codebaseutil.GetRepositoryCredentialsIfExists(ctx, cb, c)
44+
if err != nil {
45+
return fmt.Errorf("failed to get repository credentials for checkout (clone strategy): %w", err)
6546
}
6647

67-
cfg := gitproviderv2.Config{}
68-
if user != nil && password != nil {
69-
cfg.Username = *user
70-
cfg.Token = *password
48+
cfg := gitproviderv2.Config{
49+
Username: user,
50+
Token: password,
7151
}
7252

7353
if err := gitProviderFactory(cfg).Checkout(ctx, repoContext.WorkDir, branchName, true); err != nil {

controllers/codebase/service/chain/checkout_branch_test.go

Lines changed: 6 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -24,63 +24,6 @@ const (
2424
fakeName = "fake-name"
2525
)
2626

27-
func TestGetRepositoryCredentialsIfExists_ShouldPass(t *testing.T) {
28-
c := &codebaseApi.Codebase{
29-
ObjectMeta: metaV1.ObjectMeta{
30-
Name: "fake-name",
31-
Namespace: fakeNamespace,
32-
},
33-
Spec: codebaseApi.CodebaseSpec{
34-
Repository: &codebaseApi.Repository{
35-
Url: "repo",
36-
},
37-
},
38-
}
39-
s := &coreV1.Secret{
40-
ObjectMeta: metaV1.ObjectMeta{
41-
Name: "repository-codebase-fake-name-temp",
42-
Namespace: fakeNamespace,
43-
},
44-
Data: map[string][]byte{
45-
"username": []byte("user"),
46-
"password": []byte("pass"),
47-
},
48-
}
49-
scheme := runtime.NewScheme()
50-
scheme.AddKnownTypes(coreV1.SchemeGroupVersion, s)
51-
scheme.AddKnownTypes(codebaseApi.GroupVersion, c)
52-
fakeCl := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(s, c).Build()
53-
u, p, err := GetRepositoryCredentialsIfExists(c, fakeCl)
54-
assert.Equal(t, u, util.GetStringP("user"))
55-
assert.Equal(t, p, util.GetStringP("pass"))
56-
assert.NoError(t, err)
57-
}
58-
59-
func TestGetRepositoryCredentialsIfExists_ShouldFail(t *testing.T) {
60-
c := &codebaseApi.Codebase{
61-
ObjectMeta: metaV1.ObjectMeta{
62-
Name: "fake-name",
63-
Namespace: fakeNamespace,
64-
},
65-
Spec: codebaseApi.CodebaseSpec{
66-
Repository: &codebaseApi.Repository{
67-
Url: "repo",
68-
},
69-
},
70-
}
71-
72-
scheme := runtime.NewScheme()
73-
scheme.AddKnownTypes(codebaseApi.GroupVersion, c)
74-
fakeCl := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(c).Build()
75-
76-
_, _, err := GetRepositoryCredentialsIfExists(c, fakeCl)
77-
assert.Error(t, err)
78-
79-
if !strings.Contains(err.Error(), "failed to get secret repository-codebase-fake-name-temp") {
80-
t.Fatalf("wrong error returned: %s", err.Error())
81-
}
82-
}
83-
8427
func TestCheckoutBranch_ShouldFailOnGetCurrentBranchName(t *testing.T) {
8528
c := &codebaseApi.Codebase{
8629
ObjectMeta: metaV1.ObjectMeta{
@@ -236,6 +179,11 @@ func TestCheckoutBranch_ShouldPassForCloneStrategy(t *testing.T) {
236179
Url: "repo",
237180
},
238181
Strategy: codebaseApi.Import,
182+
CloneRepositoryCredentials: &codebaseApi.CloneRepositoryCredentials{
183+
SecretRef: coreV1.LocalObjectReference{
184+
Name: "repository-creds",
185+
},
186+
},
239187
},
240188
}
241189
gs := &codebaseApi.GitServer{
@@ -252,7 +200,7 @@ func TestCheckoutBranch_ShouldPassForCloneStrategy(t *testing.T) {
252200
}
253201
s := &coreV1.Secret{
254202
ObjectMeta: metaV1.ObjectMeta{
255-
Name: "repository-codebase-fake-name-temp",
203+
Name: "repository-creds",
256204
Namespace: fakeNamespace,
257205
},
258206
Data: map[string][]byte{

controllers/codebase/service/chain/cleaner.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package chain
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67

78
v1 "k8s.io/api/core/v1"
@@ -39,39 +40,46 @@ func (h *Cleaner) ServeRequest(ctx context.Context, codebase *codebaseApi.Codeba
3940
}
4041

4142
func (h *Cleaner) clean(ctx context.Context, codebase *codebaseApi.Codebase) error {
42-
log := ctrl.LoggerFrom(ctx)
43-
44-
s := fmt.Sprintf("repository-codebase-%v-temp", codebase.Name)
43+
var errs []error
4544

46-
if err := h.deleteSecret(ctx, s, codebase.Namespace); err != nil {
47-
return fmt.Errorf("failed to delete secret %v: %w", s, err)
45+
if err := h.deleteSecret(ctx, codebase); err != nil {
46+
errs = append(errs, err)
4847
}
4948

5049
wd := util.GetWorkDir(codebase.Name, codebase.Namespace)
5150

52-
log.Info("Deleting work directory", "directory", wd)
51+
ctrl.LoggerFrom(ctx).Info("Deleting work directory", "directory", wd)
52+
53+
if err := deleteWorkDirectory(wd); err != nil {
54+
errs = append(errs, err)
55+
}
5356

54-
return deleteWorkDirectory(wd)
57+
return errors.Join(errs...)
5558
}
5659

57-
func (h *Cleaner) deleteSecret(ctx context.Context, secretName, namespace string) error {
60+
func (h *Cleaner) deleteSecret(ctx context.Context, codebase *codebaseApi.Codebase) error {
61+
if codebase.Spec.CloneRepositoryCredentials != nil && !codebase.Spec.CloneRepositoryCredentials.ClearSecretAfterUse {
62+
return nil
63+
}
64+
65+
secretName := codebase.GetCloneRepositoryCredentialSecret()
5866
log := ctrl.LoggerFrom(ctx).WithValues("secret", secretName)
5967

60-
log.Info("Deleting secret")
68+
log.Info("Deleting secret with repository credentials for clone")
6169

6270
if err := h.client.Delete(ctx, &v1.Secret{
6371
ObjectMeta: metaV1.ObjectMeta{
6472
Name: secretName,
65-
Namespace: namespace,
73+
Namespace: codebase.Namespace,
6674
},
6775
}); err != nil {
6876
if k8sErrors.IsNotFound(err) {
69-
log.Info("Secret is not found. Skip deleting")
77+
log.Info("Secret not found. Skipping deletion")
7078

7179
return nil
7280
}
7381

74-
return fmt.Errorf("failed to Delete 'Secret' resource %q: %w", secretName, err)
82+
return fmt.Errorf("failed to delete secret with repository credentials for clone: %w", err)
7583
}
7684

7785
log.Info("Secret was deleted")

0 commit comments

Comments
 (0)