Skip to content

Commit

Permalink
clusterctl: validate no objects exist from CRDs before deleting them
Browse files Browse the repository at this point in the history
  • Loading branch information
chrischdi authored and sbueringer committed Dec 8, 2023
1 parent b2239c6 commit 6e6430c
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 0 deletions.
59 changes: 59 additions & 0 deletions cmd/clusterctl/client/cluster/components.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@ limitations under the License.
package cluster

import (
"context"
"fmt"
"strings"

"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
kerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -65,6 +68,9 @@ type ComponentsClient interface {
// DeleteWebhookNamespace deletes the core provider webhook namespace (eg. capi-webhook-system).
// This is required when upgrading to v1alpha4 where webhooks are included in the controller itself.
DeleteWebhookNamespace() error

// ValidateNoObjectsExist checks if custom resources of the custom resource definitions exist and returns an error if so.
ValidateNoObjectsExist(ctx context.Context, provider clusterctlv1.Provider) error
}

// providerComponents implements ComponentsClient.
Expand Down Expand Up @@ -256,6 +262,59 @@ func (p *providerComponents) DeleteWebhookNamespace() error {
return nil
}

func (p *providerComponents) ValidateNoObjectsExist(ctx context.Context, provider clusterctlv1.Provider) error {
log := logf.Log
log.Info("Checking for CRs", "Provider", provider.Name, "Version", provider.Version, "Namespace", provider.Namespace)

proxyClient, err := p.proxy.NewClient()
if err != nil {
return err
}

// Fetch all the components belonging to a provider.
// We want that the delete operation is able to clean-up everything.
labels := map[string]string{
clusterctlv1.ClusterctlLabel: "",
clusterv1.ProviderNameLabel: provider.ManifestLabel(),
}

customResources := &apiextensionsv1.CustomResourceDefinitionList{}
if err := proxyClient.List(ctx, customResources, client.MatchingLabels(labels)); err != nil {
return err
}

// Filter the resources according to the delete options
crsHavingObjects := []string{}
for _, crd := range customResources.Items {
crd := crd
storageVersion, err := storageVersionForCRD(&crd)
if err != nil {
return err
}

list := &unstructured.UnstructuredList{}
list.SetGroupVersionKind(schema.GroupVersionKind{
Group: crd.Spec.Group,
Version: storageVersion,
Kind: crd.Spec.Names.ListKind,
})

if err := proxyClient.List(ctx, list); err != nil {
return err
}

if len(list.Items) > 0 {
crsHavingObjects = append(crsHavingObjects, crd.Kind)
}
}

if len(crsHavingObjects) > 0 {
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, ", "))
}

return nil
}

// newComponentsClient returns a providerComponents.
func newComponentsClient(proxy Proxy) *providerComponents {
return &providerComponents{
Expand Down
76 changes: 76 additions & 0 deletions cmd/clusterctl/client/cluster/components_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
Expand Down Expand Up @@ -494,3 +495,78 @@ func Test_providerComponents_Create(t *testing.T) {
})
}
}

func Test_providerComponents_ValidateNoObjectsExist(t *testing.T) {
labels := map[string]string{
clusterv1.ProviderNameLabel: "infrastructure-infra",
}

crd := &apiextensionsv1.CustomResourceDefinition{
TypeMeta: metav1.TypeMeta{
Kind: "CustomResourceDefinition",
APIVersion: apiextensionsv1.SchemeGroupVersion.Identifier(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "crd1",
Labels: labels,
},
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
Group: "some.group",
Names: apiextensionsv1.CustomResourceDefinitionNames{
ListKind: "SomeCRDList",
Kind: "SomeCRD",
},
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
{Name: "v1", Storage: true},
},
},
}
crd.ObjectMeta.Labels[clusterctlv1.ClusterctlLabel] = ""

cr := &unstructured.Unstructured{}
cr.SetAPIVersion("some.group/v1")
cr.SetKind("SomeCRD")
cr.SetName("cr1")

tests := []struct {
name string
provider clusterctlv1.Provider
initObjs []client.Object
wantErr bool
}{
{
name: "No objects exist",
provider: clusterctlv1.Provider{ObjectMeta: metav1.ObjectMeta{Name: "infrastructure-infra", Namespace: "ns1"}, ProviderName: "infra", Type: string(clusterctlv1.InfrastructureProviderType)},
initObjs: []client.Object{},
wantErr: false,
},
{
name: "CRD exists but no objects",
provider: clusterctlv1.Provider{ObjectMeta: metav1.ObjectMeta{Name: "infrastructure-infra", Namespace: "ns1"}, ProviderName: "infra", Type: string(clusterctlv1.InfrastructureProviderType)},
initObjs: []client.Object{
crd,
},
wantErr: false,
},
{
name: "CRD exists but and also objects",
provider: clusterctlv1.Provider{ObjectMeta: metav1.ObjectMeta{Name: "infrastructure-infra", Namespace: "ns1"}, ProviderName: "infra", Type: string(clusterctlv1.InfrastructureProviderType)},
initObjs: []client.Object{
crd,
cr,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
proxy := test.NewFakeProxy().WithObjs(tt.initObjs...)

c := newComponentsClient(proxy)

if err := c.ValidateNoObjectsExist(context.Background(), tt.provider); (err != nil) != tt.wantErr {

Check failure on line 567 in cmd/clusterctl/client/cluster/components_test.go

View workflow job for this annotation

GitHub Actions / lint

undefined: context (typecheck)
t.Errorf("providerComponents.ValidateNoObjectsExist() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
16 changes: 16 additions & 0 deletions cmd/clusterctl/client/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ limitations under the License.
package client

import (
"context"

"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kerrors "k8s.io/apimachinery/pkg/util/errors"

clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster"
Expand Down Expand Up @@ -154,6 +157,19 @@ func (c *clusterctlClient) Delete(options DeleteOptions) error {
}
}

if options.IncludeCRDs {
errList := []error{}
for _, provider := range providersToDelete {
err = clusterClient.ProviderComponents().ValidateNoObjectsExist(context.TODO(), provider)
if err != nil {
errList = append(errList, err)
}
}
if len(errList) > 0 {
return kerrors.NewAggregate(errList)
}
}

// Delete the selected providers.
for _, provider := range providersToDelete {
if err := clusterClient.ProviderComponents().Delete(cluster.DeleteOptions{Provider: provider, IncludeNamespace: options.IncludeNamespace, IncludeCRDs: options.IncludeCRDs, SkipInventory: options.SkipInventory}); err != nil {
Expand Down
21 changes: 21 additions & 0 deletions cmd/clusterctl/client/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,27 @@ func Test_clusterctlClient_Delete(t *testing.T) {
wantProviders: sets.Set[string]{},
wantErr: false,
},
{
name: "Delete all the providers including CRDs",
fields: fields{
client: fakeClusterForDelete(),
},
args: args{
options: DeleteOptions{
Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"},
IncludeNamespace: false,
IncludeCRDs: true,
SkipInventory: false,
CoreProvider: "",
BootstrapProviders: nil,
InfrastructureProviders: nil,
ControlPlaneProviders: nil,
DeleteAll: true, // delete all the providers
},
},
wantProviders: sets.Set[string]{},
wantErr: false,
},
{
name: "Delete single provider auto-detect namespace",
fields: fields{
Expand Down

0 comments on commit 6e6430c

Please sign in to comment.