From 5718db2db4e201908b5cc687c497f465b46927f5 Mon Sep 17 00:00:00 2001 From: Nico Andres Date: Sun, 14 Dec 2025 16:13:53 +0100 Subject: [PATCH] Add files API Signed-off-by: Nico Andres --- .../projects/v1alpha1/zz_file_types.go | 132 ++++++ .../v1alpha1/zz_generated.deepcopy.go | 183 +++++++++ .../projects/v1alpha1/zz_generated.managed.go | 50 +++ .../v1alpha1/zz_generated.managedlist.go | 9 + .../projects/v1alpha1/zz_referencers.go | 23 ++ apis/cluster/projects/v1alpha1/zz_register.go | 9 + .../projects/v1alpha1/file_types.go | 132 ++++++ .../projects/v1alpha1/referencers.go | 23 ++ apis/namespaced/projects/v1alpha1/register.go | 9 + .../v1alpha1/zz_generated.deepcopy.go | 183 +++++++++ .../projects/v1alpha1/zz_generated.managed.go | 40 ++ .../v1alpha1/zz_generated.managedlist.go | 9 + .../projects.gitlab.crossplane.io_files.yaml | 375 ++++++++++++++++++ ...projects.gitlab.m.crossplane.io_files.yaml | 339 ++++++++++++++++ pkg/cluster/clients/projects/fake/zz_fake.go | 32 ++ pkg/cluster/clients/projects/zz_file.go | 226 +++++++++++ .../projects/files/zz_controller.go | 232 +++++++++++ pkg/cluster/controller/projects/zz_setup.go | 3 + pkg/namespaced/clients/projects/fake/fake.go | 32 ++ pkg/namespaced/clients/projects/file.go | 225 +++++++++++ .../controller/projects/files/controller.go | 230 +++++++++++ pkg/namespaced/controller/projects/setup.go | 3 + 22 files changed, 2499 insertions(+) create mode 100644 apis/cluster/projects/v1alpha1/zz_file_types.go create mode 100644 apis/namespaced/projects/v1alpha1/file_types.go create mode 100644 package/crds/projects.gitlab.crossplane.io_files.yaml create mode 100644 package/crds/projects.gitlab.m.crossplane.io_files.yaml create mode 100644 pkg/cluster/clients/projects/zz_file.go create mode 100644 pkg/cluster/controller/projects/files/zz_controller.go create mode 100644 pkg/namespaced/clients/projects/file.go create mode 100644 pkg/namespaced/controller/projects/files/controller.go diff --git a/apis/cluster/projects/v1alpha1/zz_file_types.go b/apis/cluster/projects/v1alpha1/zz_file_types.go new file mode 100644 index 00000000..e78745e9 --- /dev/null +++ b/apis/cluster/projects/v1alpha1/zz_file_types.go @@ -0,0 +1,132 @@ +/* +Copyright 2021 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by hack/generate-cluster-scope.go - DO NOT EDIT. + +package v1alpha1 + +import ( + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type FileParameters struct { + // ProjectID is the ID of the project. + // +optional + // +immutable + ProjectID *int `json:"projectId,omitempty"` + + // ProjectIDRef is a reference to a project to retrieve its projectId + // +optional + // +immutable + ProjectIDRef *xpv1.Reference `json:"projectIdRef,omitempty"` + + // ProjectIDSelector selects reference to a project to retrieve its projectId. + // +optional + // +immutable + ProjectIDSelector *xpv1.Selector `json:"projectIdSelector,omitempty"` + + // Branch is the file commit branch. + Branch *string `json:"branch"` + + // CommitMessage is the file commit message. + CommitMessage *string `json:"commitMessage"` + + // Content is the content of the file. + Content *string `json:"content"` + + // FilePath is the path of the file. + // +immutable + FilePath *string `json:"filePath"` + + // AuthorEmail is the commit author's email. + // +optional + AuthorEmail *string `json:"authorEmail,omitempty"` + + // AuthorName is the commit author's name. + // +optional + AuthorName *string `json:"authorName,omitempty"` + + // Encoding is the encoding of the file's content. + // +optional + // +kubebuilder:validation:Enum:=text;base64 + Encoding *string `json:"encoding,omitempty"` + + // ExecuteFilemode is whether the execute flag should be enabled on the file. + // +optional + ExecuteFilemode *bool `json:"executeFilemode,omitempty"` + + // StartBranch is the name of the base branch from which the target branch will be created. + // +optional + // +nullable + StartBranch *string `json:"startBranch,omitempty"` +} + +// FileObservation is the observed state of a File. +// +// GitLab API docs: +// https://docs.gitlab.com/api/repository_files/#get-file-from-repository +type FileObservation struct { + BlobID string `json:"blobId,omitempty"` + CommitID string `json:"commitId,omitempty"` + Content string `json:"content,omitempty"` + ContentSHA256 string `json:"contentSHA256,omitempty"` + Encoding string `json:"encoding,omitempty"` + ExecuteFilemode bool `json:"executeFilemode,omitempty"` + FileName string `json:"fileName,omitempty"` + FilePath string `json:"filePath,omitempty"` + LastCommitId string `json:"lastCommitId,omitempty"` + Ref string `json:"ref,omitempty"` + Size int `json:"size,omitempty"` +} + +// A VariableSpec defines the desired state of a Gitlab Project File +type FileSpec struct { + xpv1.ResourceSpec `json:",inline"` + ForProvider FileParameters `json:"forProvider"` +} + +// A FileStatus represents the observed state of a Gitlab Project File +type FileStatus struct { + xpv1.ResourceStatus `json:",inline"` + AtProvider FileObservation `json:"atProvider,omitempty"` +} + +// +kubebuilder:object:root=true + +// A File is a managed resource that represents a Gitlab Project File +// +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" +// +kubebuilder:printcolumn:name="SYNCED",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:printcolumn:name="Project ID",type="integer",JSONPath=".spec.forProvider.projectId" +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster,categories={crossplane,managed,gitlab} +type File struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FileSpec `json:"spec"` + Status FileStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// FileList contains a list of File items +type FileList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []File `json:"items"` +} diff --git a/apis/cluster/projects/v1alpha1/zz_generated.deepcopy.go b/apis/cluster/projects/v1alpha1/zz_generated.deepcopy.go index 210dec69..8e268e36 100644 --- a/apis/cluster/projects/v1alpha1/zz_generated.deepcopy.go +++ b/apis/cluster/projects/v1alpha1/zz_generated.deepcopy.go @@ -804,6 +804,189 @@ func (in *DeployTokenStatus) DeepCopy() *DeployTokenStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *File) DeepCopyInto(out *File) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new File. +func (in *File) DeepCopy() *File { + if in == nil { + return nil + } + out := new(File) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *File) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileList) DeepCopyInto(out *FileList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]File, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileList. +func (in *FileList) DeepCopy() *FileList { + if in == nil { + return nil + } + out := new(FileList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FileList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileObservation) DeepCopyInto(out *FileObservation) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileObservation. +func (in *FileObservation) DeepCopy() *FileObservation { + if in == nil { + return nil + } + out := new(FileObservation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileParameters) DeepCopyInto(out *FileParameters) { + *out = *in + if in.ProjectID != nil { + in, out := &in.ProjectID, &out.ProjectID + *out = new(int) + **out = **in + } + if in.ProjectIDRef != nil { + in, out := &in.ProjectIDRef, &out.ProjectIDRef + *out = new(v1.Reference) + (*in).DeepCopyInto(*out) + } + if in.ProjectIDSelector != nil { + in, out := &in.ProjectIDSelector, &out.ProjectIDSelector + *out = new(v1.Selector) + (*in).DeepCopyInto(*out) + } + if in.Branch != nil { + in, out := &in.Branch, &out.Branch + *out = new(string) + **out = **in + } + if in.CommitMessage != nil { + in, out := &in.CommitMessage, &out.CommitMessage + *out = new(string) + **out = **in + } + if in.Content != nil { + in, out := &in.Content, &out.Content + *out = new(string) + **out = **in + } + if in.FilePath != nil { + in, out := &in.FilePath, &out.FilePath + *out = new(string) + **out = **in + } + if in.AuthorEmail != nil { + in, out := &in.AuthorEmail, &out.AuthorEmail + *out = new(string) + **out = **in + } + if in.AuthorName != nil { + in, out := &in.AuthorName, &out.AuthorName + *out = new(string) + **out = **in + } + if in.Encoding != nil { + in, out := &in.Encoding, &out.Encoding + *out = new(string) + **out = **in + } + if in.ExecuteFilemode != nil { + in, out := &in.ExecuteFilemode, &out.ExecuteFilemode + *out = new(bool) + **out = **in + } + if in.StartBranch != nil { + in, out := &in.StartBranch, &out.StartBranch + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileParameters. +func (in *FileParameters) DeepCopy() *FileParameters { + if in == nil { + return nil + } + out := new(FileParameters) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileSpec) DeepCopyInto(out *FileSpec) { + *out = *in + in.ResourceSpec.DeepCopyInto(&out.ResourceSpec) + in.ForProvider.DeepCopyInto(&out.ForProvider) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSpec. +func (in *FileSpec) DeepCopy() *FileSpec { + if in == nil { + return nil + } + out := new(FileSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileStatus) DeepCopyInto(out *FileStatus) { + *out = *in + in.ResourceStatus.DeepCopyInto(&out.ResourceStatus) + out.AtProvider = in.AtProvider +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileStatus. +func (in *FileStatus) DeepCopy() *FileStatus { + if in == nil { + return nil + } + out := new(FileStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ForkParent) DeepCopyInto(out *ForkParent) { *out = *in diff --git a/apis/cluster/projects/v1alpha1/zz_generated.managed.go b/apis/cluster/projects/v1alpha1/zz_generated.managed.go index 4ad8ebb9..7b6b7f54 100644 --- a/apis/cluster/projects/v1alpha1/zz_generated.managed.go +++ b/apis/cluster/projects/v1alpha1/zz_generated.managed.go @@ -220,6 +220,56 @@ func (mg *DeployToken) SetWriteConnectionSecretToReference(r *xpv1.SecretReferen mg.Spec.WriteConnectionSecretToReference = r } +// GetCondition of this File. +func (mg *File) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetDeletionPolicy of this File. +func (mg *File) GetDeletionPolicy() xpv1.DeletionPolicy { + return mg.Spec.DeletionPolicy +} + +// GetManagementPolicies of this File. +func (mg *File) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this File. +func (mg *File) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +// GetWriteConnectionSecretToReference of this File. +func (mg *File) GetWriteConnectionSecretToReference() *xpv1.SecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this File. +func (mg *File) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetDeletionPolicy of this File. +func (mg *File) SetDeletionPolicy(r xpv1.DeletionPolicy) { + mg.Spec.DeletionPolicy = r +} + +// SetManagementPolicies of this File. +func (mg *File) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this File. +func (mg *File) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +// SetWriteConnectionSecretToReference of this File. +func (mg *File) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} + // GetCondition of this Hook. func (mg *Hook) GetCondition(ct xpv1.ConditionType) xpv1.Condition { return mg.Status.GetCondition(ct) diff --git a/apis/cluster/projects/v1alpha1/zz_generated.managedlist.go b/apis/cluster/projects/v1alpha1/zz_generated.managedlist.go index 9fd9db20..9a126534 100644 --- a/apis/cluster/projects/v1alpha1/zz_generated.managedlist.go +++ b/apis/cluster/projects/v1alpha1/zz_generated.managedlist.go @@ -56,6 +56,15 @@ func (l *DeployTokenList) GetItems() []resource.Managed { return items } +// GetItems of this FileList. +func (l *FileList) GetItems() []resource.Managed { + items := make([]resource.Managed, len(l.Items)) + for i := range l.Items { + items[i] = &l.Items[i] + } + return items +} + // GetItems of this HookList. func (l *HookList) GetItems() []resource.Managed { items := make([]resource.Managed, len(l.Items)) diff --git a/apis/cluster/projects/v1alpha1/zz_referencers.go b/apis/cluster/projects/v1alpha1/zz_referencers.go index 410a86a7..16f83b20 100644 --- a/apis/cluster/projects/v1alpha1/zz_referencers.go +++ b/apis/cluster/projects/v1alpha1/zz_referencers.go @@ -211,3 +211,26 @@ func (mg *Runner) ResolveReferences(ctx context.Context, c client.Reader) error return nil } + +// ResolveReferences of this File +func (mg *File) ResolveReferences(ctx context.Context, c client.Reader) error { + r := reference.NewAPIResolver(c, mg) + + // resolve spec.forProvider.projectIdRef + rsp, err := r.Resolve(ctx, reference.ResolutionRequest{ + CurrentValue: fromPtrValue(mg.Spec.ForProvider.ProjectID), + Reference: mg.Spec.ForProvider.ProjectIDRef, + Selector: mg.Spec.ForProvider.ProjectIDSelector, + To: reference.To{Managed: &Project{}, List: &ProjectList{}}, + Extract: reference.ExternalName(), + }) + + if err != nil { + return errors.Wrap(err, "spec.forProvider.projectId") + } + + mg.Spec.ForProvider.ProjectID = toPtrValue(rsp.ResolvedValue) + mg.Spec.ForProvider.ProjectIDRef = rsp.ResolvedReference + + return nil +} diff --git a/apis/cluster/projects/v1alpha1/zz_register.go b/apis/cluster/projects/v1alpha1/zz_register.go index 2847c49d..5850a6d4 100644 --- a/apis/cluster/projects/v1alpha1/zz_register.go +++ b/apis/cluster/projects/v1alpha1/zz_register.go @@ -126,6 +126,14 @@ var ( ProtectedBranchGroupVersionKind = SchemeGroupVersion.WithKind(ProtectedBranchKind) ) +// File type metadata +var ( + FileKind = reflect.TypeOf(File{}).Name() + FileGroupKind = schema.GroupKind{Group: Group, Kind: FileKind}.String() + FileKindAPIVersion = FileKind + "." + SchemeGroupVersion.String() + FileGroupVersionKind = SchemeGroupVersion.WithKind(FileKind) +) + func init() { SchemeBuilder.Register(&Project{}, &ProjectList{}) SchemeBuilder.Register(&Hook{}, &HookList{}) @@ -138,4 +146,5 @@ func init() { SchemeBuilder.Register(&PipelineSchedule{}, &PipelineScheduleList{}) SchemeBuilder.Register(&Runner{}, &RunnerList{}) SchemeBuilder.Register(&ProtectedBranch{}, &ProtectedBranchList{}) + SchemeBuilder.Register(&File{}, &FileList{}) } diff --git a/apis/namespaced/projects/v1alpha1/file_types.go b/apis/namespaced/projects/v1alpha1/file_types.go new file mode 100644 index 00000000..7c8eda25 --- /dev/null +++ b/apis/namespaced/projects/v1alpha1/file_types.go @@ -0,0 +1,132 @@ +/* +Copyright 2021 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + // +cluster-scope:delete=1 + xpv2 "github.com/crossplane/crossplane-runtime/v2/apis/common/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type FileParameters struct { + // ProjectID is the ID of the project. + // +optional + // +immutable + ProjectID *int `json:"projectId,omitempty"` + + // ProjectIDRef is a reference to a project to retrieve its projectId + // +optional + // +immutable + ProjectIDRef *xpv1.NamespacedReference `json:"projectIdRef,omitempty"` + + // ProjectIDSelector selects reference to a project to retrieve its projectId. + // +optional + // +immutable + ProjectIDSelector *xpv1.NamespacedSelector `json:"projectIdSelector,omitempty"` + + // Branch is the file commit branch. + Branch *string `json:"branch"` + + // CommitMessage is the file commit message. + CommitMessage *string `json:"commitMessage"` + + // Content is the content of the file. + Content *string `json:"content"` + + // FilePath is the path of the file. + // +immutable + FilePath *string `json:"filePath"` + + // AuthorEmail is the commit author's email. + // +optional + AuthorEmail *string `json:"authorEmail,omitempty"` + + // AuthorName is the commit author's name. + // +optional + AuthorName *string `json:"authorName,omitempty"` + + // Encoding is the encoding of the file's content. + // +optional + // +kubebuilder:validation:Enum:=text;base64 + Encoding *string `json:"encoding,omitempty"` + + // ExecuteFilemode is whether the execute flag should be enabled on the file. + // +optional + ExecuteFilemode *bool `json:"executeFilemode,omitempty"` + + // StartBranch is the name of the base branch from which the target branch will be created. + // +optional + // +nullable + StartBranch *string `json:"startBranch,omitempty"` +} + +// FileObservation is the observed state of a File. +// +// GitLab API docs: +// https://docs.gitlab.com/api/repository_files/#get-file-from-repository +type FileObservation struct { + BlobID string `json:"blobId,omitempty"` + CommitID string `json:"commitId,omitempty"` + Content string `json:"content,omitempty"` + ContentSHA256 string `json:"contentSHA256,omitempty"` + Encoding string `json:"encoding,omitempty"` + ExecuteFilemode bool `json:"executeFilemode,omitempty"` + FileName string `json:"fileName,omitempty"` + FilePath string `json:"filePath,omitempty"` + LastCommitId string `json:"lastCommitId,omitempty"` + Ref string `json:"ref,omitempty"` + Size int `json:"size,omitempty"` +} + +// A VariableSpec defines the desired state of a Gitlab Project File +type FileSpec struct { + xpv2.ManagedResourceSpec `json:",inline"` + ForProvider FileParameters `json:"forProvider"` +} + +// A FileStatus represents the observed state of a Gitlab Project File +type FileStatus struct { + xpv1.ResourceStatus `json:",inline"` + AtProvider FileObservation `json:"atProvider,omitempty"` +} + +// +kubebuilder:object:root=true + +// A File is a managed resource that represents a Gitlab Project File +// +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" +// +kubebuilder:printcolumn:name="SYNCED",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:printcolumn:name="Project ID",type="integer",JSONPath=".spec.forProvider.projectId" +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,categories={crossplane,managed,gitlab} +type File struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FileSpec `json:"spec"` + Status FileStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// FileList contains a list of File items +type FileList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []File `json:"items"` +} diff --git a/apis/namespaced/projects/v1alpha1/referencers.go b/apis/namespaced/projects/v1alpha1/referencers.go index e80c9560..c48fac73 100644 --- a/apis/namespaced/projects/v1alpha1/referencers.go +++ b/apis/namespaced/projects/v1alpha1/referencers.go @@ -209,3 +209,26 @@ func (mg *Runner) ResolveReferences(ctx context.Context, c client.Reader) error return nil } + +// ResolveReferences of this File +func (mg *File) ResolveReferences(ctx context.Context, c client.Reader) error { + r := reference.NewAPINamespacedResolver(c, mg) + + // resolve spec.forProvider.projectIdRef + rsp, err := r.Resolve(ctx, reference.NamespacedResolutionRequest{ + CurrentValue: fromPtrValue(mg.Spec.ForProvider.ProjectID), + Reference: mg.Spec.ForProvider.ProjectIDRef, + Selector: mg.Spec.ForProvider.ProjectIDSelector, + To: reference.To{Managed: &Project{}, List: &ProjectList{}}, + Extract: reference.ExternalName(), + }) + + if err != nil { + return errors.Wrap(err, "spec.forProvider.projectId") + } + + mg.Spec.ForProvider.ProjectID = toPtrValue(rsp.ResolvedValue) + mg.Spec.ForProvider.ProjectIDRef = rsp.ResolvedReference + + return nil +} diff --git a/apis/namespaced/projects/v1alpha1/register.go b/apis/namespaced/projects/v1alpha1/register.go index ba2d94b7..04bce3ca 100644 --- a/apis/namespaced/projects/v1alpha1/register.go +++ b/apis/namespaced/projects/v1alpha1/register.go @@ -124,6 +124,14 @@ var ( ProtectedBranchGroupVersionKind = SchemeGroupVersion.WithKind(ProtectedBranchKind) ) +// File type metadata +var ( + FileKind = reflect.TypeOf(File{}).Name() + FileGroupKind = schema.GroupKind{Group: Group, Kind: FileKind}.String() + FileKindAPIVersion = FileKind + "." + SchemeGroupVersion.String() + FileGroupVersionKind = SchemeGroupVersion.WithKind(FileKind) +) + func init() { SchemeBuilder.Register(&Project{}, &ProjectList{}) SchemeBuilder.Register(&Hook{}, &HookList{}) @@ -136,4 +144,5 @@ func init() { SchemeBuilder.Register(&PipelineSchedule{}, &PipelineScheduleList{}) SchemeBuilder.Register(&Runner{}, &RunnerList{}) SchemeBuilder.Register(&ProtectedBranch{}, &ProtectedBranchList{}) + SchemeBuilder.Register(&File{}, &FileList{}) } diff --git a/apis/namespaced/projects/v1alpha1/zz_generated.deepcopy.go b/apis/namespaced/projects/v1alpha1/zz_generated.deepcopy.go index 95f53c35..1744e069 100644 --- a/apis/namespaced/projects/v1alpha1/zz_generated.deepcopy.go +++ b/apis/namespaced/projects/v1alpha1/zz_generated.deepcopy.go @@ -804,6 +804,189 @@ func (in *DeployTokenStatus) DeepCopy() *DeployTokenStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *File) DeepCopyInto(out *File) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new File. +func (in *File) DeepCopy() *File { + if in == nil { + return nil + } + out := new(File) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *File) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileList) DeepCopyInto(out *FileList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]File, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileList. +func (in *FileList) DeepCopy() *FileList { + if in == nil { + return nil + } + out := new(FileList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FileList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileObservation) DeepCopyInto(out *FileObservation) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileObservation. +func (in *FileObservation) DeepCopy() *FileObservation { + if in == nil { + return nil + } + out := new(FileObservation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileParameters) DeepCopyInto(out *FileParameters) { + *out = *in + if in.ProjectID != nil { + in, out := &in.ProjectID, &out.ProjectID + *out = new(int) + **out = **in + } + if in.ProjectIDRef != nil { + in, out := &in.ProjectIDRef, &out.ProjectIDRef + *out = new(v1.NamespacedReference) + (*in).DeepCopyInto(*out) + } + if in.ProjectIDSelector != nil { + in, out := &in.ProjectIDSelector, &out.ProjectIDSelector + *out = new(v1.NamespacedSelector) + (*in).DeepCopyInto(*out) + } + if in.Branch != nil { + in, out := &in.Branch, &out.Branch + *out = new(string) + **out = **in + } + if in.CommitMessage != nil { + in, out := &in.CommitMessage, &out.CommitMessage + *out = new(string) + **out = **in + } + if in.Content != nil { + in, out := &in.Content, &out.Content + *out = new(string) + **out = **in + } + if in.FilePath != nil { + in, out := &in.FilePath, &out.FilePath + *out = new(string) + **out = **in + } + if in.AuthorEmail != nil { + in, out := &in.AuthorEmail, &out.AuthorEmail + *out = new(string) + **out = **in + } + if in.AuthorName != nil { + in, out := &in.AuthorName, &out.AuthorName + *out = new(string) + **out = **in + } + if in.Encoding != nil { + in, out := &in.Encoding, &out.Encoding + *out = new(string) + **out = **in + } + if in.ExecuteFilemode != nil { + in, out := &in.ExecuteFilemode, &out.ExecuteFilemode + *out = new(bool) + **out = **in + } + if in.StartBranch != nil { + in, out := &in.StartBranch, &out.StartBranch + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileParameters. +func (in *FileParameters) DeepCopy() *FileParameters { + if in == nil { + return nil + } + out := new(FileParameters) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileSpec) DeepCopyInto(out *FileSpec) { + *out = *in + in.ManagedResourceSpec.DeepCopyInto(&out.ManagedResourceSpec) + in.ForProvider.DeepCopyInto(&out.ForProvider) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSpec. +func (in *FileSpec) DeepCopy() *FileSpec { + if in == nil { + return nil + } + out := new(FileSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileStatus) DeepCopyInto(out *FileStatus) { + *out = *in + in.ResourceStatus.DeepCopyInto(&out.ResourceStatus) + out.AtProvider = in.AtProvider +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileStatus. +func (in *FileStatus) DeepCopy() *FileStatus { + if in == nil { + return nil + } + out := new(FileStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ForkParent) DeepCopyInto(out *ForkParent) { *out = *in diff --git a/apis/namespaced/projects/v1alpha1/zz_generated.managed.go b/apis/namespaced/projects/v1alpha1/zz_generated.managed.go index 08523226..49f3e993 100644 --- a/apis/namespaced/projects/v1alpha1/zz_generated.managed.go +++ b/apis/namespaced/projects/v1alpha1/zz_generated.managed.go @@ -180,6 +180,46 @@ func (mg *DeployToken) SetWriteConnectionSecretToReference(r *xpv1.LocalSecretRe mg.Spec.WriteConnectionSecretToReference = r } +// GetCondition of this File. +func (mg *File) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetManagementPolicies of this File. +func (mg *File) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this File. +func (mg *File) GetProviderConfigReference() *xpv1.ProviderConfigReference { + return mg.Spec.ProviderConfigReference +} + +// GetWriteConnectionSecretToReference of this File. +func (mg *File) GetWriteConnectionSecretToReference() *xpv1.LocalSecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this File. +func (mg *File) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetManagementPolicies of this File. +func (mg *File) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this File. +func (mg *File) SetProviderConfigReference(r *xpv1.ProviderConfigReference) { + mg.Spec.ProviderConfigReference = r +} + +// SetWriteConnectionSecretToReference of this File. +func (mg *File) SetWriteConnectionSecretToReference(r *xpv1.LocalSecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} + // GetCondition of this Hook. func (mg *Hook) GetCondition(ct xpv1.ConditionType) xpv1.Condition { return mg.Status.GetCondition(ct) diff --git a/apis/namespaced/projects/v1alpha1/zz_generated.managedlist.go b/apis/namespaced/projects/v1alpha1/zz_generated.managedlist.go index 9fd9db20..9a126534 100644 --- a/apis/namespaced/projects/v1alpha1/zz_generated.managedlist.go +++ b/apis/namespaced/projects/v1alpha1/zz_generated.managedlist.go @@ -56,6 +56,15 @@ func (l *DeployTokenList) GetItems() []resource.Managed { return items } +// GetItems of this FileList. +func (l *FileList) GetItems() []resource.Managed { + items := make([]resource.Managed, len(l.Items)) + for i := range l.Items { + items[i] = &l.Items[i] + } + return items +} + // GetItems of this HookList. func (l *HookList) GetItems() []resource.Managed { items := make([]resource.Managed, len(l.Items)) diff --git a/package/crds/projects.gitlab.crossplane.io_files.yaml b/package/crds/projects.gitlab.crossplane.io_files.yaml new file mode 100644 index 00000000..e85a1f2f --- /dev/null +++ b/package/crds/projects.gitlab.crossplane.io_files.yaml @@ -0,0 +1,375 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: files.projects.gitlab.crossplane.io +spec: + group: projects.gitlab.crossplane.io + names: + categories: + - crossplane + - managed + - gitlab + kind: File + listKind: FileList + plural: files + singular: file + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: READY + type: string + - jsonPath: .status.conditions[?(@.type=='Synced')].status + name: SYNCED + type: string + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + - jsonPath: .spec.forProvider.projectId + name: Project ID + type: integer + name: v1alpha1 + schema: + openAPIV3Schema: + description: A File is a managed resource that represents a Gitlab Project + File + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: A VariableSpec defines the desired state of a Gitlab Project + File + properties: + deletionPolicy: + default: Delete + description: |- + DeletionPolicy specifies what will happen to the underlying external + when this managed resource is deleted - either "Delete" or "Orphan" the + external resource. + This field is planned to be deprecated in favor of the ManagementPolicies + field in a future release. Currently, both could be set independently and + non-default values would be honored if the feature flag is enabled. + See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 + enum: + - Orphan + - Delete + type: string + forProvider: + properties: + authorEmail: + description: AuthorEmail is the commit author's email. + type: string + authorName: + description: AuthorName is the commit author's name. + type: string + branch: + description: Branch is the file commit branch. + type: string + commitMessage: + description: CommitMessage is the file commit message. + type: string + content: + description: Content is the content of the file. + type: string + encoding: + description: Encoding is the encoding of the file's content. + enum: + - text + - base64 + type: string + executeFilemode: + description: ExecuteFilemode is whether the execute flag should + be enabled on the file. + type: boolean + filePath: + description: FilePath is the path of the file. + type: string + projectId: + description: ProjectID is the ID of the project. + type: integer + projectIdRef: + description: ProjectIDRef is a reference to a project to retrieve + its projectId + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + projectIdSelector: + description: ProjectIDSelector selects reference to a project + to retrieve its projectId. + properties: + matchControllerRef: + description: |- + MatchControllerRef ensures an object with the same controller reference + as the selecting object is selected. + type: boolean + matchLabels: + additionalProperties: + type: string + description: MatchLabels ensures an object with matching labels + is selected. + type: object + policy: + description: Policies for selection. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + type: object + startBranch: + description: StartBranch is the name of the base branch from which + the target branch will be created. + nullable: true + type: string + required: + - branch + - commitMessage + - content + - filePath + type: object + managementPolicies: + default: + - '*' + description: |- + THIS IS A BETA FIELD. It is on by default but can be opted out + through a Crossplane feature flag. + ManagementPolicies specify the array of actions Crossplane is allowed to + take on the managed and external resources. + This field is planned to replace the DeletionPolicy field in a future + release. Currently, both could be set independently and non-default + values would be honored if the feature flag is enabled. If both are + custom, the DeletionPolicy field will be ignored. + See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 + and this one: https://github.com/crossplane/crossplane/blob/444267e84783136daa93568b364a5f01228cacbe/design/one-pager-ignore-changes.md + items: + description: |- + A ManagementAction represents an action that the Crossplane controllers + can take on an external resource. + enum: + - Observe + - Create + - Update + - Delete + - LateInitialize + - '*' + type: string + type: array + providerConfigRef: + default: + name: default + description: |- + ProviderConfigReference specifies how the provider that will be used to + create, observe, update, and delete this managed resource should be + configured. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + writeConnectionSecretToRef: + description: |- + WriteConnectionSecretToReference specifies the namespace and name of a + Secret to which any connection details for this managed resource should + be written. Connection details frequently include the endpoint, username, + and password required to connect to the managed resource. + properties: + name: + description: Name of the secret. + type: string + namespace: + description: Namespace of the secret. + type: string + required: + - name + - namespace + type: object + required: + - forProvider + type: object + status: + description: A FileStatus represents the observed state of a Gitlab Project + File + properties: + atProvider: + description: |- + FileObservation is the observed state of a File. + + GitLab API docs: + https://docs.gitlab.com/api/repository_files/#get-file-from-repository + properties: + blobId: + type: string + commitId: + type: string + content: + type: string + contentSHA256: + type: string + encoding: + type: string + executeFilemode: + type: boolean + fileName: + type: string + filePath: + type: string + lastCommitId: + type: string + ref: + type: string + size: + type: integer + type: object + conditions: + description: Conditions of the resource. + items: + description: A Condition that may apply to a resource. + properties: + lastTransitionTime: + description: |- + LastTransitionTime is the last time this condition transitioned from one + status to another. + format: date-time + type: string + message: + description: |- + A Message containing details about this condition's last transition from + one status to another, if any. + type: string + observedGeneration: + description: |- + ObservedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + type: integer + reason: + description: A Reason for this condition's last transition from + one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown? + type: string + type: + description: |- + Type of this condition. At most one of each condition type may apply to + a resource at any point in time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedGeneration: + description: |- + ObservedGeneration is the latest metadata.generation + which resulted in either a ready state, or stalled due to error + it can not recover from without human intervention. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/package/crds/projects.gitlab.m.crossplane.io_files.yaml b/package/crds/projects.gitlab.m.crossplane.io_files.yaml new file mode 100644 index 00000000..31e4a52a --- /dev/null +++ b/package/crds/projects.gitlab.m.crossplane.io_files.yaml @@ -0,0 +1,339 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: files.projects.gitlab.m.crossplane.io +spec: + group: projects.gitlab.m.crossplane.io + names: + categories: + - crossplane + - managed + - gitlab + kind: File + listKind: FileList + plural: files + singular: file + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: READY + type: string + - jsonPath: .status.conditions[?(@.type=='Synced')].status + name: SYNCED + type: string + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + - jsonPath: .spec.forProvider.projectId + name: Project ID + type: integer + name: v1alpha1 + schema: + openAPIV3Schema: + description: A File is a managed resource that represents a Gitlab Project + File + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: A VariableSpec defines the desired state of a Gitlab Project + File + properties: + forProvider: + properties: + authorEmail: + description: AuthorEmail is the commit author's email. + type: string + authorName: + description: AuthorName is the commit author's name. + type: string + branch: + description: Branch is the file commit branch. + type: string + commitMessage: + description: CommitMessage is the file commit message. + type: string + content: + description: Content is the content of the file. + type: string + encoding: + description: Encoding is the encoding of the file's content. + enum: + - text + - base64 + type: string + executeFilemode: + description: ExecuteFilemode is whether the execute flag should + be enabled on the file. + type: boolean + filePath: + description: FilePath is the path of the file. + type: string + projectId: + description: ProjectID is the ID of the project. + type: integer + projectIdRef: + description: ProjectIDRef is a reference to a project to retrieve + its projectId + properties: + name: + description: Name of the referenced object. + type: string + namespace: + description: Namespace of the referenced object + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + projectIdSelector: + description: ProjectIDSelector selects reference to a project + to retrieve its projectId. + properties: + matchControllerRef: + description: |- + MatchControllerRef ensures an object with the same controller reference + as the selecting object is selected. + type: boolean + matchLabels: + additionalProperties: + type: string + description: MatchLabels ensures an object with matching labels + is selected. + type: object + namespace: + description: Namespace for the selector + type: string + policy: + description: Policies for selection. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + type: object + startBranch: + description: StartBranch is the name of the base branch from which + the target branch will be created. + nullable: true + type: string + required: + - branch + - commitMessage + - content + - filePath + type: object + managementPolicies: + default: + - '*' + description: |- + THIS IS A BETA FIELD. It is on by default but can be opted out + through a Crossplane feature flag. + ManagementPolicies specify the array of actions Crossplane is allowed to + take on the managed and external resources. + See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 + and this one: https://github.com/crossplane/crossplane/blob/444267e84783136daa93568b364a5f01228cacbe/design/one-pager-ignore-changes.md + items: + description: |- + A ManagementAction represents an action that the Crossplane controllers + can take on an external resource. + enum: + - Observe + - Create + - Update + - Delete + - LateInitialize + - '*' + type: string + type: array + providerConfigRef: + default: + kind: ClusterProviderConfig + name: default + description: |- + ProviderConfigReference specifies how the provider that will be used to + create, observe, update, and delete this managed resource should be + configured. + properties: + kind: + description: Kind of the referenced object. + type: string + name: + description: Name of the referenced object. + type: string + required: + - kind + - name + type: object + writeConnectionSecretToRef: + description: |- + WriteConnectionSecretToReference specifies the namespace and name of a + Secret to which any connection details for this managed resource should + be written. Connection details frequently include the endpoint, username, + and password required to connect to the managed resource. + properties: + name: + description: Name of the secret. + type: string + required: + - name + type: object + required: + - forProvider + type: object + status: + description: A FileStatus represents the observed state of a Gitlab Project + File + properties: + atProvider: + description: |- + FileObservation is the observed state of a File. + + GitLab API docs: + https://docs.gitlab.com/api/repository_files/#get-file-from-repository + properties: + blobId: + type: string + commitId: + type: string + content: + type: string + contentSHA256: + type: string + encoding: + type: string + executeFilemode: + type: boolean + fileName: + type: string + filePath: + type: string + lastCommitId: + type: string + ref: + type: string + size: + type: integer + type: object + conditions: + description: Conditions of the resource. + items: + description: A Condition that may apply to a resource. + properties: + lastTransitionTime: + description: |- + LastTransitionTime is the last time this condition transitioned from one + status to another. + format: date-time + type: string + message: + description: |- + A Message containing details about this condition's last transition from + one status to another, if any. + type: string + observedGeneration: + description: |- + ObservedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + type: integer + reason: + description: A Reason for this condition's last transition from + one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown? + type: string + type: + description: |- + Type of this condition. At most one of each condition type may apply to + a resource at any point in time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedGeneration: + description: |- + ObservedGeneration is the latest metadata.generation + which resulted in either a ready state, or stalled due to error + it can not recover from without human intervention. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/pkg/cluster/clients/projects/fake/zz_fake.go b/pkg/cluster/clients/projects/fake/zz_fake.go index dc71e828..f4316924 100644 --- a/pkg/cluster/clients/projects/fake/zz_fake.go +++ b/pkg/cluster/clients/projects/fake/zz_fake.go @@ -85,6 +85,13 @@ type MockClient struct { MockGetProtectedBranch func(pid any, branch string, options ...gitlab.RequestOptionFunc) (*gitlab.ProtectedBranch, *gitlab.Response, error) MockProtectRepositoryBranches func(pid any, opt *gitlab.ProtectRepositoryBranchesOptions, options ...gitlab.RequestOptionFunc) (*gitlab.ProtectedBranch, *gitlab.Response, error) MockUnprotectRepositoryBranches func(pid any, branch string, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) + + MockGetFile func(pid any, fileName string, opt *gitlab.GetFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.File, *gitlab.Response, error) + MockCreateFile func(pid any, fileName string, opt *gitlab.CreateFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.FileInfo, *gitlab.Response, error) + MockUpdateFile func(pid any, fileName string, opt *gitlab.UpdateFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.FileInfo, *gitlab.Response, error) + MockDeleteFile func(pid any, fileName string, opt *gitlab.DeleteFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) + + MockGetCommit func(pid any, sha string, opt *gitlab.GetCommitOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Commit, *gitlab.Response, error) } // GetPipelineSchedule calls the underlying MockGetPipelineSchedule method. @@ -307,3 +314,28 @@ func (c *MockClient) ProtectRepositoryBranches(pid any, opt *gitlab.ProtectRepos func (c *MockClient) UnprotectRepositoryBranches(pid any, branch string, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) { return c.MockUnprotectRepositoryBranches(pid, branch, options...) } + +// GetFile calls the underlying MockGetFile method. +func (c *MockClient) GetFile(pid any, fileName string, opt *gitlab.GetFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.File, *gitlab.Response, error) { + return c.MockGetFile(pid, fileName, opt, options...) +} + +// CreateFile calls the underlying MockCreateFile method. +func (c *MockClient) CreateFile(pid any, fileName string, opt *gitlab.CreateFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.FileInfo, *gitlab.Response, error) { + return c.MockCreateFile(pid, fileName, opt, options...) +} + +// UpdateFile calls the underlying MockUpdateFile method. +func (c *MockClient) UpdateFile(pid any, fileName string, opt *gitlab.UpdateFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.FileInfo, *gitlab.Response, error) { + return c.MockUpdateFile(pid, fileName, opt, options...) +} + +// DeleteFile calls the underlying MockDeleteFile method. +func (c *MockClient) DeleteFile(pid any, fileName string, opt *gitlab.DeleteFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) { + return c.MockDeleteFile(pid, fileName, opt, options...) +} + +// GetCommit calls the underlying MockGetCommit method. +func (c *MockClient) GetCommit(pid any, sha string, opt *gitlab.GetCommitOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Commit, *gitlab.Response, error) { + return c.MockGetCommit(pid, sha, opt, options...) +} diff --git a/pkg/cluster/clients/projects/zz_file.go b/pkg/cluster/clients/projects/zz_file.go new file mode 100644 index 00000000..24721c36 --- /dev/null +++ b/pkg/cluster/clients/projects/zz_file.go @@ -0,0 +1,226 @@ +/* +Copyright 2021 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by hack/generate-cluster-scope.go - DO NOT EDIT. + +package projects + +import ( + "crypto/sha256" + "encoding/base64" + "fmt" + "strings" + + "github.com/crossplane-contrib/provider-gitlab/apis/cluster/projects/v1alpha1" + "github.com/crossplane-contrib/provider-gitlab/pkg/cluster/clients" + "github.com/crossplane-contrib/provider-gitlab/pkg/common" + "github.com/google/go-cmp/cmp" + gitlab "gitlab.com/gitlab-org/api/client-go" +) + +const ( + errFileNotFound = "404 Not found" +) + +// FileClient defines Gitlab File service operations +type FileClient interface { + GetFile(pid any, fileName string, opt *gitlab.GetFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.File, *gitlab.Response, error) + CreateFile(pid any, fileName string, opt *gitlab.CreateFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.FileInfo, *gitlab.Response, error) + UpdateFile(pid any, fileName string, opt *gitlab.UpdateFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.FileInfo, *gitlab.Response, error) + DeleteFile(pid any, fileName string, opt *gitlab.DeleteFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) +} + +// CommitsClient defines Gitlab File service operations +type CommitClient interface { + GetCommit(pid any, sha string, opt *gitlab.GetCommitOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Commit, *gitlab.Response, error) +} + +// NewFileClient returns a new Gitlab Project service +func NewFileClient(cfg common.Config) FileClient { + git := common.NewClient(cfg) + return git.RepositoryFiles +} + +func NewCommitsClient(cfg common.Config) CommitClient { + git := common.NewClient(cfg) + return git.Commits +} + +// IsErrorFileNotFound helper function to test for errProjectNotFound error. +func IsErrorFileNotFound(err error) bool { + if err == nil { + return false + } + return strings.Contains(err.Error(), errFileNotFound) +} + +// LateInitializeFile fills the empty fields in the file spec with the +// values seen in gitlab.File. +func LateInitializeFile(in *v1alpha1.FileParameters, file *gitlab.File, commit *gitlab.Commit) { + if file == nil { + return + } + + if in.Encoding == nil { + in.Encoding = clients.StringToPtr("text") + } + + if in.ExecuteFilemode == nil { + in.ExecuteFilemode = &file.ExecuteFilemode + } + + if in.AuthorEmail == nil { + in.AuthorEmail = &commit.AuthorEmail + } + + if in.AuthorName == nil { + in.AuthorName = &commit.AuthorName + } + + if in.StartBranch == nil { + in.StartBranch = clients.StringToPtr("") + } +} + +// GenerateFileObservation is used to produce v1alpha1.FileObservation from +// gitlab.File. +func GenerateFileObservation(file *gitlab.File) v1alpha1.FileObservation { + if file == nil { + return v1alpha1.FileObservation{} + } + + o := v1alpha1.FileObservation{ + BlobID: file.BlobID, + CommitID: file.CommitID, + Content: file.Content, + ContentSHA256: file.SHA256, + Encoding: file.Encoding, + ExecuteFilemode: file.ExecuteFilemode, + FileName: file.FileName, + FilePath: file.FilePath, + LastCommitId: file.LastCommitID, + Ref: file.Ref, + Size: file.Size, + } + + return o +} + +// GenerateGetFileOptions generates project get options +func GenerateGetFileOptions(p *v1alpha1.FileParameters) *gitlab.GetFileOptions { + + o := &gitlab.GetFileOptions{ + Ref: p.Branch, + } + + return o +} + +// GenerateCreateFileOptions generates project creation options +func GenerateCreateFileOptions(p *v1alpha1.FileParameters) *gitlab.CreateFileOptions { + + o := &gitlab.CreateFileOptions{ + Branch: p.Branch, + StartBranch: p.StartBranch, + Encoding: p.Encoding, + AuthorEmail: p.AuthorEmail, + AuthorName: p.AuthorName, + Content: p.Content, + CommitMessage: p.CommitMessage, + ExecuteFilemode: p.ExecuteFilemode, + } + + return o +} + +// GenerateUpdateFileOptions generates project update options +func GenerateUpdateFileOptions(p *v1alpha1.FileParameters) *gitlab.UpdateFileOptions { + cm := "Update file " + *p.FilePath + + o := &gitlab.UpdateFileOptions{ + Branch: p.Branch, + Encoding: p.Encoding, + AuthorEmail: p.AuthorEmail, + AuthorName: p.AuthorName, + Content: p.Content, + CommitMessage: clients.StringToPtr(cm), + ExecuteFilemode: p.ExecuteFilemode, + } + + return o +} + +// GenerateDeleteFileOptions generates project delete options +func GenerateDeleteFileOptions(p *v1alpha1.FileParameters) *gitlab.DeleteFileOptions { + cm := "Delete file " + *p.FilePath + + o := &gitlab.DeleteFileOptions{ + Branch: p.Branch, + AuthorEmail: p.AuthorEmail, + AuthorName: p.AuthorName, + CommitMessage: &cm, + } + + return o +} + +// GenerateGetCommitOptions generates project get options +func GenerateGetCommitOptions() *gitlab.GetCommitOptions { + b := false + o := &gitlab.GetCommitOptions{ + Stats: &b, + } + + return o +} + +// Calculate SHA256 checksum on file content and return as hexadecimal string +func getFileSHA256(s *string, encoding *string) string { + var cont string + if !cmp.Equal(encoding, clients.StringToPtr("text")) { + cont = base64Decode(*s) + } else { + cont = *s + } + sum := sha256.Sum256([]byte(cont)) + hex := fmt.Sprintf("%x", sum) + return hex +} + +// Return Base64 decoded string +func base64Decode(enc string) string { + dec, _ := base64.StdEncoding.DecodeString(enc) + s := string(dec) + return s +} + +// IsFileUpToDate checks whether there is a change in any of the modifiable fields. +func IsFileUpToDate(p *v1alpha1.FileParameters, g *gitlab.File) bool { + sha256 := getFileSHA256(p.Content, p.Encoding) + + if !cmp.Equal(sha256, g.SHA256) { + return false + } + + if !cmp.Equal(p.Branch, clients.StringToPtr(g.Ref)) { + return false + } + + if !clients.IsBoolEqualToBoolPtr(p.ExecuteFilemode, g.ExecuteFilemode) { + return false + } + return true +} diff --git a/pkg/cluster/controller/projects/files/zz_controller.go b/pkg/cluster/controller/projects/files/zz_controller.go new file mode 100644 index 00000000..e6ca3812 --- /dev/null +++ b/pkg/cluster/controller/projects/files/zz_controller.go @@ -0,0 +1,232 @@ +/* +Copyright 2021 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by hack/generate-cluster-scope.go - DO NOT EDIT. + +package files + +import ( + "context" + + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + "github.com/crossplane/crossplane-runtime/v2/pkg/controller" + "github.com/crossplane/crossplane-runtime/v2/pkg/event" + "github.com/crossplane/crossplane-runtime/v2/pkg/feature" + "github.com/crossplane/crossplane-runtime/v2/pkg/meta" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + "github.com/crossplane/crossplane-runtime/v2/pkg/statemetrics" + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" + gitlab "gitlab.com/gitlab-org/api/client-go" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/crossplane-contrib/provider-gitlab/apis/cluster/projects/v1alpha1" + "github.com/crossplane-contrib/provider-gitlab/pkg/cluster/clients" + "github.com/crossplane-contrib/provider-gitlab/pkg/cluster/clients/projects" + "github.com/crossplane-contrib/provider-gitlab/pkg/common" +) + +const ( + errNotFile = "managed resource is not a Gitlab file custom resource" + errGetFailed = "cannot get Gitlab file" + errCreateFailed = "cannot create Gitlab file" + errUpdateFailed = "cannot update Gitlab file" + errDeleteFailed = "cannot delete Gitlab file" + errProjectIDMissing = "ProjectID is missing" +) + +// SetupFile adds a controller that reconciles Files. +func SetupFile(mgr ctrl.Manager, o controller.Options) error { + name := managed.ControllerName(v1alpha1.FileGroupKind) + + reconcilerOpts := []managed.ReconcilerOption{ + managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), newGitlabFileClientFn: projects.NewFileClient, newGitlabCommitClientFn: projects.NewCommitsClient}), + managed.WithInitializers(), + managed.WithPollInterval(o.PollInterval), + managed.WithLogger(o.Logger.WithValues("controller", name)), + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), + } + + if o.Features.Enabled(feature.EnableBetaManagementPolicies) { + reconcilerOpts = append(reconcilerOpts, managed.WithManagementPolicies()) + } + + r := managed.NewReconciler(mgr, + resource.ManagedKind(v1alpha1.FileGroupVersionKind), + reconcilerOpts...) + + if err := mgr.Add(statemetrics.NewMRStateRecorder( + mgr.GetClient(), o.Logger, o.MetricOptions.MRStateMetrics, &v1alpha1.FileList{}, o.MetricOptions.PollStateMetricInterval)); err != nil { + return err + } + + return ctrl.NewControllerManagedBy(mgr). + Named(name). + For(&v1alpha1.File{}). + Complete(r) +} + +// SetupFileGated adds a controller with CRD gate support. +func SetupFileGated(mgr ctrl.Manager, o controller.Options) error { + o.Gate.Register(func() { + if err := SetupFile(mgr, o); err != nil { + mgr.GetLogger().Error(err, "unable to setup reconciler", "gvk", v1alpha1.FileGroupVersionKind.String()) + } + }, v1alpha1.FileGroupVersionKind) + return nil +} + +type connector struct { + kube client.Client + newGitlabFileClientFn func(cfg common.Config) projects.FileClient + newGitlabCommitClientFn func(cfg common.Config) projects.CommitClient +} + +func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { + cr, ok := mg.(*v1alpha1.File) + if !ok { + return nil, errors.New(errNotFile) + } + cfg, err := common.GetConfig(ctx, c.kube, cr) + if err != nil { + return nil, err + } + return &external{kube: c.kube, fileClient: c.newGitlabFileClientFn(*cfg), commitClient: c.newGitlabCommitClientFn(*cfg)}, nil +} + +type external struct { + kube client.Client + fileClient projects.FileClient + commitClient projects.CommitClient +} + +func (e *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { + cr, ok := mg.(*v1alpha1.File) + if !ok { + return managed.ExternalObservation{}, errors.New(errNotFile) + } + + externalName := meta.GetExternalName(cr) + if externalName == "" { + return managed.ExternalObservation{}, nil + } + + if cr.Spec.ForProvider.ProjectID == nil { + return managed.ExternalObservation{}, errors.New(errProjectIDMissing) + } + + file, res, err := e.fileClient.GetFile( + *cr.Spec.ForProvider.ProjectID, + *cr.Spec.ForProvider.FilePath, + projects.GenerateGetFileOptions(&cr.Spec.ForProvider), + gitlab.WithContext(ctx)) + + if err != nil { + if clients.IsResponseNotFound(res) { + return managed.ExternalObservation{}, nil + } + return managed.ExternalObservation{}, errors.Wrap(err, errGetFailed) + } + + commit, _, _ := e.commitClient.GetCommit( + *cr.Spec.ForProvider.ProjectID, + file.CommitID, + projects.GenerateGetCommitOptions(), + gitlab.WithContext(ctx)) + + current := cr.Spec.ForProvider.DeepCopy() + projects.LateInitializeFile(&cr.Spec.ForProvider, file, commit) + + cr.Status.AtProvider = projects.GenerateFileObservation(file) + cr.Status.SetConditions(xpv1.Available()) + + return managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: projects.IsFileUpToDate(&cr.Spec.ForProvider, file), + ResourceLateInitialized: !cmp.Equal(current, &cr.Spec.ForProvider), + }, nil +} + +func (e *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) { + cr, ok := mg.(*v1alpha1.File) + if !ok { + return managed.ExternalCreation{}, errors.New(errNotFile) + } + + if cr.Spec.ForProvider.ProjectID == nil { + return managed.ExternalCreation{}, errors.New(errProjectIDMissing) + } + + cr.Status.SetConditions(xpv1.Creating()) + file, _, err := e.fileClient.CreateFile( + *cr.Spec.ForProvider.ProjectID, + *cr.Spec.ForProvider.FilePath, + projects.GenerateCreateFileOptions(&cr.Spec.ForProvider), + gitlab.WithContext(ctx)) + + if err != nil { + return managed.ExternalCreation{}, errors.Wrap(err, errCreateFailed) + } + + meta.SetExternalName(cr, file.FilePath) + return managed.ExternalCreation{}, nil +} + +func (e *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { + cr, ok := mg.(*v1alpha1.File) + if !ok { + return managed.ExternalUpdate{}, errors.New(errNotFile) + } + + if cr.Spec.ForProvider.ProjectID == nil { + return managed.ExternalUpdate{}, errors.New(errProjectIDMissing) + } + + _, _, err := e.fileClient.UpdateFile( + *cr.Spec.ForProvider.ProjectID, + *cr.Spec.ForProvider.FilePath, + projects.GenerateUpdateFileOptions(&cr.Spec.ForProvider), + gitlab.WithContext(ctx), + ) + return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateFailed) +} + +func (e *external) Delete(ctx context.Context, mg resource.Managed) (managed.ExternalDelete, error) { + cr, ok := mg.(*v1alpha1.File) + if !ok { + return managed.ExternalDelete{}, errors.New(errNotFile) + } + + if cr.Spec.ForProvider.ProjectID == nil { + return managed.ExternalDelete{}, errors.New(errProjectIDMissing) + } + + cr.Status.SetConditions(xpv1.Deleting()) + _, err := e.fileClient.DeleteFile( + *cr.Spec.ForProvider.ProjectID, + *cr.Spec.ForProvider.FilePath, + projects.GenerateDeleteFileOptions(&cr.Spec.ForProvider), + gitlab.WithContext(ctx), + ) + return managed.ExternalDelete{}, errors.Wrap(err, errDeleteFailed) +} + +func (e *external) Disconnect(ctx context.Context) error { + // Disconnect is not implemented as it is a new method required by the SDK + return nil +} diff --git a/pkg/cluster/controller/projects/zz_setup.go b/pkg/cluster/controller/projects/zz_setup.go index 89a530c7..5bb05374 100644 --- a/pkg/cluster/controller/projects/zz_setup.go +++ b/pkg/cluster/controller/projects/zz_setup.go @@ -26,6 +26,7 @@ import ( "github.com/crossplane-contrib/provider-gitlab/pkg/cluster/controller/projects/approvalrules" "github.com/crossplane-contrib/provider-gitlab/pkg/cluster/controller/projects/deploykeys" "github.com/crossplane-contrib/provider-gitlab/pkg/cluster/controller/projects/deploytokens" + "github.com/crossplane-contrib/provider-gitlab/pkg/cluster/controller/projects/files" "github.com/crossplane-contrib/provider-gitlab/pkg/cluster/controller/projects/hooks" "github.com/crossplane-contrib/provider-gitlab/pkg/cluster/controller/projects/members" "github.com/crossplane-contrib/provider-gitlab/pkg/cluster/controller/projects/pipelineschedules" @@ -49,6 +50,7 @@ func Setup(mgr ctrl.Manager, o controller.Options) error { approvalrules.SetupRules, runners.SetupRunner, protectedbranches.SetupProtectedBranch, + files.SetupFile, } { if err := setup(mgr, o); err != nil { return err @@ -72,6 +74,7 @@ func SetupGated(mgr ctrl.Manager, o controller.Options) error { approvalrules.SetupRulesGated, runners.SetupRunnerGated, protectedbranches.SetupProtectedBranchGated, + files.SetupFileGated, } { if err := setup(mgr, o); err != nil { return err diff --git a/pkg/namespaced/clients/projects/fake/fake.go b/pkg/namespaced/clients/projects/fake/fake.go index b0f49855..1bdff0ef 100644 --- a/pkg/namespaced/clients/projects/fake/fake.go +++ b/pkg/namespaced/clients/projects/fake/fake.go @@ -83,6 +83,13 @@ type MockClient struct { MockGetProtectedBranch func(pid any, branch string, options ...gitlab.RequestOptionFunc) (*gitlab.ProtectedBranch, *gitlab.Response, error) MockProtectRepositoryBranches func(pid any, opt *gitlab.ProtectRepositoryBranchesOptions, options ...gitlab.RequestOptionFunc) (*gitlab.ProtectedBranch, *gitlab.Response, error) MockUnprotectRepositoryBranches func(pid any, branch string, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) + + MockGetFile func(pid any, fileName string, opt *gitlab.GetFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.File, *gitlab.Response, error) + MockCreateFile func(pid any, fileName string, opt *gitlab.CreateFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.FileInfo, *gitlab.Response, error) + MockUpdateFile func(pid any, fileName string, opt *gitlab.UpdateFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.FileInfo, *gitlab.Response, error) + MockDeleteFile func(pid any, fileName string, opt *gitlab.DeleteFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) + + MockGetCommit func(pid any, sha string, opt *gitlab.GetCommitOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Commit, *gitlab.Response, error) } // GetPipelineSchedule calls the underlying MockGetPipelineSchedule method. @@ -305,3 +312,28 @@ func (c *MockClient) ProtectRepositoryBranches(pid any, opt *gitlab.ProtectRepos func (c *MockClient) UnprotectRepositoryBranches(pid any, branch string, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) { return c.MockUnprotectRepositoryBranches(pid, branch, options...) } + +// GetFile calls the underlying MockGetFile method. +func (c *MockClient) GetFile(pid any, fileName string, opt *gitlab.GetFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.File, *gitlab.Response, error) { + return c.MockGetFile(pid, fileName, opt, options...) +} + +// CreateFile calls the underlying MockCreateFile method. +func (c *MockClient) CreateFile(pid any, fileName string, opt *gitlab.CreateFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.FileInfo, *gitlab.Response, error) { + return c.MockCreateFile(pid, fileName, opt, options...) +} + +// UpdateFile calls the underlying MockUpdateFile method. +func (c *MockClient) UpdateFile(pid any, fileName string, opt *gitlab.UpdateFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.FileInfo, *gitlab.Response, error) { + return c.MockUpdateFile(pid, fileName, opt, options...) +} + +// DeleteFile calls the underlying MockDeleteFile method. +func (c *MockClient) DeleteFile(pid any, fileName string, opt *gitlab.DeleteFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) { + return c.MockDeleteFile(pid, fileName, opt, options...) +} + +// GetCommit calls the underlying MockGetCommit method. +func (c *MockClient) GetCommit(pid any, sha string, opt *gitlab.GetCommitOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Commit, *gitlab.Response, error) { + return c.MockGetCommit(pid, sha, opt, options...) +} diff --git a/pkg/namespaced/clients/projects/file.go b/pkg/namespaced/clients/projects/file.go new file mode 100644 index 00000000..eeb46a0a --- /dev/null +++ b/pkg/namespaced/clients/projects/file.go @@ -0,0 +1,225 @@ +/* +Copyright 2021 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package projects + +import ( + "crypto/sha256" + "encoding/base64" + "fmt" + "strings" + + "github.com/crossplane-contrib/provider-gitlab/apis/namespaced/projects/v1alpha1" + "github.com/crossplane-contrib/provider-gitlab/pkg/common" + "github.com/crossplane-contrib/provider-gitlab/pkg/namespaced/clients" + "github.com/google/go-cmp/cmp" + gitlab "gitlab.com/gitlab-org/api/client-go" +) + +const ( + errFileNotFound = "404 Not found" +) + +// FileClient defines Gitlab File service operations +type FileClient interface { + GetFile(pid any, fileName string, opt *gitlab.GetFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.File, *gitlab.Response, error) + CreateFile(pid any, fileName string, opt *gitlab.CreateFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.FileInfo, *gitlab.Response, error) + UpdateFile(pid any, fileName string, opt *gitlab.UpdateFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.FileInfo, *gitlab.Response, error) + DeleteFile(pid any, fileName string, opt *gitlab.DeleteFileOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) +} + +// CommitsClient defines Gitlab Commit service operations +type CommitClient interface { + GetCommit(pid any, sha string, opt *gitlab.GetCommitOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Commit, *gitlab.Response, error) +} + +// NewFileClient returns a new Gitlab File service +func NewFileClient(cfg common.Config) FileClient { + git := common.NewClient(cfg) + return git.RepositoryFiles +} + +// NewCommitsClient returns a new Gitlab Commit service +func NewCommitsClient(cfg common.Config) CommitClient { + git := common.NewClient(cfg) + return git.Commits +} + +// IsErrorFileNotFound helper function to test for errProjectNotFound error. +func IsErrorFileNotFound(err error) bool { + if err == nil { + return false + } + return strings.Contains(err.Error(), errFileNotFound) +} + +// LateInitializeFile fills the empty fields in the file spec with the +// values seen in gitlab.File. +func LateInitializeFile(in *v1alpha1.FileParameters, file *gitlab.File, commit *gitlab.Commit) { + if file == nil { + return + } + + if in.Encoding == nil { + in.Encoding = clients.StringToPtr("text") + } + + if in.ExecuteFilemode == nil { + in.ExecuteFilemode = &file.ExecuteFilemode + } + + if in.AuthorEmail == nil { + in.AuthorEmail = &commit.AuthorEmail + } + + if in.AuthorName == nil { + in.AuthorName = &commit.AuthorName + } + + if in.StartBranch == nil { + in.StartBranch = clients.StringToPtr("") + } +} + +// GenerateFileObservation is used to produce v1alpha1.FileObservation from +// gitlab.File. +func GenerateFileObservation(file *gitlab.File) v1alpha1.FileObservation { + if file == nil { + return v1alpha1.FileObservation{} + } + + o := v1alpha1.FileObservation{ + BlobID: file.BlobID, + CommitID: file.CommitID, + Content: file.Content, + ContentSHA256: file.SHA256, + Encoding: file.Encoding, + ExecuteFilemode: file.ExecuteFilemode, + FileName: file.FileName, + FilePath: file.FilePath, + LastCommitId: file.LastCommitID, + Ref: file.Ref, + Size: file.Size, + } + + return o +} + +// GenerateGetFileOptions generates project get options +func GenerateGetFileOptions(p *v1alpha1.FileParameters) *gitlab.GetFileOptions { + + o := &gitlab.GetFileOptions{ + Ref: p.Branch, + } + + return o +} + +// GenerateCreateFileOptions generates project creation options +func GenerateCreateFileOptions(p *v1alpha1.FileParameters) *gitlab.CreateFileOptions { + + o := &gitlab.CreateFileOptions{ + Branch: p.Branch, + StartBranch: p.StartBranch, + Encoding: p.Encoding, + AuthorEmail: p.AuthorEmail, + AuthorName: p.AuthorName, + Content: p.Content, + CommitMessage: p.CommitMessage, + ExecuteFilemode: p.ExecuteFilemode, + } + + return o +} + +// GenerateUpdateFileOptions generates project update options +func GenerateUpdateFileOptions(p *v1alpha1.FileParameters) *gitlab.UpdateFileOptions { + cm := "Update file " + *p.FilePath + + o := &gitlab.UpdateFileOptions{ + Branch: p.Branch, + Encoding: p.Encoding, + AuthorEmail: p.AuthorEmail, + AuthorName: p.AuthorName, + Content: p.Content, + CommitMessage: clients.StringToPtr(cm), + ExecuteFilemode: p.ExecuteFilemode, + } + + return o +} + +// GenerateDeleteFileOptions generates project delete options +func GenerateDeleteFileOptions(p *v1alpha1.FileParameters) *gitlab.DeleteFileOptions { + cm := "Delete file " + *p.FilePath + + o := &gitlab.DeleteFileOptions{ + Branch: p.Branch, + AuthorEmail: p.AuthorEmail, + AuthorName: p.AuthorName, + CommitMessage: &cm, + } + + return o +} + +// GenerateGetCommitOptions generates project get options +func GenerateGetCommitOptions() *gitlab.GetCommitOptions { + b := false + o := &gitlab.GetCommitOptions{ + Stats: &b, + } + + return o +} + +// Calculate SHA256 checksum on file content and return as hexadecimal string +func getFileSHA256(s *string, encoding *string) string { + var cont string + if !cmp.Equal(encoding, clients.StringToPtr("text")) { + cont = base64Decode(*s) + } else { + cont = *s + } + sum := sha256.Sum256([]byte(cont)) + hex := fmt.Sprintf("%x", sum) + return hex +} + +// Return Base64 decoded string +func base64Decode(enc string) string { + dec, _ := base64.StdEncoding.DecodeString(enc) + s := string(dec) + return s +} + +// IsFileUpToDate checks whether there is a change in any of the modifiable fields. +func IsFileUpToDate(p *v1alpha1.FileParameters, g *gitlab.File) bool { + sha256 := getFileSHA256(p.Content, p.Encoding) + + if !cmp.Equal(sha256, g.SHA256) { + return false + } + + if !cmp.Equal(p.Branch, clients.StringToPtr(g.Ref)) { + return false + } + + if !clients.IsBoolEqualToBoolPtr(p.ExecuteFilemode, g.ExecuteFilemode) { + return false + } + return true +} diff --git a/pkg/namespaced/controller/projects/files/controller.go b/pkg/namespaced/controller/projects/files/controller.go new file mode 100644 index 00000000..41597702 --- /dev/null +++ b/pkg/namespaced/controller/projects/files/controller.go @@ -0,0 +1,230 @@ +/* +Copyright 2021 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package files + +import ( + "context" + + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + "github.com/crossplane/crossplane-runtime/v2/pkg/controller" + "github.com/crossplane/crossplane-runtime/v2/pkg/event" + "github.com/crossplane/crossplane-runtime/v2/pkg/feature" + "github.com/crossplane/crossplane-runtime/v2/pkg/meta" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + "github.com/crossplane/crossplane-runtime/v2/pkg/statemetrics" + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" + gitlab "gitlab.com/gitlab-org/api/client-go" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/crossplane-contrib/provider-gitlab/apis/namespaced/projects/v1alpha1" + "github.com/crossplane-contrib/provider-gitlab/pkg/common" + "github.com/crossplane-contrib/provider-gitlab/pkg/namespaced/clients" + "github.com/crossplane-contrib/provider-gitlab/pkg/namespaced/clients/projects" +) + +const ( + errNotFile = "managed resource is not a Gitlab file custom resource" + errGetFailed = "cannot get Gitlab file" + errCreateFailed = "cannot create Gitlab file" + errUpdateFailed = "cannot update Gitlab file" + errDeleteFailed = "cannot delete Gitlab file" + errProjectIDMissing = "ProjectID is missing" +) + +// SetupFile adds a controller that reconciles Files. +func SetupFile(mgr ctrl.Manager, o controller.Options) error { + name := managed.ControllerName(v1alpha1.FileGroupKind) + + reconcilerOpts := []managed.ReconcilerOption{ + managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), newGitlabFileClientFn: projects.NewFileClient, newGitlabCommitClientFn: projects.NewCommitsClient}), + managed.WithInitializers(), + managed.WithPollInterval(o.PollInterval), + managed.WithLogger(o.Logger.WithValues("controller", name)), + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), + } + + if o.Features.Enabled(feature.EnableBetaManagementPolicies) { + reconcilerOpts = append(reconcilerOpts, managed.WithManagementPolicies()) + } + + r := managed.NewReconciler(mgr, + resource.ManagedKind(v1alpha1.FileGroupVersionKind), + reconcilerOpts...) + + if err := mgr.Add(statemetrics.NewMRStateRecorder( + mgr.GetClient(), o.Logger, o.MetricOptions.MRStateMetrics, &v1alpha1.FileList{}, o.MetricOptions.PollStateMetricInterval)); err != nil { + return err + } + + return ctrl.NewControllerManagedBy(mgr). + Named(name). + For(&v1alpha1.File{}). + Complete(r) +} + +// SetupFileGated adds a controller with CRD gate support. +func SetupFileGated(mgr ctrl.Manager, o controller.Options) error { + o.Gate.Register(func() { + if err := SetupFile(mgr, o); err != nil { + mgr.GetLogger().Error(err, "unable to setup reconciler", "gvk", v1alpha1.FileGroupVersionKind.String()) + } + }, v1alpha1.FileGroupVersionKind) + return nil +} + +type connector struct { + kube client.Client + newGitlabFileClientFn func(cfg common.Config) projects.FileClient + newGitlabCommitClientFn func(cfg common.Config) projects.CommitClient +} + +func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { + cr, ok := mg.(*v1alpha1.File) + if !ok { + return nil, errors.New(errNotFile) + } + cfg, err := common.GetConfig(ctx, c.kube, cr) + if err != nil { + return nil, err + } + return &external{kube: c.kube, fileClient: c.newGitlabFileClientFn(*cfg), commitClient: c.newGitlabCommitClientFn(*cfg)}, nil +} + +type external struct { + kube client.Client + fileClient projects.FileClient + commitClient projects.CommitClient +} + +func (e *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { + cr, ok := mg.(*v1alpha1.File) + if !ok { + return managed.ExternalObservation{}, errors.New(errNotFile) + } + + externalName := meta.GetExternalName(cr) + if externalName == "" { + return managed.ExternalObservation{}, nil + } + + if cr.Spec.ForProvider.ProjectID == nil { + return managed.ExternalObservation{}, errors.New(errProjectIDMissing) + } + + file, res, err := e.fileClient.GetFile( + *cr.Spec.ForProvider.ProjectID, + *cr.Spec.ForProvider.FilePath, + projects.GenerateGetFileOptions(&cr.Spec.ForProvider), + gitlab.WithContext(ctx)) + + if err != nil { + if clients.IsResponseNotFound(res) { + return managed.ExternalObservation{}, nil + } + return managed.ExternalObservation{}, errors.Wrap(err, errGetFailed) + } + + commit, _, _ := e.commitClient.GetCommit( + *cr.Spec.ForProvider.ProjectID, + file.CommitID, + projects.GenerateGetCommitOptions(), + gitlab.WithContext(ctx)) + + current := cr.Spec.ForProvider.DeepCopy() + projects.LateInitializeFile(&cr.Spec.ForProvider, file, commit) + + cr.Status.AtProvider = projects.GenerateFileObservation(file) + cr.Status.SetConditions(xpv1.Available()) + + return managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: projects.IsFileUpToDate(&cr.Spec.ForProvider, file), + ResourceLateInitialized: !cmp.Equal(current, &cr.Spec.ForProvider), + }, nil +} + +func (e *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) { + cr, ok := mg.(*v1alpha1.File) + if !ok { + return managed.ExternalCreation{}, errors.New(errNotFile) + } + + if cr.Spec.ForProvider.ProjectID == nil { + return managed.ExternalCreation{}, errors.New(errProjectIDMissing) + } + + cr.Status.SetConditions(xpv1.Creating()) + file, _, err := e.fileClient.CreateFile( + *cr.Spec.ForProvider.ProjectID, + *cr.Spec.ForProvider.FilePath, + projects.GenerateCreateFileOptions(&cr.Spec.ForProvider), + gitlab.WithContext(ctx)) + + if err != nil { + return managed.ExternalCreation{}, errors.Wrap(err, errCreateFailed) + } + + meta.SetExternalName(cr, file.FilePath) + return managed.ExternalCreation{}, nil +} + +func (e *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { + cr, ok := mg.(*v1alpha1.File) + if !ok { + return managed.ExternalUpdate{}, errors.New(errNotFile) + } + + if cr.Spec.ForProvider.ProjectID == nil { + return managed.ExternalUpdate{}, errors.New(errProjectIDMissing) + } + + _, _, err := e.fileClient.UpdateFile( + *cr.Spec.ForProvider.ProjectID, + *cr.Spec.ForProvider.FilePath, + projects.GenerateUpdateFileOptions(&cr.Spec.ForProvider), + gitlab.WithContext(ctx), + ) + return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateFailed) +} + +func (e *external) Delete(ctx context.Context, mg resource.Managed) (managed.ExternalDelete, error) { + cr, ok := mg.(*v1alpha1.File) + if !ok { + return managed.ExternalDelete{}, errors.New(errNotFile) + } + + if cr.Spec.ForProvider.ProjectID == nil { + return managed.ExternalDelete{}, errors.New(errProjectIDMissing) + } + + cr.Status.SetConditions(xpv1.Deleting()) + _, err := e.fileClient.DeleteFile( + *cr.Spec.ForProvider.ProjectID, + *cr.Spec.ForProvider.FilePath, + projects.GenerateDeleteFileOptions(&cr.Spec.ForProvider), + gitlab.WithContext(ctx), + ) + return managed.ExternalDelete{}, errors.Wrap(err, errDeleteFailed) +} + +func (e *external) Disconnect(ctx context.Context) error { + // Disconnect is not implemented as it is a new method required by the SDK + return nil +} diff --git a/pkg/namespaced/controller/projects/setup.go b/pkg/namespaced/controller/projects/setup.go index 589145a6..11a5f72d 100644 --- a/pkg/namespaced/controller/projects/setup.go +++ b/pkg/namespaced/controller/projects/setup.go @@ -24,6 +24,7 @@ import ( "github.com/crossplane-contrib/provider-gitlab/pkg/namespaced/controller/projects/approvalrules" "github.com/crossplane-contrib/provider-gitlab/pkg/namespaced/controller/projects/deploykeys" "github.com/crossplane-contrib/provider-gitlab/pkg/namespaced/controller/projects/deploytokens" + "github.com/crossplane-contrib/provider-gitlab/pkg/namespaced/controller/projects/files" "github.com/crossplane-contrib/provider-gitlab/pkg/namespaced/controller/projects/hooks" "github.com/crossplane-contrib/provider-gitlab/pkg/namespaced/controller/projects/members" "github.com/crossplane-contrib/provider-gitlab/pkg/namespaced/controller/projects/pipelineschedules" @@ -47,6 +48,7 @@ func Setup(mgr ctrl.Manager, o controller.Options) error { approvalrules.SetupRules, runners.SetupRunner, protectedbranches.SetupProtectedBranch, + files.SetupFile, } { if err := setup(mgr, o); err != nil { return err @@ -70,6 +72,7 @@ func SetupGated(mgr ctrl.Manager, o controller.Options) error { approvalrules.SetupRulesGated, runners.SetupRunnerGated, protectedbranches.SetupProtectedBranchGated, + files.SetupFileGated, } { if err := setup(mgr, o); err != nil { return err