diff --git a/apis/groups/v1alpha1/group_types.go b/apis/groups/v1alpha1/group_types.go index 21b8dfc..b5c6265 100644 --- a/apis/groups/v1alpha1/group_types.go +++ b/apis/groups/v1alpha1/group_types.go @@ -158,6 +158,16 @@ type GroupParameters struct { // SharedWithGroups create links for sharing a group with another group. // +optional SharedWithGroups []SharedWithGroups `json:"sharedWithGroups,omitempty"` + + // Force the immediate deletion of the group when removed. In GitLab Premium and Ultimate a group is by default + // just marked for deletion and removed permanently after seven days. Defaults to false. + // +optional + PermanentlyRemove *bool `json:"permanentlyRemove,omitempty"` + + // Full path of group to delete permanently. Only required if PermanentlyRemove is set to true. + // GitLab Premium and Ultimate only. + // +optional + FullPathToRemove *string `json:"fullPathToRemove,omitempty"` } // AccessLevelValue represents a permission level within GitLab. diff --git a/apis/groups/v1alpha1/zz_generated.deepcopy.go b/apis/groups/v1alpha1/zz_generated.deepcopy.go index 56a3320..7da1d18 100644 --- a/apis/groups/v1alpha1/zz_generated.deepcopy.go +++ b/apis/groups/v1alpha1/zz_generated.deepcopy.go @@ -598,6 +598,16 @@ func (in *GroupParameters) DeepCopyInto(out *GroupParameters) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.PermanentlyRemove != nil { + in, out := &in.PermanentlyRemove, &out.PermanentlyRemove + *out = new(bool) + **out = **in + } + if in.FullPathToRemove != nil { + in, out := &in.FullPathToRemove, &out.FullPathToRemove + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupParameters. diff --git a/package/crds/groups.gitlab.crossplane.io_groups.yaml b/package/crds/groups.gitlab.crossplane.io_groups.yaml index d44f7cf..93d1e8f 100644 --- a/package/crds/groups.gitlab.crossplane.io_groups.yaml +++ b/package/crds/groups.gitlab.crossplane.io_groups.yaml @@ -95,6 +95,11 @@ spec: description: Extra pipeline minutes quota for this group (purchased in addition to the minutes included in the plan). type: integer + fullPathToRemove: + description: |- + Full path of group to delete permanently. Only required if PermanentlyRemove is set to true. + GitLab Premium and Ultimate only. + type: string lfsEnabled: description: Enable/disable Large File Storage (LFS) for the projects in this group. @@ -194,6 +199,11 @@ spec: path: description: The path of the group. type: string + permanentlyRemove: + description: |- + Force the immediate deletion of the group when removed. In GitLab Premium and Ultimate a group is by default + just marked for deletion and removed permanently after seven days. Defaults to false. + type: boolean projectCreationLevel: description: |- developers can create projects in the group. diff --git a/pkg/controller/groups/groups/group.go b/pkg/controller/groups/groups/group.go index 45909b8..df1f8aa 100644 --- a/pkg/controller/groups/groups/group.go +++ b/pkg/controller/groups/groups/group.go @@ -19,8 +19,14 @@ package groups import ( "context" "strconv" + "strings" "time" + "github.com/crossplane-contrib/provider-gitlab/apis/groups/v1alpha1" + secretstoreapi "github.com/crossplane-contrib/provider-gitlab/apis/v1alpha1" + "github.com/crossplane-contrib/provider-gitlab/pkg/clients" + "github.com/crossplane-contrib/provider-gitlab/pkg/clients/groups" + "github.com/crossplane-contrib/provider-gitlab/pkg/features" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" "github.com/crossplane/crossplane-runtime/pkg/connection" "github.com/crossplane/crossplane-runtime/pkg/controller" @@ -34,12 +40,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/crossplane-contrib/provider-gitlab/apis/groups/v1alpha1" - secretstoreapi "github.com/crossplane-contrib/provider-gitlab/apis/v1alpha1" - "github.com/crossplane-contrib/provider-gitlab/pkg/clients" - "github.com/crossplane-contrib/provider-gitlab/pkg/clients/groups" - "github.com/crossplane-contrib/provider-gitlab/pkg/features" ) const ( @@ -237,6 +237,17 @@ func (e *external) Delete(ctx context.Context, mg resource.Managed) error { } _, err := e.client.DeleteGroup(meta.GetExternalName(cr), &gitlab.DeleteGroupOptions{}, gitlab.WithContext(ctx)) + // if the group is for some reason already marked for deletion, we ignore the error and continue to delete the group permanently + if err != nil && !strings.Contains(err.Error(), "Group has been already marked for deletion") { + return errors.Wrap(err, errDeleteFailed) + } + + if cr.Spec.ForProvider.PermanentlyRemove != nil && *cr.Spec.ForProvider.PermanentlyRemove { + _, err = e.client.DeleteGroup(meta.GetExternalName(cr), &gitlab.DeleteGroupOptions{ + PermanentlyRemove: cr.Spec.ForProvider.PermanentlyRemove, + FullPath: cr.Spec.ForProvider.FullPathToRemove, + }, gitlab.WithContext(ctx)) + } return errors.Wrap(err, errDeleteFailed) } diff --git a/pkg/controller/groups/groups/group_test.go b/pkg/controller/groups/groups/group_test.go index 11d20da..9d75001 100644 --- a/pkg/controller/groups/groups/group_test.go +++ b/pkg/controller/groups/groups/group_test.go @@ -82,6 +82,14 @@ func withPath(s string) groupModifier { return func(r *v1alpha1.Group) { r.Spec.ForProvider.Path = s } } +func withPermanentlyRemove(b *bool) groupModifier { + return func(r *v1alpha1.Group) { r.Spec.ForProvider.PermanentlyRemove = b } +} + +func withFullPathToRemove(s *string) groupModifier { + return func(r *v1alpha1.Group) { r.Spec.ForProvider.FullPathToRemove = s } +} + func withDescription(s *string) groupModifier { return func(r *v1alpha1.Group) { r.Spec.ForProvider.Description = s } } @@ -844,9 +852,16 @@ func TestUpdate(t *testing.T) { } func TestDelete(t *testing.T) { + type deleteGroupCalls struct { + Pid interface{} + Opt *gitlab.DeleteGroupOptions + } + var recordedCalls []deleteGroupCalls + type want struct { - cr resource.Managed - err error + cr resource.Managed + calls []deleteGroupCalls + err error } cases := map[string]struct { @@ -866,21 +881,23 @@ func TestDelete(t *testing.T) { args: args{ group: &fake.MockClient{ MockDeleteGroup: func(pid interface{}, opt *gitlab.DeleteGroupOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) { + recordedCalls = append(recordedCalls, deleteGroupCalls{Pid: pid, Opt: opt}) return &gitlab.Response{}, nil }, }, cr: group(withExternalName("0")), }, want: want{ - cr: group(withExternalName("0")), - err: nil, + cr: group(withExternalName("0")), + calls: []deleteGroupCalls{{Pid: "0", Opt: &gitlab.DeleteGroupOptions{}}}, + err: nil, }, }, "FailedDeletion": { args: args{ group: &fake.MockClient{ MockDeleteGroup: func(pid interface{}, opt *gitlab.DeleteGroupOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) { - return &gitlab.Response{}, errBoom + return nil, errBoom }, }, cr: group(), @@ -890,9 +907,35 @@ func TestDelete(t *testing.T) { err: errors.Wrap(errBoom, errDeleteFailed), }, }, + "SuccessfulPermanentlyDeletion": { + args: args{ + group: &fake.MockClient{ + MockDeleteGroup: func(pid interface{}, opt *gitlab.DeleteGroupOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) { + recordedCalls = append(recordedCalls, deleteGroupCalls{Pid: pid, Opt: opt}) + return &gitlab.Response{}, nil + }, + }, + cr: group( + withExternalName("0"), + withPermanentlyRemove(gitlab.Ptr(true)), + withFullPathToRemove(gitlab.Ptr("path/to/remove"))), + }, + want: want{ + cr: group( + withExternalName("0"), + withPermanentlyRemove(gitlab.Ptr(true)), + withFullPathToRemove(gitlab.Ptr("path/to/remove"))), + calls: []deleteGroupCalls{ + {Pid: "0", Opt: &gitlab.DeleteGroupOptions{}}, + {Pid: "0", Opt: &gitlab.DeleteGroupOptions{PermanentlyRemove: gitlab.Ptr(true), FullPath: gitlab.Ptr("path/to/remove")}}, + }, + err: nil, + }, + }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { + recordedCalls = nil e := &external{kube: tc.kube, client: tc.group} err := e.Delete(context.Background(), tc.args.cr) @@ -902,6 +945,9 @@ func TestDelete(t *testing.T) { if diff := cmp.Diff(tc.want.cr, tc.args.cr, test.EquateConditions()); diff != "" { t.Errorf("r: -want, +got:\n%s", diff) } + if diff := cmp.Diff(tc.want.calls, recordedCalls, test.EquateConditions()); diff != "" { + t.Errorf("r: -want, +got:\n%s", diff) + } }) } }