Skip to content

Commit b8f1cc0

Browse files
committed
clusterctl: validate no objects exist from CRDs before deleting them
1 parent e401cf0 commit b8f1cc0

File tree

4 files changed

+169
-0
lines changed

4 files changed

+169
-0
lines changed

cmd/clusterctl/client/cluster/components.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ import (
2323

2424
"github.com/pkg/errors"
2525
corev1 "k8s.io/api/core/v1"
26+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2627
apierrors "k8s.io/apimachinery/pkg/api/errors"
2728
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2829
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
30+
"k8s.io/apimachinery/pkg/runtime/schema"
2931
kerrors "k8s.io/apimachinery/pkg/util/errors"
3032
"k8s.io/apimachinery/pkg/util/sets"
3133
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -66,6 +68,9 @@ type ComponentsClient interface {
6668
// DeleteWebhookNamespace deletes the core provider webhook namespace (eg. capi-webhook-system).
6769
// This is required when upgrading to v1alpha4 where webhooks are included in the controller itself.
6870
DeleteWebhookNamespace(ctx context.Context) error
71+
72+
// ValidateNoObjectsExist checks if custom resources of the custom resource definitions exist and returns an error if so.
73+
ValidateNoObjectsExist(ctx context.Context, provider clusterctlv1.Provider) error
6974
}
7075

7176
// providerComponents implements ComponentsClient.
@@ -257,6 +262,59 @@ func (p *providerComponents) DeleteWebhookNamespace(ctx context.Context) error {
257262
return nil
258263
}
259264

265+
func (p *providerComponents) ValidateNoObjectsExist(ctx context.Context, provider clusterctlv1.Provider) error {
266+
log := logf.Log
267+
log.Info("Checking for CRs", "Provider", provider.Name, "Version", provider.Version, "Namespace", provider.Namespace)
268+
269+
proxyClient, err := p.proxy.NewClient(ctx)
270+
if err != nil {
271+
return err
272+
}
273+
274+
// Fetch all the components belonging to a provider.
275+
// We want that the delete operation is able to clean-up everything.
276+
labels := map[string]string{
277+
clusterctlv1.ClusterctlLabel: "",
278+
clusterv1.ProviderNameLabel: provider.ManifestLabel(),
279+
}
280+
281+
customResources := &apiextensionsv1.CustomResourceDefinitionList{}
282+
if err := proxyClient.List(ctx, customResources, client.MatchingLabels(labels)); err != nil {
283+
return err
284+
}
285+
286+
// Filter the resources according to the delete options
287+
crsHavingObjects := []string{}
288+
for _, crd := range customResources.Items {
289+
crd := crd
290+
storageVersion, err := storageVersionForCRD(&crd)
291+
if err != nil {
292+
return err
293+
}
294+
295+
list := &unstructured.UnstructuredList{}
296+
list.SetGroupVersionKind(schema.GroupVersionKind{
297+
Group: crd.Spec.Group,
298+
Version: storageVersion,
299+
Kind: crd.Spec.Names.ListKind,
300+
})
301+
302+
if err := proxyClient.List(ctx, list); err != nil {
303+
return err
304+
}
305+
306+
if len(list.Items) > 0 {
307+
crsHavingObjects = append(crsHavingObjects, crd.Kind)
308+
}
309+
}
310+
311+
if len(crsHavingObjects) > 0 {
312+
return fmt.Errorf("found existing objects for provider CRDs %q: [%s]. Please delete these objects first before running clusterctl delete with --include-crd", provider.GetName(), strings.Join(crsHavingObjects, ", "))
313+
}
314+
315+
return nil
316+
}
317+
260318
// newComponentsClient returns a providerComponents.
261319
func newComponentsClient(proxy Proxy) *providerComponents {
262320
return &providerComponents{

cmd/clusterctl/client/cluster/components_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
. "github.com/onsi/gomega"
2626
corev1 "k8s.io/api/core/v1"
2727
rbacv1 "k8s.io/api/rbac/v1"
28+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2829
apierrors "k8s.io/apimachinery/pkg/api/errors"
2930
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3031
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -495,3 +496,78 @@ func Test_providerComponents_Create(t *testing.T) {
495496
})
496497
}
497498
}
499+
500+
func Test_providerComponents_ValidateNoObjectsExist(t *testing.T) {
501+
labels := map[string]string{
502+
clusterv1.ProviderNameLabel: "infrastructure-infra",
503+
}
504+
505+
crd := &apiextensionsv1.CustomResourceDefinition{
506+
TypeMeta: metav1.TypeMeta{
507+
Kind: "CustomResourceDefinition",
508+
APIVersion: apiextensionsv1.SchemeGroupVersion.Identifier(),
509+
},
510+
ObjectMeta: metav1.ObjectMeta{
511+
Name: "crd1",
512+
Labels: labels,
513+
},
514+
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
515+
Group: "some.group",
516+
Names: apiextensionsv1.CustomResourceDefinitionNames{
517+
ListKind: "SomeCRDList",
518+
Kind: "SomeCRD",
519+
},
520+
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
521+
{Name: "v1", Storage: true},
522+
},
523+
},
524+
}
525+
crd.ObjectMeta.Labels[clusterctlv1.ClusterctlLabel] = ""
526+
527+
cr := &unstructured.Unstructured{}
528+
cr.SetAPIVersion("some.group/v1")
529+
cr.SetKind("SomeCRD")
530+
cr.SetName("cr1")
531+
532+
tests := []struct {
533+
name string
534+
provider clusterctlv1.Provider
535+
initObjs []client.Object
536+
wantErr bool
537+
}{
538+
{
539+
name: "No objects exist",
540+
provider: clusterctlv1.Provider{ObjectMeta: metav1.ObjectMeta{Name: "infrastructure-infra", Namespace: "ns1"}, ProviderName: "infra", Type: string(clusterctlv1.InfrastructureProviderType)},
541+
initObjs: []client.Object{},
542+
wantErr: false,
543+
},
544+
{
545+
name: "CRD exists but no objects",
546+
provider: clusterctlv1.Provider{ObjectMeta: metav1.ObjectMeta{Name: "infrastructure-infra", Namespace: "ns1"}, ProviderName: "infra", Type: string(clusterctlv1.InfrastructureProviderType)},
547+
initObjs: []client.Object{
548+
crd,
549+
},
550+
wantErr: false,
551+
},
552+
{
553+
name: "CRD exists but and also objects",
554+
provider: clusterctlv1.Provider{ObjectMeta: metav1.ObjectMeta{Name: "infrastructure-infra", Namespace: "ns1"}, ProviderName: "infra", Type: string(clusterctlv1.InfrastructureProviderType)},
555+
initObjs: []client.Object{
556+
crd,
557+
cr,
558+
},
559+
wantErr: true,
560+
},
561+
}
562+
for _, tt := range tests {
563+
t.Run(tt.name, func(t *testing.T) {
564+
proxy := test.NewFakeProxy().WithObjs(tt.initObjs...)
565+
566+
c := newComponentsClient(proxy)
567+
568+
if err := c.ValidateNoObjectsExist(context.Background(), tt.provider); (err != nil) != tt.wantErr {
569+
t.Errorf("providerComponents.ValidateNoObjectsExist() error = %v, wantErr %v", err, tt.wantErr)
570+
}
571+
})
572+
}
573+
}

