diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index c41a29a..5bc7283 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,85 +1,4 @@ -#------------------------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. -#------------------------------------------------------------------------------------------------------------- +FROM mcr.microsoft.com/devcontainers/go:1-1.23-bookworm -FROM golang:1.17-bullseye - -# Avoid warnings by switching to noninteractive -ENV DEBIAN_FRONTEND=noninteractive - -# Configure apt, install packages and tools -RUN apt-get update \ - && apt-get -y install --no-install-recommends apt-utils dialog nano sudo bsdmainutils \ - # - # Verify git, process tools, lsb-release (common in install instructions for CLIs) installed - && apt-get -y install git iproute2 procps lsb-release build-essential \ - # Install Release Tools - # - # --> RPM used by goreleaser - && apt install -y rpm - -# This Dockerfile adds a non-root user with sudo access. Use the "remoteUser" -# property in devcontainer.json to use it. On Linux, the container user's GID/UIDs -# will be updated to match your local UID/GID (when using the dockerFile property). -# See https://aka.ms/vscode-remote/containers/non-root-user for details. -ARG USERNAME=vscode -ARG USER_UID=1000 -ARG USER_GID=$USER_UID - -# Create the user -RUN groupadd --gid $USER_GID $USERNAME \ - && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \ - && apt-get update \ - && apt-get install -y sudo \ - && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ - && chmod 0440 /etc/sudoers.d/$USERNAME - -# Set default user -USER $USERNAME -RUN mkdir -p ~/.local/bin -ENV PATH /home/${USERNAME}/.local/bin:$PATH - -# Set env for tracking that we're running in a devcontainer -ENV DEVCONTAINER=true - -# Enable go modules -ENV GO111MODULE=on - -# Install Go tools -ARG GO_PLS_VERSION=0.7.2 -ARG DLV_VERSION=1.7.2 -ARG GOLANGCI_LINT_VERSION=1.42.1 -RUN \ - # --> Delve for debugging - go get github.com/go-delve/delve/cmd/dlv@v${DLV_VERSION}\ - # --> Go language server - && go get golang.org/x/tools/gopls@v${GO_PLS_VERSION} \ - # --> Go symbols and outline for go to symbol support and test support - && go get github.com/acroca/go-symbols@v0.1.1 && go get github.com/ramya-rao-a/go-outline@7182a932836a71948db4a81991a494751eccfe77 \ - # --> GolangCI-lint - && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v${GOLANGCI_LINT_VERSION} \ - # --> Go releaser - && echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | sudo tee /etc/apt/sources.list.d/goreleaser.list \ - && sudo apt update \ - && sudo apt install goreleaser -y \ - # --> Install junit converter - && go get github.com/jstemmer/go-junit-report@v0.9.1 \ - && sudo rm -rf /go/src/ \ - && sudo rm -rf /go/pkg - -# Switch back to dialog for any ad-hoc use of apt-get -ENV DEBIAN_FRONTEND=dialog - -# gh -COPY scripts/gh.sh /tmp/ -RUN /tmp/gh.sh - -# symlink gh config folder -RUN echo 'if [[ ! -d /home/vscode/.config/gh ]]; then mkdir -p /home/vscode/.config; ln -s /config/gh /home/vscode/.config/gh; fi ' >> ~/.bashrc - -ARG DOCKER_GROUP_ID - -# docker-from-docker -COPY scripts/docker-client.sh /tmp/ -RUN /tmp/docker-client.sh +# workaround for https://github.com/yarnpkg/yarn/issues/9216 +RUN sudo rm -f /etc/apt/sources.list.d/yarn.list \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 129cfb8..67c20e1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,81 +1,38 @@ - -y// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/go +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/go { - "name": "devcontainer-cli", - "dockerFile": "Dockerfile", + "name": "devcontainerx", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile "build": { - "cacheFrom": "ghcr.io/stuartleeks/devcontainer-cli-devcontainer", - "args": { - // To ensure that the group ID for the docker group in the container - // matches the group ID on the host, add this to your .bash_profile on the host - // export DOCKER_GROUP_ID=$(getent group docker | awk -F ":" '{ print $3 }') - "DOCKER_GROUP_ID": "${localEnv:DOCKER_GROUP_ID}" - } + // Path is relataive to the devcontainer.json file. + "dockerfile": "Dockerfile" }, - "runArgs": [ - // Uncomment the next line to use a non-root user. On Linux, this will prevent - // new files getting created as root, but you may need to update the USER_UID - // and USER_GID in .devcontainer/Dockerfile to match your user if not 1000. - // "-u", "vscode", - "--cap-add=SYS_PTRACE", - "--security-opt", - "seccomp=unconfined", - - // Mount go mod cache - "-v", "devcontainer-cli-gomodcache:/go/pkg", - // Use host network - "--network=host", - ], - - // Set *default* container specific settings.json values on container create. - "settings": { - "terminal.integrated.shell.linux": "/bin/bash", - "go.gopath": "/go", - "go.useLanguageServer": true, - "[go]": { - "editor.snippetSuggestions": "none", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports": true, - } - }, - "gopls": { - "usePlaceholders": true, // add parameter placeholders when completing a function - // Experimental settings - "completeUnimported": true, // autocomplete unimported packages - "deepCompletion": true, // enable deep completion + "features": { + "ghcr.io/devcontainers/features/docker-outside-of-docker": {}, + "ghcr.io/stuartleeks/dev-container-features/shell-history:0": {}, + "ghcr.io/guiyomh/features/golangci-lint:0": { + "version": "latest" }, - "files.eol": "\n", // formatting only supports LF line endings + "ghcr.io/guiyomh/features/goreleaser:0": { + "version": "1.15.2" + } }, - - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "golang.go", - "stuartleeks.vscode-go-by-example", - "darkriszty.markdown-table-prettify", - "davidanson.vscode-markdownlint", - "mushan.vscode-paste-image" - , - "ms-azuretools.vscode-docker",], - // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], - // Use 'postCreateCommand' to run commands after the container is created. "postCreateCommand": "make post-create", - - // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "vscode", - "mounts": [ - // Keep command history - "source=devcontainer-cli-bashhistory,target=/home/vscode/commandhistory", - // Mounts the .config/gh host folder into the dev container to pick up host gh CLI login details - // NOTE that mounting directly to ~/.config/gh makes ~/.config only root-writable - // Instead monut to another location and symlink in Dockerfile - "type=bind,source=${env:HOME}${env:USERPROFILE}/.config/gh,target=/config/gh", - // Mounts the .azure host folder into the dev container to pick up host az CLI login details - "type=bind,source=${env:HOME}${env:USERPROFILE}/.azure,target=/home/vscode/.azure", - // Mount host docker socket - "type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock", - ], + // Configure tool-specific properties. + "customizations": { + // TODO + "extensions": [ + "golang.go", + "stuartleeks.vscode-go-by-example", + "darkriszty.markdown-table-prettify", + "davidanson.vscode-markdownlint", + "mushan.vscode-paste-image", + "ms-azuretools.vscode-docker", + ], + }, + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" } \ No newline at end of file diff --git a/.github/workflows/build-and-release.yaml b/.github/workflows/build-and-release.yaml index fa25916..7d9e5a5 100644 --- a/.github/workflows/build-and-release.yaml +++ b/.github/workflows/build-and-release.yaml @@ -43,6 +43,8 @@ jobs: BRANCH: ${{ github.ref }} with: imageName: ghcr.io/stuartleeks/devcontainer-cli-devcontainer + cacheFrom: ghcr.io/stuartleeks/devcontainer-cli-devcontainer + cacheTo: type=inline runCmd: | sudo chown -R $(whoami) /go/pkg ./scripts/ci_release.sh diff --git a/.vscode/launch.json b/.vscode/launch.json index ca4fc5d..b126c94 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,14 +4,21 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "Launch file", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${file}" + }, { "name": "Launch", "type": "go", "request": "launch", "mode": "auto", - "program": "${workspaceFolder}/cmd/devcontainer", + "program": "${workspaceFolder}/cmd/devcontainerx", "env": {}, - "args": ["list"], + "args": ["list", "--output", "table"], "dlvLoadConfig": { "followPointers": true, "maxVariableRecurse": 1, @@ -35,6 +42,6 @@ "maxArrayValues": 64, "maxStructFields": -1 } - }, + }, ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index f8212de..4ca93ac 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,8 @@ "apiVersion": 2, "showGlobalVariables": false, "debugAdapter": "legacy" + }, + "chat.tools.terminal.autoApprove": { + "make": true } } \ No newline at end of file diff --git a/Makefile b/Makefile index 5725a73..5ece787 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ endif test: - richgo test -v ./... + go test -v ./... fmt: @@ -39,5 +39,5 @@ fmt: post-create: - sudo chown vscode /go/pkg - go get -u github.com/kyoh86/richgo \ No newline at end of file +# #sudo chown vscode /go/pkg +# # go get -u github.com/kyoh86/richgo \ No newline at end of file diff --git a/cmd/devcontainerx/config.go b/cmd/devcontainerx/config.go index e43fb35..d9004e2 100644 --- a/cmd/devcontainerx/config.go +++ b/cmd/devcontainerx/config.go @@ -25,7 +25,7 @@ func createConfigShowCommand() *cobra.Command { c := config.GetAll() jsonConfig, err := json.MarshalIndent(c, "", " ") if err != nil { - return fmt.Errorf("Error converting to JSON: %s\n", err) + return fmt.Errorf("error converting to JSON: %s", err) } fmt.Println(string(jsonConfig)) return nil @@ -40,7 +40,7 @@ func createConfigWriteCommand() *cobra.Command { Long: "Write out the config file to ~/.devcontainer-cli/devcontainer-cli.json", RunE: func(cmd *cobra.Command, args []string) error { if err := config.SaveConfig(); err != nil { - return fmt.Errorf("Error saving config: %s\n", err) + return fmt.Errorf("error saving config: %s", err) } fmt.Println("Config saved") return nil diff --git a/cmd/devcontainerx/devcontainer.go b/cmd/devcontainerx/devcontainer.go index c8a024d..87e1e83 100644 --- a/cmd/devcontainerx/devcontainer.go +++ b/cmd/devcontainerx/devcontainer.go @@ -1,64 +1,39 @@ package main import ( - "encoding/json" "fmt" "os" "sort" - "text/tabwriter" "github.com/spf13/cobra" "github.com/stuartleeks/devcontainer-cli/internal/pkg/devcontainers" + "github.com/stuartleeks/devcontainer-cli/internal/pkg/output" ) func createListCommand() *cobra.Command { - var listIncludeContainerNames bool - var listVerbose bool cmdList := &cobra.Command{ Use: "list", Short: "List devcontainers", Long: "Lists running devcontainers", RunE: func(cmd *cobra.Command, args []string) error { - if listIncludeContainerNames && listVerbose { - fmt.Println("Can't use both verbose and include-container-names") - os.Exit(1) + outputFormat, query, err := output.GetOutputAndQueryValues(cmd, `[].{name: devcontainerName, containerName: containerName, containerID: containerID, localFolderPath: localFolderPath}`) + if err != nil { + return err } - devcontainers, err := devcontainers.ListDevcontainers() + devcontainerList, err := devcontainers.ListDevcontainers() if err != nil { return err } - if listVerbose { - sort.Slice(devcontainers, func(i, j int) bool { return devcontainers[i].DevcontainerName < devcontainers[j].DevcontainerName }) - - w := new(tabwriter.Writer) - // minwidth, tabwidth, padding, padchar, flags - w.Init(os.Stdout, 8, 8, 0, '\t', 0) - defer w.Flush() - - fmt.Fprintf(w, "%s\t%s\n", "DEVCONTAINER NAME", "CONTAINER NAME") - fmt.Fprintf(w, "%s\t%s\n", "-----------------", "--------------") - for _, devcontainer := range devcontainers { - fmt.Fprintf(w, "%s\t%s\n", devcontainer.DevcontainerName, devcontainer.ContainerName) - } - return nil - } - names := []string{} - for _, devcontainer := range devcontainers { - names = append(names, devcontainer.DevcontainerName) - if listIncludeContainerNames { - names = append(names, devcontainer.ContainerName) - } - } - sort.Strings(names) - for _, name := range names { - fmt.Println(name) + err = output.OutputResult(os.Stdout, devcontainerList, outputFormat, query, []string{"name", "containerName", "containerID", "localFolderPath"}) + if err != nil { + return fmt.Errorf("error outputting result: %s", err) } return nil }, } - cmdList.Flags().BoolVar(&listIncludeContainerNames, "include-container-names", false, "Also include container names in the list") - cmdList.Flags().BoolVarP(&listVerbose, "verbose", "v", false, "Verbose output") + + output.AddOutputAndQueryFlags(cmdList) return cmdList } @@ -69,30 +44,36 @@ func createShowCommand() *cobra.Command { Short: "Show devcontainer info", Long: "Show information about a running dev container", RunE: func(cmd *cobra.Command, args []string) error { - devcontainers, err := devcontainers.ListDevcontainers() + outputFormat, query, err := output.GetOutputAndQueryValues(cmd, `[].{name: devcontainerName, containerName: containerName, containerID: containerID, localFolderPath: localFolderPath}`) + if err != nil { + return err + } + devcontainerList, err := devcontainers.ListDevcontainers() if err != nil { return err } containerIDOrName := argDevcontainerName // Get container ID - for _, devcontainer := range devcontainers { + for _, devcontainer := range devcontainerList { if devcontainer.ContainerName == containerIDOrName || devcontainer.DevcontainerName == containerIDOrName || devcontainer.ContainerID == containerIDOrName { - output, err := json.MarshalIndent(devcontainer, "", "\t") + + wrapped := []devcontainers.DevcontainerInfo{devcontainer} + err = output.OutputResult(os.Stdout, wrapped, outputFormat, query, []string{"name", "containerName", "containerID", "localFolderPath"}) if err != nil { - return fmt.Errorf("Failed to serialise devcontainer info: %s", err) + return fmt.Errorf("error outputting result: %s", err) } - fmt.Printf("%s\n", output) return nil } } - return fmt.Errorf("Failed to find a matching (running) dev container for %q", containerIDOrName) + return fmt.Errorf("failed to find a matching (running) dev container for %q", containerIDOrName) }, } - cmd.Flags().StringVarP(&argDevcontainerName, "name", "n", "", "name of dev container to exec into") + cmd.Flags().StringVarP(&argDevcontainerName, "name", "n", "", "name of dev container to show") + output.AddOutputAndQueryFlags(cmd) _ = cmd.RegisterFlagCompletionFunc("name", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { devcontainers, err := devcontainers.ListDevcontainers() @@ -171,7 +152,7 @@ func createExecCommand() *cobra.Command { } if containerID == "" { - return fmt.Errorf("Failed to find a matching (running) dev container for %q", containerIDOrName) + return fmt.Errorf("failed to find a matching (running) dev container for %q", containerIDOrName) } } else if argPromptForDevcontainer { // prompt user @@ -182,7 +163,7 @@ func createExecCommand() *cobra.Command { selection := -1 _, _ = fmt.Scanf("%d", &selection) if selection < 0 || selection >= len(devcontainerList) { - return fmt.Errorf("Invalid option") + return fmt.Errorf("invalid option") } containerID = devcontainerList[selection].ContainerID } else { diff --git a/cmd/devcontainerx/main.go b/cmd/devcontainerx/main.go index 7bf43fd..7bee2d6 100644 --- a/cmd/devcontainerx/main.go +++ b/cmd/devcontainerx/main.go @@ -2,7 +2,6 @@ package main import ( "github.com/spf13/cobra" - "github.com/stuartleeks/devcontainer-cli/internal/pkg/config" "github.com/stuartleeks/devcontainer-cli/internal/pkg/update" ) @@ -29,9 +28,6 @@ func main() { rootCmd.AddCommand(createListCommand()) rootCmd.AddCommand(createShowCommand()) rootCmd.AddCommand(createTemplateCommand()) - if config.GetExperimentalFeaturesEnabled() { - rootCmd.AddCommand(createSnippetCommand()) - } rootCmd.AddCommand(createUpdateCommand()) rootCmd.AddCommand(createOpenInCodeCommand()) rootCmd.AddCommand(createOpenInCodeInsidersCommand()) diff --git a/cmd/devcontainerx/snippet.go b/cmd/devcontainerx/snippet.go deleted file mode 100644 index de34abe..0000000 --- a/cmd/devcontainerx/snippet.go +++ /dev/null @@ -1,106 +0,0 @@ -package main - -import ( - "fmt" - "os" - "sort" - "text/tabwriter" - - "github.com/spf13/cobra" - "github.com/stuartleeks/devcontainer-cli/internal/pkg/devcontainers" -) - -func createSnippetCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "snippet", - Short: "work with snippets (experimental)", - Long: "Use subcommands to work with devcontainer snippets (experimental)", - } - cmd.AddCommand(createSnippetListCommand()) - cmd.AddCommand(createSnippetAddCommand()) - return cmd -} - -func createSnippetListCommand() *cobra.Command { - var listVerbose bool - cmd := &cobra.Command{ - Use: "list", - Short: "list snippets", - Long: "List devcontainer snippets", - RunE: func(cmd *cobra.Command, args []string) error { - - snippets, err := devcontainers.GetSnippets() - if err != nil { - return err - } - - if listVerbose { - w := new(tabwriter.Writer) - // minwidth, tabwidth, padding, padchar, flags - w.Init(os.Stdout, 8, 8, 0, '\t', 0) - defer w.Flush() - - fmt.Fprintf(w, "%s\t%s\n", "SNIPPET NAME", "PATH") - fmt.Fprintf(w, "%s\t%s\n", "-------------", "----") - - for _, snippet := range snippets { - fmt.Fprintf(w, "%s\t%s\n", snippet.Name, snippet.Path) - } - return nil - } - - for _, snippet := range snippets { - fmt.Println(snippet.Name) - } - return nil - }, - } - cmd.Flags().BoolVarP(&listVerbose, "verbose", "v", false, "Verbose output") - return cmd -} - -func createSnippetAddCommand() *cobra.Command { - var devcontainerName string - cmd := &cobra.Command{ - Use: "add SNIPPET_NAME", - Short: "add snippet to devcontainer", - Long: "Add a snippet to the devcontainer definition for the current folder", - RunE: func(cmd *cobra.Command, args []string) error { - - if len(args) != 1 { - return cmd.Usage() - } - name := args[0] - - currentDirectory, err := os.Getwd() - if err != nil { - return fmt.Errorf("Error reading current directory: %s\n", err) - } - - err = devcontainers.AddSnippetToDevcontainer(currentDirectory, name) - if err != nil { - return fmt.Errorf("Error setting devcontainer name: %s", err) - } - - return nil - }, - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // only completing the first arg (template name) - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - templates, err := devcontainers.GetSnippets() - if err != nil { - os.Exit(1) - } - names := []string{} - for _, template := range templates { - names = append(names, template.Name) - } - sort.Strings(names) - return names, cobra.ShellCompDirectiveNoFileComp - }, - } - cmd.Flags().StringVar(&devcontainerName, "devcontainer-name", "", "Value to set the devcontainer.json name property to (default is folder name)") - return cmd -} diff --git a/cmd/devcontainerx/template.go b/cmd/devcontainerx/template.go index 564ff50..a5a3766 100644 --- a/cmd/devcontainerx/template.go +++ b/cmd/devcontainerx/template.go @@ -2,14 +2,13 @@ package main import ( "fmt" - "io/ioutil" "os" "sort" - "text/tabwriter" "github.com/spf13/cobra" "github.com/stuartleeks/devcontainer-cli/internal/pkg/devcontainers" ioutil2 "github.com/stuartleeks/devcontainer-cli/internal/pkg/ioutil" + "github.com/stuartleeks/devcontainer-cli/internal/pkg/output" ) func createTemplateCommand() *cobra.Command { @@ -25,40 +24,29 @@ func createTemplateCommand() *cobra.Command { } func createTemplateListCommand() *cobra.Command { - var listVerbose bool cmd := &cobra.Command{ Use: "list", Short: "list templates", Long: "List devcontainer templates", RunE: func(cmd *cobra.Command, args []string) error { - - templates, err := devcontainers.GetTemplates() + outputFormat, query, err := output.GetOutputAndQueryValues(cmd, `[].{name: name, path: path}`) if err != nil { return err } - if listVerbose { - w := new(tabwriter.Writer) - // minwidth, tabwidth, padding, padchar, flags - w.Init(os.Stdout, 8, 8, 0, '\t', 0) - defer w.Flush() - - fmt.Fprintf(w, "%s\t%s\n", "TEMPLATE NAME", "PATH") - fmt.Fprintf(w, "%s\t%s\n", "-------------", "----") - - for _, template := range templates { - fmt.Fprintf(w, "%s\t%s\n", template.Name, template.Path) - } - return nil + templates, err := devcontainers.GetTemplates() + if err != nil { + return err } - for _, template := range templates { - fmt.Println(template.Name) + err = output.OutputResult(os.Stdout, templates, outputFormat, query, []string{"name", "path"}) + if err != nil { + return fmt.Errorf("error outputting result: %s", err) } return nil }, } - cmd.Flags().BoolVarP(&listVerbose, "verbose", "v", false, "Verbose output") + output.AddOutputAndQueryFlags(cmd) return cmd } @@ -85,12 +73,12 @@ func createTemplateAddCommand() *cobra.Command { info, err := os.Stat("./.devcontainer") if info != nil && err == nil { - return fmt.Errorf("Current folder already contains a .devcontainer folder - exiting") + return fmt.Errorf("current folder already contains a .devcontainer folder - exiting") } currentDirectory, err := os.Getwd() if err != nil { - return fmt.Errorf("Error reading current directory: %s\n", err) + return fmt.Errorf("error reading current directory: %s", err) } err = devcontainers.CopyTemplateToFolder(template.Path, currentDirectory, devcontainerName) @@ -138,25 +126,25 @@ func createTemplateAddLinkCommand() *cobra.Command { return err } if template == nil { - return fmt.Errorf("Template '%s' not found\n", name) + return fmt.Errorf("template '%s' not found", name) } info, err := os.Stat("./.devcontainer") if info != nil && err == nil { - return fmt.Errorf("Current folder already contains a .devcontainer folder - exiting") + return fmt.Errorf("current folder already contains a .devcontainer folder - exiting") } currentDirectory, err := os.Getwd() if err != nil { - return fmt.Errorf("Error reading current directory: %s\n", err) + return fmt.Errorf("error reading current directory: %s", err) } if err = ioutil2.LinkFolder(template.Path, currentDirectory+"/.devcontainer"); err != nil { - return fmt.Errorf("Error linking folder: %s\n", err) + return fmt.Errorf("error linking folder: %s", err) } content := []byte("*\n") - if err := ioutil.WriteFile(currentDirectory+"/.devcontainer/.gitignore", content, 0644); err != nil { // -rw-r--r-- - return fmt.Errorf("Error writing .gitignore: %s\n", err) + if err := os.WriteFile(currentDirectory+"/.devcontainer/.gitignore", content, 0644); err != nil { // -rw-r--r-- + return fmt.Errorf("error writing .gitignore: %s", err) } return err }, diff --git a/cmd/devcontainerx/update.go b/cmd/devcontainerx/update.go index 678ae8a..3bfff0e 100644 --- a/cmd/devcontainerx/update.go +++ b/cmd/devcontainerx/update.go @@ -25,7 +25,7 @@ func createUpdateCommand() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { latest, err := update.CheckForUpdate(version) if err != nil { - return fmt.Errorf("Error occurred while checking for updates: %v", err) + return fmt.Errorf("error occurred while checking for updates: %v", err) } if latest == nil { @@ -51,10 +51,10 @@ func createUpdateCommand() *cobra.Command { exe, err := os.Executable() if err != nil { - return fmt.Errorf("Could not locate executable path: %v", err) + return fmt.Errorf("could not locate executable path: %v", err) } if err := selfupdate.UpdateTo(latest.AssetURL, exe); err != nil { - return fmt.Errorf("Error occurred while updating binary: %v", err) + return fmt.Errorf("error occurred while updating binary: %v", err) } fmt.Printf("Successfully updated to version %s\n", latest.Version) return nil diff --git a/cmd/tmp/main.go b/cmd/tmp/main.go new file mode 100644 index 0000000..5be09b3 --- /dev/null +++ b/cmd/tmp/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + + jmespath "github.com/jmespath/go-jmespath" +) + +func main() { + + parseAndOutput("[].{name:name, foo:foo}") + + parseAndOutput("locations[?state == 'WA'].name | sort(@) | {WashingtonCities: join(', ', @)}") + parseAndOutput("locations[?state == 'WA']") +} + +func parseAndOutput(query string) { + parser := jmespath.NewParser() + ast, err := parser.Parse(query) + if err != nil { + fmt.Printf("Error parsing: %s\n", err) + } + + fmt.Printf("%s\n\n", query) + fmt.Printf("%s\n\n", ast.PrettyPrint(2)) + fields, err := ast.GetResultFields() + if err != nil { + fmt.Printf("Error getting fields: %s\n", err) + } + fmt.Printf("Fields: %v\n\n", fields) + +} diff --git a/go.mod b/go.mod index 7d858e7..c67add5 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,53 @@ module github.com/stuartleeks/devcontainer-cli -go 1.14 +go 1.23.0 + +toolchain go1.23.7 require ( github.com/blang/semver v3.5.1+incompatible github.com/bradford-hamilton/dora v0.1.1 - github.com/kyoh86/richgo v0.3.12 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/fatih/color v1.18.0 + github.com/jmespath/go-jmespath v0.4.0 + github.com/mattn/go-colorable v0.1.14 + github.com/neilotoole/jsoncolor v0.7.1 github.com/rhysd/go-github-selfupdate v1.2.2 github.com/spf13/cobra v1.0.0 github.com/spf13/viper v1.4.0 - github.com/stretchr/testify v1.8.2 - golang.org/x/sys v0.10.0 // indirect + github.com/stretchr/testify v1.8.4 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.4.7 // indirect + github.com/golang/protobuf v1.3.2 // indirect + github.com/google/go-github/v30 v30.1.0 // indirect + github.com/google/go-querystring v1.0.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/magiconair/properties v1.8.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.1.2 // indirect + github.com/pelletier/go-toml v1.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/afero v1.1.2 // indirect + github.com/spf13/cast v1.3.0 // indirect + github.com/spf13/jwalterweatherman v1.0.0 // indirect + github.com/spf13/pflag v1.0.3 // indirect + github.com/tcnksm/go-gitconfig v0.1.2 // indirect + github.com/ulikunitz/xz v0.5.5 // indirect + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 // indirect + golang.org/x/net v0.0.0-20190522155817-f3200d17e092 // indirect + golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.14.0 // indirect + golang.org/x/text v0.3.0 // indirect + google.golang.org/appengine v1.3.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/bradford-hamilton/dora v0.1.1 => github.com/stuartleeks/dora v0.1.5 + +replace github.com/jmespath/go-jmespath => github.com/stuartleeks/go-jmespath v0.0.1 diff --git a/go.sum b/go.sum index f48d0fe..106be6a 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -50,12 +54,13 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -67,32 +72,27 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kyoh86/richgo v0.3.10 h1:iSGvcjhtQN2IVrBDhPk0if0R/RMQnCN1E/9OyAW4UUs= -github.com/kyoh86/richgo v0.3.10/go.mod h1:2Odx7Qx2iJxXEWmoXVWaNbUcBXhHY43qSgyK7dbTaII= -github.com/kyoh86/richgo v0.3.12 h1:W66IRaaC1BWoCPIyI94t/PChWBQ38QFDRsF1nWu4904= -github.com/kyoh86/richgo v0.3.12/go.mod h1:op8jQyhnxSZQelHDyupswwJGerEDB6zkKfzCnuqAwx4= -github.com/kyoh86/xdg v1.2.0 h1:CERuT/ShdTDj+A2UaX3hQ3mOV369+Sj+wyn2nIRIIkI= -github.com/kyoh86/xdg v1.2.0/go.mod h1:/mg8zwu1+qe76oTFUBnyS7rJzk7LLC0VGEzJyJ19DHs= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/neilotoole/jsoncolor v0.7.1 h1:/MoU7KPLcto+ykcy592Y8eX9WFQhoi3IBEbwrP89dgs= +github.com/neilotoole/jsoncolor v0.7.1/go.mod h1:KZ9hUYN5xMrvyhqlFQ3QTmu11OcoqFgSnWAcYkN6abg= +github.com/nwidger/jsoncolor v0.3.2 h1:rVJJlwAWDJShnbTYOQ5RM7yTA20INyKXlJ/fg4JMhHQ= +github.com/nwidger/jsoncolor v0.3.2/go.mod h1:Cs34umxLbJvgBMnVNVqhji9BhoT/N/KinHqZptQ7cf4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I= github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -110,11 +110,14 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= github.com/rhysd/go-github-selfupdate v1.2.2 h1:G+mNzkc1wEtpmM6sFS/Ghkeq+ad4Yp6EZEHyp//wGEo= github.com/rhysd/go-github-selfupdate v1.2.2/go.mod h1:khesvSyKcXDUxeySCedFh621iawCks0dS/QnHPcpCws= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/segmentio/encoding v0.3.6 h1:E6lVLyDPseWEulBmCmAKPanDd3jiyGDo5gMcugCRwZQ= +github.com/segmentio/encoding v0.3.6/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -136,23 +139,21 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stuartleeks/dora v0.1.5 h1:/t8/kPA2t1qMUI9W0cASGGScpgyNDQ5O+QOYud8+AlQ= github.com/stuartleeks/dora v0.1.5/go.mod h1:Xu0GCcuQv5e9Ta0U3PUiu/rgaXJnHn1+LTaQkCgYMhc= +github.com/stuartleeks/go-jmespath v0.0.1 h1:T3Lj1veDa4ocU7zuQQEeSAQs36XW8NnaCkZ53s8hMAI= +github.com/stuartleeks/go-jmespath v0.0.1/go.mod h1:rR85kpDbxkMj8TInsxzfnmD2AKQV+cGgXIXuHhDDhCo= github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw= github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ulikunitz/xz v0.5.5 h1:pFrO0lVpTBXLpYw+pnLj6TbvHuyjXMfjGeCwSqCVwok= github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= -github.com/wacul/ptr v1.0.0 h1:FIKu08Wx0YUIf9MNsfF62OCmBSmz5A1Tk65zWhOIL/I= -github.com/wacul/ptr v1.0.0/go.mod h1:BD0gjsZrCwtoR+yWDB9v2hQ8STlq9tT84qKfa+3txOc= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -184,20 +185,16 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211030160813-b3129d9d1021/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220926163933-8cfa568d3c25 h1:nwzwVf0l2Y/lkov/+IYgMMbFyI+QypZDds9RxlSmsFQ= -golang.org/x/sys v0.0.0-20220926163933-8cfa568d3c25/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= -golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -214,17 +211,14 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/pkg/devcontainers/dockerutils.go b/internal/pkg/devcontainers/dockerutils.go index d96b1f2..7d1bbb4 100644 --- a/internal/pkg/devcontainers/dockerutils.go +++ b/internal/pkg/devcontainers/dockerutils.go @@ -27,6 +27,7 @@ type DevcontainerInfo struct { ContainerID string `json:"containerID"` ContainerName string `json:"containerName"` DevcontainerName string `json:"devcontainerName"` + FolderPath string `json:"folderPath"` LocalFolderPath string `json:"localFolderPath"` } @@ -47,23 +48,24 @@ func ListDevcontainers() ([]DevcontainerInfo, error) { output, err := cmd.Output() if err != nil { - return []DevcontainerInfo{}, fmt.Errorf("Failed to read docker stdout: %v", err) + return []DevcontainerInfo{}, fmt.Errorf("failed to read docker stdout: %v", err) } reader := bytes.NewReader(output) scanner := bufio.NewScanner(reader) if scanner == nil { - return []DevcontainerInfo{}, fmt.Errorf("Failed to parse stdout") + return []DevcontainerInfo{}, fmt.Errorf("failed to parse stdout") } devcontainers := []DevcontainerInfo{} for scanner.Scan() { line := scanner.Text() parts := strings.Split(line, "|") - localPath := parts[listPartLocalFolder] - if localPath == "" { + path := parts[listPartLocalFolder] + if path == "" { // not a dev container continue } + localPath := path if wsl.HasWslPathPrefix(localPath) && wsl.IsWsl() { localPath, err = wsl.ConvertWindowsPathToWslPath(localPath) if err != nil { @@ -83,6 +85,7 @@ func ListDevcontainers() ([]DevcontainerInfo, error) { devcontainer := DevcontainerInfo{ ContainerID: parts[listPartID], ContainerName: parts[listPartContainerName], + FolderPath: path, LocalFolderPath: localPath, DevcontainerName: name, } @@ -98,7 +101,7 @@ func GetLocalFolderFromDevContainer(containerIDOrName string) (string, error) { output, err := cmd.Output() if err != nil { - return "", fmt.Errorf("Failed to read docker stdout: %v", err) + return "", fmt.Errorf("failed to read docker stdout: %v", err) } return strings.TrimSpace(string(output)), nil @@ -162,12 +165,15 @@ func GetSourceInfoFromDevContainer(containerIDOrName string) (SourceInfo, error) output, err := cmd.Output() if err != nil { - return SourceInfo{}, fmt.Errorf("Failed to read docker stdout: %v", err) + return SourceInfo{}, fmt.Errorf("failed to read docker stdout: %v", err) } var mount DockerMount err = json.Unmarshal(output, &mount) if err != nil { + fmt.Printf("Failed to parse JSON: %s\n", string(output)) + fmt.Printf("containerIDOrName: %s\n", containerIDOrName) + fmt.Printf("mountFolder: %s\n", mountFolder) return SourceInfo{}, fmt.Errorf("failed to parse JSON getting mount folder for container %q (path=%q): %s", containerIDOrName, mountFolder, err) } @@ -196,7 +202,7 @@ func GetClosestPathMatchForPath(devContainers []DevcontainerInfo, devcontainerPa } absPath, err := filepath.Abs(devcontainerPath) if err != nil { - return DevcontainerInfo{}, fmt.Errorf("Error handling path %q: %s", devcontainerPath, err) + return DevcontainerInfo{}, fmt.Errorf("error handling path %q: %s", devcontainerPath, err) } matchingPaths := byLocalPathLength{} @@ -207,7 +213,7 @@ func GetClosestPathMatchForPath(devContainers []DevcontainerInfo, devcontainerPa testPath, err = wsl.ConvertWindowsPathToWslPath(testPath) fmt.Println("Converted to..") if err != nil { - return DevcontainerInfo{}, fmt.Errorf("Error converting path from dev container list (%q): %s", testPath, err) + return DevcontainerInfo{}, fmt.Errorf("error converting path from dev container list (%q): %s", testPath, err) } } if strings.HasPrefix(absPath, testPath) { @@ -215,7 +221,7 @@ func GetClosestPathMatchForPath(devContainers []DevcontainerInfo, devcontainerPa } } if len(matchingPaths) == 0 { - return DevcontainerInfo{}, fmt.Errorf("Could not find running container for path %q", devcontainerPath) + return DevcontainerInfo{}, fmt.Errorf("could not find running container for path %q", devcontainerPath) } // return longest prefix match @@ -378,11 +384,11 @@ func ExecInDevContainer(containerID string, workDir string, args []string) error err = dockerCmd.Start() if err != nil { - return fmt.Errorf("Exec: start error: %s", err) + return fmt.Errorf("exec: start error: %s", err) } err = dockerCmd.Wait() if err != nil { - return fmt.Errorf("Exec: wait error: %s", err) + return fmt.Errorf("exec: wait error: %s", err) } return nil } @@ -438,7 +444,7 @@ func getLatestFileMatch(containerID string, userName string, pattern string) (st buf, err := dockerCmd.CombinedOutput() if err != nil { errMessage := string(buf) - return "", fmt.Errorf("Docker exec error: %s (%s)", err, strings.TrimSpace(errMessage)) + return "", fmt.Errorf("docker exec error: %s (%s)", err, strings.TrimSpace(errMessage)) } output := string(buf) @@ -457,7 +463,7 @@ func getContainerEnvVar(containerID string, varName string) (string, error) { buf, err := dockerCmd.CombinedOutput() if err != nil { errMessage := string(buf) - return "", fmt.Errorf("Docker exec error: %s (%s)", err, strings.TrimSpace(errMessage)) + return "", fmt.Errorf("docker exec error: %s (%s)", err, strings.TrimSpace(errMessage)) } return string(buf), nil @@ -471,7 +477,7 @@ func getContainerUserID(containerID string, userName string) (string, error) { buf, err := dockerCmd.CombinedOutput() if err != nil { errMessage := string(buf) - return "", fmt.Errorf("Docker exec error: %s (%s)", err, strings.TrimSpace(errMessage)) + return "", fmt.Errorf("docker exec error: %s (%s)", err, strings.TrimSpace(errMessage)) } output := string(buf) @@ -488,7 +494,7 @@ func testContainerPathExists(containerID string, path string) (bool, error) { buf, err := dockerCmd.CombinedOutput() if err != nil { errMessage := string(buf) - return false, fmt.Errorf("Docker exec error: %s (%s)", err, strings.TrimSpace(errMessage)) + return false, fmt.Errorf("docker exec error: %s (%s)", err, strings.TrimSpace(errMessage)) } response := strings.TrimSpace(string(buf)) @@ -501,7 +507,7 @@ func getUserNameFromRunningContainer(containerID string) (string, error) { buf, err := dockerCmd.CombinedOutput() if err != nil { errMessage := string(buf) - return "", fmt.Errorf("Docker exec error: %s (%s)", err, strings.TrimSpace(errMessage)) + return "", fmt.Errorf("docker exec error: %s (%s)", err, strings.TrimSpace(errMessage)) } var metadata []interface{} diff --git a/internal/pkg/devcontainers/remoteuri.go b/internal/pkg/devcontainers/remoteuri.go index 744dc75..2ad6397 100644 --- a/internal/pkg/devcontainers/remoteuri.go +++ b/internal/pkg/devcontainers/remoteuri.go @@ -3,7 +3,7 @@ package devcontainers import ( "encoding/hex" "fmt" - "io/ioutil" + "os" "path/filepath" "regexp" @@ -16,7 +16,7 @@ func GetDevContainerURI(folderPath string) (string, error) { absPath, err := filepath.Abs(folderPath) if err != nil { - return "", fmt.Errorf("Error handling path %q: %s", folderPath, err) + return "", fmt.Errorf("error handling path %q: %s", folderPath, err) } launchPath := absPath @@ -59,16 +59,16 @@ func GetWorkspaceMountPath(folderPath string) (string, error) { devcontainerDefinitionPath, err := getDevContainerJsonPath(folderPath) if err != nil { - return "", fmt.Errorf("Error getting devcontainer definition path: %s", err) + return "", fmt.Errorf("error getting devcontainer definition path: %s", err) } - buf, err := ioutil.ReadFile(devcontainerDefinitionPath) + buf, err := os.ReadFile(devcontainerDefinitionPath) if err != nil { - return "", fmt.Errorf("Error loading devcontainer definition: %s", err) + return "", fmt.Errorf("error loading devcontainer definition: %s", err) } workspaceMountPath, err := getWorkspaceMountPathFromDevcontainerDefinition(buf) if err != nil { - return "", fmt.Errorf("Error parsing devcontainer definition: %s", err) + return "", fmt.Errorf("error parsing devcontainer definition: %s", err) } if workspaceMountPath != "" { return workspaceMountPath, nil @@ -77,7 +77,7 @@ func GetWorkspaceMountPath(folderPath string) (string, error) { // No `workspaceFolder` found in devcontainer.json - use default devcontainerPath, err := getDefaultWorkspaceFolderForPath(folderPath) if err != nil { - return "", fmt.Errorf("Error getting default workspace path: %s", err) + return "", fmt.Errorf("error getting default workspace path: %s", err) } return fmt.Sprintf("/workspaces/%s", devcontainerPath), nil } @@ -90,7 +90,7 @@ func GetWorkspaceMountPath(folderPath string) (string, error) { func getWorkspaceMountPathFromDevcontainerDefinition(definition []byte) (string, error) { r, err := regexp.Compile("(?m)^\\s*\"workspaceFolder\"\\s*:\\s*\"(.*)\"") if err != nil { - return "", fmt.Errorf("Error compiling regex: %s", err) + return "", fmt.Errorf("error compiling regex: %s", err) } matches := r.FindSubmatch(definition) if len(matches) == 2 { diff --git a/internal/pkg/devcontainers/snippet.go b/internal/pkg/devcontainers/snippet.go deleted file mode 100644 index 96bc848..0000000 --- a/internal/pkg/devcontainers/snippet.go +++ /dev/null @@ -1,442 +0,0 @@ -package devcontainers - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "sort" - "strings" - - "github.com/stuartleeks/devcontainer-cli/internal/pkg/config" - "github.com/stuartleeks/devcontainer-cli/internal/pkg/errors" - ioutil2 "github.com/stuartleeks/devcontainer-cli/internal/pkg/ioutil" - - dora_ast "github.com/bradford-hamilton/dora/pkg/ast" - "github.com/bradford-hamilton/dora/pkg/dora" - dora_lexer "github.com/bradford-hamilton/dora/pkg/lexer" - dora_merge "github.com/bradford-hamilton/dora/pkg/merge" - dora_parser "github.com/bradford-hamilton/dora/pkg/parser" -) - -type SubstitutionValues struct { - Name string - UserName string - HomeFolder string -} - -type DevcontainerSnippetType string - -const ( - DevcontainerSnippetTypeSingleFile = "Snippet:SingleFile" - DevcontainerSnippetTypeFolder = "Snippet:Folder" -) - -// DevcontainerSnippet holds info on snippets for list/add etc -// Snippets can be either single script files or a directory with a set of files -type DevcontainerSnippet struct { - Name string - Type DevcontainerSnippetType - // Path is the path to either the path to the single script file or to the directory for multi-file snippets - Path string -} - -type FolderSnippetActionType string - -const ( - FolderSnippetActionMergeJSON FolderSnippetActionType = "mergeJSON" // merge JSON file from snippet with target JSON file - FolderSnippetActionCopyAndRun FolderSnippetActionType = "copyAndRun" // COPY and RUN script from snippet in the Dockerfile (as with single-file snippet) - FolderSnippetActionDockerfileSnippet FolderSnippetActionType = "dockerfileSnippet" // snippet to include as-is in the Dockerfile -) - -type FolderSnippetAction struct { - Type FolderSnippetActionType `json:"type"` - SourcePath string `json:"source"` // for mergeJSON this is snippet-relative path to JSON. for copyAndRun this is the script filename - TargetPath string `json:"target"` // for mergeJSON this is project-relative path to JSON - Content string `json:"content"` // for dockerfileSnippet this is the content to include - ContentPath string `json:"contentPath"` // for dockerfileSnippet this is the path to content to include -} - -// FolderSnippet maps to the content of the snippet.json file for folder-based snippets -type FolderSnippet struct { - Actions []FolderSnippetAction `json:"actions"` -} - -// GetSnippetByName returns the template with the specified name or nil if not found -func GetSnippetByName(name string) (*DevcontainerSnippet, error) { - // TODO - could possibly make this quicker by searching using the name rather than listing all and filtering - snippets, err := GetSnippets() - if err != nil { - return nil, err - } - for _, snippet := range snippets { - if snippet.Name == name { - return &snippet, nil - } - } - return nil, nil -} - -// GetSnippets returns a list of discovered templates -func GetSnippets() ([]DevcontainerSnippet, error) { - - folders := config.GetSnippetFolders() - if len(folders) == 0 { - return []DevcontainerSnippet{}, &errors.StatusError{Message: "No snippet folders configured - see https://github.com/stuartleeks/devcontainer-cli/#working-with-devcontainer-snippets"} - } - - snippets, err := getSnippetsFromFolders(folders) - if err != nil { - return []DevcontainerSnippet{}, err - } - return snippets, nil -} - -func getSnippetsFromFolders(folders []string) ([]DevcontainerSnippet, error) { - snippets := []DevcontainerSnippet{} - snippetNames := map[string]bool{} - for _, folder := range folders { - folder := os.ExpandEnv(folder) - newSnippets, err := getSnippetsFromFolder(folder) - if err != nil { - return []DevcontainerSnippet{}, err - } - for _, snippet := range newSnippets { - if !snippetNames[snippet.Name] { - snippetNames[snippet.Name] = true - snippets = append(snippets, snippet) - } - } - } - sort.Slice(snippets, func(i int, j int) bool { return snippets[i].Name < snippets[j].Name }) - return snippets, nil -} - -func getSnippetsFromFolder(folder string) ([]DevcontainerSnippet, error) { - c, err := ioutil.ReadDir(folder) - - if err != nil { - return []DevcontainerSnippet{}, fmt.Errorf("Error reading snippet definitions: %s\n", err) - } - - snippets := []DevcontainerSnippet{} - for _, entry := range c { - if strings.HasPrefix(entry.Name(), ".") || strings.HasPrefix(entry.Name(), "_") { - // ignore files/directories starting with "_" or "." - continue - } - if entry.IsDir() { - // TODO! - snippetJSONPath := filepath.Join(folder, entry.Name(), "snippet.json") - snippetJSONInfo, err := os.Stat(snippetJSONPath) - if err != nil || snippetJSONInfo.IsDir() { - continue - } - snippet := DevcontainerSnippet{ - Name: entry.Name(), - Type: DevcontainerSnippetTypeFolder, - Path: filepath.Join(folder, entry.Name()), - } - snippets = append(snippets, snippet) - } else { - if strings.HasSuffix(entry.Name(), ".sh") { - snippet := DevcontainerSnippet{ - Name: strings.TrimSuffix(entry.Name(), ".sh"), - Type: DevcontainerSnippetTypeSingleFile, - Path: filepath.Join(folder, entry.Name()), - } - snippets = append(snippets, snippet) - } - } - } - return snippets, nil -} - -func AddSnippetToDevcontainer(projectFolder string, snippetName string) error { - snippet, err := GetSnippetByName(snippetName) - if err != nil { - return err - } - if snippet == nil { - return fmt.Errorf("Snippet '%s' not found\n", snippetName) - } - return addSnippetToDevcontainer(projectFolder, snippet) -} -func addSnippetToDevcontainer(projectFolder string, snippet *DevcontainerSnippet) error { - switch snippet.Type { - case DevcontainerSnippetTypeSingleFile: - return addSingleFileSnippetToDevContainer(projectFolder, snippet) - case DevcontainerSnippetTypeFolder: - return addFolderSnippetToDevContainer(projectFolder, snippet) - default: - return fmt.Errorf("Unhandled snippet type: %q", snippet.Type) - } -} - -func addSingleFileSnippetToDevContainer(projectFolder string, snippet *DevcontainerSnippet) error { - - if snippet.Type != DevcontainerSnippetTypeSingleFile { - return fmt.Errorf("Expected single file snippet") - } - snippetBasePath, scriptFilename := filepath.Split(snippet.Path) - - scriptFolderPath := filepath.Join(projectFolder, ".devcontainer", "scripts") - err := copyAndRunScriptFile(projectFolder, snippet, snippetBasePath, scriptFolderPath, scriptFilename) - return err -} - -func addFolderSnippetToDevContainer(projectFolder string, snippet *DevcontainerSnippet) error { - if snippet.Type != DevcontainerSnippetTypeFolder { - return fmt.Errorf("Expected folder snippet") - } - - snippetJSONPath := filepath.Join(snippet.Path, "snippet.json") - buf, err := ioutil.ReadFile(snippetJSONPath) - if err != nil { - return err - } - var snippetJSON FolderSnippet - err = json.Unmarshal(buf, &snippetJSON) - if err != nil { - return err - } - - for _, action := range snippetJSON.Actions { - switch action.Type { - case FolderSnippetActionMergeJSON: - if action.SourcePath == "" { - return fmt.Errorf("source must be set for %s actions", action.Type) - } - if action.TargetPath == "" { - return fmt.Errorf("target must be set for %s actions", action.Type) - } - err = mergeJSON(projectFolder, snippet, action.SourcePath, action.TargetPath) - if err != nil { - return err - } - case FolderSnippetActionCopyAndRun: - if action.SourcePath == "" { - return fmt.Errorf("source must be set for %s actions", action.Type) - } - targetPath := filepath.Join(projectFolder, ".devcontainer", "scripts") - sourceParent, sourceFileName := filepath.Split(action.SourcePath) - sourceBasePath := filepath.Join(snippet.Path, sourceParent) - err = copyAndRunScriptFile(projectFolder, snippet, sourceBasePath, targetPath, sourceFileName) - if err != nil { - return err - } - case FolderSnippetActionDockerfileSnippet: - var content string - if action.Content != "" { - if action.ContentPath != "" { - return fmt.Errorf("can only set one of content and contentPath") - } - content = action.Content + "\n" - } else if action.ContentPath != "" { - buf, err = ioutil.ReadFile(filepath.Join(snippet.Path, action.ContentPath)) - if err != nil { - return err - } - content = string(buf) - } else { - return fmt.Errorf("one of content and contentPath must be set for %s actions", action.Type) - } - dockerfileFilename := filepath.Join(projectFolder, ".devcontainer", "Dockerfile") - err = insertDockerfileSnippet(projectFolder, dockerfileFilename, content) - if err != nil { - return err - } - default: - return fmt.Errorf("unhandled action type: %q", action.Type) - } - } - - return nil -} - -func copyAndRunScriptFile(projectFolder string, snippet *DevcontainerSnippet, snippetBasePath string, targetPath, scriptFilename string) error { - if err := os.MkdirAll(targetPath, 0755); err != nil { - return err - } - if err := ioutil2.CopyFile(filepath.Join(snippetBasePath, scriptFilename), filepath.Join(targetPath, scriptFilename), 0755); err != nil { - return err - } - - snippetContent := fmt.Sprintf(`# %[1]s -COPY scripts/%[2]s /tmp/ -RUN /tmp/%[2]s -`, snippet.Name, scriptFilename) - dockerfileFilename := filepath.Join(projectFolder, ".devcontainer", "Dockerfile") - - err := insertDockerfileSnippet(projectFolder, dockerfileFilename, snippetContent) - return err -} - -func insertDockerfileSnippet(projectFolder string, dockerfileFilename string, snippetContent string) error { - - buf, err := ioutil.ReadFile(dockerfileFilename) - if err != nil { - return fmt.Errorf("Error reading Dockerfile: %s", err) - } - - dockerfileContent := string(buf) - dockerFileLines := strings.Split(dockerfileContent, "\n") - addSeparator := false - addedSnippetContent := false - var newContent strings.Builder - for _, line := range dockerFileLines { - if addSeparator { - if _, err = newContent.WriteString("\n"); err != nil { - return err - } - } - addSeparator = true - - if strings.Contains(line, "__DEVCONTAINER_SNIPPET_INSERT__") { - if _, err = newContent.WriteString(snippetContent); err != nil { - return err - } - if _, err = newContent.WriteString("\n"); err != nil { - return err - } - line += "\n" - addedSnippetContent = true - addSeparator = false // avoid extra separator - } - - if _, err = newContent.WriteString(line); err != nil { - return err - } - } - - if !addedSnippetContent { - if _, err = newContent.WriteString("\n"); err != nil { - return err - } - if _, err = newContent.WriteString(snippetContent); err != nil { - return err - } - } - - content := newContent.String() - // TODO - decide whether to support .devcontainer.json or just remove snippet support - values, err := getSubstitutionValuesFromFile(filepath.Join(projectFolder, ".devcontainer/devcontainer.json")) - if err != nil { - return fmt.Errorf("failed to get dev container values: %s", err) - } - content = performSubstitutionString(values, content) - - err = ioutil.WriteFile(dockerfileFilename, []byte(content), 0) - - return err - -} -func mergeJSON(projectFolder string, snippet *DevcontainerSnippet, relativeMergePath string, relativeBasePath string) error { - mergePath := filepath.Join(snippet.Path, relativeMergePath) - _, err := os.Stat(mergePath) - if err != nil { - return err - } - basePath := filepath.Join(projectFolder, relativeBasePath) - baseDocument, err := loadJSONDocument(basePath) - if err != nil { - return err - } - - mergeDocument, err := loadJSONDocument(mergePath) - if err != nil { - return err - } - - resultDocument, err := dora_merge.MergeJSON(*baseDocument, *mergeDocument) - if err != nil { - return err - } - - resultJSON, err := dora_ast.WriteJSONString(resultDocument) - if err != nil { - return err - } - - // TODO - decide whether to support .devcontainer.json or just remove snippet support - values, err := getSubstitutionValuesFromFile(filepath.Join(projectFolder, ".devcontainer/devcontainer.json")) - if err != nil { - return fmt.Errorf("failed to get dev container values: %s", err) - } - resultJSON = performSubstitutionString(values, resultJSON) - - err = ioutil.WriteFile(basePath, []byte(resultJSON), 0666) - if err != nil { - return fmt.Errorf("failed to write file: %s", err) - } - - return nil -} - -func loadJSONDocument(path string) (*dora_ast.RootNode, error) { - - buf, err := ioutil.ReadFile(path) - if err != nil { - return nil, err - } - l := dora_lexer.New(string(buf)) - p := dora_parser.New(l) - baseDocument, err := p.ParseJSON() - if err != nil { - return nil, err - } - return &baseDocument, nil -} - -func getSubstitutionValuesFromFile(devContainerJsonPath string) (*SubstitutionValues, error) { - // This doesn't use standard `json` pkg as devcontainer.json permits comments (and the default templates include them!) - - buf, err := ioutil.ReadFile(devContainerJsonPath) - if err != nil { - return nil, err - } - - c, err := dora.NewFromBytes(buf) - if err != nil { - return nil, err - } - - name, err := c.GetString("$.name") - if err != nil { - name = "" - } - userName, err := c.GetString("$.remoteUser") - if err != nil { - userName = "root" - } - homeFolder := "/home/" + userName - if userName == "root" { - homeFolder = "/root" - } - - return &SubstitutionValues{ - Name: name, - UserName: userName, - HomeFolder: homeFolder, - }, nil -} - -func performSubstitutionFile(substitutionValues *SubstitutionValues, filename string) error { - buf, err := ioutil.ReadFile(filename) - if err != nil { - return err - } - content := string(buf) - content = performSubstitutionString(substitutionValues, content) - err = ioutil.WriteFile(filename, []byte(content), 0) - return err -} - -func performSubstitutionString(substitutionValues *SubstitutionValues, content string) string { - // replace __DEVCONTAINER_NAME__ with name etc - content = strings.ReplaceAll(content, "__DEVCONTAINER_NAME__", substitutionValues.Name) - content = strings.ReplaceAll(content, "__DEVCONTAINER_USER_NAME__", substitutionValues.UserName) - content = strings.ReplaceAll(content, "__DEVCONTAINER_HOME__", substitutionValues.HomeFolder) - return content -} diff --git a/internal/pkg/devcontainers/snippet_test.go b/internal/pkg/devcontainers/snippet_test.go deleted file mode 100644 index f6e8d3d..0000000 --- a/internal/pkg/devcontainers/snippet_test.go +++ /dev/null @@ -1,794 +0,0 @@ -package devcontainers - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGetSnippets_ListsSingleFileTemplates(t *testing.T) { - - root, err := ioutil.TempDir("", "devcontainer*") - if !assert.NoError(t, err) { - return - } - defer os.RemoveAll(root) - - folders := []string{root} - - _ = ioutil.WriteFile(filepath.Join(root, "test1.sh"), []byte{}, 0755) - _ = ioutil.WriteFile(filepath.Join(root, "test2.sh"), []byte{}, 0755) - - snippets, err := getSnippetsFromFolders(folders) - if !assert.NoError(t, err) { - return - } - - expectedTemplates := []DevcontainerSnippet{ - { - Name: "test1", - Type: DevcontainerSnippetTypeSingleFile, - Path: filepath.Join(root, "test1.sh"), - }, - { - Name: "test2", - Type: DevcontainerSnippetTypeSingleFile, - Path: filepath.Join(root, "test2.sh"), - }, - } - - assert.ElementsMatch(t, expectedTemplates, snippets) -} -func TestGetSnippets_IgnoresFilesWithIncorrectPrefix(t *testing.T) { - - root, err := ioutil.TempDir("", "devcontainer*") - if !assert.NoError(t, err) { - return - } - defer os.RemoveAll(root) - - folders := []string{root} - - _ = ioutil.WriteFile(filepath.Join(root, "_ignore.sh"), []byte{}, 0755) - _ = ioutil.WriteFile(filepath.Join(root, ".ignore.sh"), []byte{}, 0755) - _ = ioutil.WriteFile(filepath.Join(root, "test1.sh"), []byte{}, 0755) - - snippets, err := getSnippetsFromFolders(folders) - if !assert.NoError(t, err) { - return - } - - expectedTemplates := []DevcontainerSnippet{ - { - Name: "test1", - Type: DevcontainerSnippetTypeSingleFile, - Path: filepath.Join(root, "test1.sh"), - }, - } - - assert.ElementsMatch(t, expectedTemplates, snippets) -} -func TestGetSnippets_ListsFolderTemplate(t *testing.T) { - - root1, err := ioutil.TempDir("", "devcontainer*") - if !assert.NoError(t, err) { - return - } - defer os.RemoveAll(root1) - root2, err := ioutil.TempDir("", "devcontainer*") - if !assert.NoError(t, err) { - return - } - defer os.RemoveAll(root1) - - folders := []string{root1, root2} - - _ = os.MkdirAll(filepath.Join(root1, "test1"), 0755) - _ = ioutil.WriteFile(filepath.Join(root1, "test1/snippet.json"), []byte{}, 0755) - _ = ioutil.WriteFile(filepath.Join(root2, "test1.sh"), []byte{}, 0755) - - snippets, err := getSnippetsFromFolders(folders) - if !assert.NoError(t, err) { - return - } - - expectedTemplates := []DevcontainerSnippet{ - { - Name: "test1", - Type: DevcontainerSnippetTypeFolder, - Path: filepath.Join(root1, "test1"), // Uses root1 as it is in the list first - }, - } - - assert.ElementsMatch(t, expectedTemplates, snippets) -} -func TestGetSnippets_TakesFilesInPriorityOrder(t *testing.T) { - - root1, err := ioutil.TempDir("", "devcontainer*") - if !assert.NoError(t, err) { - return - } - defer os.RemoveAll(root1) - root2, err := ioutil.TempDir("", "devcontainer*") - if !assert.NoError(t, err) { - return - } - defer os.RemoveAll(root1) - - folders := []string{root1, root2} - - _ = ioutil.WriteFile(filepath.Join(root1, "test1.sh"), []byte{}, 0755) - _ = ioutil.WriteFile(filepath.Join(root2, "test1.sh"), []byte{}, 0755) - - snippets, err := getSnippetsFromFolders(folders) - if !assert.NoError(t, err) { - return - } - - expectedTemplates := []DevcontainerSnippet{ - { - Name: "test1", - Type: DevcontainerSnippetTypeSingleFile, - Path: filepath.Join(root1, "test1.sh"), // Uses root1 as it is in the list first - }, - } - - assert.ElementsMatch(t, expectedTemplates, snippets) -} - -func TestSingleFileAddSnippet_NoInsertionPoint(t *testing.T) { - - root, _ := ioutil.TempDir("", "devcontainer*") - defer os.RemoveAll(root) - - // set up snippet - snippetFolder := filepath.Join(root, "snippets") - _ = os.MkdirAll(snippetFolder, 0755) - snippetFilename := filepath.Join(snippetFolder, "test1.sh") - _ = ioutil.WriteFile(snippetFilename, []byte("# dummy file"), 0755) - - // set up devcontainer - targetFolder := filepath.Join(root, "target") - devcontainerFolder := filepath.Join(targetFolder, ".devcontainer") - _ = os.MkdirAll(devcontainerFolder, 0755) - - _ = ioutil.WriteFile(filepath.Join(devcontainerFolder, "Dockerfile"), []byte(`FROM foo -RUN echo hi -`), 0755) - _ = ioutil.WriteFile(filepath.Join(devcontainerFolder, "devcontainer.json"), []byte(`{ - "name" : "testname" - }`), 0755) - - // Add snippet - snippet := DevcontainerSnippet{ - Name: "test", - Path: snippetFilename, - Type: DevcontainerSnippetTypeSingleFile, - } - err := addSingleFileSnippetToDevContainer(targetFolder, &snippet) - if !assert.NoError(t, err) { - return - } - - buf, err := ioutil.ReadFile(filepath.Join(devcontainerFolder, "scripts", "test1.sh")) - if !assert.NoError(t, err) { - return - } - assert.Equal(t, "# dummy file", string(buf)) - - buf, err = ioutil.ReadFile(filepath.Join(devcontainerFolder, "Dockerfile")) - if !assert.NoError(t, err) { - return - } - assert.Equal(t, `FROM foo -RUN echo hi - -# test -COPY scripts/test1.sh /tmp/ -RUN /tmp/test1.sh -`, string(buf)) -} -func TestSingleFileAddSnippet_WithInsertionPoint(t *testing.T) { - - root, _ := ioutil.TempDir("", "devcontainer*") - defer os.RemoveAll(root) - - // set up snippet - snippetFolder := filepath.Join(root, "snippets") - _ = os.MkdirAll(snippetFolder, 0755) - snippetFilename := filepath.Join(snippetFolder, "test1.sh") - _ = ioutil.WriteFile(snippetFilename, []byte("# dummy file"), 0755) - - // set up devcontainer - targetFolder := filepath.Join(root, "target") - devcontainerFolder := filepath.Join(targetFolder, ".devcontainer") - _ = os.MkdirAll(devcontainerFolder, 0755) - - _ = ioutil.WriteFile(filepath.Join(devcontainerFolder, "Dockerfile"), []byte(`FROM foo -RUN echo hi -# __DEVCONTAINER_SNIPPET_INSERT__ - -RUN echo hi2 -`), 0755) - _ = ioutil.WriteFile(filepath.Join(devcontainerFolder, "devcontainer.json"), []byte(`{ - "name" : "testname" -}`), 0755) - - // Add snippet - snippet := DevcontainerSnippet{ - Name: "test", - Path: snippetFilename, - Type: DevcontainerSnippetTypeSingleFile, - } - err := addSingleFileSnippetToDevContainer(targetFolder, &snippet) - if !assert.NoError(t, err) { - return - } - - buf, err := ioutil.ReadFile(filepath.Join(devcontainerFolder, "scripts", "test1.sh")) - if !assert.NoError(t, err) { - return - } - assert.Equal(t, "# dummy file", string(buf)) - - buf, err = ioutil.ReadFile(filepath.Join(devcontainerFolder, "Dockerfile")) - if !assert.NoError(t, err) { - return - } - assert.Equal(t, `FROM foo -RUN echo hi -# test -COPY scripts/test1.sh /tmp/ -RUN /tmp/test1.sh - -# __DEVCONTAINER_SNIPPET_INSERT__ - -RUN echo hi2 -`, string(buf)) -} - -func TestFolderAddSnippet_MergesDevcontainerJSON(t *testing.T) { - - root, _ := ioutil.TempDir("", "devcontainer*") - defer os.RemoveAll(root) - - // set up snippet - snippetFolder := filepath.Join(root, "snippets/test1") - _ = os.MkdirAll(snippetFolder, 0755) - snippetJSONFilename := filepath.Join(snippetFolder, "snippet.json") - _ = ioutil.WriteFile(snippetJSONFilename, []byte(`{ - "actions": [ - { - "type": "mergeJSON", - "source": "devcontainer.json", - "target": ".devcontainer/devcontainer.json" - } - ] - }`), 0755) - - snippetDevcontainerFilename := filepath.Join(snippetFolder, "devcontainer.json") - _ = ioutil.WriteFile(snippetDevcontainerFilename, []byte(`// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/go -{ - "runArgs": [ - // Mount go mod cache - "-v", "devcontainer-cli-gomodcache:/go/pkg", - ], - - // Set *default* container specific settings.json values on container create. - "settings": { - "go.gopath": "/go", - "go.useLanguageServer": true, - "[go]": { - "editor.snippetSuggestions": "none", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports": true, - } - }, - "gopls": { - "usePlaceholders": true, // add parameter placeholders when completing a function - // Experimental settings - "completeUnimported": true, // autocomplete unimported packages - "watchFileChanges": true, // watch file changes outside of the editor - "deepCompletion": true, // enable deep completion - }, - }, - - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "stuartleeks.vscode-go-by-example", - "golang.go", - ], -}`), 0755) - - // set up devcontainer - targetFolder := filepath.Join(root, "target") - devcontainerFolder := filepath.Join(targetFolder, ".devcontainer") - _ = os.MkdirAll(devcontainerFolder, 0755) - - _ = ioutil.WriteFile(filepath.Join(devcontainerFolder, "devcontainer.json"), []byte(`// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/go -{ - "name": "test", - "dockerFile": "Dockerfile", - "runArgs": [ - // Use host network - "--network=host", - ], - - // Set *default* container specific settings.json values on container create. - "settings": { - "terminal.integrated.shell.linux": "/bin/bash", - }, - - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "example.test", - ], - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "vscode" -}`), 0755) - - // Add snippet - snippet := DevcontainerSnippet{ - Name: "test", - Path: snippetFolder, - Type: DevcontainerSnippetTypeFolder, - } - err := addSnippetToDevcontainer(targetFolder, &snippet) - if !assert.NoError(t, err) { - return - } - - buf, err := ioutil.ReadFile(filepath.Join(devcontainerFolder, "devcontainer.json")) - if !assert.NoError(t, err) { - return - } - stringContent := string(buf) - assert.Equal(t, `// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/go -{ - "name": "test", - "dockerFile": "Dockerfile", - "runArgs": [ - // Use host network - "--network=host", - // Mount go mod cache - "-v", "devcontainer-cli-gomodcache:/go/pkg", - ], - - // Set *default* container specific settings.json values on container create. - "settings": { - "terminal.integrated.shell.linux": "/bin/bash", - "go.gopath": "/go", - "go.useLanguageServer": true, - "[go]": { - "editor.snippetSuggestions": "none", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports": true, - } - }, - "gopls": { - "usePlaceholders": true, // add parameter placeholders when completing a function - // Experimental settings - "completeUnimported": true, // autocomplete unimported packages - "watchFileChanges": true, // watch file changes outside of the editor - "deepCompletion": true, // enable deep completion - }, - }, - - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "example.test", - "stuartleeks.vscode-go-by-example", - "golang.go", - ], - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "vscode" -}`, stringContent) -} - -func TestFolderAddSnippet_CopiesScriptAndUpdatesDockerfile(t *testing.T) { - - root, _ := ioutil.TempDir("", "devcontainer*") - defer os.RemoveAll(root) - - // set up snippet - snippetFolder := filepath.Join(root, "snippets/test1") - _ = os.MkdirAll(snippetFolder, 0755) - snippetJSONFilename := filepath.Join(snippetFolder, "snippet.json") - _ = ioutil.WriteFile(snippetJSONFilename, []byte(`{ - "actions": [ - { - "type": "copyAndRun", - "source": "script.sh" - } - ] - }`), 0755) - - scriptFilename := filepath.Join(snippetFolder, "script.sh") - _ = ioutil.WriteFile(scriptFilename, []byte("# dummy file"), 0755) - - // set up devcontainer - targetFolder := filepath.Join(root, "target") - devcontainerFolder := filepath.Join(targetFolder, ".devcontainer") - _ = os.MkdirAll(devcontainerFolder, 0755) - - _ = ioutil.WriteFile(filepath.Join(devcontainerFolder, "Dockerfile"), []byte(`FROM foo -RUN echo hi - -# __DEVCONTAINER_SNIPPET_INSERT__ - -RUN echo hi2 -`), 0755) - _ = ioutil.WriteFile(filepath.Join(devcontainerFolder, "devcontainer.json"), []byte(`{ - "name" : "testname" -}`), 0755) - - // Add snippet - snippet := DevcontainerSnippet{ - Name: "test", - Path: snippetFolder, - Type: DevcontainerSnippetTypeFolder, - } - err := addSnippetToDevcontainer(targetFolder, &snippet) - if !assert.NoError(t, err) { - return - } - - buf, err := ioutil.ReadFile(filepath.Join(devcontainerFolder, "scripts", "script.sh")) - if !assert.NoError(t, err) { - return - } - assert.Equal(t, "# dummy file", string(buf)) - - buf, err = ioutil.ReadFile(filepath.Join(devcontainerFolder, "Dockerfile")) - if !assert.NoError(t, err) { - return - } - assert.Equal(t, `FROM foo -RUN echo hi - -# test -COPY scripts/script.sh /tmp/ -RUN /tmp/script.sh - -# __DEVCONTAINER_SNIPPET_INSERT__ - -RUN echo hi2 -`, string(buf)) -} - -func TestFolderAddSnippet_InsertsTextSnippetsInDockerfile(t *testing.T) { - - root, _ := ioutil.TempDir("", "devcontainer*") - defer os.RemoveAll(root) - - // set up snippet - snippetFolder := filepath.Join(root, "snippets/test1") - _ = os.MkdirAll(snippetFolder, 0755) - snippetJSONFilename := filepath.Join(snippetFolder, "snippet.json") - _ = ioutil.WriteFile(snippetJSONFilename, []byte(`{ - "actions": [ - { - "type": "dockerfileSnippet", - "content": "ENV FOO=BAR" - }, - { - "type": "dockerfileSnippet", - "content": "# testing\nENV WIBBLE=BIBBLE" - } - ] - }`), 0755) - - // set up devcontainer - targetFolder := filepath.Join(root, "target") - devcontainerFolder := filepath.Join(targetFolder, ".devcontainer") - _ = os.MkdirAll(devcontainerFolder, 0755) - - _ = ioutil.WriteFile(filepath.Join(devcontainerFolder, "Dockerfile"), []byte(`FROM foo -RUN echo hi - -# __DEVCONTAINER_SNIPPET_INSERT__ - -RUN echo hi2 -`), 0755) - _ = ioutil.WriteFile(filepath.Join(devcontainerFolder, "devcontainer.json"), []byte(`{ - "name" : "testname" -}`), 0755) - - // Add snippet - snippet := DevcontainerSnippet{ - Name: "test", - Path: snippetFolder, - Type: DevcontainerSnippetTypeFolder, - } - err := addSnippetToDevcontainer(targetFolder, &snippet) - if !assert.NoError(t, err) { - return - } - - buf, err := ioutil.ReadFile(filepath.Join(devcontainerFolder, "Dockerfile")) - if !assert.NoError(t, err) { - return - } - assert.Equal(t, `FROM foo -RUN echo hi - -ENV FOO=BAR - -# testing -ENV WIBBLE=BIBBLE - -# __DEVCONTAINER_SNIPPET_INSERT__ - -RUN echo hi2 -`, string(buf)) -} - -func TestFolderAddSnippet_InsertsFileSnippetInDockerfile(t *testing.T) { - - root, _ := ioutil.TempDir("", "devcontainer*") - defer os.RemoveAll(root) - - // set up snippet - snippetFolder := filepath.Join(root, "snippets/test1") - _ = os.MkdirAll(snippetFolder, 0755) - snippetJSONFilename := filepath.Join(snippetFolder, "snippet.json") - _ = ioutil.WriteFile(snippetJSONFilename, []byte(`{ - "actions": [ - { - "type": "dockerfileSnippet", - "content": "ENV FOO=BAR" - }, - { - "type": "dockerfileSnippet", - "contentPath": "Dockerfile" - } - ] - }`), 0755) - snippetDockerfileFilename := filepath.Join(snippetFolder, "Dockerfile") - _ = ioutil.WriteFile(snippetDockerfileFilename, []byte(`# from snippet file -ENV WIBBLE BIBBLE -`), 0755) - - // set up devcontainer - targetFolder := filepath.Join(root, "target") - devcontainerFolder := filepath.Join(targetFolder, ".devcontainer") - _ = os.MkdirAll(devcontainerFolder, 0755) - - _ = ioutil.WriteFile(filepath.Join(devcontainerFolder, "Dockerfile"), []byte(`FROM foo -RUN echo hi - -# __DEVCONTAINER_SNIPPET_INSERT__ - -RUN echo hi2 -`), 0755) - _ = ioutil.WriteFile(filepath.Join(devcontainerFolder, "devcontainer.json"), []byte(`{ - "name" : "testname" -}`), 0755) - - // Add snippet - snippet := DevcontainerSnippet{ - Name: "test", - Path: snippetFolder, - Type: DevcontainerSnippetTypeFolder, - } - err := addSnippetToDevcontainer(targetFolder, &snippet) - if !assert.NoError(t, err) { - return - } - - buf, err := ioutil.ReadFile(filepath.Join(devcontainerFolder, "Dockerfile")) - if !assert.NoError(t, err) { - return - } - assert.Equal(t, `FROM foo -RUN echo hi - -ENV FOO=BAR - -# from snippet file -ENV WIBBLE BIBBLE - -# __DEVCONTAINER_SNIPPET_INSERT__ - -RUN echo hi2 -`, string(buf)) -} - -func TestFolderAddSnippet_PerformsSubstitutionWithoutUserName(t *testing.T) { - - root, _ := ioutil.TempDir("", "devcontainer*") - defer os.RemoveAll(root) - - // set up snippet - snippetFolder := filepath.Join(root, "snippets/test1") - _ = os.MkdirAll(snippetFolder, 0755) - snippetJSONFilename := filepath.Join(snippetFolder, "snippet.json") - _ = ioutil.WriteFile(snippetJSONFilename, []byte(`{ - "actions": [ - { - "type": "mergeJSON", - "source": "devcontainer.json", - "target": ".devcontainer/devcontainer.json" - }, - { - "type": "dockerfileSnippet", - "content": "ENV DC_NAME=__DEVCONTAINER_NAME__\nENV DC_USER_NAME=__DEVCONTAINER_USER_NAME__\nENV DC_HOME=__DEVCONTAINER_HOME__" - } - ] - }`), 0755) - - snippetDevcontainerFilename := filepath.Join(snippetFolder, "devcontainer.json") - _ = ioutil.WriteFile(snippetDevcontainerFilename, []byte(`{ - "settings": { - "DC_NAME": "__DEVCONTAINER_NAME__", - "DC_USER_NAME": "__DEVCONTAINER_USER_NAME__", - "DC_HOME": "__DEVCONTAINER_HOME__" - }, -}`), 0755) - - // set up devcontainer - targetFolder := filepath.Join(root, "target") - devcontainerFolder := filepath.Join(targetFolder, ".devcontainer") - _ = os.MkdirAll(devcontainerFolder, 0755) - - _ = ioutil.WriteFile(filepath.Join(devcontainerFolder, "Dockerfile"), []byte(`FROM foo -RUN echo hi - -# __DEVCONTAINER_SNIPPET_INSERT__ - -RUN echo hi2 -`), 0755) - _ = ioutil.WriteFile(filepath.Join(devcontainerFolder, "devcontainer.json"), []byte(`{ - "name" : "testname" -}`), 0755) - - // Add snippet - snippet := DevcontainerSnippet{ - Name: "test", - Path: snippetFolder, - Type: DevcontainerSnippetTypeFolder, - } - err := addSnippetToDevcontainer(targetFolder, &snippet) - if !assert.NoError(t, err) { - return - } - - buf, err := ioutil.ReadFile(filepath.Join(devcontainerFolder, "Dockerfile")) - if !assert.NoError(t, err) { - return - } - assert.Equal(t, `FROM foo -RUN echo hi - -ENV DC_NAME=testname -ENV DC_USER_NAME=root -ENV DC_HOME=/root - -# __DEVCONTAINER_SNIPPET_INSERT__ - -RUN echo hi2 -`, string(buf)) - - buf, err = ioutil.ReadFile(filepath.Join(devcontainerFolder, "devcontainer.json")) - if !assert.NoError(t, err) { - return - } - stringContent := string(buf) - assert.Equal(t, `{ - "name" : "testname", - "settings": { - "DC_NAME": "testname", - "DC_USER_NAME": "root", - "DC_HOME": "/root" - }, -}`, stringContent) - -} -func TestFolderAddSnippet_PerformsSubstitutionWithUserName(t *testing.T) { - - root, _ := ioutil.TempDir("", "devcontainer*") - defer os.RemoveAll(root) - - // set up snippet - snippetFolder := filepath.Join(root, "snippets/test1") - _ = os.MkdirAll(snippetFolder, 0755) - snippetJSONFilename := filepath.Join(snippetFolder, "snippet.json") - _ = ioutil.WriteFile(snippetJSONFilename, []byte(`{ - "actions": [ - { - "type": "mergeJSON", - "source": "devcontainer.json", - "target": ".devcontainer/devcontainer.json" - }, - { - "type": "dockerfileSnippet", - "content": "ENV DC_NAME=__DEVCONTAINER_NAME__\nENV DC_USER_NAME=__DEVCONTAINER_USER_NAME__\nENV DC_HOME=__DEVCONTAINER_HOME__" - } - ] - }`), 0755) - - snippetDevcontainerFilename := filepath.Join(snippetFolder, "devcontainer.json") - _ = ioutil.WriteFile(snippetDevcontainerFilename, []byte(`{ - "settings": { - "DC_NAME": "__DEVCONTAINER_NAME__", - "DC_USER_NAME": "__DEVCONTAINER_USER_NAME__", - "DC_HOME": "__DEVCONTAINER_HOME__" - }, -}`), 0755) - - // set up devcontainer - targetFolder := filepath.Join(root, "target") - devcontainerFolder := filepath.Join(targetFolder, ".devcontainer") - _ = os.MkdirAll(devcontainerFolder, 0755) - - _ = ioutil.WriteFile(filepath.Join(devcontainerFolder, "Dockerfile"), []byte(`FROM foo -RUN echo hi - -# __DEVCONTAINER_SNIPPET_INSERT__ - -RUN echo hi2 -`), 0755) - _ = ioutil.WriteFile(filepath.Join(devcontainerFolder, "devcontainer.json"), []byte(`{ - "name" : "testname", - "remoteUser": "dcuser" -}`), 0755) - - // Add snippet - snippet := DevcontainerSnippet{ - Name: "test", - Path: snippetFolder, - Type: DevcontainerSnippetTypeFolder, - } - err := addSnippetToDevcontainer(targetFolder, &snippet) - if !assert.NoError(t, err) { - return - } - - buf, err := ioutil.ReadFile(filepath.Join(devcontainerFolder, "Dockerfile")) - if !assert.NoError(t, err) { - return - } - assert.Equal(t, `FROM foo -RUN echo hi - -ENV DC_NAME=testname -ENV DC_USER_NAME=dcuser -ENV DC_HOME=/home/dcuser - -# __DEVCONTAINER_SNIPPET_INSERT__ - -RUN echo hi2 -`, string(buf)) - - buf, err = ioutil.ReadFile(filepath.Join(devcontainerFolder, "devcontainer.json")) - if !assert.NoError(t, err) { - return - } - stringContent := string(buf) - assert.Equal(t, `{ - "name" : "testname", - "remoteUser": "dcuser", - "settings": { - "DC_NAME": "testname", - "DC_USER_NAME": "dcuser", - "DC_HOME": "/home/dcuser" - }, -}`, stringContent) - -} diff --git a/internal/pkg/devcontainers/template.go b/internal/pkg/devcontainers/template.go index 34e08ff..fba0fd7 100644 --- a/internal/pkg/devcontainers/template.go +++ b/internal/pkg/devcontainers/template.go @@ -2,13 +2,13 @@ package devcontainers import ( "fmt" - "io/ioutil" "os" "path/filepath" "regexp" "sort" "strings" + "github.com/bradford-hamilton/dora/pkg/dora" "github.com/stuartleeks/devcontainer-cli/internal/pkg/config" "github.com/stuartleeks/devcontainer-cli/internal/pkg/errors" ioutil2 "github.com/stuartleeks/devcontainer-cli/internal/pkg/ioutil" @@ -21,6 +21,12 @@ type DevcontainerTemplate struct { Path string } +type SubstitutionValues struct { + Name string + UserName string + HomeFolder string +} + // GetTemplateByName returns the template with the specified name or nil if not found func GetTemplateByName(name string) (*DevcontainerTemplate, error) { // TODO - could possibly make this quicker by searching using the name rather than listing all and filtering @@ -72,7 +78,7 @@ func getTemplatesFromFolders(folders []string) ([]DevcontainerTemplate, error) { } func getTemplatesFromFolder(folder string) ([]DevcontainerTemplate, error) { - isDevcontainerFolder := func(parentPath string, fi os.FileInfo) bool { + isDevcontainerFolder := func(parentPath string, fi os.DirEntry) bool { if !fi.IsDir() { return false } @@ -81,10 +87,10 @@ func getTemplatesFromFolder(folder string) ([]DevcontainerTemplate, error) { devContainerJsonInfo, err := os.Stat(devcontainerJsonPath) return err == nil && !devContainerJsonInfo.IsDir() } - c, err := ioutil.ReadDir(folder) + c, err := os.ReadDir(folder) if err != nil { - return []DevcontainerTemplate{}, fmt.Errorf("Error reading devcontainer definitions: %s\n", err) + return []DevcontainerTemplate{}, fmt.Errorf("error reading devcontainer definitions: %s", err) } templates := []DevcontainerTemplate{} @@ -115,7 +121,7 @@ func CopyTemplateToFolder(templatePath string, targetFolder string, devcontainer var err error if err = ioutil2.CopyFolder(templatePath, filepath.Join(targetFolder, ".devcontainer")); err != nil { - return fmt.Errorf("Error copying folder: %s\n", err) + return fmt.Errorf("error copying folder: %s", err) } // by default the "name" in devcontainer.json is set to the name of the template @@ -123,22 +129,22 @@ func CopyTemplateToFolder(templatePath string, targetFolder string, devcontainer if devcontainerName == "" { devcontainerName, err = GetDefaultDevcontainerNameForFolder(targetFolder) if err != nil { - return fmt.Errorf("Error getting default devcontainer name: %s", err) + return fmt.Errorf("error getting default devcontainer name: %s", err) } } devcontainerJsonPath := filepath.Join(targetFolder, ".devcontainer", "devcontainer.json") err = SetDevcontainerName(devcontainerJsonPath, devcontainerName) if err != nil { - return fmt.Errorf("Error setting devcontainer name: %s", err) + return fmt.Errorf("error setting devcontainer name: %s", err) } values, err := getSubstitutionValuesFromFile(devcontainerJsonPath) if err != nil { - return fmt.Errorf("Error getting substituion values: %s", err) + return fmt.Errorf("error getting substituion values: %s", err) } err = recursiveSubstituteValues(values, filepath.Join(targetFolder, ".devcontainer")) if err != nil { - return fmt.Errorf("Error performing substitution: %s", err) + return fmt.Errorf("error performing substitution: %s", err) } return nil @@ -147,12 +153,12 @@ func CopyTemplateToFolder(templatePath string, targetFolder string, devcontainer func recursiveSubstituteValues(values *SubstitutionValues, path string) error { _, err := os.Stat(path) if err != nil { - return fmt.Errorf("Error reading folder: %s\n", err) + return fmt.Errorf("error reading folder: %s", err) } - subItems, err := ioutil.ReadDir(path) + subItems, err := os.ReadDir(path) if err != nil { - return fmt.Errorf("Error reading source folder contents: %s\n", err) + return fmt.Errorf("error reading source folder contents: %s", err) } for _, subItem := range subItems { @@ -175,7 +181,7 @@ func SetDevcontainerName(devContainerJsonPath string, name string) error { // TODO - update this to use dora to query // TODO - update this to replace __DEVCONTAINER_USER_NAME__ and __DEVCONTAINER_HOME__ - buf, err := ioutil.ReadFile(devContainerJsonPath) + buf, err := os.ReadFile(devContainerJsonPath) if err != nil { return fmt.Errorf("error reading file %q: %s", devContainerJsonPath, err) } @@ -190,7 +196,7 @@ func SetDevcontainerName(devContainerJsonPath string, name string) error { content = strings.ReplaceAll(content, "__DEVCONTAINER_NAME__", name) buf = []byte(content) - if err = ioutil.WriteFile(devContainerJsonPath, buf, 0777); err != nil { + if err = os.WriteFile(devContainerJsonPath, buf, 0777); err != nil { return fmt.Errorf("error writing file %q: %s", devContainerJsonPath, err) } @@ -199,7 +205,7 @@ func SetDevcontainerName(devContainerJsonPath string, name string) error { // "remoteUser": "vscode" func GetDevContainerUserName(devContainerJsonPath string) (string, error) { - buf, err := ioutil.ReadFile(devContainerJsonPath) + buf, err := os.ReadFile(devContainerJsonPath) if err != nil { return "", fmt.Errorf("error reading file %q: %s", devContainerJsonPath, err) } @@ -212,3 +218,55 @@ func GetDevContainerUserName(devContainerJsonPath string) (string, error) { } return match[1], nil } + +func getSubstitutionValuesFromFile(devContainerJsonPath string) (*SubstitutionValues, error) { + // This doesn't use standard `json` pkg as devcontainer.json permits comments (and the default templates include them!) + + buf, err := os.ReadFile(devContainerJsonPath) + if err != nil { + return nil, err + } + + c, err := dora.NewFromBytes(buf) + if err != nil { + return nil, err + } + + name, err := c.GetString("$.name") + if err != nil { + name = "" + } + userName, err := c.GetString("$.remoteUser") + if err != nil { + userName = "root" + } + homeFolder := "/home/" + userName + if userName == "root" { + homeFolder = "/root" + } + + return &SubstitutionValues{ + Name: name, + UserName: userName, + HomeFolder: homeFolder, + }, nil +} + +func performSubstitutionFile(substitutionValues *SubstitutionValues, filename string) error { + buf, err := os.ReadFile(filename) + if err != nil { + return err + } + content := string(buf) + content = performSubstitutionString(substitutionValues, content) + err = os.WriteFile(filename, []byte(content), 0) + return err +} + +func performSubstitutionString(substitutionValues *SubstitutionValues, content string) string { + // replace __DEVCONTAINER_NAME__ with name etc + content = strings.ReplaceAll(content, "__DEVCONTAINER_NAME__", substitutionValues.Name) + content = strings.ReplaceAll(content, "__DEVCONTAINER_USER_NAME__", substitutionValues.UserName) + content = strings.ReplaceAll(content, "__DEVCONTAINER_HOME__", substitutionValues.HomeFolder) + return content +} diff --git a/internal/pkg/devcontainers/template_test.go b/internal/pkg/devcontainers/template_test.go index a65551c..75abd2b 100644 --- a/internal/pkg/devcontainers/template_test.go +++ b/internal/pkg/devcontainers/template_test.go @@ -1,7 +1,6 @@ package devcontainers import ( - "io/ioutil" "os" "path/filepath" "testing" @@ -11,11 +10,12 @@ import ( func TestSetDevcontainerName(t *testing.T) { - f, err := ioutil.TempFile("", "test.json") + f, err := os.CreateTemp("", "test.json") if !assert.NoError(t, err) { return } - defer os.Remove(f.Name()) + defer func() { _ = f.Close() }() + defer func() { _ = os.Remove(f.Name()) }() _, _ = f.WriteString(`{ "name": "initial", @@ -31,7 +31,7 @@ func TestSetDevcontainerName(t *testing.T) { return } - buf, err := ioutil.ReadFile(f.Name()) + buf, err := os.ReadFile(f.Name()) if !assert.NoError(t, err) { return } @@ -48,11 +48,12 @@ func TestSetDevcontainerName(t *testing.T) { func TestGetDevContainerUserName_Uncommented(t *testing.T) { - f, err := ioutil.TempFile("", "test.json") + f, err := os.CreateTemp("", "test.json") if !assert.NoError(t, err) { return } - defer os.Remove(f.Name()) + defer func() { _ = f.Close() }() + defer func() { _ = os.Remove(f.Name()) }() _, _ = f.WriteString(`{ "name": "initial", @@ -73,11 +74,12 @@ func TestGetDevContainerUserName_Uncommented(t *testing.T) { func TestGetDevContainerUserName_NotSet(t *testing.T) { - f, err := ioutil.TempFile("", "test.json") + f, err := os.CreateTemp("", "test.json") if !assert.NoError(t, err) { return } - defer os.Remove(f.Name()) + defer func() { _ = f.Close() }() + defer func() { _ = os.Remove(f.Name()) }() _, _ = f.WriteString(`{ "name": "initial", @@ -97,11 +99,12 @@ func TestGetDevContainerUserName_NotSet(t *testing.T) { func TestGetDevContainerUserName_Commented(t *testing.T) { - f, err := ioutil.TempFile("", "test.json") + f, err := os.CreateTemp("", "test.json") if !assert.NoError(t, err) { return } - defer os.Remove(f.Name()) + defer func() { _ = f.Close() }() + defer func() { _ = os.Remove(f.Name()) }() _, _ = f.WriteString(`{ "name": "initial", @@ -122,11 +125,12 @@ func TestGetDevContainerUserName_Commented(t *testing.T) { func TestSetDevcontainerName_SubstitutionValue(t *testing.T) { - f, err := ioutil.TempFile("", "test.json") + f, err := os.CreateTemp("", "test.json") if !assert.NoError(t, err) { return } - defer os.Remove(f.Name()) + defer func() { _ = f.Close() }() + defer func() { _ = os.Remove(f.Name()) }() _, _ = f.WriteString(`{ "name": "initial", @@ -142,7 +146,7 @@ func TestSetDevcontainerName_SubstitutionValue(t *testing.T) { return } - buf, err := ioutil.ReadFile(f.Name()) + buf, err := os.ReadFile(f.Name()) if !assert.NoError(t, err) { return } @@ -159,19 +163,19 @@ func TestSetDevcontainerName_SubstitutionValue(t *testing.T) { func TestGetTemplateFolders_ListsFoldersWithDevcontainers(t *testing.T) { - root, err := ioutil.TempDir("", "devcontainer*") + root, err := os.MkdirTemp("", "devcontainer*") if !assert.NoError(t, err) { return } - defer os.RemoveAll(root) + defer func() { _ = os.RemoveAll(root) }() folders := []string{root} _ = os.MkdirAll(filepath.Join(root, "test1", ".devcontainer"), 0755) - _ = ioutil.WriteFile(filepath.Join(root, "test1", ".devcontainer", "devcontainer.json"), []byte{}, 0755) + _ = os.WriteFile(filepath.Join(root, "test1", ".devcontainer", "devcontainer.json"), []byte{}, 0755) _ = os.MkdirAll(filepath.Join(root, "test2", ".devcontainer"), 0755) - _ = ioutil.WriteFile(filepath.Join(root, "test2", ".devcontainer", "devcontainer.json"), []byte{}, 0755) + _ = os.WriteFile(filepath.Join(root, "test2", ".devcontainer", "devcontainer.json"), []byte{}, 0755) templates, err := getTemplatesFromFolders(folders) if !assert.NoError(t, err) { @@ -193,25 +197,25 @@ func TestGetTemplateFolders_ListsFoldersWithDevcontainers(t *testing.T) { } func TestGetTemplateFolders_TakesFolderInPrioirtyOrder(t *testing.T) { - root1, err := ioutil.TempDir("", "devcontainer*") + root1, err := os.MkdirTemp("", "devcontainer*") if !assert.NoError(t, err) { return } - defer os.RemoveAll(root1) + defer func() { _ = os.RemoveAll(root1) }() - root2, err := ioutil.TempDir("", "devcontainer*") + root2, err := os.MkdirTemp("", "devcontainer*") if !assert.NoError(t, err) { return } - defer os.RemoveAll(root2) + defer func() { _ = os.RemoveAll(root2) }() folders := []string{root1, root2} _ = os.MkdirAll(filepath.Join(root1, "test1", ".devcontainer"), 0755) - _ = ioutil.WriteFile(filepath.Join(root1, "test1", ".devcontainer", "devcontainer.json"), []byte{}, 0755) + _ = os.WriteFile(filepath.Join(root1, "test1", ".devcontainer", "devcontainer.json"), []byte{}, 0755) _ = os.MkdirAll(filepath.Join(root2, "test1", ".devcontainer"), 0755) - _ = ioutil.WriteFile(filepath.Join(root2, "test1", ".devcontainer", "devcontainer.json"), []byte{}, 0755) + _ = os.WriteFile(filepath.Join(root2, "test1", ".devcontainer", "devcontainer.json"), []byte{}, 0755) templates, err := getTemplatesFromFolders(folders) if !assert.NoError(t, err) { @@ -229,25 +233,25 @@ func TestGetTemplateFolders_TakesFolderInPrioirtyOrder(t *testing.T) { } func TestGetTemplateFolders_IgnoresFolderWithoutDevcontainer(t *testing.T) { - root1, err := ioutil.TempDir("", "devcontainer*") + root1, err := os.MkdirTemp("", "devcontainer*") if !assert.NoError(t, err) { return } - defer os.RemoveAll(root1) + defer func() { _ = os.RemoveAll(root1) }() - root2, err := ioutil.TempDir("", "devcontainer*") + root2, err := os.MkdirTemp("", "devcontainer*") if !assert.NoError(t, err) { return } - defer os.RemoveAll(root2) + defer func() { _ = os.RemoveAll(root2) }() folders := []string{root1, root2} _ = os.MkdirAll(filepath.Join(root1, "test1", ".devcontainer"), 0755) - _ = ioutil.WriteFile(filepath.Join(root1, "test1", ".devcontainer", "not-a-devcontainer.json"), []byte{}, 0755) + _ = os.WriteFile(filepath.Join(root1, "test1", ".devcontainer", "not-a-devcontainer.json"), []byte{}, 0755) _ = os.MkdirAll(filepath.Join(root2, "test1", ".devcontainer"), 0755) - _ = ioutil.WriteFile(filepath.Join(root2, "test1", ".devcontainer", "devcontainer.json"), []byte{}, 0755) + _ = os.WriteFile(filepath.Join(root2, "test1", ".devcontainer", "devcontainer.json"), []byte{}, 0755) templates, err := getTemplatesFromFolders(folders) if !assert.NoError(t, err) { @@ -266,14 +270,14 @@ func TestGetTemplateFolders_IgnoresFolderWithoutDevcontainer(t *testing.T) { func TestAddTemplate_PerformsSubstitutionWithUserName(t *testing.T) { - root, err := ioutil.TempDir("", "devcontainer*") + root, err := os.MkdirTemp("", "devcontainer*") if !assert.NoError(t, err) { return } - defer os.RemoveAll(root) + defer func() { _ = os.RemoveAll(root) }() _ = os.MkdirAll(filepath.Join(root, "test1", ".devcontainer"), 0755) - _ = ioutil.WriteFile(filepath.Join(root, "test1", ".devcontainer", "devcontainer.json"), []byte(`{ + _ = os.WriteFile(filepath.Join(root, "test1", ".devcontainer", "devcontainer.json"), []byte(`{ "name": "expect this to be replaced", "settings": { "DC_NAME": "__DEVCONTAINER_NAME__", @@ -282,14 +286,14 @@ func TestAddTemplate_PerformsSubstitutionWithUserName(t *testing.T) { }, "remoteUser": "dcuser" }`), 0755) - _ = ioutil.WriteFile(filepath.Join(root, "test1", ".devcontainer", "Dockerfile"), []byte(`FROM foo + _ = os.WriteFile(filepath.Join(root, "test1", ".devcontainer", "Dockerfile"), []byte(`FROM foo RUN echo hi ENV DC_NAME=__DEVCONTAINER_NAME__ ENV DC_USER_NAME=__DEVCONTAINER_USER_NAME__ ENV DC_HOME=__DEVCONTAINER_HOME__ -# __DEVCONTAINER_SNIPPET_INSERT__ +# __DEVCONTAINER_SNIPPET_INSERT__ RUN echo hi2 `), 0755) @@ -305,7 +309,7 @@ RUN echo hi2 } devcontainerFolder := filepath.Join(targetFolder, ".devcontainer") - buf, err := ioutil.ReadFile(filepath.Join(devcontainerFolder, "Dockerfile")) + buf, err := os.ReadFile(filepath.Join(devcontainerFolder, "Dockerfile")) if !assert.NoError(t, err) { return } @@ -316,12 +320,12 @@ ENV DC_NAME=NewName ENV DC_USER_NAME=dcuser ENV DC_HOME=/home/dcuser -# __DEVCONTAINER_SNIPPET_INSERT__ +# __DEVCONTAINER_SNIPPET_INSERT__ RUN echo hi2 `, string(buf)) - buf, err = ioutil.ReadFile(filepath.Join(devcontainerFolder, "devcontainer.json")) + buf, err = os.ReadFile(filepath.Join(devcontainerFolder, "devcontainer.json")) if !assert.NoError(t, err) { return } @@ -339,14 +343,14 @@ RUN echo hi2 } func TestAddTemplate_PerformsSubstitutionWithoutUserName(t *testing.T) { - root, err := ioutil.TempDir("", "devcontainer*") + root, err := os.MkdirTemp("", "devcontainer*") if !assert.NoError(t, err) { return } - defer os.RemoveAll(root) + defer func() { _ = os.RemoveAll(root) }() _ = os.MkdirAll(filepath.Join(root, "test1", ".devcontainer"), 0755) - _ = ioutil.WriteFile(filepath.Join(root, "test1", ".devcontainer", "devcontainer.json"), []byte(`{ + _ = os.WriteFile(filepath.Join(root, "test1", ".devcontainer", "devcontainer.json"), []byte(`{ "name": "expect this to be replaced", "settings": { "DC_NAME": "__DEVCONTAINER_NAME__", @@ -354,14 +358,14 @@ func TestAddTemplate_PerformsSubstitutionWithoutUserName(t *testing.T) { "DC_HOME": "__DEVCONTAINER_HOME__" }, }`), 0755) - _ = ioutil.WriteFile(filepath.Join(root, "test1", ".devcontainer", "Dockerfile"), []byte(`FROM foo + _ = os.WriteFile(filepath.Join(root, "test1", ".devcontainer", "Dockerfile"), []byte(`FROM foo RUN echo hi ENV DC_NAME=__DEVCONTAINER_NAME__ ENV DC_USER_NAME=__DEVCONTAINER_USER_NAME__ ENV DC_HOME=__DEVCONTAINER_HOME__ -# __DEVCONTAINER_SNIPPET_INSERT__ +# __DEVCONTAINER_SNIPPET_INSERT__ RUN echo hi2 `), 0755) @@ -377,7 +381,7 @@ RUN echo hi2 } devcontainerFolder := filepath.Join(targetFolder, ".devcontainer") - buf, err := ioutil.ReadFile(filepath.Join(devcontainerFolder, "Dockerfile")) + buf, err := os.ReadFile(filepath.Join(devcontainerFolder, "Dockerfile")) if !assert.NoError(t, err) { return } @@ -388,12 +392,12 @@ ENV DC_NAME=NewName ENV DC_USER_NAME=root ENV DC_HOME=/root -# __DEVCONTAINER_SNIPPET_INSERT__ +# __DEVCONTAINER_SNIPPET_INSERT__ RUN echo hi2 `, string(buf)) - buf, err = ioutil.ReadFile(filepath.Join(devcontainerFolder, "devcontainer.json")) + buf, err = os.ReadFile(filepath.Join(devcontainerFolder, "devcontainer.json")) if !assert.NoError(t, err) { return } diff --git a/internal/pkg/git/git.go b/internal/pkg/git/git.go index ae33da9..d1d7ff0 100644 --- a/internal/pkg/git/git.go +++ b/internal/pkg/git/git.go @@ -22,9 +22,9 @@ func GetTopLevelPath(path string) (string, error) { return "", nil } } - return "", fmt.Errorf("Error git rev-parse --show-toplevel: %s", err) + return "", fmt.Errorf("error git rev-parse --show-toplevel: %s", err) } - return "", fmt.Errorf("Error git rev-parse --show-toplevel: %s", err) + return "", fmt.Errorf("error git rev-parse --show-toplevel: %s", err) } return strings.TrimSpace(string(buf)), nil } diff --git a/internal/pkg/ioutil/files.go b/internal/pkg/ioutil/files.go index ae6457c..d5121a8 100644 --- a/internal/pkg/ioutil/files.go +++ b/internal/pkg/ioutil/files.go @@ -3,36 +3,39 @@ package ioutil import ( "fmt" "io" - "io/ioutil" "os" "path/filepath" ) func CopyFolder(source string, target string) error { - copy := func(sourceFolder string, targetFolder string, item os.FileInfo) error { - return CopyFile(filepath.Join(sourceFolder, item.Name()), filepath.Join(targetFolder, item.Name()), item.Mode()) + copy := func(sourceFolder string, targetFolder string, item os.DirEntry) error { + info, err := item.Info() + if err != nil { + return fmt.Errorf("error getting item info: %s", err) + } + return CopyFile(filepath.Join(sourceFolder, item.Name()), filepath.Join(targetFolder, item.Name()), info.Mode()) } return processFolder(source, target, copy) } func LinkFolder(source string, target string) error { - symlink := func(sourceFolder string, targetFolder string, item os.FileInfo) error { + symlink := func(sourceFolder string, targetFolder string, item os.DirEntry) error { return os.Symlink(filepath.Join(sourceFolder, item.Name()), filepath.Join(targetFolder, item.Name())) } return processFolder(source, target, symlink) } -func processFolder(source string, target string, fileHandler func(sourceFolder string, targetFolder string, item os.FileInfo) error) error { +func processFolder(source string, target string, fileHandler func(sourceFolder string, targetFolder string, item os.DirEntry) error) error { sourceItem, err := os.Stat(source) if err != nil { - return fmt.Errorf("Error reading source folder: %s\n", err) + return fmt.Errorf("error reading source folder: %s", err) } if err = os.Mkdir(target, sourceItem.Mode()); err != nil { - return fmt.Errorf("Error creating directory '%s': %s", target, err) + return fmt.Errorf("error creating directory '%s': %s", target, err) } - sourceSubItems, err := ioutil.ReadDir(source) + sourceSubItems, err := os.ReadDir(source) if err != nil { - return fmt.Errorf("Error reading source folder contents: %s\n", err) + return fmt.Errorf("error reading source folder contents: %s", err) } for _, sourceSubItem := range sourceSubItems { @@ -54,13 +57,13 @@ func CopyFile(source string, target string, perm os.FileMode) error { if err != nil { return err } - defer sourceFile.Close() + defer func() { _ = sourceFile.Close() }() targetFile, err := os.OpenFile(target, os.O_RDWR|os.O_CREATE|os.O_EXCL|os.O_TRUNC, perm) if err != nil { return err } - defer targetFile.Close() + defer func() { _ = targetFile.Close() }() _, err = io.Copy(targetFile, sourceFile) return err } diff --git a/internal/pkg/output/output.go b/internal/pkg/output/output.go new file mode 100644 index 0000000..10666e3 --- /dev/null +++ b/internal/pkg/output/output.go @@ -0,0 +1,282 @@ +package output + +import ( + "encoding/json" + "fmt" + "io" + "os" + "strings" + + "github.com/fatih/color" + "github.com/mattn/go-colorable" + "github.com/neilotoole/jsoncolor" + + "github.com/spf13/cobra" + + jmespath "github.com/jmespath/go-jmespath" +) + +type OutputFormat string + +const ( + OutputJson OutputFormat = "json" + OutputJsonC OutputFormat = "jsonc" + OutputTsv OutputFormat = "tsv" + OutputTable OutputFormat = "table" + OutputTableBody OutputFormat = "tablebody" + // TODO - does raw still make sense? offer value? + // Raw OutputFormat = "raw" +) + +func AddOutputAndQueryFlags(cmd *cobra.Command) { + cmd.Flags().StringP("output", "o", "table", "output format") + cmd.Flags().StringP("query", "q", "", "JMESPath query to apply to the result") +} +func GetOutputAndQueryValues(cmd *cobra.Command, defaultQuery string) (string, string, error) { + outputFormat, err := cmd.Flags().GetString("output") + if err != nil { + return "", "", err + } + query, err := cmd.Flags().GetString("query") + if err != nil { + return "", "", err + } + if query == "" { + query = defaultQuery + } + return outputFormat, query, err +} + +// RoundtripMarshal marshals and unmarshals the data +// so that properties match the JSON property names +func RoundtripMarshal(data interface{}) ([]interface{}, error) { + buf, err := json.Marshal(data) + if err != nil { + return []interface{}{}, fmt.Errorf("error marshalling result: %s", err) + } + var result []interface{} + err = json.Unmarshal(buf, &result) + if err != nil { + return []interface{}{}, fmt.Errorf("error unmarshalling result: %s", err) + } + return result, nil +} + +type OutputLink struct { + Text string + Link string +} + +func (ol OutputLink) Encode() string { + return fmt.Sprintf("||OutputLink||%s||%s", ol.Text, ol.Link) +} +func DecodeOutputLink(encodedValue string) *OutputLink { + if !strings.HasPrefix(encodedValue, "||OutputLink||") { + return nil + } + parts := strings.Split(strings.TrimPrefix(encodedValue, "||OutputLink||"), "||") + if len(parts) != 2 { + return nil + } + return &OutputLink{ + Text: parts[0], + Link: parts[1], + } +} + +func GetTerminalLink(url string, displayText string) string { + return fmt.Sprintf("\033]8;;%s\033\\%s\033]8;;\033\\", url, displayText) +} + +// func formatEscape(s string) string { +// return strings.ReplaceAll(s, "%", "%%") +// } + +// func formatterFromColor(color *color.Color) func(int, int, interface{}) string { +// colorFmt := color.SprintfFunc() +// return func(index int, width int, val interface{}) string { +// return colorFmt("%-*s", width, val) +// } +// } +func firstColumnFormatterFromColor(color *color.Color) func(int, int, interface{}) string { + colorFmt := color.SprintfFunc() + return func(index int, width int, val interface{}) string { + if index == 0 { + return colorFmt("%-*s", width, val) + } + return fmt.Sprintf("%-*s", width, val) + } +} +func newLinkFormatter(inner Formatter) func(int, int, interface{}) string { + return func(index int, width int, val interface{}) string { + var valText string + switch v := val.(type) { + case int: + valText = fmt.Sprintf("%d", v) + case float64: + if v == float64(int(v)) { + valText = fmt.Sprintf("%.0f", v) + } else { + valText = fmt.Sprintf("%.1f", v) + } + case string: + valText = v + case OutputLink: + amountToPad := width - len(v.Text) + valText = GetTerminalLink(v.Link, v.Text) + strings.Repeat(" ", amountToPad) + default: + valText = fmt.Sprintf("%v", v) + } + + return inner(index, width, valText) + } +} + +func newHeaderFormatter(color *color.Color) func(int, int, interface{}) string { + // TODO take color as an input and return a formatter function + colorFmt := color.SprintfFunc() + return func(index int, width int, val interface{}) string { + return colorFmt("%s", fmt.Sprintf("%-*s", width, val)) + } + +} + +func newLinkWidthFunc() WidthFunc { + return func(value interface{}) int { + if link, ok := value.(OutputLink); ok { + return DefaultWidthFunc(link.Text) + } + return DefaultWidthFunc(value) + } +} + +func OutputResult(w io.Writer, result interface{}, outputFormat string, query string, defaultTableFields []string) error { + + if outputFormat == "" { + outputFormat = string(OutputJsonC) + } + + output := result + + tableFields := []string{} + if query != "" { + var err error + output, err = jmespath.Search(query, output) + if err != nil { + return fmt.Errorf("error executing query: %s", err) + } + parser := jmespath.NewParser() + ast, err := parser.Parse(query) + if err != nil { + return fmt.Errorf("error parsing query: %s", err) + } + tableFields, err = ast.GetResultFields() + if err != nil { + return fmt.Errorf("error getting query result fields: %s", err) + } + } + if len(tableFields) == 0 { + tableFields = defaultTableFields + } + + switch outputFormat { + case string(OutputJson): + outputJson, _ := json.Marshal(output) + _, _ = fmt.Fprintln(w, string(outputJson)) + case string(OutputJsonC): + // formattedJson, _ := json.MarshalIndent(output, "", " ") + var enc *jsoncolor.Encoder + if jsoncolor.IsColorTerminal(w) { + // Safe to use color + var out io.Writer + if f, ok := w.(*os.File); ok { + out = colorable.NewColorable(f) // needed for Windows + } else { + out = w + } + enc = jsoncolor.NewEncoder(out) + // DefaultColors are similar to jq + clrs := jsoncolor.DefaultColors() + + enc.SetColors(clrs) + } else { + // Can't use color; but the encoder will still work + enc = jsoncolor.NewEncoder(w) + } + enc.SetIndent("", " ") + // TODO: Implement JSON syntax highlighting if needed + err := enc.Encode(output) + if err != nil { + return fmt.Errorf("error encoding JSON: %s", err) + } + // case string(Raw): + // fmt.Fprintln(w, output) + case string(OutputTsv): + // Assuming output is a slice of maps + rows, ok := output.([]map[string]interface{}) + if ok { + for _, row := range rows { + values := make([]string, 0, len(row)) + for _, value := range row { + values = append(values, fmt.Sprint(value)) + } + _, _ = fmt.Fprintln(w, strings.Join(values, "\t")) + } + } else { + _, _ = fmt.Fprintln(w, output) + } + case string(OutputTable), string(OutputTableBody): + if output == nil { + return nil + } + + var ok bool + var outputList []interface{} + if outputList, ok = output.([]interface{}); !ok { + return fmt.Errorf("output is not a list") + } + + tmpTableFields := make([]interface{}, len(tableFields)) // convert to []interface{} + for i, field := range tableFields { + tmpTableFields[i] = field + } + + tbl := NewTable(tmpTableFields...).WithWriter(w) + valueFmt := newLinkFormatter(firstColumnFormatterFromColor(color.New(color.FgYellow))) + widthFunc := newLinkWidthFunc() + headerFmt := newHeaderFormatter(color.New(color.FgGreen, color.Bold)) + tbl.WithHeaderFormatter(headerFmt).WithValueFormatter(valueFmt).WithWidthFunc(widthFunc) + for _, listItem := range outputList { + var itemMap map[string]interface{} + if itemMap, ok = listItem.(map[string]interface{}); !ok { + return fmt.Errorf("output is not a list of maps") + } + values := make([]interface{}, len(tableFields)) + for i := 0; i < len(tableFields); i++ { + value := itemMap[tableFields[i]] + if tmpMap, ok := value.(map[string]interface{}); ok { + if len(tmpMap) == 2 && tmpMap["Text"] != nil && tmpMap["Link"] != nil { + // we have a round-tripped OutputLink + values[i] = OutputLink{Text: tmpMap["Text"].(string), Link: tmpMap["Link"].(string)} //.Encode() + } + } else { + values[i] = value + } + } + tbl.AddRow(values...) + } + if outputFormat == string(OutputTable) { + if err := tbl.Print(); err != nil { + return err + } + } else { + if err := tbl.PrintRows(); err != nil { + return err + } + } + + default: + return fmt.Errorf("unhandled output format: '%s'", outputFormat) + } + return nil +} diff --git a/internal/pkg/output/table.go b/internal/pkg/output/table.go new file mode 100644 index 0000000..de7bc06 --- /dev/null +++ b/internal/pkg/output/table.go @@ -0,0 +1,195 @@ +package output + +import ( + "fmt" + "io" + "os" + "strings" + "unicode/utf8" +) + +// Formatter is a function that formats a cell value for a table. +// The first argument is the column index +// The second argument is the width of the column +// The third argument is the value to format +type Formatter func(int, int, interface{}) string +type WidthFunc func(interface{}) int + +// These are the default properties for all Tables created from this package +// and can be modified. +var ( + // DefaultPadding specifies the number of spaces between columns in a table. + DefaultPadding = 2 + + // DefaultWriter specifies the output io.Writer for the Table.Print method. + DefaultWriter io.Writer = os.Stdout + + // DefaultHeaderFormatter specifies the default Formatter for the table header. + DefaultHeaderFormatter Formatter +) + +func DefaultWidthFunc(value interface{}) int { + return utf8.RuneCountInString(fmt.Sprint(value)) +} +func DefaultValueFormatter(columnIndex int, width int, val interface{}) string { + return fmt.Sprintf(fmt.Sprintf("%%-%ds", width), val) +} + +type table struct { + ValueFormatter Formatter + HeaderFormatter Formatter + Padding int + Writer io.Writer + Width WidthFunc + + header []string + rows [][]interface{} + widths []int +} + +type Table interface { + WithHeaderFormatter(f Formatter) Table + WithValueFormatter(f Formatter) Table + WithPadding(p int) Table + WithWriter(w io.Writer) Table + WithWidthFunc(f WidthFunc) Table + + AddRow(vals ...interface{}) Table + Print() error + PrintRows() error +} + +// New creates a Table instance with the specified header(s) provided. The number +// of columns is fixed at this point to len(columnHeaders) and the defined defaults +// are set on the instance. +func NewTable(columnHeaders ...interface{}) Table { + t := table{header: make([]string, len(columnHeaders))} + + t.WithPadding(DefaultPadding) + t.WithWriter(DefaultWriter) + t.WithHeaderFormatter(DefaultHeaderFormatter) + t.WithValueFormatter(DefaultValueFormatter) + t.WithWidthFunc(DefaultWidthFunc) + + for i, col := range columnHeaders { + t.header[i] = fmt.Sprint(col) + } + + return &t +} + +func (t *table) WithHeaderFormatter(f Formatter) Table { + t.HeaderFormatter = f + return t +} + +func (t *table) WithValueFormatter(f Formatter) Table { + t.ValueFormatter = f + return t +} + +func (t *table) WithPadding(p int) Table { + if p < 0 { + p = 0 + } + + t.Padding = p + return t +} + +func (t *table) WithWriter(w io.Writer) Table { + t.Writer = w + return t +} + +func (t *table) WithWidthFunc(f WidthFunc) Table { + t.Width = f + return t +} + +func (t *table) AddRow(vals ...interface{}) Table { + t.rows = append(t.rows, vals) + return t +} + +func (t *table) Print() error { + if t.Writer == nil { + return fmt.Errorf("writer not set") + } + + t.calculateWidths(true) + if err := t.printHeader(); err != nil { + return err + } + return t.printRows() +} +func (t *table) PrintRows() error { + if t.Writer == nil { + return nil + } + + t.calculateWidths(false) + return t.printRows() +} + +func (t *table) calculateWidths(includeHeader bool) { + t.widths = make([]int, len(t.header)) + if includeHeader { + for i, header := range t.header { + t.widths[i] = t.Width(header) + } + } + + for _, row := range t.rows { + for i, cell := range row { + width := t.Width(cell) + if width > t.widths[i] { + t.widths[i] = width + } + } + } +} + +func (t *table) printHeader() error { + if t.HeaderFormatter == nil { + t.HeaderFormatter = t.ValueFormatter + } + + for i, header := range t.header { + formatted := t.HeaderFormatter(i, t.widths[i], header) + _, err := fmt.Fprint(t.Writer, formatted) + if err != nil { + return err + } + if i < len(t.header)-1 { + _, err = fmt.Fprint(t.Writer, strings.Repeat(" ", t.Padding)) + if err != nil { + return err + } + } + } + _, err := fmt.Fprintln(t.Writer) + return err +} +func (t *table) printRows() error { + for _, row := range t.rows { + for i, cell := range row { + formatted := t.ValueFormatter(i, t.widths[i], cell) + _, err := fmt.Fprint(t.Writer, formatted) + if err != nil { + return err + } + if i < len(row)-1 { + _, err = fmt.Fprint(t.Writer, strings.Repeat(" ", t.Padding)) + if err != nil { + return err + } + } + } + _, err := fmt.Fprintln(t.Writer) + if err != nil { + return err + } + } + return nil +} diff --git a/internal/pkg/update/update.go b/internal/pkg/update/update.go index 3444739..f5dbfe8 100644 --- a/internal/pkg/update/update.go +++ b/internal/pkg/update/update.go @@ -14,12 +14,12 @@ func CheckForUpdate(currentVersion string) (*selfupdate.Release, error) { latest, found, err := selfupdate.DetectLatest("stuartleeks/devcontainer-cli") if err != nil { - return nil, fmt.Errorf("Error occurred while detecting version: %v", err) + return nil, fmt.Errorf("error occurred while detecting version: %v", err) } v, err := semver.Parse(currentVersion) if err != nil { - return nil, fmt.Errorf("Error occurred while parsing version: %v", err) + return nil, fmt.Errorf("error occurred while parsing version: %v", err) } if !found || latest.Version.LTE(v) { @@ -43,12 +43,12 @@ func PeriodicCheckForUpdate(currentVersion string) { fmt.Println("Checking for updates...") latest, err := CheckForUpdate(currentVersion) if err != nil { - fmt.Printf("Error checking for updates: %s", err) + fmt.Printf("error checking for updates: %s", err) } status.SetLastUpdateCheck(time.Now()) if err = status.SaveStatus(); err != nil { - fmt.Printf("Error saving last update check time: :%s\n", err) + fmt.Printf("error saving last update check time: :%s", err) } if latest == nil { diff --git a/internal/pkg/wsl/wsl.go b/internal/pkg/wsl/wsl.go index 14f39ea..f143d8e 100644 --- a/internal/pkg/wsl/wsl.go +++ b/internal/pkg/wsl/wsl.go @@ -19,7 +19,7 @@ func ConvertWslPathToWindowsPath(path string) (string, error) { buf, err := cmd.Output() if err != nil { - return "", fmt.Errorf("Error running wslpath (for %q): %s", path, err) + return "", fmt.Errorf("error running wslpath (for %q): %s", path, err) } return strings.TrimSpace(string(buf)), nil } @@ -30,7 +30,7 @@ func ConvertWindowsPathToWslPath(path string) (string, error) { buf, err := cmd.Output() if err != nil { - return "", fmt.Errorf("Error running wslpath (for %q): %s", path, err) + return "", fmt.Errorf("error running wslpath (for %q): %s", path, err) } return strings.TrimSpace(string(buf)), nil }