github.com/tilt-dev/tilt@v0.36.0/internal/cli/down.go (about) 1 package cli 2 3 import ( 4 "context" 5 "fmt" 6 "strings" 7 "time" 8 9 "github.com/pkg/errors" 10 "github.com/spf13/cobra" 11 "k8s.io/apimachinery/pkg/runtime/schema" 12 "k8s.io/apimachinery/pkg/types" 13 utilerrors "k8s.io/apimachinery/pkg/util/errors" 14 15 "github.com/tilt-dev/tilt/internal/analytics" 16 ctrltiltfile "github.com/tilt-dev/tilt/internal/controllers/apis/tiltfile" 17 "github.com/tilt-dev/tilt/internal/k8s" 18 "github.com/tilt-dev/tilt/internal/localexec" 19 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 20 "github.com/tilt-dev/tilt/pkg/logger" 21 "github.com/tilt-dev/tilt/pkg/model" 22 ) 23 24 type downCmd struct { 25 fileName string 26 deleteNamespaces bool 27 deleteVolumes bool 28 downDepsProvider func(ctx context.Context, tiltAnalytics *analytics.TiltAnalytics, subcommand model.TiltSubcommand) (DownDeps, error) 29 } 30 31 type dependencyNode struct { 32 manifest model.Manifest 33 dependents []*dependencyNode 34 processed bool 35 } 36 37 func newDownCmd() *downCmd { 38 return &downCmd{downDepsProvider: wireDownDeps} 39 } 40 41 func (c *downCmd) name() model.TiltSubcommand { return "down" } 42 43 func (c *downCmd) register() *cobra.Command { 44 cmd := &cobra.Command{ 45 Use: "down [<tilt flags>] [-- <Tiltfile args>]", 46 DisableFlagsInUseLine: true, 47 Short: "Delete resources created by 'tilt up'", 48 Long: ` 49 Deletes resources specified in the Tiltfile 50 51 Specify additional flags and arguments to control which resources are deleted. 52 53 Namespaces are not deleted by default. Use --delete-namespaces to change that. 54 55 Docker Volumes are not deleted by default. Use --delete-volumes to change that. 56 57 Kubernetes resources with the annotation 'tilt.dev/down-policy: keep' are not deleted. 58 59 For more complex cases, the Tiltfile has APIs to add additional flags and arguments to the Tilt CLI. 60 These arguments can be scripted to define custom subsets of resources to delete. 61 See https://docs.tilt.dev/tiltfile_config.html for examples. 62 `, 63 } 64 65 addTiltfileFlag(cmd, &c.fileName) 66 addKubeContextFlag(cmd) 67 addNamespaceFlag(cmd) 68 cmd.Flags().BoolVar(&c.deleteNamespaces, "delete-namespaces", false, "delete namespaces defined in the Tiltfile (by default, don't)") 69 cmd.Flags().BoolVar(&c.deleteVolumes, "delete-volumes", false, "delete docker volumes defined in the Tiltfile (by default, don't)") 70 71 return cmd 72 } 73 74 func (c *downCmd) run(ctx context.Context, args []string) error { 75 a := analytics.Get(ctx) 76 a.Incr("cmd.down", map[string]string{}) 77 defer a.Flush(time.Second) 78 79 downDeps, err := c.downDepsProvider(ctx, a, "down") 80 if err != nil { 81 return err 82 } 83 return c.down(ctx, downDeps, args) 84 } 85 86 func (c *downCmd) down(ctx context.Context, downDeps DownDeps, args []string) error { 87 tlr := downDeps.tfl.Load(ctx, ctrltiltfile.MainTiltfile(c.fileName, args), nil) 88 err := tlr.Error 89 if err != nil { 90 return err 91 } 92 93 sortedManifests := sortManifestsForDeletion(tlr.Manifests, tlr.EnabledManifests) 94 95 if err := deleteK8sEntities(ctx, sortedManifests, tlr.UpdateSettings, downDeps, c.deleteNamespaces); err != nil { 96 return err 97 } 98 99 dcProjects := make(map[string]v1alpha1.DockerComposeProject) 100 for _, m := range sortedManifests { 101 if !m.IsDC() { 102 continue 103 } 104 proj := m.DockerComposeTarget().Spec.Project 105 106 if _, exists := dcProjects[proj.Name]; !exists { 107 dcProjects[proj.Name] = proj 108 } 109 } 110 111 for _, dcProject := range dcProjects { 112 dcc := downDeps.dcClient 113 err = dcc.Down(ctx, dcProject, logger.Get(ctx).Writer(logger.InfoLvl), logger.Get(ctx).Writer(logger.InfoLvl), c.deleteVolumes) 114 if err != nil { 115 return errors.Wrap(err, "Running `docker-compose down`") 116 } 117 } 118 119 return nil 120 } 121 122 func sortManifestsForDeletion(manifests []model.Manifest, enabledManifests []model.ManifestName) []model.Manifest { 123 enabledNames := make(map[model.ManifestName]bool, len(enabledManifests)) 124 for _, n := range enabledManifests { 125 enabledNames[n] = true 126 } 127 128 nodes := []*dependencyNode{} 129 nodeMap := map[model.ManifestName]*dependencyNode{} 130 131 for i := range manifests { 132 manifest := manifests[len(manifests)-i-1] 133 134 node := &dependencyNode{ 135 manifest: manifest, 136 dependents: []*dependencyNode{}, 137 } 138 139 nodes = append(nodes, node) 140 nodeMap[manifest.Name] = node 141 } 142 143 for _, node := range nodes { 144 for _, resourceDep := range node.manifest.ResourceDependencies { 145 if dependency, ok := nodeMap[resourceDep]; ok { 146 dependency.dependents = append(dependency.dependents, node) 147 } 148 } 149 } 150 151 // The tiltfile loader returns all manifests, 152 // with the ones that weren't selected disabled. 153 var sortedManifests []model.Manifest 154 for _, node := range nodes { 155 for _, m := range manifestsForNode(node) { 156 if enabledNames[m.Name] { 157 sortedManifests = append(sortedManifests, m) 158 } 159 } 160 } 161 162 return sortedManifests 163 } 164 165 func manifestsForNode(node *dependencyNode) []model.Manifest { 166 if node.processed { 167 return []model.Manifest{} 168 } 169 170 node.processed = true 171 172 var manifests []model.Manifest 173 174 for _, dependent := range node.dependents { 175 manifests = append(manifests, manifestsForNode(dependent)...) 176 } 177 178 return append(manifests, node.manifest) 179 } 180 181 func deleteK8sEntities(ctx context.Context, manifests []model.Manifest, updateSettings model.UpdateSettings, downDeps DownDeps, deleteNamespaces bool) error { 182 kubeconfigWriter := downDeps.kubeconfigWriter 183 kClient := downDeps.kClient 184 185 entities, deleteCmds, err := k8sToDelete(manifests...) 186 if err != nil { 187 return errors.Wrap(err, "Parsing manifest YAML") 188 } 189 190 // If we need to inject the kubeconfig into external 191 // commands, freeze it first, so that we capture all the cli flags. 192 kubeconfigPath := "" 193 if len(deleteCmds) > 0 { 194 var err error 195 kubeconfigPath, err = kubeconfigWriter.WriteFrozenKubeConfig( 196 ctx, 197 types.NamespacedName{Name: v1alpha1.ClusterNameDefault}, 198 kClient.APIConfig()) 199 if err != nil { 200 return errors.Wrap(err, "Writing kubeconfig connection") 201 } 202 defer func() { 203 _ = downDeps.fs.Remove(kubeconfigPath) 204 }() 205 } 206 207 entities, _, err = k8s.Filter(entities, func(e k8s.K8sEntity) (b bool, err error) { 208 downPolicy, exists := e.Annotations()["tilt.dev/down-policy"] 209 return !exists || downPolicy != "keep", nil 210 }) 211 if err != nil { 212 return errors.Wrap(err, "Filtering entities by down policy") 213 } 214 215 if !deleteNamespaces { 216 var namespaces []k8s.K8sEntity 217 entities, namespaces, err = k8s.Filter(entities, func(e k8s.K8sEntity) (b bool, err error) { 218 return e.GVK() != schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"}, nil 219 }) 220 if err != nil { 221 return errors.Wrap(err, "filtering out namespaces") 222 } 223 if len(namespaces) > 0 { 224 var nsNames []string 225 for _, ns := range namespaces { 226 nsNames = append(nsNames, ns.Name()) 227 } 228 logger.Get(ctx).Infof("Not deleting namespaces: %s", strings.Join(nsNames, ", ")) 229 logger.Get(ctx).Infof("Run with --delete-namespaces to delete namespaces as well.") 230 } 231 } 232 233 errs := []error{} 234 if len(entities) > 0 { 235 dCtx, cancel := context.WithTimeout(ctx, updateSettings.K8sUpsertTimeout()) 236 err = downDeps.kClient.Delete(dCtx, entities, 0) 237 cancel() 238 if err != nil { 239 errs = append(errs, errors.Wrap(err, "Deleting k8s entities")) 240 } 241 } 242 243 for _, deleteCmd := range deleteCmds { 244 dCtx, cancel := context.WithTimeout(ctx, updateSettings.K8sUpsertTimeout()) 245 deleteCmd.Env = append(deleteCmd.Env, fmt.Sprintf("KUBECONFIG=%s", kubeconfigPath)) 246 err := localexec.OneShotToLogger(dCtx, downDeps.execer, deleteCmd) 247 cancel() 248 249 if err != nil { 250 errs = append(errs, errors.Wrapf(err, "Deleting k8s entities for cmd: %s", deleteCmd.String())) 251 } 252 } 253 254 return utilerrors.NewAggregate(errs) 255 } 256 257 func k8sToDelete(manifests ...model.Manifest) ([]k8s.K8sEntity, []model.Cmd, error) { 258 var allEntities []k8s.K8sEntity 259 var deleteCmds []model.Cmd 260 for _, m := range manifests { 261 if !m.IsK8s() { 262 continue 263 } 264 kt := m.K8sTarget() 265 266 if kt.DeleteCmd != nil { 267 deleteCmds = append(deleteCmds, model.Cmd{ 268 Argv: kt.DeleteCmd.Args, 269 Dir: kt.DeleteCmd.Dir, 270 Env: kt.DeleteCmd.Env, 271 }) 272 } else { 273 entities, err := k8s.ParseYAMLFromString(kt.YAML) 274 if err != nil { 275 return nil, nil, err 276 } 277 allEntities = append(allEntities, k8s.ReverseSortedEntities(entities)...) 278 } 279 } 280 return allEntities, deleteCmds, nil 281 }