github.com/SamarSidharth/kpt@v0.0.0-20231122062228-c7d747ae3ace/commands/alpha/sync/delete/command.go (about)

     1  // Copyright 2022 The kpt Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package delete
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"time"
    21  
    22  	"github.com/GoogleContainerTools/kpt/commands/util"
    23  	"github.com/GoogleContainerTools/kpt/internal/docs/generated/syncdocs"
    24  	"github.com/GoogleContainerTools/kpt/internal/errors"
    25  	"github.com/GoogleContainerTools/kpt/internal/util/porch"
    26  	"github.com/spf13/cobra"
    27  	coreapi "k8s.io/api/core/v1"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    30  	"k8s.io/apimachinery/pkg/runtime/schema"
    31  	"k8s.io/cli-runtime/pkg/genericclioptions"
    32  	"sigs.k8s.io/cli-utils/pkg/kstatus/status"
    33  	"sigs.k8s.io/controller-runtime/pkg/client"
    34  )
    35  
    36  const (
    37  	command         = "cmdsync.delete"
    38  	emptyRepo       = "https://github.com/platkrm/empty"
    39  	emptyRepoBranch = "main"
    40  	defaultTimeout  = 2 * time.Minute
    41  )
    42  
    43  var (
    44  	rootSyncGVK = schema.GroupVersionKind{
    45  		Group:   "configsync.gke.io",
    46  		Version: "v1beta1",
    47  		Kind:    "RootSync",
    48  	}
    49  	resourceGroupGVK = schema.GroupVersionKind{
    50  		Group:   "kpt.dev",
    51  		Version: "v1alpha1",
    52  		Kind:    "ResourceGroup",
    53  	}
    54  )
    55  
    56  func NewCommand(ctx context.Context, rcg *genericclioptions.ConfigFlags) *cobra.Command {
    57  	return newRunner(ctx, rcg).Command
    58  }
    59  
    60  func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner {
    61  	r := &runner{
    62  		ctx: ctx,
    63  		cfg: rcg,
    64  	}
    65  	c := &cobra.Command{
    66  		Use:     "del REPOSITORY [flags]",
    67  		Aliases: []string{"delete"},
    68  		Short:   syncdocs.DeleteShort,
    69  		Long:    syncdocs.DeleteShort + "\n" + syncdocs.DeleteLong,
    70  		Example: syncdocs.DeleteExamples,
    71  		PreRunE: r.preRunE,
    72  		RunE:    r.runE,
    73  		Hidden:  porch.HidePorchCommands,
    74  	}
    75  	r.Command = c
    76  
    77  	c.Flags().BoolVar(&r.keepSecret, "keep-auth-secret", false, "Keep the auth secret associated with the RootSync resource, if any")
    78  	c.Flags().DurationVar(&r.timeout, "timeout", defaultTimeout, "How long to wait for Config Sync to delete package RootSync")
    79  
    80  	return r
    81  }
    82  
    83  type runner struct {
    84  	ctx     context.Context
    85  	cfg     *genericclioptions.ConfigFlags
    86  	client  client.WithWatch
    87  	Command *cobra.Command
    88  
    89  	// Flags
    90  	keepSecret bool
    91  	timeout    time.Duration
    92  }
    93  
    94  func (r *runner) preRunE(_ *cobra.Command, _ []string) error {
    95  	const op errors.Op = command + ".preRunE"
    96  	client, err := porch.CreateDynamicClient(r.cfg)
    97  	if err != nil {
    98  		return errors.E(op, err)
    99  	}
   100  	r.client = client
   101  	return nil
   102  }
   103  
   104  func (r *runner) runE(_ *cobra.Command, args []string) error {
   105  	const op errors.Op = command + ".runE"
   106  
   107  	if len(args) == 0 {
   108  		return errors.E(op, fmt.Errorf("NAME is a required positional argument"))
   109  	}
   110  
   111  	name := args[0]
   112  	namespace := util.RootSyncNamespace
   113  	if *r.cfg.Namespace != "" {
   114  		namespace = *r.cfg.Namespace
   115  	}
   116  	key := client.ObjectKey{
   117  		Namespace: namespace,
   118  		Name:      name,
   119  	}
   120  	rs := unstructured.Unstructured{}
   121  	rs.SetGroupVersionKind(rootSyncGVK)
   122  	if err := r.client.Get(r.ctx, key, &rs); err != nil {
   123  		return errors.E(op, fmt.Errorf("cannot get %s: %v", key, err))
   124  	}
   125  
   126  	git, found, err := unstructured.NestedMap(rs.Object, "spec", "git")
   127  	if err != nil || !found {
   128  		return errors.E(op, fmt.Errorf("couldn't find `spec.git`: %v", err))
   129  	}
   130  
   131  	git["repo"] = emptyRepo
   132  	git["branch"] = emptyRepoBranch
   133  	git["dir"] = ""
   134  	git["revision"] = ""
   135  
   136  	if err := unstructured.SetNestedMap(rs.Object, git, "spec", "git"); err != nil {
   137  		return errors.E(op, err)
   138  	}
   139  
   140  	fmt.Println("Deleting synced resources..")
   141  	if err := r.client.Update(r.ctx, &rs); err != nil {
   142  		return errors.E(op, err)
   143  	}
   144  
   145  	if err := func() error {
   146  		ctx, cancel := context.WithTimeout(r.ctx, r.timeout)
   147  		defer cancel()
   148  
   149  		if err := r.waitForRootSync(ctx, name, namespace); err != nil {
   150  			return err
   151  		}
   152  
   153  		fmt.Println("Waiting for deleted resources to be removed..")
   154  		return r.waitForResourceGroup(ctx, name, namespace)
   155  	}(); err != nil {
   156  		// TODO: See if we can expose more information here about what might have prevented a package
   157  		// from being deleted.
   158  		e := fmt.Errorf("package %s failed to be deleted after %f seconds: %v", name, r.timeout.Seconds(), err)
   159  		return errors.E(op, e)
   160  	}
   161  
   162  	if err := r.client.Delete(r.ctx, &rs); err != nil {
   163  		return errors.E(op, fmt.Errorf("failed to clean up RootSync: %w", err))
   164  	}
   165  
   166  	rg := unstructured.Unstructured{}
   167  	rg.SetGroupVersionKind(resourceGroupGVK)
   168  	rg.SetName(rs.GetName())
   169  	rg.SetNamespace(rs.GetNamespace())
   170  	if err := r.client.Delete(r.ctx, &rg); err != nil {
   171  		return errors.E(op, fmt.Errorf("failed to clean up ResourceGroup: %w", err))
   172  	}
   173  
   174  	if r.keepSecret {
   175  		return nil
   176  	}
   177  
   178  	secret := getSecretName(&rs)
   179  	if secret == "" {
   180  		return nil
   181  	}
   182  
   183  	if err := r.client.Delete(r.ctx, &coreapi.Secret{
   184  		TypeMeta: metav1.TypeMeta{
   185  			Kind:       "Secret",
   186  			APIVersion: coreapi.SchemeGroupVersion.Identifier(),
   187  		},
   188  		ObjectMeta: metav1.ObjectMeta{
   189  			Name:      secret,
   190  			Namespace: namespace,
   191  		},
   192  	}); err != nil {
   193  		return errors.E(op, fmt.Errorf("failed to delete Secret %s: %w", secret, err))
   194  	}
   195  
   196  	fmt.Printf("Sync %s successfully deleted\n", name)
   197  	return nil
   198  }
   199  
   200  func (r *runner) waitForRootSync(ctx context.Context, name string, namespace string) error {
   201  	const op errors.Op = command + ".waitForRootSync"
   202  
   203  	return r.waitForResource(ctx, resourceGroupGVK, name, namespace, func(u *unstructured.Unstructured) (bool, error) {
   204  		res, err := status.Compute(u)
   205  		if err != nil {
   206  			return false, errors.E(op, err)
   207  		}
   208  		if res.Status == status.CurrentStatus {
   209  			return true, nil
   210  		}
   211  		return false, nil
   212  	})
   213  }
   214  
   215  func (r *runner) waitForResourceGroup(ctx context.Context, name string, namespace string) error {
   216  	const op errors.Op = command + ".waitForResourceGroup"
   217  
   218  	return r.waitForResource(ctx, resourceGroupGVK, name, namespace, func(u *unstructured.Unstructured) (bool, error) {
   219  		resources, found, err := unstructured.NestedSlice(u.Object, "spec", "resources")
   220  		if err != nil {
   221  			return false, errors.E(op, err)
   222  		}
   223  		if !found {
   224  			return true, nil
   225  		}
   226  		if len(resources) == 0 {
   227  			return true, nil
   228  		}
   229  		return false, nil
   230  	})
   231  }
   232  
   233  type ReconcileFunc func(*unstructured.Unstructured) (bool, error)
   234  
   235  func (r *runner) waitForResource(ctx context.Context, gvk schema.GroupVersionKind, name, namespace string, reconcileFunc ReconcileFunc) error {
   236  	const op errors.Op = command + ".waitForResource"
   237  
   238  	u := unstructured.UnstructuredList{}
   239  	u.SetGroupVersionKind(gvk)
   240  	watch, err := r.client.Watch(r.ctx, &u)
   241  	if err != nil {
   242  		return errors.E(op, err)
   243  	}
   244  	defer watch.Stop()
   245  
   246  	for {
   247  		select {
   248  		case ev, ok := <-watch.ResultChan():
   249  			if !ok {
   250  				return errors.E(op, fmt.Errorf("watch closed unexpectedly"))
   251  			}
   252  			if ev.Object == nil {
   253  				continue
   254  			}
   255  
   256  			u := ev.Object.(*unstructured.Unstructured)
   257  
   258  			if u.GetName() != name || u.GetNamespace() != namespace {
   259  				continue
   260  			}
   261  
   262  			reconciled, err := reconcileFunc(u)
   263  			if err != nil {
   264  				return err
   265  			}
   266  			if reconciled {
   267  				return nil
   268  			}
   269  		case <-ctx.Done():
   270  			return ctx.Err()
   271  		}
   272  	}
   273  }
   274  
   275  func getSecretName(repo *unstructured.Unstructured) string {
   276  	name, _, _ := unstructured.NestedString(repo.Object, "spec", "git", "secretRef", "name")
   277  	return name
   278  }