diff --git a/cmd/nitro/main.go b/cmd/nitro/main.go index a1c663ec..56a33222 100644 --- a/cmd/nitro/main.go +++ b/cmd/nitro/main.go @@ -11,6 +11,7 @@ import ( "github.com/craftcms/nitro/command/add" "github.com/craftcms/nitro/command/apply" + "github.com/craftcms/nitro/command/clean" "github.com/craftcms/nitro/command/completion" "github.com/craftcms/nitro/command/composer" "github.com/craftcms/nitro/command/context" @@ -124,6 +125,7 @@ func init() { npm.New(client, term), completion.New(), apply.New(home, client, nitrod, term), + clean.NewCommand(home, client, term), context.New(home, client, term), trust.New(client, term), version.New(client, nitrod, term), diff --git a/command/clean/clean.go b/command/clean/clean.go new file mode 100644 index 00000000..bafcbfb5 --- /dev/null +++ b/command/clean/clean.go @@ -0,0 +1,126 @@ +package clean + +import ( + "fmt" + "strings" + + "github.com/craftcms/nitro/config" + "github.com/craftcms/nitro/labels" + "github.com/craftcms/nitro/terminal" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/client" + "github.com/spf13/cobra" +) + +var ( + // ErrExample is used when we want to share an error + ErrExample = fmt.Errorf("some example error") +) + +const exampleText = ` # remove unused containers + nitro clean` + +func NewCommand(home string, docker client.CommonAPIClient, output terminal.Outputer) *cobra.Command { + cmd := &cobra.Command{ + Use: "clean", + Short: "Remove unused containers", + Example: exampleText, + RunE: func(cmd *cobra.Command, args []string) error { + env := cmd.Flag("environment").Value.String() + + // load the config file + cfg, err := config.Load(home, env) + if err != nil { + return fmt.Errorf("unable to load config, %w", err) + } + + output.Info("Cleaning up...") + + output.Pending("gathering details") + + // get a list of the known containers + known := make(map[string]bool) + + // get all current sites + for _, s := range cfg.Sites { + known[s.Hostname] = true + } + + // get all current databases + for _, d := range cfg.Databases { + hostname, err := d.GetHostname() + if err != nil { + return err + } + + known[hostname] = true + } + + // get all of the containers for the environment + filter := filters.NewArgs() + filter.Add("label", labels.Environment+"="+env) + containers, err := docker.ContainerList(cmd.Context(), types.ContainerListOptions{All: true, Filters: filter}) + if err != nil { + return err + } + + // check if each container exists + remove := make(map[string]types.Container) + for _, c := range containers { + name := strings.TrimLeft(c.Names[0], "/") + + // check if the container is a known container + if _, ok := known[name]; ok { + continue + } + + // if this is a proxy container + if c.Labels[labels.Proxy] == env { + continue + } + + // we should remove the container + remove[name] = c + } + + output.Done() + + // if there is nothing to remove don't remove it + if len(remove) == 0 { + output.Info("Nothing to remove ๐Ÿ˜…") + + return nil + } + + // remove each of the containers + for name, c := range remove { + output.Pending("removing", name) + + // TODO(jasonmccallister) if this is a database container + + // stop the container + if err := docker.ContainerStop(cmd.Context(), c.ID, nil); err != nil { + output.Warning() + output.Info(err.Error()) + continue + } + + // remove the container + if err := docker.ContainerRemove(cmd.Context(), c.ID, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil { + output.Warning() + output.Info(err.Error()) + continue + } + + output.Done() + } + + output.Info("Cleanup completed ๐Ÿงน") + + return nil + }, + } + + return cmd +}