diff --git a/src/cmd/prompts.go b/src/cmd/prompts.go index 99d8332..1960e8c 100644 --- a/src/cmd/prompts.go +++ b/src/cmd/prompts.go @@ -12,26 +12,67 @@ func GetOrPromptContextNames(contexts yey.Contexts, names []string) ([]string, e availableNames := contexts.GetNamesInAllLayers() // Prompt unspecified names - for i := len(names); i < len(contexts.Layers); i++ { + for layer := len(names); layer < len(contexts.Layers); layer++ { // Don't prompt when single name in layer - if len(availableNames[i]) == 1 { - names = append(names, availableNames[i][0]) + if len(availableNames[layer]) == 1 { + names = append(names, availableNames[layer][0]) continue } prompt := &survey.Select{ - Message: fmt.Sprintf("Select %s", contexts.Layers[i].Name), - Options: availableNames[i], + Message: fmt.Sprintf("Select %s", contexts.Layers[layer].Name), + Options: availableNames[layer], } - selectedIndex := 0 - if err := survey.AskOne(prompt, &selectedIndex); err != nil { + var selectedName string + if err := survey.AskOne(prompt, &selectedName); err != nil { return nil, err } - names = append(names, availableNames[i][selectedIndex]) + names = append(names, selectedName) } return names, nil } +// Parses given value into context name and variant and, as needed, prompt user for those values +func GetOrPromptMultipleContextNames(contexts yey.Contexts, names []string, predicate func(name string, layer int) bool) ([][]string, error) { + availableNames := contexts.GetNamesInAllLayers() + + outputNames := make([][]string, 0, len(contexts.Layers)) + for layer := 0; layer < len(contexts.Layers); layer++ { + // Context name for this layer already specified by user? + if layer < len(names) { + // Just take name specified by user + outputNames = append(outputNames, []string{names[layer]}) + } else { + // Filter context names through predicate + filteredNames := make([]string, 0, len(availableNames[layer])) + for _, name := range availableNames[layer] { + if predicate(name, layer) { + filteredNames = append(filteredNames, name) + } + } + + // Don't prompt when single option + if len(filteredNames) == 1 { + outputNames = append(outputNames, filteredNames) + continue + } + + // Prompt to multiselect context names for unspecified layer + prompt := &survey.MultiSelect{ + Message: fmt.Sprintf("Select %s(s)", contexts.Layers[layer].Name), + Options: filteredNames, + } + var selectedNames []string + if err := survey.AskOne(prompt, &selectedNames); err != nil { + return nil, err + } + outputNames = append(outputNames, selectedNames) + } + } + + return outputNames, nil +} + // Prompts user to multi-select among given images func PromptImageNames(allImages []string) ([]string, error) { diff --git a/src/cmd/remove/remove.go b/src/cmd/remove/remove.go index 2bd1aec..3d61a1d 100644 --- a/src/cmd/remove/remove.go +++ b/src/cmd/remove/remove.go @@ -3,7 +3,10 @@ package remove import ( "context" "fmt" + "os" + "strings" + "github.com/TwinProduction/go-color" "github.com/spf13/cobra" "github.com/silphid/yey/src/cmd" @@ -36,11 +39,63 @@ func run(ctx context.Context, names []string, options docker.RemoveOptions) erro return err } - names, err = cmd.GetOrPromptContextNames(contexts, names) + containers, err := docker.ListContainers(ctx) + if err != nil { + return fmt.Errorf("failed to list containers to prompt for removal: %w", err) + } + + // Abort if no containers to remove + if len(containers) == 0 { + fmt.Fprintln(os.Stderr, color.Ize(color.Green, "no containers to remove")) + return nil + } + + // Parse container names to slices + containerNames := make([][]string, len(containers)) + for i, container := range containers { + // TODO: improve this logic to support context names with dashes in them + // We need a more deterministic way to trace back a container name to its context names + containerNames[i] = strings.Split(container, "-") + } + + // Predicate to determine whether context name in given layer has a corresponding container + predicate := func(name string, layer int) bool { + for _, containerName := range containerNames { + skipContainerPrefixes := 3 + if containerName[skipContainerPrefixes+layer] == name { + return true + } + } + return false + } + + // Prompt + selectedNames, err := cmd.GetOrPromptMultipleContextNames(contexts, names, predicate) if err != nil { return fmt.Errorf("failed to prompt for context: %w", err) } + return removeRecursively(ctx, contexts, selectedNames, []string{}, 0, options) +} + +func removeRecursively(ctx context.Context, contexts yey.Contexts, selectedNames [][]string, names []string, layer int, options docker.RemoveOptions) error { + for _, name := range selectedNames[layer] { + currentNames := append(names, name) + var err error + if layer == len(selectedNames)-1 { + err = remove(ctx, contexts, currentNames, options) + } else { + // Recurse + err = removeRecursively(ctx, contexts, selectedNames, currentNames, layer+1, options) + } + if err != nil { + return err + } + } + return nil +} + +func remove(ctx context.Context, contexts yey.Contexts, names []string, options docker.RemoveOptions) error { context, err := contexts.GetContext(names) if err != nil { return fmt.Errorf("failed to get context: %w", err) @@ -48,5 +103,6 @@ func run(ctx context.Context, names []string, options docker.RemoveOptions) erro container := yey.ContainerName(contexts.Path, context) + yey.Log("Removing %s", container) return docker.Remove(ctx, container, options) } diff --git a/src/internal/docker/cli.go b/src/internal/docker/cli.go index c3dd23e..491fee0 100644 --- a/src/internal/docker/cli.go +++ b/src/internal/docker/cli.go @@ -8,6 +8,7 @@ import ( "os/exec" "path/filepath" "regexp" + "sort" "strings" yey "github.com/silphid/yey/src/internal" @@ -100,12 +101,21 @@ var newlines = regexp.MustCompile(`\r?\n`) func ListContainers(ctx context.Context) ([]string, error) { cmd := exec.Command("docker", "ps", "--all", "--filter", "name=yey-*", "--format", "{{.Names}}") - output, err := cmd.Output() + + // Parse output + outputBuf, err := cmd.Output() if err != nil { return nil, err } - output = bytes.TrimSpace(output) - return newlines.Split(string(output), -1), nil + output := string(bytes.TrimSpace(outputBuf)) + if output == "" { + return []string{}, nil + } + containers := newlines.Split(string(output), -1) + + // Sort + sort.Strings(containers) + return containers, nil } func imageExists(ctx context.Context, tag string) (bool, error) {