cmd/clusterctl/client/delete.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"github.com/pkg/errors"
2323
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
kerrors "k8s.io/apimachinery/pkg/util/errors"
2425

2526
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
2627
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster"
@@ -156,6 +157,19 @@ func (c *clusterctlClient) Delete(ctx context.Context, options DeleteOptions) er
156157
}
157158
}
158159

160+
if options.IncludeCRDs {
161+
errList := []error{}
162+
for _, provider := range providersToDelete {
163+
err = clusterClient.ProviderComponents().ValidateNoObjectsExist(ctx, provider)
164+
if err != nil {
165+
errList = append(errList, err)
166+
}
167+
}
168+
if len(errList) > 0 {
169+
return kerrors.NewAggregate(errList)
170+
}
171+
}
172+
159173
// Delete the selected providers.
160174
for _, provider := range providersToDelete {
161175
if err := clusterClient.ProviderComponents().Delete(ctx, cluster.DeleteOptions{Provider: provider, IncludeNamespace: options.IncludeNamespace, IncludeCRDs: options.IncludeCRDs, SkipInventory: options.SkipInventory}); err != nil {

cmd/clusterctl/client/delete_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,27 @@ func Test_clusterctlClient_Delete(t *testing.T) {
6767
wantProviders: sets.Set[string]{},
6868
wantErr: false,
6969
},
70+
{
71+
name: "Delete all the providers including CRDs",
72+
fields: fields{
73+
client: fakeClusterForDelete(),
74+
},
75+
args: args{
76+
options: DeleteOptions{
77+
Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"},
78+
IncludeNamespace: false,
79+
IncludeCRDs: true,
80+
SkipInventory: false,
81+
CoreProvider: "",
82+
BootstrapProviders: nil,
83+
InfrastructureProviders: nil,
84+
ControlPlaneProviders: nil,
85+
DeleteAll: true, // delete all the providers
86+
},
87+
},
88+
wantProviders: sets.Set[string]{},
89+
wantErr: false,
90+
},
7091
{
7192
name: "Delete single provider auto-detect namespace",
7293
fields: fields{

0 commit comments

Comments
 (0)