diff --git a/apis/cluster/projects/v1alpha1/zz_generated.deepcopy.go b/apis/cluster/projects/v1alpha1/zz_generated.deepcopy.go index aab0c96f..359f1b0a 100644 --- a/apis/cluster/projects/v1alpha1/zz_generated.deepcopy.go +++ b/apis/cluster/projects/v1alpha1/zz_generated.deepcopy.go @@ -3536,3 +3536,152 @@ func (in *VariableStatus) DeepCopy() *VariableStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IntegrationGithub) DeepCopyInto(out *IntegrationGithub) { + *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 IntegrationGithub. +func (in *IntegrationGithub) DeepCopy() *IntegrationGithub { + if in == nil { + return nil + } + out := new(IntegrationGithub) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IntegrationGithub) 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 *IntegrationGithubList) DeepCopyInto(out *IntegrationGithubList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]IntegrationGithub, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IntegrationGithubList. +func (in *IntegrationGithubList) DeepCopy() *IntegrationGithubList { + if in == nil { + return nil + } + out := new(IntegrationGithubList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IntegrationGithubList) 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 *IntegrationGithubObservation) DeepCopyInto(out *IntegrationGithubObservation) { + *out = *in + in.CommonIntegrationObservation.DeepCopyInto(&out.CommonIntegrationObservation) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IntegrationGithubObservation. +func (in *IntegrationGithubObservation) DeepCopy() *IntegrationGithubObservation { + if in == nil { + return nil + } + out := new(IntegrationGithubObservation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IntegrationGithubParameters) DeepCopyInto(out *IntegrationGithubParameters) { + *out = *in + if in.ProjectID != nil { + in, out := &in.ProjectID, &out.ProjectID + *out = new(int64) + **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.RepositoryURL != nil { + in, out := &in.RepositoryURL, &out.RepositoryURL + *out = new(string) + **out = **in + } + if in.StaticContext != nil { + in, out := &in.StaticContext, &out.StaticContext + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IntegrationGithubParameters. +func (in *IntegrationGithubParameters) DeepCopy() *IntegrationGithubParameters { + if in == nil { + return nil + } + out := new(IntegrationGithubParameters) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IntegrationGithubSpec) DeepCopyInto(out *IntegrationGithubSpec) { + *out = *in + in.ResourceSpec.DeepCopyInto(&out.ResourceSpec) + in.ForProvider.DeepCopyInto(&out.ForProvider) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IntegrationGithubSpec. +func (in *IntegrationGithubSpec) DeepCopy() *IntegrationGithubSpec { + if in == nil { + return nil + } + out := new(IntegrationGithubSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IntegrationGithubStatus) DeepCopyInto(out *IntegrationGithubStatus) { + *out = *in + in.ResourceStatus.DeepCopyInto(&out.ResourceStatus) + in.AtProvider.DeepCopyInto(&out.AtProvider) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IntegrationGithubStatus. +func (in *IntegrationGithubStatus) DeepCopy() *IntegrationGithubStatus { + if in == nil { + return nil + } + out := new(IntegrationGithubStatus) + in.DeepCopyInto(out) + return out +} diff --git a/apis/cluster/projects/v1alpha1/zz_generated.managed.go b/apis/cluster/projects/v1alpha1/zz_generated.managed.go index 6faf2753..b411f89a 100644 --- a/apis/cluster/projects/v1alpha1/zz_generated.managed.go +++ b/apis/cluster/projects/v1alpha1/zz_generated.managed.go @@ -719,3 +719,53 @@ func (mg *Variable) SetProviderConfigReference(r *xpv1.Reference) { func (mg *Variable) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { mg.Spec.WriteConnectionSecretToReference = r } + +// GetCondition of this IntegrationGithub. +func (mg *IntegrationGithub) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetDeletionPolicy of this IntegrationGithub. +func (mg *IntegrationGithub) GetDeletionPolicy() xpv1.DeletionPolicy { + return mg.Spec.DeletionPolicy +} + +// GetManagementPolicies of this IntegrationGithub. +func (mg *IntegrationGithub) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this IntegrationGithub. +func (mg *IntegrationGithub) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +// GetWriteConnectionSecretToReference of this IntegrationGithub. +func (mg *IntegrationGithub) GetWriteConnectionSecretToReference() *xpv1.SecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this IntegrationGithub. +func (mg *IntegrationGithub) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetDeletionPolicy of this IntegrationGithub. +func (mg *IntegrationGithub) SetDeletionPolicy(r xpv1.DeletionPolicy) { + mg.Spec.DeletionPolicy = r +} + +// SetManagementPolicies of this IntegrationGithub. +func (mg *IntegrationGithub) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this IntegrationGithub. +func (mg *IntegrationGithub) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +// SetWriteConnectionSecretToReference of this IntegrationGithub. +func (mg *IntegrationGithub) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} diff --git a/apis/cluster/projects/v1alpha1/zz_generated.managedlist.go b/apis/cluster/projects/v1alpha1/zz_generated.managedlist.go index cb624c35..a320c80a 100644 --- a/apis/cluster/projects/v1alpha1/zz_generated.managedlist.go +++ b/apis/cluster/projects/v1alpha1/zz_generated.managedlist.go @@ -83,6 +83,15 @@ func (l *IntegrationMattermostList) GetItems() []resource.Managed { return items } +// GetItems of this IntegrationGithubList. +func (l *IntegrationGithubList) 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 MemberList. func (l *MemberList) GetItems() []resource.Managed { items := make([]resource.Managed, len(l.Items)) diff --git a/apis/cluster/projects/v1alpha1/zz_integration_github_types.go b/apis/cluster/projects/v1alpha1/zz_integration_github_types.go new file mode 100644 index 00000000..737890a6 --- /dev/null +++ b/apis/cluster/projects/v1alpha1/zz_integration_github_types.go @@ -0,0 +1,105 @@ +/* +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" + + "github.com/crossplane-contrib/provider-gitlab/apis/common/v1alpha1" +) + +// IntegrationGithubParameters defines the desired state of a GitLab Project GitHub Integration. +type IntegrationGithubParameters struct { + // ProjectID is the ID of the project. + // +optional + // +immutable + ProjectID *int64 `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 + ProjectIDSelector *xpv1.Selector `json:"projectIdSelector,omitempty"` + + // GitHub authentication token for status updates. + // WARNING: This field is NOT reconciled as the GitLab API does not return it as it is a write-only field. + // +required + Token string `json:"token,omitempty"` + + // The URL of the GitHub repository (for example, https://github.com/gitlabhq/gitlabhq). + // +optional + RepositoryURL *string `json:"repositoryUrl,omitempty"` + + // Append the pipeline description to the commit status message. + // +optional + StaticContext *bool `json:"staticContext,omitempty"` +} + +// IntegrationGithubObservation represents the observed state of a GitLab Project GitHub Integration. +type IntegrationGithubObservation struct { + v1alpha1.CommonIntegrationObservation `json:",inline"` + // The URL of the GitHub repository. + // This field is not returned by GitLab API for security reasons. So it will always be empty in the observation. + RepositoryURL string `json:"repositoryUrl"` + // Whether the pipeline description is appended to the commit status message. + StaticContext bool `json:"staticContext"` +} + +// A IntegrationGithubSpec defines the desired state of a GitLab Project GitHub Integration. +type IntegrationGithubSpec struct { + xpv1.ResourceSpec `json:",inline"` + // ForProvider represents the desired state of the GitHub integration + ForProvider IntegrationGithubParameters `json:"forProvider"` +} + +// A IntegrationGithubStatus represents the observed state of a GitLab Project GitHub Integration. +type IntegrationGithubStatus struct { + xpv1.ResourceStatus `json:",inline"` + // AtProvider represents the observed state of the GitHub integration + AtProvider IntegrationGithubObservation `json:"atProvider,omitempty"` +} + +// +kubebuilder:object:root=true + +// A IntegrationGithub is a managed resource that represents a GitLab Project GitHub Integration +// +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",type="string",JSONPath=".spec.forProvider.projectId" +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster,categories={crossplane,managed,gitlab} +type IntegrationGithub struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec IntegrationGithubSpec `json:"spec"` + Status IntegrationGithubStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// IntegrationGithubList contains a list of IntegrationGithub items +type IntegrationGithubList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []IntegrationGithub `json:"items"` +} diff --git a/apis/cluster/projects/v1alpha1/zz_referencers.go b/apis/cluster/projects/v1alpha1/zz_referencers.go index 1e8694be..0b46995e 100644 --- a/apis/cluster/projects/v1alpha1/zz_referencers.go +++ b/apis/cluster/projects/v1alpha1/zz_referencers.go @@ -301,3 +301,30 @@ func (mg *IntegrationMattermost) ResolveReferences(ctx context.Context, c client return nil } + +// ResolveReferences of this IntegrationGithub +func (mg *IntegrationGithub) 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") + } + + resolvedID, err := toPtrValue(rsp.ResolvedValue) + if err != nil { + return errors.Wrap(err, "spec.forProvider") + } + mg.Spec.ForProvider.ProjectID = resolvedID + 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 0f1c2002..bbeae12b 100644 --- a/apis/cluster/projects/v1alpha1/zz_register.go +++ b/apis/cluster/projects/v1alpha1/zz_register.go @@ -150,6 +150,14 @@ var ( IntegrationMattermostGroupVersionKind = SchemeGroupVersion.WithKind(IntegrationMattermostKind) ) +// IntegrationGithub type metadata +var ( + IntegrationGithubKind = reflect.TypeOf(IntegrationGithub{}).Name() + IntegrationGithubGroupKind = schema.GroupKind{Group: Group, Kind: IntegrationGithubKind}.String() + IntegrationGithubKindAPIVersion = IntegrationGithubKind + "." + SchemeGroupVersion.String() + IntegrationGithubGroupVersionKind = SchemeGroupVersion.WithKind(IntegrationGithubKind) +) + func init() { SchemeBuilder.Register(&Project{}, &ProjectList{}) SchemeBuilder.Register(&Hook{}, &HookList{}) @@ -167,4 +175,7 @@ func init() { // Mattermost SchemeBuilder.Register(&IntegrationMattermost{}, &IntegrationMattermostList{}) + + // GitHub + SchemeBuilder.Register(&IntegrationGithub{}, &IntegrationGithubList{}) } diff --git a/apis/namespaced/projects/v1alpha1/integration_github_types.go b/apis/namespaced/projects/v1alpha1/integration_github_types.go new file mode 100644 index 00000000..d14d7481 --- /dev/null +++ b/apis/namespaced/projects/v1alpha1/integration_github_types.go @@ -0,0 +1,105 @@ +/* +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" + + "github.com/crossplane-contrib/provider-gitlab/apis/common/v1alpha1" +) + +// IntegrationGithubParameters defines the desired state of a GitLab Project GitHub Integration. +type IntegrationGithubParameters struct { + // ProjectID is the ID of the project. + // +optional + // +immutable + ProjectID *int64 `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 + ProjectIDSelector *xpv1.NamespacedSelector `json:"projectIdSelector,omitempty"` + + // GitHub authentication token for status updates. + // WARNING: This field is NOT reconciled as the GitLab API does not return it as it is a write-only field. + // +required + Token string `json:"token,omitempty"` + + // The URL of the GitHub repository (for example, https://github.com/gitlabhq/gitlabhq). + // +optional + RepositoryURL *string `json:"repositoryUrl,omitempty"` + + // Append the pipeline description to the commit status message. + // +optional + StaticContext *bool `json:"staticContext,omitempty"` +} + +// IntegrationGithubObservation represents the observed state of a GitLab Project GitHub Integration. +type IntegrationGithubObservation struct { + v1alpha1.CommonIntegrationObservation `json:",inline"` + // The URL of the GitHub repository. + // This field is not returned by GitLab API for security reasons. So it will always be empty in the observation. + RepositoryURL string `json:"repositoryUrl"` + // Whether the pipeline description is appended to the commit status message. + StaticContext bool `json:"staticContext"` +} + +// A IntegrationGithubSpec defines the desired state of a GitLab Project GitHub Integration. +type IntegrationGithubSpec struct { + xpv2.ManagedResourceSpec `json:",inline"` + // ForProvider represents the desired state of the GitHub integration + ForProvider IntegrationGithubParameters `json:"forProvider"` +} + +// A IntegrationGithubStatus represents the observed state of a GitLab Project GitHub Integration. +type IntegrationGithubStatus struct { + xpv1.ResourceStatus `json:",inline"` + // AtProvider represents the observed state of the GitHub integration + AtProvider IntegrationGithubObservation `json:"atProvider,omitempty"` +} + +// +kubebuilder:object:root=true + +// A IntegrationGithub is a managed resource that represents a GitLab Project GitHub Integration +// +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",type="string",JSONPath=".spec.forProvider.projectId" +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,categories={crossplane,managed,gitlab} +type IntegrationGithub struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec IntegrationGithubSpec `json:"spec"` + Status IntegrationGithubStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// IntegrationGithubList contains a list of IntegrationGithub items +type IntegrationGithubList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []IntegrationGithub `json:"items"` +} diff --git a/apis/namespaced/projects/v1alpha1/referencers.go b/apis/namespaced/projects/v1alpha1/referencers.go index 9b9b1646..8900a476 100644 --- a/apis/namespaced/projects/v1alpha1/referencers.go +++ b/apis/namespaced/projects/v1alpha1/referencers.go @@ -299,3 +299,30 @@ func (mg *IntegrationMattermost) ResolveReferences(ctx context.Context, c client return nil } + +// ResolveReferences of this IntegrationGithub +func (mg *IntegrationGithub) 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") + } + + resolvedID, err := toPtrValue(rsp.ResolvedValue) + if err != nil { + return errors.Wrap(err, "spec.forProvider") + } + mg.Spec.ForProvider.ProjectID = resolvedID + 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 4e36ad42..eb6d23ed 100644 --- a/apis/namespaced/projects/v1alpha1/register.go +++ b/apis/namespaced/projects/v1alpha1/register.go @@ -148,6 +148,14 @@ var ( IntegrationMattermostGroupVersionKind = SchemeGroupVersion.WithKind(IntegrationMattermostKind) ) +// IntegrationGithub type metadata +var ( + IntegrationGithubKind = reflect.TypeOf(IntegrationGithub{}).Name() + IntegrationGithubGroupKind = schema.GroupKind{Group: Group, Kind: IntegrationGithubKind}.String() + IntegrationGithubKindAPIVersion = IntegrationGithubKind + "." + SchemeGroupVersion.String() + IntegrationGithubGroupVersionKind = SchemeGroupVersion.WithKind(IntegrationGithubKind) +) + func init() { SchemeBuilder.Register(&Project{}, &ProjectList{}) SchemeBuilder.Register(&Hook{}, &HookList{}) @@ -165,4 +173,7 @@ func init() { // Mattermost SchemeBuilder.Register(&IntegrationMattermost{}, &IntegrationMattermostList{}) + + // GitHub + SchemeBuilder.Register(&IntegrationGithub{}, &IntegrationGithubList{}) } diff --git a/apis/namespaced/projects/v1alpha1/zz_generated.deepcopy.go b/apis/namespaced/projects/v1alpha1/zz_generated.deepcopy.go index 0f844fd4..f02c783b 100644 --- a/apis/namespaced/projects/v1alpha1/zz_generated.deepcopy.go +++ b/apis/namespaced/projects/v1alpha1/zz_generated.deepcopy.go @@ -3536,3 +3536,152 @@ func (in *VariableStatus) DeepCopy() *VariableStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IntegrationGithub) DeepCopyInto(out *IntegrationGithub) { + *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 IntegrationGithub. +func (in *IntegrationGithub) DeepCopy() *IntegrationGithub { + if in == nil { + return nil + } + out := new(IntegrationGithub) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IntegrationGithub) 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 *IntegrationGithubList) DeepCopyInto(out *IntegrationGithubList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]IntegrationGithub, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IntegrationGithubList. +func (in *IntegrationGithubList) DeepCopy() *IntegrationGithubList { + if in == nil { + return nil + } + out := new(IntegrationGithubList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IntegrationGithubList) 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 *IntegrationGithubObservation) DeepCopyInto(out *IntegrationGithubObservation) { + *out = *in + in.CommonIntegrationObservation.DeepCopyInto(&out.CommonIntegrationObservation) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IntegrationGithubObservation. +func (in *IntegrationGithubObservation) DeepCopy() *IntegrationGithubObservation { + if in == nil { + return nil + } + out := new(IntegrationGithubObservation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IntegrationGithubParameters) DeepCopyInto(out *IntegrationGithubParameters) { + *out = *in + if in.ProjectID != nil { + in, out := &in.ProjectID, &out.ProjectID + *out = new(int64) + **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.RepositoryURL != nil { + in, out := &in.RepositoryURL, &out.RepositoryURL + *out = new(string) + **out = **in + } + if in.StaticContext != nil { + in, out := &in.StaticContext, &out.StaticContext + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IntegrationGithubParameters. +func (in *IntegrationGithubParameters) DeepCopy() *IntegrationGithubParameters { + if in == nil { + return nil + } + out := new(IntegrationGithubParameters) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IntegrationGithubSpec) DeepCopyInto(out *IntegrationGithubSpec) { + *out = *in + in.ManagedResourceSpec.DeepCopyInto(&out.ManagedResourceSpec) + in.ForProvider.DeepCopyInto(&out.ForProvider) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IntegrationGithubSpec. +func (in *IntegrationGithubSpec) DeepCopy() *IntegrationGithubSpec { + if in == nil { + return nil + } + out := new(IntegrationGithubSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IntegrationGithubStatus) DeepCopyInto(out *IntegrationGithubStatus) { + *out = *in + in.ResourceStatus.DeepCopyInto(&out.ResourceStatus) + in.AtProvider.DeepCopyInto(&out.AtProvider) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IntegrationGithubStatus. +func (in *IntegrationGithubStatus) DeepCopy() *IntegrationGithubStatus { + if in == nil { + return nil + } + out := new(IntegrationGithubStatus) + in.DeepCopyInto(out) + return out +} diff --git a/apis/namespaced/projects/v1alpha1/zz_generated.managed.go b/apis/namespaced/projects/v1alpha1/zz_generated.managed.go index 7b922952..b41ba591 100644 --- a/apis/namespaced/projects/v1alpha1/zz_generated.managed.go +++ b/apis/namespaced/projects/v1alpha1/zz_generated.managed.go @@ -579,3 +579,43 @@ func (mg *Variable) SetProviderConfigReference(r *xpv1.ProviderConfigReference) func (mg *Variable) SetWriteConnectionSecretToReference(r *xpv1.LocalSecretReference) { mg.Spec.WriteConnectionSecretToReference = r } + +// GetCondition of this IntegrationGithub. +func (mg *IntegrationGithub) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetManagementPolicies of this IntegrationGithub. +func (mg *IntegrationGithub) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this IntegrationGithub. +func (mg *IntegrationGithub) GetProviderConfigReference() *xpv1.ProviderConfigReference { + return mg.Spec.ProviderConfigReference +} + +// GetWriteConnectionSecretToReference of this IntegrationGithub. +func (mg *IntegrationGithub) GetWriteConnectionSecretToReference() *xpv1.LocalSecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this IntegrationGithub. +func (mg *IntegrationGithub) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetManagementPolicies of this IntegrationGithub. +func (mg *IntegrationGithub) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this IntegrationGithub. +func (mg *IntegrationGithub) SetProviderConfigReference(r *xpv1.ProviderConfigReference) { + mg.Spec.ProviderConfigReference = r +} + +// SetWriteConnectionSecretToReference of this IntegrationGithub. +func (mg *IntegrationGithub) SetWriteConnectionSecretToReference(r *xpv1.LocalSecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} diff --git a/apis/namespaced/projects/v1alpha1/zz_generated.managedlist.go b/apis/namespaced/projects/v1alpha1/zz_generated.managedlist.go index cb624c35..a320c80a 100644 --- a/apis/namespaced/projects/v1alpha1/zz_generated.managedlist.go +++ b/apis/namespaced/projects/v1alpha1/zz_generated.managedlist.go @@ -83,6 +83,15 @@ func (l *IntegrationMattermostList) GetItems() []resource.Managed { return items } +// GetItems of this IntegrationGithubList. +func (l *IntegrationGithubList) 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 MemberList. func (l *MemberList) GetItems() []resource.Managed { items := make([]resource.Managed, len(l.Items)) diff --git a/examples/projects/integration_github.yaml b/examples/projects/integration_github.yaml new file mode 100644 index 00000000..45498b03 --- /dev/null +++ b/examples/projects/integration_github.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: projects.gitlab.m.crossplane.io/v1alpha1 +kind: IntegrationGithub +metadata: + name: github-p7 + namespace: default +spec: + providerConfigRef: + name: gitlab-provider + kind: ProviderConfig + forProvider: + projectId: 14 + token: "ghp_yourgithubtoken" + repositoryUrl: "https://github.com/example/repo" + staticContext: true diff --git a/pkg/cluster/clients/projects/fake/zz_fake.go b/pkg/cluster/clients/projects/fake/zz_fake.go index 99bfc88c..b225da45 100644 --- a/pkg/cluster/clients/projects/fake/zz_fake.go +++ b/pkg/cluster/clients/projects/fake/zz_fake.go @@ -90,6 +90,10 @@ type MockClient struct { MockGetMattermostService func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.MattermostService, *gitlab.Response, error) MockSetMattermostService func(pid any, opt *gitlab.SetMattermostServiceOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MattermostService, *gitlab.Response, error) MockDeleteMattermostService func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) + + MockGetGithubService func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) + MockSetGithubService func(pid any, opt *gitlab.SetGithubServiceOptions, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) + MockDeleteGithubService func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) } // GetPipelineSchedule calls the underlying MockGetPipelineSchedule method. @@ -332,3 +336,18 @@ func (c *MockClient) SetMattermostService(pid any, opt *gitlab.SetMattermostServ func (c *MockClient) DeleteMattermostService(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) { return c.MockDeleteMattermostService(pid, options...) } + +// GetGithubService calls the underlying MockGetGithubService method. +func (c *MockClient) GetGithubService(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return c.MockGetGithubService(pid, options...) +} + +// SetGithubService calls the underlying MockSetGithubService method. +func (c *MockClient) SetGithubService(pid any, opt *gitlab.SetGithubServiceOptions, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return c.MockSetGithubService(pid, opt, options...) +} + +// DeleteGithubService calls the underlying MockDeleteGithubService method. +func (c *MockClient) DeleteGithubService(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) { + return c.MockDeleteGithubService(pid, options...) +} diff --git a/pkg/cluster/clients/projects/zz_integration_github.go b/pkg/cluster/clients/projects/zz_integration_github.go new file mode 100644 index 00000000..eada7185 --- /dev/null +++ b/pkg/cluster/clients/projects/zz_integration_github.go @@ -0,0 +1,100 @@ +/* +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 ( + gitlab "gitlab.com/gitlab-org/api/client-go" + + "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" +) + +// GithubClient defines GitLab GitHub integration operations. +type GithubClient interface { + GetGithubService(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) + SetGithubService(pid any, opt *gitlab.SetGithubServiceOptions, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) + DeleteGithubService(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) +} + +// NewGithubClient returns a new GitLab Services client. +func NewGithubClient(cfg common.Config) GithubClient { + git := common.NewClient(cfg) + return git.Services +} + +// GenerateSetGithubServiceOptions produces SetGithubServiceOptions from IntegrationGithubParameters. +func GenerateSetGithubServiceOptions(in *v1alpha1.IntegrationGithubParameters) *gitlab.SetGithubServiceOptions { + if in == nil { + return &gitlab.SetGithubServiceOptions{} + } + + opts := gitlab.SetGithubServiceOptions{ + RepositoryURL: in.RepositoryURL, + StaticContext: in.StaticContext, + } + + // Token is write-only in GitLab and typically required when initially setting the service. + // Only include it when provided to avoid unintentionally clearing remote configuration. + if in.Token != "" { + opts.Token = &in.Token + } + + return &opts +} + +// GenerateIntegrationGithubObservation converts gitlab.GithubService to IntegrationGithubObservation. +func GenerateIntegrationGithubObservation(observation *gitlab.GithubService) v1alpha1.IntegrationGithubObservation { + if observation == nil || observation.Properties == nil { + return v1alpha1.IntegrationGithubObservation{} + } + + commonObservation := common.GenerateCommonIntegrationObservation(&observation.Service) + + return v1alpha1.IntegrationGithubObservation{ + CommonIntegrationObservation: commonObservation, + RepositoryURL: observation.Properties.RepositoryURL, + StaticContext: observation.Properties.StaticContext, + } +} + +// IsIntegrationGithubUpToDate returns true if spec matches the observed GitLab GitHub service. +// +// Note: Token is intentionally excluded from comparison because GitLab does not return it (write-only). +func IsIntegrationGithubUpToDate(spec *v1alpha1.IntegrationGithubParameters, observation *gitlab.GithubService) bool { + if observation == nil || observation.Properties == nil { + return false + } + + return clients.IsComparableEqualToComparablePtr(spec.RepositoryURL, observation.Properties.RepositoryURL) && + clients.IsComparableEqualToComparablePtr(spec.StaticContext, observation.Properties.StaticContext) +} + +// LateInitializeIntegrationGithub fills nil spec fields using values from the remote GitHub service. +// It mutates the spec in place and does NOT touch write-only fields like Token. +func LateInitializeIntegrationGithub(in *v1alpha1.IntegrationGithubParameters, svc *gitlab.GithubService) { + if in == nil || svc == nil || svc.Properties == nil { + return + } + + in.RepositoryURL = clients.LateInitializeStringPtr(in.RepositoryURL, svc.Properties.RepositoryURL) + in.StaticContext = clients.LateInitializeFromValue(in.StaticContext, svc.Properties.StaticContext) + + // Token is write-only; do NOT late-initialize it from observation. +} diff --git a/pkg/cluster/clients/projects/zz_integration_github_test.go b/pkg/cluster/clients/projects/zz_integration_github_test.go new file mode 100644 index 00000000..8a74d880 --- /dev/null +++ b/pkg/cluster/clients/projects/zz_integration_github_test.go @@ -0,0 +1,303 @@ +// Code generated by hack/generate-cluster-scope.go - DO NOT EDIT. + +package projects + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + gitlab "gitlab.com/gitlab-org/api/client-go" + "k8s.io/utils/ptr" + + projectsv1alpha1 "github.com/crossplane-contrib/provider-gitlab/apis/cluster/projects/v1alpha1" + "github.com/crossplane-contrib/provider-gitlab/apis/common/v1alpha1" + "github.com/crossplane-contrib/provider-gitlab/pkg/cluster/clients" +) + +var ( + testGithubProjectID int64 = 123 + testGithubToken = "ghp_secrettoken" + testGithubRepositoryURL = "https://github.com/example/repo" + testGithubStaticContext = true + + testGithubID int64 = 456 + testGithubTitle = "GitHub" + testGithubSlug = "github" + testGithubActive = true + testGithubCreatedAt = time.Now() + testGithubUpdatedAt = time.Now() +) + +// TestGenerateSetGithubServiceOptions tests the conversion from +// IntegrationGithubParameters to GitLab SetGithubServiceOptions +func TestGenerateSetGithubServiceOptions(t *testing.T) { + type args struct { + parameters *projectsv1alpha1.IntegrationGithubParameters + } + cases := map[string]struct { + args args + want *gitlab.SetGithubServiceOptions + }{ + "AllFieldsSet": { + args: args{ + parameters: &projectsv1alpha1.IntegrationGithubParameters{ + ProjectID: &testGithubProjectID, + Token: testGithubToken, + RepositoryURL: &testGithubRepositoryURL, + StaticContext: &testGithubStaticContext, + }, + }, + want: &gitlab.SetGithubServiceOptions{ + Token: &testGithubToken, + RepositoryURL: &testGithubRepositoryURL, + StaticContext: &testGithubStaticContext, + }, + }, + "OnlyRequiredFields": { + args: args{ + parameters: &projectsv1alpha1.IntegrationGithubParameters{ + ProjectID: &testGithubProjectID, + Token: testGithubToken, + }, + }, + want: &gitlab.SetGithubServiceOptions{ + Token: &testGithubToken, + }, + }, + "EmptyTokenNotIncluded": { + args: args{ + parameters: &projectsv1alpha1.IntegrationGithubParameters{ + ProjectID: &testGithubProjectID, + Token: "", + }, + }, + want: &gitlab.SetGithubServiceOptions{}, + }, + "NilInput": { + args: args{ + parameters: nil, + }, + want: &gitlab.SetGithubServiceOptions{}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := GenerateSetGithubServiceOptions(tc.args.parameters) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("GenerateSetGithubServiceOptions(): -want, +got:\n%s", diff) + } + }) + } +} + +// TestGenerateIntegrationGithubObservation tests the conversion from +// GitLab GithubService to IntegrationGithubObservation +func TestGenerateIntegrationGithubObservation(t *testing.T) { + type args struct { + service *gitlab.GithubService + } + cases := map[string]struct { + args args + want projectsv1alpha1.IntegrationGithubObservation + }{ + "FullObservation": { + args: args{ + service: &gitlab.GithubService{ + Service: gitlab.Service{ + ID: testGithubID, + Title: testGithubTitle, + Slug: testGithubSlug, + CreatedAt: &testGithubCreatedAt, + UpdatedAt: &testGithubUpdatedAt, + Active: testGithubActive, + }, + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testGithubRepositoryURL, + StaticContext: testGithubStaticContext, + }, + }, + }, + want: projectsv1alpha1.IntegrationGithubObservation{ + CommonIntegrationObservation: v1alpha1.CommonIntegrationObservation{ + ID: ptr.To(testGithubID), + Title: ptr.To(testGithubTitle), + Slug: ptr.To(testGithubSlug), + CreatedAt: clients.TimeToMetaTime(&testGithubCreatedAt), + UpdatedAt: clients.TimeToMetaTime(&testGithubUpdatedAt), + Active: ptr.To(true), + AlertEvents: ptr.To(false), + CommitEvents: ptr.To(false), + ConfidentialIssuesEvents: ptr.To(false), + ConfidentialNoteEvents: ptr.To(false), + DeploymentEvents: ptr.To(false), + GroupConfidentialMentionEvents: ptr.To(false), + GroupMentionEvents: ptr.To(false), + IncidentEvents: ptr.To(false), + IssuesEvents: ptr.To(false), + JobEvents: ptr.To(false), + MergeRequestsEvents: ptr.To(false), + NoteEvents: ptr.To(false), + PipelineEvents: ptr.To(false), + PushEvents: ptr.To(false), + TagPushEvents: ptr.To(false), + VulnerabilityEvents: ptr.To(false), + WikiPageEvents: ptr.To(false), + CommentOnEventEnabled: ptr.To(false), + Inherited: ptr.To(false), + }, + RepositoryURL: testGithubRepositoryURL, + StaticContext: testGithubStaticContext, + }, + }, + "MinimalObservation": { + args: args{ + service: &gitlab.GithubService{ + Service: gitlab.Service{ + ID: testGithubID, + Title: testGithubTitle, + }, + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testGithubRepositoryURL, + }, + }, + }, + want: projectsv1alpha1.IntegrationGithubObservation{ + CommonIntegrationObservation: v1alpha1.CommonIntegrationObservation{ + ID: ptr.To(testGithubID), + Title: ptr.To(testGithubTitle), + Slug: ptr.To(""), + CreatedAt: nil, + UpdatedAt: nil, + Active: ptr.To(false), + AlertEvents: ptr.To(false), + CommitEvents: ptr.To(false), + ConfidentialIssuesEvents: ptr.To(false), + ConfidentialNoteEvents: ptr.To(false), + DeploymentEvents: ptr.To(false), + GroupConfidentialMentionEvents: ptr.To(false), + GroupMentionEvents: ptr.To(false), + IncidentEvents: ptr.To(false), + IssuesEvents: ptr.To(false), + JobEvents: ptr.To(false), + MergeRequestsEvents: ptr.To(false), + NoteEvents: ptr.To(false), + PipelineEvents: ptr.To(false), + PushEvents: ptr.To(false), + TagPushEvents: ptr.To(false), + VulnerabilityEvents: ptr.To(false), + WikiPageEvents: ptr.To(false), + CommentOnEventEnabled: ptr.To(false), + Inherited: ptr.To(false), + }, + RepositoryURL: testGithubRepositoryURL, + }, + }, + "NilObservation": { + args: args{ + service: nil, + }, + want: projectsv1alpha1.IntegrationGithubObservation{}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := GenerateIntegrationGithubObservation(tc.args.service) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("GenerateIntegrationGithubObservation(): -want, +got:\n%s", diff) + } + }) + } +} + +// TestIsIntegrationGithubUpToDate tests whether the spec matches the observation +func TestIsIntegrationGithubUpToDate(t *testing.T) { + type args struct { + spec *projectsv1alpha1.IntegrationGithubParameters + observation *gitlab.GithubService + } + cases := map[string]struct { + args args + want bool + }{ + "UpToDate": { + args: args{ + spec: &projectsv1alpha1.IntegrationGithubParameters{ + ProjectID: &testGithubProjectID, + Token: testGithubToken, + RepositoryURL: &testGithubRepositoryURL, + StaticContext: &testGithubStaticContext, + }, + observation: &gitlab.GithubService{ + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testGithubRepositoryURL, + StaticContext: testGithubStaticContext, + }, + }, + }, + want: true, + }, + "DifferentTokenIsIgnored": { + args: args{ + spec: &projectsv1alpha1.IntegrationGithubParameters{ + Token: "different-token", + RepositoryURL: &testGithubRepositoryURL, + StaticContext: &testGithubStaticContext, + }, + observation: &gitlab.GithubService{ + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testGithubRepositoryURL, + StaticContext: testGithubStaticContext, + }, + }, + }, + want: true, + }, + "OutOfDateRepositoryURL": { + args: args{ + spec: &projectsv1alpha1.IntegrationGithubParameters{ + Token: testGithubToken, + RepositoryURL: func() *string { s := "https://github.com/other/repo"; return &s }(), + }, + observation: &gitlab.GithubService{ + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testGithubRepositoryURL, + }, + }, + }, + want: false, + }, + "OutOfDateStaticContext": { + args: args{ + spec: &projectsv1alpha1.IntegrationGithubParameters{ + Token: testGithubToken, + StaticContext: func() *bool { b := false; return &b }(), + }, + observation: &gitlab.GithubService{ + Properties: &gitlab.GithubServiceProperties{ + StaticContext: true, + }, + }, + }, + want: false, + }, + "NilObservationReturnsFalse": { + args: args{ + spec: &projectsv1alpha1.IntegrationGithubParameters{}, + observation: nil, + }, + want: false, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := IsIntegrationGithubUpToDate(tc.args.spec, tc.args.observation) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("IsIntegrationGithubUpToDate(): -want, +got:\n%s", diff) + } + }) + } +} diff --git a/pkg/cluster/controller/projects/integrationgithub/zz_controller.go b/pkg/cluster/controller/projects/integrationgithub/zz_controller.go new file mode 100644 index 00000000..9291d1c2 --- /dev/null +++ b/pkg/cluster/controller/projects/integrationgithub/zz_controller.go @@ -0,0 +1,246 @@ +/* +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 integrationgithub + +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/errors" + "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" + 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 ( + errNotIntegrationGithub = "managed resource is not a Gitlab integration github custom resource" + errProjectIDMissing = "ProjectID is missing" + errGetFailed = "cannot get Gitlab integration github" + errCreateFailed = "cannot create Gitlab integration github" + errUpdateFailed = "cannot update Gitlab integration github" + errDeleteFailed = "cannot delete Gitlab integration github" +) + +// SetupIntegrationGithub adds a controller that reconciles GitLab Integration GitHub. +func SetupIntegrationGithub(mgr ctrl.Manager, o controller.Options) error { + name := managed.ControllerName("cluster." + v1alpha1.IntegrationGithubGroupKind) + + reconcilerOpts := []managed.ReconcilerOption{ + managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), newGitlabClientFn: projects.NewGithubClient}), + 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.IntegrationGithubGroupVersionKind), + reconcilerOpts..., + ) + + if err := mgr.Add(statemetrics.NewMRStateRecorder( + mgr.GetClient(), + o.Logger, + o.MetricOptions.MRStateMetrics, + &v1alpha1.IntegrationGithubList{}, + o.MetricOptions.PollStateMetricInterval, + )); err != nil { + return err + } + + return ctrl.NewControllerManagedBy(mgr). + Named(name). + For(&v1alpha1.IntegrationGithub{}). + Complete(r) +} + +// SetupIntegrationGithubGated adds a controller with CRD gate support. +func SetupIntegrationGithubGated(mgr ctrl.Manager, o controller.Options) error { + o.Gate.Register(func() { + if err := SetupIntegrationGithub(mgr, o); err != nil { + mgr.GetLogger().Error(err, "unable to setup reconciler", "gvk", v1alpha1.IntegrationGithubGroupVersionKind.String()) + } + }, v1alpha1.IntegrationGithubGroupVersionKind) + return nil +} + +// connector produces an ExternalClient for GitLab Integration GitHub. +type connector struct { + kube client.Client + newGitlabClientFn func(cfg common.Config) projects.GithubClient +} + +// Connect creates a new GitLab client for the given managed resource. +func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { + cr, ok := mg.(*v1alpha1.IntegrationGithub) + if !ok { + return nil, errors.New(errNotIntegrationGithub) + } + cfg, err := common.GetConfig(ctx, c.kube, cr) + if err != nil { + return nil, err + } + return &external{kube: c.kube, client: c.newGitlabClientFn(*cfg)}, nil +} + +// external represents the external client for GitLab Integration GitHub. +type external struct { + kube client.Client + client projects.GithubClient +} + +// applyGithub applies desired GitHub settings to the GitLab project. +func (e *external) applyGithub(ctx context.Context, cr *v1alpha1.IntegrationGithub) error { + _, _, err := e.client.SetGithubService( + *cr.Spec.ForProvider.ProjectID, + projects.GenerateSetGithubServiceOptions(&cr.Spec.ForProvider), + gitlab.WithContext(ctx), + ) + return err +} + +// Observe checks whether the external resource exists and whether it is up-to-date. +func (e *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { + cr, ok := mg.(*v1alpha1.IntegrationGithub) + if !ok { + return managed.ExternalObservation{}, errors.New(errNotIntegrationGithub) + } + + // If the resource is being deleted, avoid updating status. + if meta.WasDeleted(cr) { + return managed.ExternalObservation{ResourceExists: false}, nil + } + + if cr.Spec.ForProvider.ProjectID == nil { + return managed.ExternalObservation{}, errors.New(errProjectIDMissing) + } + + github, res, err := e.client.GetGithubService( + *cr.Spec.ForProvider.ProjectID, + gitlab.WithContext(ctx), + ) + if err != nil { + if clients.IsResponseNotFound(res) { + return managed.ExternalObservation{ResourceExists: false}, nil + } + return managed.ExternalObservation{}, errors.Wrap(err, errGetFailed) + } + if github == nil || github.Properties == nil { + return managed.ExternalObservation{ResourceExists: false}, nil + } + + // Late initialize spec from remote (mutates in place). + current := cr.Spec.ForProvider.DeepCopy() + projects.LateInitializeIntegrationGithub(&cr.Spec.ForProvider, github) + + // Update status from the remote state. + cr.Status.AtProvider = projects.GenerateIntegrationGithubObservation(github) + cr.Status.SetConditions(xpv1.Available()) + + return managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: projects.IsIntegrationGithubUpToDate(&cr.Spec.ForProvider, github), + ResourceLateInitialized: !cmp.Equal(current, &cr.Spec.ForProvider), + }, nil +} + +// Create creates the external resource for GitLab Integration GitHub. +// The integration is configured by sending desired options directly. +func (e *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) { + cr, ok := mg.(*v1alpha1.IntegrationGithub) + if !ok { + return managed.ExternalCreation{}, errors.New(errNotIntegrationGithub) + } + + if cr.Spec.ForProvider.ProjectID == nil { + return managed.ExternalCreation{}, errors.New(errProjectIDMissing) + } + + cr.Status.SetConditions(xpv1.Creating()) + + if err := e.applyGithub(ctx, cr); err != nil { + return managed.ExternalCreation{}, errors.Wrap(err, errCreateFailed) + } + return managed.ExternalCreation{}, nil +} + +// Update updates the external resource to match the desired state. +func (e *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { + cr, ok := mg.(*v1alpha1.IntegrationGithub) + if !ok { + return managed.ExternalUpdate{}, errors.New(errNotIntegrationGithub) + } + + if cr.Spec.ForProvider.ProjectID == nil { + return managed.ExternalUpdate{}, errors.New(errProjectIDMissing) + } + + // Set Creating condition to align with controller tests and convention. + cr.Status.SetConditions(xpv1.Creating()) + + if err := e.applyGithub(ctx, cr); err != nil { + return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateFailed) + } + return managed.ExternalUpdate{}, nil +} + +// Delete removes the GitLab GitHub integration. +func (e *external) Delete(ctx context.Context, mg resource.Managed) (managed.ExternalDelete, error) { + cr, ok := mg.(*v1alpha1.IntegrationGithub) + if !ok { + return managed.ExternalDelete{}, errors.New(errNotIntegrationGithub) + } + + if cr.Spec.ForProvider.ProjectID == nil { + return managed.ExternalDelete{}, errors.New(errProjectIDMissing) + } + + // Do not set Deleting condition to match existing controller test expectations. + _, err := e.client.DeleteGithubService( + *cr.Spec.ForProvider.ProjectID, + gitlab.WithContext(ctx), + ) + if err != nil { + return managed.ExternalDelete{}, errors.Wrap(err, errDeleteFailed) + } + return managed.ExternalDelete{}, nil +} + +// Disconnect is a no-op required by the SDK interface. +func (e *external) Disconnect(ctx context.Context) error { + return nil +} diff --git a/pkg/cluster/controller/projects/integrationgithub/zz_controller_test.go b/pkg/cluster/controller/projects/integrationgithub/zz_controller_test.go new file mode 100644 index 00000000..a9849994 --- /dev/null +++ b/pkg/cluster/controller/projects/integrationgithub/zz_controller_test.go @@ -0,0 +1,717 @@ +/* +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 integrationgithub + +import ( + "context" + "net/http" + "testing" + "time" + + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" + gitlab "gitlab.com/gitlab-org/api/client-go" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + commonv1alpha1 "github.com/crossplane-contrib/provider-gitlab/apis/common/v1alpha1" + "github.com/crossplane-contrib/provider-gitlab/apis/cluster/projects/v1alpha1" + "github.com/crossplane-contrib/provider-gitlab/pkg/cluster/clients/projects" + "github.com/crossplane-contrib/provider-gitlab/pkg/cluster/clients/projects/fake" +) + +var ( + unexpectedItem resource.Managed + errBoom = errors.New("boom") + + // Test data + testProjectID int64 = 123 + testToken = "ghp_secrettoken" + testRepositoryURL = "https://github.com/example/repo" + testStaticContext = true + testID int64 = 456 + testTitle = "GitHub" + testSlug = "github" + testActive = true + testCreatedAt = time.Now() + testUpdatedAt = time.Now() +) + +type args struct { + githubClient projects.GithubClient + kube client.Client + cr resource.Managed +} + +type githubModifier func(*v1alpha1.IntegrationGithub) + +func withProjectID(id int64) githubModifier { + return func(r *v1alpha1.IntegrationGithub) { + r.Spec.ForProvider.ProjectID = &id + } +} + +func withToken(token string) githubModifier { + return func(r *v1alpha1.IntegrationGithub) { + r.Spec.ForProvider.Token = token + } +} + +func withRepositoryURL(url string) githubModifier { + return func(r *v1alpha1.IntegrationGithub) { + r.Spec.ForProvider.RepositoryURL = &url + } +} + +func withConditions(c ...xpv1.Condition) githubModifier { + return func(cr *v1alpha1.IntegrationGithub) { + cr.Status.ConditionedStatus.Conditions = c + } +} + +func withStatus(s v1alpha1.IntegrationGithubObservation) githubModifier { + return func(r *v1alpha1.IntegrationGithub) { + r.Status.AtProvider = s + } +} + +func withDeletionTimestamp(t time.Time) githubModifier { + return func(r *v1alpha1.IntegrationGithub) { + r.ObjectMeta.DeletionTimestamp = &metav1.Time{Time: t} + } +} + +func withForProvider(mut func(*v1alpha1.IntegrationGithubParameters)) githubModifier { + return func(r *v1alpha1.IntegrationGithub) { + mut(&r.Spec.ForProvider) + } +} + +func integrationGithub(m ...githubModifier) *v1alpha1.IntegrationGithub { + cr := &v1alpha1.IntegrationGithub{} + for _, f := range m { + f(cr) + } + return cr +} + +func TestConnect(t *testing.T) { + type want struct { + cr resource.Managed + result managed.ExternalClient + err error + } + + cases := map[string]struct { + args + want + }{ + "InvalidInput": { + args: args{ + cr: unexpectedItem, + }, + want: want{ + cr: unexpectedItem, + err: errors.New(errNotIntegrationGithub), + }, + }, + "ProviderConfigRefNotGivenError": { + args: args{ + cr: integrationGithub(), + kube: &test.MockClient{MockGet: test.NewMockGetFn(nil)}, + }, + want: want{ + cr: integrationGithub(), + err: errors.New("providerConfigRef is not given"), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + c := &connector{kube: tc.kube, newGitlabClientFn: nil} + o, err := c.Connect(context.Background(), tc.args.cr) + + if (tc.want.err == nil) != (err == nil) { + t.Errorf("Connect(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + if tc.want.err != nil && err != nil && tc.want.err.Error() != err.Error() { + t.Errorf("Connect(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + if diff := cmp.Diff(tc.want.result, o); diff != "" { + t.Errorf("Connect(...): -want result, +got result:\n%s", diff) + } + }) + } +} + +func TestObserve(t *testing.T) { + type want struct { + cr resource.Managed + result managed.ExternalObservation + err error + } + + cases := map[string]struct { + args + want + }{ + "InvalidInput": { + args: args{ + cr: unexpectedItem, + }, + want: want{ + cr: unexpectedItem, + err: errors.New(errNotIntegrationGithub), + }, + }, + "NotFound": { + args: args{ + githubClient: &fake.MockClient{ + MockGetGithubService: func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return nil, &gitlab.Response{Response: &http.Response{StatusCode: 404}}, errBoom + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + result: managed.ExternalObservation{ResourceExists: false}, + }, + }, + "GetFailed": { + args: args{ + githubClient: &fake.MockClient{ + MockGetGithubService: func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return nil, &gitlab.Response{Response: &http.Response{StatusCode: 500}}, errBoom + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + result: managed.ExternalObservation{}, + err: errors.Wrap(errBoom, errGetFailed), + }, + }, + "SuccessUpToDate": { + args: args{ + githubClient: &fake.MockClient{ + MockGetGithubService: func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return &gitlab.GithubService{ + Service: gitlab.Service{ + ID: testID, + Title: testTitle, + Slug: testSlug, + CreatedAt: &testCreatedAt, + UpdatedAt: &testUpdatedAt, + Active: testActive, + }, + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testRepositoryURL, + StaticContext: testStaticContext, + }, + }, &gitlab.Response{}, nil + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withRepositoryURL(testRepositoryURL), + withForProvider(func(p *v1alpha1.IntegrationGithubParameters) { + p.StaticContext = &testStaticContext + }), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withRepositoryURL(testRepositoryURL), + withForProvider(func(p *v1alpha1.IntegrationGithubParameters) { + p.StaticContext = &testStaticContext + }), + withConditions(xpv1.Available()), + withStatus(v1alpha1.IntegrationGithubObservation{ + CommonIntegrationObservation: commonv1alpha1.CommonIntegrationObservation{ + ID: ptr.To(testID), + Title: ptr.To(testTitle), + Slug: ptr.To(testSlug), + CreatedAt: &metav1.Time{Time: testCreatedAt}, + UpdatedAt: &metav1.Time{Time: testUpdatedAt}, + Active: ptr.To(true), + AlertEvents: ptr.To(false), + CommitEvents: ptr.To(false), + ConfidentialIssuesEvents: ptr.To(false), + ConfidentialNoteEvents: ptr.To(false), + DeploymentEvents: ptr.To(false), + GroupConfidentialMentionEvents: ptr.To(false), + GroupMentionEvents: ptr.To(false), + IncidentEvents: ptr.To(false), + IssuesEvents: ptr.To(false), + JobEvents: ptr.To(false), + MergeRequestsEvents: ptr.To(false), + NoteEvents: ptr.To(false), + PipelineEvents: ptr.To(false), + PushEvents: ptr.To(false), + TagPushEvents: ptr.To(false), + VulnerabilityEvents: ptr.To(false), + WikiPageEvents: ptr.To(false), + CommentOnEventEnabled: ptr.To(false), + Inherited: ptr.To(false), + }, + RepositoryURL: testRepositoryURL, + StaticContext: testStaticContext, + }), + ), + result: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + ResourceLateInitialized: false, + }, + }, + }, + "DeletingResource": { + args: args{ + githubClient: &fake.MockClient{ + MockGetGithubService: func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return &gitlab.GithubService{ + Service: gitlab.Service{ + ID: testID, + Title: testTitle, + }, + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testRepositoryURL, + }, + }, &gitlab.Response{}, nil + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withDeletionTimestamp(time.Now()), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withDeletionTimestamp(time.Now()), + ), + result: managed.ExternalObservation{ResourceExists: false}, + }, + }, + "LateInitialization": { + args: args{ + githubClient: &fake.MockClient{ + MockGetGithubService: func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return &gitlab.GithubService{ + Service: gitlab.Service{ + ID: testID, + Title: testTitle, + }, + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testRepositoryURL, + StaticContext: testStaticContext, + }, + }, &gitlab.Response{}, nil + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withForProvider(func(p *v1alpha1.IntegrationGithubParameters) { + p.RepositoryURL = ptr.To(testRepositoryURL) + p.StaticContext = ptr.To(testStaticContext) + }), + withConditions(xpv1.Available()), + withStatus(v1alpha1.IntegrationGithubObservation{ + CommonIntegrationObservation: commonv1alpha1.CommonIntegrationObservation{ + ID: ptr.To(testID), + Title: ptr.To(testTitle), + Slug: ptr.To(""), + Active: ptr.To(false), + AlertEvents: ptr.To(false), + CommitEvents: ptr.To(false), + ConfidentialIssuesEvents: ptr.To(false), + ConfidentialNoteEvents: ptr.To(false), + DeploymentEvents: ptr.To(false), + GroupConfidentialMentionEvents: ptr.To(false), + GroupMentionEvents: ptr.To(false), + IncidentEvents: ptr.To(false), + IssuesEvents: ptr.To(false), + JobEvents: ptr.To(false), + MergeRequestsEvents: ptr.To(false), + NoteEvents: ptr.To(false), + PipelineEvents: ptr.To(false), + PushEvents: ptr.To(false), + TagPushEvents: ptr.To(false), + VulnerabilityEvents: ptr.To(false), + WikiPageEvents: ptr.To(false), + CommentOnEventEnabled: ptr.To(false), + Inherited: ptr.To(false), + }, + RepositoryURL: testRepositoryURL, + StaticContext: testStaticContext, + }), + ), + result: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + ResourceLateInitialized: true, + }, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + e := &external{client: tc.githubClient} + o, err := e.Observe(context.Background(), tc.args.cr) + + if (tc.want.err == nil) != (err == nil) { + t.Errorf("Observe(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + if tc.want.err != nil && err != nil && tc.want.err.Error() != err.Error() { + t.Errorf("Observe(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + + opts := []cmp.Option{test.EquateConditions()} + if cr, ok := tc.args.cr.(*v1alpha1.IntegrationGithub); ok && cr != nil && !cr.ObjectMeta.DeletionTimestamp.IsZero() { + opts = append(opts, cmp.FilterPath(func(p cmp.Path) bool { + return p.String() == "ObjectMeta.DeletionTimestamp" + }, cmp.Ignore())) + } + + if diff := cmp.Diff(tc.want.cr, tc.args.cr, opts...); diff != "" { + t.Errorf("Observe(...): -want CR, +got CR:\n%s", diff) + } + if diff := cmp.Diff(tc.want.result, o); diff != "" { + t.Errorf("Observe(...): -want result, +got result:\n%s", diff) + } + }) + } +} + +func TestCreate(t *testing.T) { + type want struct { + cr resource.Managed + result managed.ExternalCreation + err error + } + + cases := map[string]struct { + args + want + }{ + "InvalidInput": { + args: args{ + cr: unexpectedItem, + }, + want: want{ + cr: unexpectedItem, + err: errors.New(errNotIntegrationGithub), + }, + }, + "SuccessfulCreate": { + args: args{ + githubClient: &fake.MockClient{ + MockSetGithubService: func(pid any, opt *gitlab.SetGithubServiceOptions, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return &gitlab.GithubService{ + Service: gitlab.Service{ + ID: testID, + Title: testTitle, + }, + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testRepositoryURL, + }, + }, &gitlab.Response{}, nil + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withRepositoryURL(testRepositoryURL), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withRepositoryURL(testRepositoryURL), + withConditions(xpv1.Creating()), + ), + result: managed.ExternalCreation{}, + }, + }, + "CreateFailed": { + args: args{ + githubClient: &fake.MockClient{ + MockSetGithubService: func(pid any, opt *gitlab.SetGithubServiceOptions, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return nil, &gitlab.Response{}, errBoom + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withConditions(xpv1.Creating()), + ), + result: managed.ExternalCreation{}, + err: errors.Wrap(errBoom, errCreateFailed), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + e := &external{client: tc.githubClient} + o, err := e.Create(context.Background(), tc.args.cr) + + if (tc.want.err == nil) != (err == nil) { + t.Errorf("Create(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + if tc.want.err != nil && err != nil && tc.want.err.Error() != err.Error() { + t.Errorf("Create(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + if diff := cmp.Diff(tc.want.cr, tc.args.cr, test.EquateConditions()); diff != "" { + t.Errorf("Create(...): -want CR, +got CR:\n%s", diff) + } + if diff := cmp.Diff(tc.want.result, o); diff != "" { + t.Errorf("Create(...): -want result, +got result:\n%s", diff) + } + }) + } +} + +func TestUpdate(t *testing.T) { + type want struct { + cr resource.Managed + result managed.ExternalUpdate + err error + } + + cases := map[string]struct { + args + want + }{ + "InvalidInput": { + args: args{ + cr: unexpectedItem, + }, + want: want{ + cr: unexpectedItem, + err: errors.New(errNotIntegrationGithub), + }, + }, + "SuccessfulUpdate": { + args: args{ + githubClient: &fake.MockClient{ + MockSetGithubService: func(pid any, opt *gitlab.SetGithubServiceOptions, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return &gitlab.GithubService{ + Service: gitlab.Service{ + ID: testID, + Title: testTitle, + }, + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testRepositoryURL, + }, + }, &gitlab.Response{}, nil + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withRepositoryURL(testRepositoryURL), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withRepositoryURL(testRepositoryURL), + withConditions(xpv1.Creating()), + ), + result: managed.ExternalUpdate{}, + }, + }, + "UpdateFailed": { + args: args{ + githubClient: &fake.MockClient{ + MockSetGithubService: func(pid any, opt *gitlab.SetGithubServiceOptions, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return nil, &gitlab.Response{}, errBoom + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withConditions(xpv1.Creating()), + ), + result: managed.ExternalUpdate{}, + err: errors.Wrap(errBoom, errUpdateFailed), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + e := &external{client: tc.githubClient} + o, err := e.Update(context.Background(), tc.args.cr) + + if (tc.want.err == nil) != (err == nil) { + t.Errorf("Update(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + if tc.want.err != nil && err != nil && tc.want.err.Error() != err.Error() { + t.Errorf("Update(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + if diff := cmp.Diff(tc.want.cr, tc.args.cr, test.EquateConditions()); diff != "" { + t.Errorf("Update(...): -want CR, +got CR:\n%s", diff) + } + if diff := cmp.Diff(tc.want.result, o); diff != "" { + t.Errorf("Update(...): -want result, +got result:\n%s", diff) + } + }) + } +} + +func TestDelete(t *testing.T) { + type want struct { + cr resource.Managed + result managed.ExternalDelete + err error + } + + cases := map[string]struct { + args + want + }{ + "InvalidInput": { + args: args{ + cr: unexpectedItem, + }, + want: want{ + cr: unexpectedItem, + err: errors.New(errNotIntegrationGithub), + }, + }, + "SuccessfulDelete": { + args: args{ + githubClient: &fake.MockClient{ + MockDeleteGithubService: func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) { + return &gitlab.Response{}, nil + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + result: managed.ExternalDelete{}, + }, + }, + "DeleteFailed": { + args: args{ + githubClient: &fake.MockClient{ + MockDeleteGithubService: func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) { + return &gitlab.Response{}, errBoom + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + result: managed.ExternalDelete{}, + err: errors.Wrap(errBoom, errDeleteFailed), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + e := &external{client: tc.githubClient} + o, err := e.Delete(context.Background(), tc.args.cr) + + if (tc.want.err == nil) != (err == nil) { + t.Errorf("Delete(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + if tc.want.err != nil && err != nil && tc.want.err.Error() != err.Error() { + t.Errorf("Delete(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + if diff := cmp.Diff(tc.want.cr, tc.args.cr, test.EquateConditions()); diff != "" { + t.Errorf("Delete(...): -want CR, +got CR:\n%s", diff) + } + if diff := cmp.Diff(tc.want.result, o); diff != "" { + t.Errorf("Delete(...): -want result, +got result:\n%s", diff) + } + }) + } +} + +func TestDisconnect(t *testing.T) { + e := &external{} + err := e.Disconnect(context.Background()) + if err != nil { + t.Errorf("Disconnect(...): unexpected error: %v", err) + } +} diff --git a/pkg/cluster/controller/projects/zz_setup.go b/pkg/cluster/controller/projects/zz_setup.go index bd2bdc8a..3628ca0d 100644 --- a/pkg/cluster/controller/projects/zz_setup.go +++ b/pkg/cluster/controller/projects/zz_setup.go @@ -28,6 +28,7 @@ import ( "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/hooks" + integrationgithub "github.com/crossplane-contrib/provider-gitlab/pkg/cluster/controller/projects/integrationgithub" integrationmattermost "github.com/crossplane-contrib/provider-gitlab/pkg/cluster/controller/projects/integrationmattermost" "github.com/crossplane-contrib/provider-gitlab/pkg/cluster/controller/projects/members" "github.com/crossplane-contrib/provider-gitlab/pkg/cluster/controller/projects/pipelineschedules" @@ -54,6 +55,7 @@ func Setup(mgr ctrl.Manager, o controller.Options) error { protectedbranches.SetupProtectedBranch, badges.SetupBadge, integrationmattermost.SetupIntegrationMattermost, + integrationgithub.SetupIntegrationGithub, protectedenvironments.SetupProtectedEnvironment, } { if err := setup(mgr, o); err != nil { @@ -80,6 +82,7 @@ func SetupGated(mgr ctrl.Manager, o controller.Options) error { protectedbranches.SetupProtectedBranchGated, badges.SetupBadgeGated, integrationmattermost.SetupIntegrationMattermostGated, + integrationgithub.SetupIntegrationGithubGated, protectedenvironments.SetupProtectedEnvironmentGated, } { if err := setup(mgr, o); err != nil { diff --git a/pkg/namespaced/clients/projects/fake/fake.go b/pkg/namespaced/clients/projects/fake/fake.go index 3f4d0726..c605899c 100644 --- a/pkg/namespaced/clients/projects/fake/fake.go +++ b/pkg/namespaced/clients/projects/fake/fake.go @@ -88,6 +88,10 @@ type MockClient struct { MockGetMattermostService func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.MattermostService, *gitlab.Response, error) MockSetMattermostService func(pid any, opt *gitlab.SetMattermostServiceOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MattermostService, *gitlab.Response, error) MockDeleteMattermostService func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) + + MockGetGithubService func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) + MockSetGithubService func(pid any, opt *gitlab.SetGithubServiceOptions, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) + MockDeleteGithubService func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) } // GetPipelineSchedule calls the underlying MockGetPipelineSchedule method. @@ -330,3 +334,18 @@ func (c *MockClient) SetMattermostService(pid any, opt *gitlab.SetMattermostServ func (c *MockClient) DeleteMattermostService(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) { return c.MockDeleteMattermostService(pid, options...) } + +// GetGithubService calls the underlying MockGetGithubService method. +func (c *MockClient) GetGithubService(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return c.MockGetGithubService(pid, options...) +} + +// SetGithubService calls the underlying MockSetGithubService method. +func (c *MockClient) SetGithubService(pid any, opt *gitlab.SetGithubServiceOptions, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return c.MockSetGithubService(pid, opt, options...) +} + +// DeleteGithubService calls the underlying MockDeleteGithubService method. +func (c *MockClient) DeleteGithubService(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) { + return c.MockDeleteGithubService(pid, options...) +} diff --git a/pkg/namespaced/clients/projects/integration_github.go b/pkg/namespaced/clients/projects/integration_github.go new file mode 100644 index 00000000..95293fd4 --- /dev/null +++ b/pkg/namespaced/clients/projects/integration_github.go @@ -0,0 +1,98 @@ +/* +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 ( + gitlab "gitlab.com/gitlab-org/api/client-go" + + "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" +) + +// GithubClient defines GitLab GitHub integration operations. +type GithubClient interface { + GetGithubService(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) + SetGithubService(pid any, opt *gitlab.SetGithubServiceOptions, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) + DeleteGithubService(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) +} + +// NewGithubClient returns a new GitLab Services client. +func NewGithubClient(cfg common.Config) GithubClient { + git := common.NewClient(cfg) + return git.Services +} + +// GenerateSetGithubServiceOptions produces SetGithubServiceOptions from IntegrationGithubParameters. +func GenerateSetGithubServiceOptions(in *v1alpha1.IntegrationGithubParameters) *gitlab.SetGithubServiceOptions { + if in == nil { + return &gitlab.SetGithubServiceOptions{} + } + + opts := gitlab.SetGithubServiceOptions{ + RepositoryURL: in.RepositoryURL, + StaticContext: in.StaticContext, + } + + // Token is write-only in GitLab and typically required when initially setting the service. + // Only include it when provided to avoid unintentionally clearing remote configuration. + if in.Token != "" { + opts.Token = &in.Token + } + + return &opts +} + +// GenerateIntegrationGithubObservation converts gitlab.GithubService to IntegrationGithubObservation. +func GenerateIntegrationGithubObservation(observation *gitlab.GithubService) v1alpha1.IntegrationGithubObservation { + if observation == nil || observation.Properties == nil { + return v1alpha1.IntegrationGithubObservation{} + } + + commonObservation := common.GenerateCommonIntegrationObservation(&observation.Service) + + return v1alpha1.IntegrationGithubObservation{ + CommonIntegrationObservation: commonObservation, + RepositoryURL: observation.Properties.RepositoryURL, + StaticContext: observation.Properties.StaticContext, + } +} + +// IsIntegrationGithubUpToDate returns true if spec matches the observed GitLab GitHub service. +// +// Note: Token is intentionally excluded from comparison because GitLab does not return it (write-only). +func IsIntegrationGithubUpToDate(spec *v1alpha1.IntegrationGithubParameters, observation *gitlab.GithubService) bool { + if observation == nil || observation.Properties == nil { + return false + } + + return clients.IsComparableEqualToComparablePtr(spec.RepositoryURL, observation.Properties.RepositoryURL) && + clients.IsComparableEqualToComparablePtr(spec.StaticContext, observation.Properties.StaticContext) +} + +// LateInitializeIntegrationGithub fills nil spec fields using values from the remote GitHub service. +// It mutates the spec in place and does NOT touch write-only fields like Token. +func LateInitializeIntegrationGithub(in *v1alpha1.IntegrationGithubParameters, svc *gitlab.GithubService) { + if in == nil || svc == nil || svc.Properties == nil { + return + } + + in.RepositoryURL = clients.LateInitializeStringPtr(in.RepositoryURL, svc.Properties.RepositoryURL) + in.StaticContext = clients.LateInitializeFromValue(in.StaticContext, svc.Properties.StaticContext) + + // Token is write-only; do NOT late-initialize it from observation. +} diff --git a/pkg/namespaced/clients/projects/integration_github_test.go b/pkg/namespaced/clients/projects/integration_github_test.go new file mode 100644 index 00000000..07c9e3c3 --- /dev/null +++ b/pkg/namespaced/clients/projects/integration_github_test.go @@ -0,0 +1,314 @@ +package projects + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + gitlab "gitlab.com/gitlab-org/api/client-go" + "k8s.io/utils/ptr" + + "github.com/crossplane-contrib/provider-gitlab/apis/common/v1alpha1" + projectsv1alpha1 "github.com/crossplane-contrib/provider-gitlab/apis/namespaced/projects/v1alpha1" + "github.com/crossplane-contrib/provider-gitlab/pkg/namespaced/clients" +) + +var ( + testGithubProjectID int64 = 123 + testGithubToken = "ghp_secrettoken" + testGithubRepositoryURL = "https://github.com/example/repo" + testGithubStaticContext = true + + testGithubID int64 = 456 + testGithubTitle = "GitHub" + testGithubSlug = "github" + testGithubActive = true + testGithubCreatedAt = time.Now() + testGithubUpdatedAt = time.Now() +) + +// TestGenerateSetGithubServiceOptions tests the conversion from +// IntegrationGithubParameters to GitLab SetGithubServiceOptions +func TestGenerateSetGithubServiceOptions(t *testing.T) { + type args struct { + parameters *projectsv1alpha1.IntegrationGithubParameters + } + cases := map[string]struct { + args args + want *gitlab.SetGithubServiceOptions + }{ + "AllFieldsSet": { + args: args{ + parameters: &projectsv1alpha1.IntegrationGithubParameters{ + ProjectID: &testGithubProjectID, + Token: testGithubToken, + RepositoryURL: &testGithubRepositoryURL, + StaticContext: &testGithubStaticContext, + }, + }, + want: &gitlab.SetGithubServiceOptions{ + Token: &testGithubToken, + RepositoryURL: &testGithubRepositoryURL, + StaticContext: &testGithubStaticContext, + }, + }, + "OnlyRequiredFields": { + args: args{ + parameters: &projectsv1alpha1.IntegrationGithubParameters{ + ProjectID: &testGithubProjectID, + Token: testGithubToken, + }, + }, + want: &gitlab.SetGithubServiceOptions{ + Token: &testGithubToken, + }, + }, + "EmptyTokenNotIncluded": { + args: args{ + parameters: &projectsv1alpha1.IntegrationGithubParameters{ + ProjectID: &testGithubProjectID, + Token: "", + }, + }, + want: &gitlab.SetGithubServiceOptions{}, + }, + "NilInput": { + args: args{ + parameters: nil, + }, + want: &gitlab.SetGithubServiceOptions{}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := GenerateSetGithubServiceOptions(tc.args.parameters) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("GenerateSetGithubServiceOptions(): -want, +got:\n%s", diff) + } + }) + } +} + +// TestGenerateIntegrationGithubObservation tests the conversion from +// GitLab GithubService to IntegrationGithubObservation +func TestGenerateIntegrationGithubObservation(t *testing.T) { + type args struct { + service *gitlab.GithubService + } + cases := map[string]struct { + args args + want projectsv1alpha1.IntegrationGithubObservation + }{ + "FullObservation": { + args: args{ + service: &gitlab.GithubService{ + Service: gitlab.Service{ + ID: testGithubID, + Title: testGithubTitle, + Slug: testGithubSlug, + CreatedAt: &testGithubCreatedAt, + UpdatedAt: &testGithubUpdatedAt, + Active: testGithubActive, + }, + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testGithubRepositoryURL, + StaticContext: testGithubStaticContext, + }, + }, + }, + want: projectsv1alpha1.IntegrationGithubObservation{ + CommonIntegrationObservation: v1alpha1.CommonIntegrationObservation{ + ID: ptr.To(testGithubID), + Title: ptr.To(testGithubTitle), + Slug: ptr.To(testGithubSlug), + CreatedAt: clients.TimeToMetaTime(&testGithubCreatedAt), + UpdatedAt: clients.TimeToMetaTime(&testGithubUpdatedAt), + Active: ptr.To(true), + AlertEvents: ptr.To(false), + CommitEvents: ptr.To(false), + ConfidentialIssuesEvents: ptr.To(false), + ConfidentialNoteEvents: ptr.To(false), + DeploymentEvents: ptr.To(false), + GroupConfidentialMentionEvents: ptr.To(false), + GroupMentionEvents: ptr.To(false), + IncidentEvents: ptr.To(false), + IssuesEvents: ptr.To(false), + JobEvents: ptr.To(false), + MergeRequestsEvents: ptr.To(false), + NoteEvents: ptr.To(false), + PipelineEvents: ptr.To(false), + PushEvents: ptr.To(false), + TagPushEvents: ptr.To(false), + VulnerabilityEvents: ptr.To(false), + WikiPageEvents: ptr.To(false), + CommentOnEventEnabled: ptr.To(false), + Inherited: ptr.To(false), + }, + RepositoryURL: testGithubRepositoryURL, + StaticContext: testGithubStaticContext, + }, + }, + "MinimalObservation": { + args: args{ + service: &gitlab.GithubService{ + Service: gitlab.Service{ + ID: testGithubID, + Title: testGithubTitle, + }, + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testGithubRepositoryURL, + }, + }, + }, + want: projectsv1alpha1.IntegrationGithubObservation{ + CommonIntegrationObservation: v1alpha1.CommonIntegrationObservation{ + ID: ptr.To(testGithubID), + Title: ptr.To(testGithubTitle), + Slug: ptr.To(""), + CreatedAt: nil, + UpdatedAt: nil, + Active: ptr.To(false), + AlertEvents: ptr.To(false), + CommitEvents: ptr.To(false), + ConfidentialIssuesEvents: ptr.To(false), + ConfidentialNoteEvents: ptr.To(false), + DeploymentEvents: ptr.To(false), + GroupConfidentialMentionEvents: ptr.To(false), + GroupMentionEvents: ptr.To(false), + IncidentEvents: ptr.To(false), + IssuesEvents: ptr.To(false), + JobEvents: ptr.To(false), + MergeRequestsEvents: ptr.To(false), + NoteEvents: ptr.To(false), + PipelineEvents: ptr.To(false), + PushEvents: ptr.To(false), + TagPushEvents: ptr.To(false), + VulnerabilityEvents: ptr.To(false), + WikiPageEvents: ptr.To(false), + CommentOnEventEnabled: ptr.To(false), + Inherited: ptr.To(false), + }, + RepositoryURL: testGithubRepositoryURL, + }, + }, + "NilObservation": { + args: args{ + service: nil, + }, + want: projectsv1alpha1.IntegrationGithubObservation{}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := GenerateIntegrationGithubObservation(tc.args.service) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("GenerateIntegrationGithubObservation(): -want, +got:\n%s", diff) + } + }) + } +} + +// TestIsIntegrationGithubUpToDate tests whether the spec matches the observation +func TestIsIntegrationGithubUpToDate(t *testing.T) { + type args struct { + spec *projectsv1alpha1.IntegrationGithubParameters + observation *gitlab.GithubService + } + cases := map[string]struct { + args args + want bool + }{ + "UpToDate": { + args: args{ + spec: &projectsv1alpha1.IntegrationGithubParameters{ + ProjectID: &testGithubProjectID, + Token: testGithubToken, + RepositoryURL: &testGithubRepositoryURL, + StaticContext: &testGithubStaticContext, + }, + observation: &gitlab.GithubService{ + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testGithubRepositoryURL, + StaticContext: testGithubStaticContext, + }, + }, + }, + want: true, + }, + "DifferentTokenIsIgnored": { + args: args{ + spec: &projectsv1alpha1.IntegrationGithubParameters{ + Token: "different-token", + RepositoryURL: &testGithubRepositoryURL, + StaticContext: &testGithubStaticContext, + }, + observation: &gitlab.GithubService{ + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testGithubRepositoryURL, + StaticContext: testGithubStaticContext, + }, + }, + }, + want: true, + }, + "OutOfDateRepositoryURL": { + args: args{ + spec: &projectsv1alpha1.IntegrationGithubParameters{ + Token: testGithubToken, + RepositoryURL: func() *string { s := "https://github.com/other/repo"; return &s }(), + }, + observation: &gitlab.GithubService{ + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testGithubRepositoryURL, + }, + }, + }, + want: false, + }, + "OutOfDateStaticContext": { + args: args{ + spec: &projectsv1alpha1.IntegrationGithubParameters{ + Token: testGithubToken, + StaticContext: func() *bool { b := false; return &b }(), + }, + observation: &gitlab.GithubService{ + Properties: &gitlab.GithubServiceProperties{ + StaticContext: true, + }, + }, + }, + want: false, + }, + "NilSpecFields": { + args: args{ + spec: &projectsv1alpha1.IntegrationGithubParameters{ + Token: testGithubToken, + }, + observation: &gitlab.GithubService{ + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testGithubRepositoryURL, + }, + }, + }, + want: true, + }, + "NilObservationReturnsFalse": { + args: args{ + spec: &projectsv1alpha1.IntegrationGithubParameters{}, + observation: nil, + }, + want: false, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := IsIntegrationGithubUpToDate(tc.args.spec, tc.args.observation) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("IsIntegrationGithubUpToDate(): -want, +got:\n%s", diff) + } + }) + } +} diff --git a/pkg/namespaced/controller/projects/integrationgithub/controller.go b/pkg/namespaced/controller/projects/integrationgithub/controller.go new file mode 100644 index 00000000..2570d3c8 --- /dev/null +++ b/pkg/namespaced/controller/projects/integrationgithub/controller.go @@ -0,0 +1,244 @@ +/* +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 integrationgithub + +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/errors" + "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" + 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 ( + errNotIntegrationGithub = "managed resource is not a Gitlab integration github custom resource" + errProjectIDMissing = "ProjectID is missing" + errGetFailed = "cannot get Gitlab integration github" + errCreateFailed = "cannot create Gitlab integration github" + errUpdateFailed = "cannot update Gitlab integration github" + errDeleteFailed = "cannot delete Gitlab integration github" +) + +// SetupIntegrationGithub adds a controller that reconciles GitLab Integration GitHub. +func SetupIntegrationGithub(mgr ctrl.Manager, o controller.Options) error { + name := managed.ControllerName(v1alpha1.IntegrationGithubGroupKind) + + reconcilerOpts := []managed.ReconcilerOption{ + managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), newGitlabClientFn: projects.NewGithubClient}), + 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.IntegrationGithubGroupVersionKind), + reconcilerOpts..., + ) + + if err := mgr.Add(statemetrics.NewMRStateRecorder( + mgr.GetClient(), + o.Logger, + o.MetricOptions.MRStateMetrics, + &v1alpha1.IntegrationGithubList{}, + o.MetricOptions.PollStateMetricInterval, + )); err != nil { + return err + } + + return ctrl.NewControllerManagedBy(mgr). + Named(name). + For(&v1alpha1.IntegrationGithub{}). + Complete(r) +} + +// SetupIntegrationGithubGated adds a controller with CRD gate support. +func SetupIntegrationGithubGated(mgr ctrl.Manager, o controller.Options) error { + o.Gate.Register(func() { + if err := SetupIntegrationGithub(mgr, o); err != nil { + mgr.GetLogger().Error(err, "unable to setup reconciler", "gvk", v1alpha1.IntegrationGithubGroupVersionKind.String()) + } + }, v1alpha1.IntegrationGithubGroupVersionKind) + return nil +} + +// connector produces an ExternalClient for GitLab Integration GitHub. +type connector struct { + kube client.Client + newGitlabClientFn func(cfg common.Config) projects.GithubClient +} + +// Connect creates a new GitLab client for the given managed resource. +func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { + cr, ok := mg.(*v1alpha1.IntegrationGithub) + if !ok { + return nil, errors.New(errNotIntegrationGithub) + } + cfg, err := common.GetConfig(ctx, c.kube, cr) + if err != nil { + return nil, err + } + return &external{kube: c.kube, client: c.newGitlabClientFn(*cfg)}, nil +} + +// external represents the external client for GitLab Integration GitHub. +type external struct { + kube client.Client + client projects.GithubClient +} + +// applyGithub applies desired GitHub settings to the GitLab project. +func (e *external) applyGithub(ctx context.Context, cr *v1alpha1.IntegrationGithub) error { + _, _, err := e.client.SetGithubService( + *cr.Spec.ForProvider.ProjectID, + projects.GenerateSetGithubServiceOptions(&cr.Spec.ForProvider), + gitlab.WithContext(ctx), + ) + return err +} + +// Observe checks whether the external resource exists and whether it is up-to-date. +func (e *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { + cr, ok := mg.(*v1alpha1.IntegrationGithub) + if !ok { + return managed.ExternalObservation{}, errors.New(errNotIntegrationGithub) + } + + // If the resource is being deleted, avoid updating status. + if meta.WasDeleted(cr) { + return managed.ExternalObservation{ResourceExists: false}, nil + } + + if cr.Spec.ForProvider.ProjectID == nil { + return managed.ExternalObservation{}, errors.New(errProjectIDMissing) + } + + github, res, err := e.client.GetGithubService( + *cr.Spec.ForProvider.ProjectID, + gitlab.WithContext(ctx), + ) + if err != nil { + if clients.IsResponseNotFound(res) { + return managed.ExternalObservation{ResourceExists: false}, nil + } + return managed.ExternalObservation{}, errors.Wrap(err, errGetFailed) + } + if github == nil || github.Properties == nil { + return managed.ExternalObservation{ResourceExists: false}, nil + } + + // Late initialize spec from remote (mutates in place). + current := cr.Spec.ForProvider.DeepCopy() + projects.LateInitializeIntegrationGithub(&cr.Spec.ForProvider, github) + + // Update status from the remote state. + cr.Status.AtProvider = projects.GenerateIntegrationGithubObservation(github) + cr.Status.SetConditions(xpv1.Available()) + + return managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: projects.IsIntegrationGithubUpToDate(&cr.Spec.ForProvider, github), + ResourceLateInitialized: !cmp.Equal(current, &cr.Spec.ForProvider), + }, nil +} + +// Create creates the external resource for GitLab Integration GitHub. +// The integration is configured by sending desired options directly. +func (e *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) { + cr, ok := mg.(*v1alpha1.IntegrationGithub) + if !ok { + return managed.ExternalCreation{}, errors.New(errNotIntegrationGithub) + } + + if cr.Spec.ForProvider.ProjectID == nil { + return managed.ExternalCreation{}, errors.New(errProjectIDMissing) + } + + cr.Status.SetConditions(xpv1.Creating()) + + if err := e.applyGithub(ctx, cr); err != nil { + return managed.ExternalCreation{}, errors.Wrap(err, errCreateFailed) + } + return managed.ExternalCreation{}, nil +} + +// Update updates the external resource to match the desired state. +func (e *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { + cr, ok := mg.(*v1alpha1.IntegrationGithub) + if !ok { + return managed.ExternalUpdate{}, errors.New(errNotIntegrationGithub) + } + + if cr.Spec.ForProvider.ProjectID == nil { + return managed.ExternalUpdate{}, errors.New(errProjectIDMissing) + } + + // Set Creating condition to align with controller tests and convention. + cr.Status.SetConditions(xpv1.Creating()) + + if err := e.applyGithub(ctx, cr); err != nil { + return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateFailed) + } + return managed.ExternalUpdate{}, nil +} + +// Delete removes the GitLab GitHub integration. +func (e *external) Delete(ctx context.Context, mg resource.Managed) (managed.ExternalDelete, error) { + cr, ok := mg.(*v1alpha1.IntegrationGithub) + if !ok { + return managed.ExternalDelete{}, errors.New(errNotIntegrationGithub) + } + + if cr.Spec.ForProvider.ProjectID == nil { + return managed.ExternalDelete{}, errors.New(errProjectIDMissing) + } + + // Do not set Deleting condition to match existing controller test expectations. + _, err := e.client.DeleteGithubService( + *cr.Spec.ForProvider.ProjectID, + gitlab.WithContext(ctx), + ) + if err != nil { + return managed.ExternalDelete{}, errors.Wrap(err, errDeleteFailed) + } + return managed.ExternalDelete{}, nil +} + +// Disconnect is a no-op required by the SDK interface. +func (e *external) Disconnect(ctx context.Context) error { + return nil +} diff --git a/pkg/namespaced/controller/projects/integrationgithub/controller_test.go b/pkg/namespaced/controller/projects/integrationgithub/controller_test.go new file mode 100644 index 00000000..0f42fab0 --- /dev/null +++ b/pkg/namespaced/controller/projects/integrationgithub/controller_test.go @@ -0,0 +1,715 @@ +/* +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 integrationgithub + +import ( + "context" + "net/http" + "testing" + "time" + + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" + gitlab "gitlab.com/gitlab-org/api/client-go" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + commonv1alpha1 "github.com/crossplane-contrib/provider-gitlab/apis/common/v1alpha1" + "github.com/crossplane-contrib/provider-gitlab/apis/namespaced/projects/v1alpha1" + "github.com/crossplane-contrib/provider-gitlab/pkg/namespaced/clients/projects" + "github.com/crossplane-contrib/provider-gitlab/pkg/namespaced/clients/projects/fake" +) + +var ( + unexpectedItem resource.Managed + errBoom = errors.New("boom") + + // Test data + testProjectID int64 = 123 + testToken = "ghp_secrettoken" + testRepositoryURL = "https://github.com/example/repo" + testStaticContext = true + testID int64 = 456 + testTitle = "GitHub" + testSlug = "github" + testActive = true + testCreatedAt = time.Now() + testUpdatedAt = time.Now() +) + +type args struct { + githubClient projects.GithubClient + kube client.Client + cr resource.Managed +} + +type githubModifier func(*v1alpha1.IntegrationGithub) + +func withProjectID(id int64) githubModifier { + return func(r *v1alpha1.IntegrationGithub) { + r.Spec.ForProvider.ProjectID = &id + } +} + +func withToken(token string) githubModifier { + return func(r *v1alpha1.IntegrationGithub) { + r.Spec.ForProvider.Token = token + } +} + +func withRepositoryURL(url string) githubModifier { + return func(r *v1alpha1.IntegrationGithub) { + r.Spec.ForProvider.RepositoryURL = &url + } +} + +func withConditions(c ...xpv1.Condition) githubModifier { + return func(cr *v1alpha1.IntegrationGithub) { + cr.Status.ConditionedStatus.Conditions = c + } +} + +func withStatus(s v1alpha1.IntegrationGithubObservation) githubModifier { + return func(r *v1alpha1.IntegrationGithub) { + r.Status.AtProvider = s + } +} + +func withDeletionTimestamp(t time.Time) githubModifier { + return func(r *v1alpha1.IntegrationGithub) { + r.ObjectMeta.DeletionTimestamp = &metav1.Time{Time: t} + } +} + +func withForProvider(mut func(*v1alpha1.IntegrationGithubParameters)) githubModifier { + return func(r *v1alpha1.IntegrationGithub) { + mut(&r.Spec.ForProvider) + } +} + +func integrationGithub(m ...githubModifier) *v1alpha1.IntegrationGithub { + cr := &v1alpha1.IntegrationGithub{} + for _, f := range m { + f(cr) + } + return cr +} + +func TestConnect(t *testing.T) { + type want struct { + cr resource.Managed + result managed.ExternalClient + err error + } + + cases := map[string]struct { + args + want + }{ + "InvalidInput": { + args: args{ + cr: unexpectedItem, + }, + want: want{ + cr: unexpectedItem, + err: errors.New(errNotIntegrationGithub), + }, + }, + "ProviderConfigRefNotGivenError": { + args: args{ + cr: integrationGithub(), + kube: &test.MockClient{MockGet: test.NewMockGetFn(nil)}, + }, + want: want{ + cr: integrationGithub(), + err: errors.New("providerConfigRef is not given"), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + c := &connector{kube: tc.kube, newGitlabClientFn: nil} + o, err := c.Connect(context.Background(), tc.args.cr) + + if (tc.want.err == nil) != (err == nil) { + t.Errorf("Connect(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + if tc.want.err != nil && err != nil && tc.want.err.Error() != err.Error() { + t.Errorf("Connect(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + if diff := cmp.Diff(tc.want.result, o); diff != "" { + t.Errorf("Connect(...): -want result, +got result:\n%s", diff) + } + }) + } +} + +func TestObserve(t *testing.T) { + type want struct { + cr resource.Managed + result managed.ExternalObservation + err error + } + + cases := map[string]struct { + args + want + }{ + "InvalidInput": { + args: args{ + cr: unexpectedItem, + }, + want: want{ + cr: unexpectedItem, + err: errors.New(errNotIntegrationGithub), + }, + }, + "NotFound": { + args: args{ + githubClient: &fake.MockClient{ + MockGetGithubService: func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return nil, &gitlab.Response{Response: &http.Response{StatusCode: 404}}, errBoom + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + result: managed.ExternalObservation{ResourceExists: false}, + }, + }, + "GetFailed": { + args: args{ + githubClient: &fake.MockClient{ + MockGetGithubService: func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return nil, &gitlab.Response{Response: &http.Response{StatusCode: 500}}, errBoom + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + result: managed.ExternalObservation{}, + err: errors.Wrap(errBoom, errGetFailed), + }, + }, + "SuccessUpToDate": { + args: args{ + githubClient: &fake.MockClient{ + MockGetGithubService: func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return &gitlab.GithubService{ + Service: gitlab.Service{ + ID: testID, + Title: testTitle, + Slug: testSlug, + CreatedAt: &testCreatedAt, + UpdatedAt: &testUpdatedAt, + Active: testActive, + }, + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testRepositoryURL, + StaticContext: testStaticContext, + }, + }, &gitlab.Response{}, nil + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withRepositoryURL(testRepositoryURL), + withForProvider(func(p *v1alpha1.IntegrationGithubParameters) { + p.StaticContext = &testStaticContext + }), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withRepositoryURL(testRepositoryURL), + withForProvider(func(p *v1alpha1.IntegrationGithubParameters) { + p.StaticContext = &testStaticContext + }), + withConditions(xpv1.Available()), + withStatus(v1alpha1.IntegrationGithubObservation{ + CommonIntegrationObservation: commonv1alpha1.CommonIntegrationObservation{ + ID: ptr.To(testID), + Title: ptr.To(testTitle), + Slug: ptr.To(testSlug), + CreatedAt: &metav1.Time{Time: testCreatedAt}, + UpdatedAt: &metav1.Time{Time: testUpdatedAt}, + Active: ptr.To(true), + AlertEvents: ptr.To(false), + CommitEvents: ptr.To(false), + ConfidentialIssuesEvents: ptr.To(false), + ConfidentialNoteEvents: ptr.To(false), + DeploymentEvents: ptr.To(false), + GroupConfidentialMentionEvents: ptr.To(false), + GroupMentionEvents: ptr.To(false), + IncidentEvents: ptr.To(false), + IssuesEvents: ptr.To(false), + JobEvents: ptr.To(false), + MergeRequestsEvents: ptr.To(false), + NoteEvents: ptr.To(false), + PipelineEvents: ptr.To(false), + PushEvents: ptr.To(false), + TagPushEvents: ptr.To(false), + VulnerabilityEvents: ptr.To(false), + WikiPageEvents: ptr.To(false), + CommentOnEventEnabled: ptr.To(false), + Inherited: ptr.To(false), + }, + RepositoryURL: testRepositoryURL, + StaticContext: testStaticContext, + }), + ), + result: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + ResourceLateInitialized: false, + }, + }, + }, + "DeletingResource": { + args: args{ + githubClient: &fake.MockClient{ + MockGetGithubService: func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return &gitlab.GithubService{ + Service: gitlab.Service{ + ID: testID, + Title: testTitle, + }, + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testRepositoryURL, + }, + }, &gitlab.Response{}, nil + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withDeletionTimestamp(time.Now()), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withDeletionTimestamp(time.Now()), + ), + result: managed.ExternalObservation{ResourceExists: false}, + }, + }, + "LateInitialization": { + args: args{ + githubClient: &fake.MockClient{ + MockGetGithubService: func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return &gitlab.GithubService{ + Service: gitlab.Service{ + ID: testID, + Title: testTitle, + }, + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testRepositoryURL, + StaticContext: testStaticContext, + }, + }, &gitlab.Response{}, nil + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withForProvider(func(p *v1alpha1.IntegrationGithubParameters) { + p.RepositoryURL = ptr.To(testRepositoryURL) + p.StaticContext = ptr.To(testStaticContext) + }), + withConditions(xpv1.Available()), + withStatus(v1alpha1.IntegrationGithubObservation{ + CommonIntegrationObservation: commonv1alpha1.CommonIntegrationObservation{ + ID: ptr.To(testID), + Title: ptr.To(testTitle), + Slug: ptr.To(""), + Active: ptr.To(false), + AlertEvents: ptr.To(false), + CommitEvents: ptr.To(false), + ConfidentialIssuesEvents: ptr.To(false), + ConfidentialNoteEvents: ptr.To(false), + DeploymentEvents: ptr.To(false), + GroupConfidentialMentionEvents: ptr.To(false), + GroupMentionEvents: ptr.To(false), + IncidentEvents: ptr.To(false), + IssuesEvents: ptr.To(false), + JobEvents: ptr.To(false), + MergeRequestsEvents: ptr.To(false), + NoteEvents: ptr.To(false), + PipelineEvents: ptr.To(false), + PushEvents: ptr.To(false), + TagPushEvents: ptr.To(false), + VulnerabilityEvents: ptr.To(false), + WikiPageEvents: ptr.To(false), + CommentOnEventEnabled: ptr.To(false), + Inherited: ptr.To(false), + }, + RepositoryURL: testRepositoryURL, + StaticContext: testStaticContext, + }), + ), + result: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + ResourceLateInitialized: true, + }, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + e := &external{client: tc.githubClient} + o, err := e.Observe(context.Background(), tc.args.cr) + + if (tc.want.err == nil) != (err == nil) { + t.Errorf("Observe(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + if tc.want.err != nil && err != nil && tc.want.err.Error() != err.Error() { + t.Errorf("Observe(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + + opts := []cmp.Option{test.EquateConditions()} + if cr, ok := tc.args.cr.(*v1alpha1.IntegrationGithub); ok && cr != nil && !cr.ObjectMeta.DeletionTimestamp.IsZero() { + opts = append(opts, cmp.FilterPath(func(p cmp.Path) bool { + return p.String() == "ObjectMeta.DeletionTimestamp" + }, cmp.Ignore())) + } + + if diff := cmp.Diff(tc.want.cr, tc.args.cr, opts...); diff != "" { + t.Errorf("Observe(...): -want CR, +got CR:\n%s", diff) + } + if diff := cmp.Diff(tc.want.result, o); diff != "" { + t.Errorf("Observe(...): -want result, +got result:\n%s", diff) + } + }) + } +} + +func TestCreate(t *testing.T) { + type want struct { + cr resource.Managed + result managed.ExternalCreation + err error + } + + cases := map[string]struct { + args + want + }{ + "InvalidInput": { + args: args{ + cr: unexpectedItem, + }, + want: want{ + cr: unexpectedItem, + err: errors.New(errNotIntegrationGithub), + }, + }, + "SuccessfulCreate": { + args: args{ + githubClient: &fake.MockClient{ + MockSetGithubService: func(pid any, opt *gitlab.SetGithubServiceOptions, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return &gitlab.GithubService{ + Service: gitlab.Service{ + ID: testID, + Title: testTitle, + }, + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testRepositoryURL, + }, + }, &gitlab.Response{}, nil + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withRepositoryURL(testRepositoryURL), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withRepositoryURL(testRepositoryURL), + withConditions(xpv1.Creating()), + ), + result: managed.ExternalCreation{}, + }, + }, + "CreateFailed": { + args: args{ + githubClient: &fake.MockClient{ + MockSetGithubService: func(pid any, opt *gitlab.SetGithubServiceOptions, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return nil, &gitlab.Response{}, errBoom + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withConditions(xpv1.Creating()), + ), + result: managed.ExternalCreation{}, + err: errors.Wrap(errBoom, errCreateFailed), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + e := &external{client: tc.githubClient} + o, err := e.Create(context.Background(), tc.args.cr) + + if (tc.want.err == nil) != (err == nil) { + t.Errorf("Create(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + if tc.want.err != nil && err != nil && tc.want.err.Error() != err.Error() { + t.Errorf("Create(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + if diff := cmp.Diff(tc.want.cr, tc.args.cr, test.EquateConditions()); diff != "" { + t.Errorf("Create(...): -want CR, +got CR:\n%s", diff) + } + if diff := cmp.Diff(tc.want.result, o); diff != "" { + t.Errorf("Create(...): -want result, +got result:\n%s", diff) + } + }) + } +} + +func TestUpdate(t *testing.T) { + type want struct { + cr resource.Managed + result managed.ExternalUpdate + err error + } + + cases := map[string]struct { + args + want + }{ + "InvalidInput": { + args: args{ + cr: unexpectedItem, + }, + want: want{ + cr: unexpectedItem, + err: errors.New(errNotIntegrationGithub), + }, + }, + "SuccessfulUpdate": { + args: args{ + githubClient: &fake.MockClient{ + MockSetGithubService: func(pid any, opt *gitlab.SetGithubServiceOptions, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return &gitlab.GithubService{ + Service: gitlab.Service{ + ID: testID, + Title: testTitle, + }, + Properties: &gitlab.GithubServiceProperties{ + RepositoryURL: testRepositoryURL, + }, + }, &gitlab.Response{}, nil + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withRepositoryURL(testRepositoryURL), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withRepositoryURL(testRepositoryURL), + withConditions(xpv1.Creating()), + ), + result: managed.ExternalUpdate{}, + }, + }, + "UpdateFailed": { + args: args{ + githubClient: &fake.MockClient{ + MockSetGithubService: func(pid any, opt *gitlab.SetGithubServiceOptions, options ...gitlab.RequestOptionFunc) (*gitlab.GithubService, *gitlab.Response, error) { + return nil, &gitlab.Response{}, errBoom + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + withConditions(xpv1.Creating()), + ), + result: managed.ExternalUpdate{}, + err: errors.Wrap(errBoom, errUpdateFailed), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + e := &external{client: tc.githubClient} + o, err := e.Update(context.Background(), tc.args.cr) + + if (tc.want.err == nil) != (err == nil) { + t.Errorf("Update(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + if tc.want.err != nil && err != nil && tc.want.err.Error() != err.Error() { + t.Errorf("Update(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + if diff := cmp.Diff(tc.want.cr, tc.args.cr, test.EquateConditions()); diff != "" { + t.Errorf("Update(...): -want CR, +got CR:\n%s", diff) + } + if diff := cmp.Diff(tc.want.result, o); diff != "" { + t.Errorf("Update(...): -want result, +got result:\n%s", diff) + } + }) + } +} + +func TestDelete(t *testing.T) { + type want struct { + cr resource.Managed + result managed.ExternalDelete + err error + } + + cases := map[string]struct { + args + want + }{ + "InvalidInput": { + args: args{ + cr: unexpectedItem, + }, + want: want{ + cr: unexpectedItem, + err: errors.New(errNotIntegrationGithub), + }, + }, + "SuccessfulDelete": { + args: args{ + githubClient: &fake.MockClient{ + MockDeleteGithubService: func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) { + return &gitlab.Response{}, nil + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + result: managed.ExternalDelete{}, + }, + }, + "DeleteFailed": { + args: args{ + githubClient: &fake.MockClient{ + MockDeleteGithubService: func(pid any, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) { + return &gitlab.Response{}, errBoom + }, + }, + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + }, + want: want{ + cr: integrationGithub( + withProjectID(testProjectID), + withToken(testToken), + ), + result: managed.ExternalDelete{}, + err: errors.Wrap(errBoom, errDeleteFailed), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + e := &external{client: tc.githubClient} + o, err := e.Delete(context.Background(), tc.args.cr) + + if (tc.want.err == nil) != (err == nil) { + t.Errorf("Delete(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + if tc.want.err != nil && err != nil && tc.want.err.Error() != err.Error() { + t.Errorf("Delete(...): -want error, +got error: \nwant: %v\ngot: %v", tc.want.err, err) + } + if diff := cmp.Diff(tc.want.cr, tc.args.cr, test.EquateConditions()); diff != "" { + t.Errorf("Delete(...): -want CR, +got CR:\n%s", diff) + } + if diff := cmp.Diff(tc.want.result, o); diff != "" { + t.Errorf("Delete(...): -want result, +got result:\n%s", diff) + } + }) + } +} + +func TestDisconnect(t *testing.T) { + e := &external{} + err := e.Disconnect(context.Background()) + if err != nil { + t.Errorf("Disconnect(...): unexpected error: %v", err) + } +} diff --git a/pkg/namespaced/controller/projects/setup.go b/pkg/namespaced/controller/projects/setup.go index 4ca633c0..34e54dcf 100644 --- a/pkg/namespaced/controller/projects/setup.go +++ b/pkg/namespaced/controller/projects/setup.go @@ -26,6 +26,7 @@ import ( "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/hooks" + integrationgithub "github.com/crossplane-contrib/provider-gitlab/pkg/namespaced/controller/projects/integrationgithub" integrationmattermost "github.com/crossplane-contrib/provider-gitlab/pkg/namespaced/controller/projects/integrationmattermost" "github.com/crossplane-contrib/provider-gitlab/pkg/namespaced/controller/projects/members" "github.com/crossplane-contrib/provider-gitlab/pkg/namespaced/controller/projects/pipelineschedules" @@ -52,6 +53,7 @@ func Setup(mgr ctrl.Manager, o controller.Options) error { protectedbranches.SetupProtectedBranch, badges.SetupBadge, integrationmattermost.SetupIntegrationMattermost, + integrationgithub.SetupIntegrationGithub, protectedenvironments.SetupProtectedEnvironment, } { if err := setup(mgr, o); err != nil { @@ -78,6 +80,7 @@ func SetupGated(mgr ctrl.Manager, o controller.Options) error { protectedbranches.SetupProtectedBranchGated, badges.SetupBadgeGated, integrationmattermost.SetupIntegrationMattermostGated, + integrationgithub.SetupIntegrationGithubGated, protectedenvironments.SetupProtectedEnvironmentGated, } { if err := setup(mgr, o); err != nil {