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