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  }