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 }