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  }