github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/commands/alpha/sync/delete/command.go (about)

     1  // Copyright 2022 Google LLC
     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(cmd *cobra.Command, args []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(cmd *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  		if err := r.waitForResourceGroup(ctx, name, namespace); err != nil {
   155  			return err
   156  		}
   157  		return nil
   158  	}(); err != nil {
   159  		// TODO: See if we can expose more information here about what might have prevented a package
   160  		// from being deleted.
   161  		e := fmt.Errorf("package %s failed to be deleted after %f seconds: %v", name, r.timeout.Seconds(), err)
   162  		return errors.E(op, e)
   163  	}
   164  
   165  	if err := r.client.Delete(r.ctx, &rs); err != nil {
   166  		return errors.E(op, fmt.Errorf("failed to clean up RootSync: %w", err))
   167  	}
   168  
   169  	rg := unstructured.Unstructured{}
   170  	rg.SetGroupVersionKind(resourceGroupGVK)
   171  	rg.SetName(rs.GetName())
   172  	rg.SetNamespace(rs.GetNamespace())
   173  	if err := r.client.Delete(r.ctx, &rg); err != nil {
   174  		return errors.E(op, fmt.Errorf("failed to clean up ResourceGroup: %w", err))
   175  	}
   176  
   177  	if r.keepSecret {
   178  		return nil
   179  	}
   180  
   181  	secret := getSecretName(&rs)
   182  	if secret == "" {
   183  		return nil
   184  	}
   185  
   186  	if err := r.client.Delete(r.ctx, &coreapi.Secret{
   187  		TypeMeta: metav1.TypeMeta{
   188  			Kind:       "Secret",
   189  			APIVersion: coreapi.SchemeGroupVersion.Identifier(),
   190  		},
   191  		ObjectMeta: metav1.ObjectMeta{
   192  			Name:      secret,
   193  			Namespace: namespace,
   194  		},
   195  	}); err != nil {
   196  		return errors.E(op, fmt.Errorf("failed to delete Secret %s: %w", secret, err))
   197  	}
   198  
   199  	fmt.Printf("Sync %s successfully deleted\n", name)
   200  	return nil
   201  }
   202  
   203  func (r *runner) waitForRootSync(ctx context.Context, name string, namespace string) error {
   204  	const op errors.Op = command + ".waitForRootSync"
   205  
   206  	return r.waitForResource(ctx, resourceGroupGVK, name, namespace, func(u *unstructured.Unstructured) (bool, error) {
   207  		res, err := status.Compute(u)
   208  		if err != nil {
   209  			return false, errors.E(op, err)
   210  		}
   211  		if res.Status == status.CurrentStatus {
   212  			return true, nil
   213  		}
   214  		return false, nil
   215  	})
   216  }
   217  
   218  func (r *runner) waitForResourceGroup(ctx context.Context, name string, namespace string) error {
   219  	const op errors.Op = command + ".waitForResourceGroup"
   220  
   221  	return r.waitForResource(ctx, resourceGroupGVK, name, namespace, func(u *unstructured.Unstructured) (bool, error) {
   222  		resources, found, err := unstructured.NestedSlice(u.Object, "spec", "resources")
   223  		if err != nil {
   224  			return false, errors.E(op, err)
   225  		}
   226  		if !found {
   227  			return true, nil
   228  		}
   229  		if len(resources) == 0 {
   230  			return true, nil
   231  		}
   232  		return false, nil
   233  	})
   234  }
   235  
   236  type ReconcileFunc func(*unstructured.Unstructured) (bool, error)
   237  
   238  func (r *runner) waitForResource(ctx context.Context, gvk schema.GroupVersionKind, name, namespace string, reconcileFunc ReconcileFunc) error {
   239  	const op errors.Op = command + ".waitForResource"
   240  
   241  	u := unstructured.UnstructuredList{}
   242  	u.SetGroupVersionKind(gvk)
   243  	watch, err := r.client.Watch(r.ctx, &u)
   244  	if err != nil {
   245  		return errors.E(op, err)
   246  	}
   247  	defer watch.Stop()
   248  
   249  	for {
   250  		select {
   251  		case ev, ok := <-watch.ResultChan():
   252  			if !ok {
   253  				return errors.E(op, fmt.Errorf("watch closed unexpectedly"))
   254  			}
   255  			if ev.Object == nil {
   256  				continue
   257  			}
   258  
   259  			u := ev.Object.(*unstructured.Unstructured)
   260  
   261  			if u.GetName() != name || u.GetNamespace() != namespace {
   262  				continue
   263  			}
   264  
   265  			reconciled, err := reconcileFunc(u)
   266  			if err != nil {
   267  				return err
   268  			}
   269  			if reconciled {
   270  				return nil
   271  			}
   272  		case <-ctx.Done():
   273  			return ctx.Err()
   274  		}
   275  	}
   276  }
   277  
   278  func getSecretName(repo *unstructured.Unstructured) string {
   279  	name, _, _ := unstructured.NestedString(repo.Object, "spec", "git", "secretRef", "name")
   280  	return name
   281  }