diff --git a/apis/projects/v1alpha1/project_types.go b/apis/projects/v1alpha1/project_types.go index b604e915..c3f94d5b 100644 --- a/apis/projects/v1alpha1/project_types.go +++ b/apis/projects/v1alpha1/project_types.go @@ -148,6 +148,14 @@ type ContainerExpirationPolicyAttributes struct { NameRegex *string `url:"name_regex,omitempty" json:"name_regex,omitempty"` } +// Struct representing the secret name and the secret namespace to use when importing a project with secret +type ImportUrlSecretRef struct { + Name string `json:"secretName"` + Namespace string `json:"secretNamespace"` + UsernameKey string `json:"username" default:"username"` + PasswordKey string `json:"password" default:"password"` +} + // ProjectParameters define the desired state of a Gitlab Project type ProjectParameters struct { // Set whether or not merge requests can be merged with skipped jobs. @@ -244,10 +252,15 @@ type ProjectParameters struct { // +immutable GroupWithProjectTemplatesID *int `json:"groupWithProjectTemplatesId,omitempty"` - // URL to import repository from. + // URL to import repository from. Provided credentials in the URL will be overwritten in case a valid secretRef is present in ImportUrlSecretRef. // +optional + // +kubebuilder:validation:MinLength=11 ImportURL *string `json:"importUrl,omitempty"` + // Secret to use when importing project with secret. Provided credentials in ImportUrl will be overwitten with the credentials found in ImportUrlSecretRel. + // +optional + ImportUrlSecretRef *ImportUrlSecretRef `json:"importUrlSecretRef,omitempty"` + // false by default. // +optional // +immutable diff --git a/apis/projects/v1alpha1/zz_generated.deepcopy.go b/apis/projects/v1alpha1/zz_generated.deepcopy.go index 773ffed1..755f87b3 100644 --- a/apis/projects/v1alpha1/zz_generated.deepcopy.go +++ b/apis/projects/v1alpha1/zz_generated.deepcopy.go @@ -817,6 +817,21 @@ func (in *HookStatus) DeepCopy() *HookStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImportUrlSecretRef) DeepCopyInto(out *ImportUrlSecretRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImportUrlSecretRef. +func (in *ImportUrlSecretRef) DeepCopy() *ImportUrlSecretRef { + if in == nil { + return nil + } + out := new(ImportUrlSecretRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LastPipeline) DeepCopyInto(out *LastPipeline) { *out = *in @@ -1535,6 +1550,11 @@ func (in *ProjectParameters) DeepCopyInto(out *ProjectParameters) { *out = new(string) **out = **in } + if in.ImportUrlSecretRef != nil { + in, out := &in.ImportUrlSecretRef, &out.ImportUrlSecretRef + *out = new(ImportUrlSecretRef) + **out = **in + } if in.InitializeWithReadme != nil { in, out := &in.InitializeWithReadme, &out.InitializeWithReadme *out = new(bool) diff --git a/package/crds/projects.gitlab.crossplane.io_projects.yaml b/package/crds/projects.gitlab.crossplane.io_projects.yaml index 85f09fd9..7c1e767c 100644 --- a/package/crds/projects.gitlab.crossplane.io_projects.yaml +++ b/package/crds/projects.gitlab.crossplane.io_projects.yaml @@ -162,8 +162,30 @@ spec: to be true. type: integer importUrl: - description: URL to import repository from. - type: string + description: URL to import repository from. Provided credentials + in the URL will be overwritten in case a valid secretRef is + present in ImportUrlSecretRef. + minLength: 11 + type: string + importUrlSecretRef: + description: Secret to use when importing project with secret. + Provided credentials in ImportUrl will be overwitten with the + credentials found in ImportUrlSecretRel. + properties: + password: + type: string + secretName: + type: string + secretNamespace: + type: string + username: + type: string + required: + - password + - secretName + - secretNamespace + - username + type: object initializeWithReadme: description: false by default. type: boolean diff --git a/pkg/controller/projects/projects/project.go b/pkg/controller/projects/projects/project.go index 26256638..2f0fba68 100644 --- a/pkg/controller/projects/projects/project.go +++ b/pkg/controller/projects/projects/project.go @@ -21,6 +21,7 @@ import ( "strconv" "github.com/xanzy/go-gitlab" + "k8s.io/apimachinery/pkg/types" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -41,6 +42,10 @@ import ( "github.com/crossplane-contrib/provider-gitlab/pkg/clients" "github.com/crossplane-contrib/provider-gitlab/pkg/clients/projects" "github.com/crossplane-contrib/provider-gitlab/pkg/features" + + "net/url" + + corev1 "k8s.io/api/core/v1" ) const ( @@ -50,6 +55,10 @@ const ( errUpdateFailed = "cannot update Gitlab project" errDeleteFailed = "cannot delete Gitlab project" errGetFailed = "cannot retrieve Gitlab project with" + errNoImportSecret = "cannot retrieve secret for import" + errUsernameNotFound = "No username in secret for import project" + errPasswordNotFound = "No password in secret for import project" + errParseUrlFailed = "Could not parse the provided importUrl" ) // SetupProject adds a controller that reconciles Projects. @@ -150,6 +159,57 @@ func (e *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext return managed.ExternalCreation{}, errors.New(errNotProject) } + keySecretRef := cr.Spec.ForProvider.ImportUrlSecretRef + importUrl := cr.Spec.ForProvider.ImportURL + + // User has provided secret for the import + secretRefIsNotEmpty := keySecretRef != nil && keySecretRef.Namespace != "" && keySecretRef.Name != "" + keysAreNotEmpty := keySecretRef != nil && keySecretRef.PasswordKey != "" && keySecretRef.UsernameKey != "" + importUrlIsNotEmpty := importUrl != nil && *importUrl != "" + + if secretRefIsNotEmpty && keysAreNotEmpty && importUrlIsNotEmpty { + // Retrieve secret from k8s + namespacedName := types.NamespacedName{ + Namespace: keySecretRef.Namespace, + Name: keySecretRef.Name, + } + + secret := &corev1.Secret{} + + err := e.kube.Get(ctx, namespacedName, secret) + + if err != nil { + return managed.ExternalCreation{}, errors.Wrap(err, errNoImportSecret) + } + + // Obtain the password from the secret. + password := string(secret.Data[keySecretRef.PasswordKey]) + if password == "" { + return managed.ExternalCreation{}, errors.Wrap(err, errPasswordNotFound) + } + + // Obtain the username from the secret. + username := string(secret.Data[keySecretRef.UsernameKey]) + if username == "" { + return managed.ExternalCreation{}, errors.Wrap(err, errUsernameNotFound) + } + + // manipulate url to add the secret. If secret is already in the url, it should be overridden + // https://username:password@example.com + parsedUrl, err := url.Parse(*importUrl) + + if err != nil { + return managed.ExternalCreation{}, errors.Wrap(err, errParseUrlFailed) + } + + userInfo := url.UserPassword(username, password) + + parsedUrl.User = userInfo + + // Override importUrl with the manipulated URL containing the credentials found in ImportSecretRef. + *importUrl = parsedUrl.String() + } + prj, _, err := e.client.CreateProject( projects.GenerateCreateProjectOptions(cr.Name, &cr.Spec.ForProvider), gitlab.WithContext(ctx),