Skip to content

Commit

Permalink
Add config:install command
Browse files Browse the repository at this point in the history
  • Loading branch information
pjcdawkins committed Jan 3, 2025
1 parent 6aadc93 commit d0bb77a
Show file tree
Hide file tree
Showing 19 changed files with 991 additions and 55 deletions.
10 changes: 5 additions & 5 deletions .goreleaser.vendor.yaml.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ builds:
- -s -w
- -X "github.com/platformsh/cli/internal/legacy.PHPVersion={{.Env.PHP_VERSION}}"
- -X "github.com/platformsh/cli/internal/legacy.LegacyCLIVersion={{.Env.LEGACY_CLI_VERSION}}"
- -X "github.com/platformsh/cli/commands.version={{.Version}}"
- -X "github.com/platformsh/cli/commands.commit={{.Commit}}"
- -X "github.com/platformsh/cli/commands.date={{.Date}}"
- -X "github.com/platformsh/cli/commands.vendor=${VENDOR_BINARY}"
- -X "github.com/platformsh/cli/commands.builtBy=goreleaser"
- -X "github.com/platformsh/cli/internal/config.Version={{.Version}}"
- -X "github.com/platformsh/cli/internal/config.Commit={{.Commit}}"
- -X "github.com/platformsh/cli/internal/config.Date={{.Date}}"
- -X "github.com/platformsh/cli/internal/config.Vendor=${VENDOR_BINARY}"
- -X "github.com/platformsh/cli/internal/config.BuiltBy=goreleaser"
main: ./cmd/platform
- binary: ${VENDOR_BINARY}
id: ${VENDOR_BINARY}-macos
Expand Down
8 changes: 4 additions & 4 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ builds:
- -s -w
- -X "github.com/platformsh/cli/internal/legacy.PHPVersion={{.Env.PHP_VERSION}}"
- -X "github.com/platformsh/cli/internal/legacy.LegacyCLIVersion={{.Env.LEGACY_CLI_VERSION}}"
- -X "github.com/platformsh/cli/commands.version={{.Version}}"
- -X "github.com/platformsh/cli/commands.commit={{.Commit}}"
- -X "github.com/platformsh/cli/commands.date={{.Date}}"
- -X "github.com/platformsh/cli/commands.builtBy=goreleaser"
- -X "github.com/platformsh/cli/internal/config.Version={{.Version}}"
- -X "github.com/platformsh/cli/internal/config.Commit={{.Commit}}"
- -X "github.com/platformsh/cli/internal/config.Date={{.Date}}"
- -X "github.com/platformsh/cli/internal/config.BuiltBy=goreleaser"
main: ./cmd/platform
- binary: platform
id: platform-macos
Expand Down
2 changes: 1 addition & 1 deletion commands/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func newCompletionCommand(cnf *config.Config) *cobra.Command {
var b bytes.Buffer
c := &legacy.CLIWrapper{
Config: cnf,
Version: version,
Version: config.Version,
CustomPharPath: viper.GetString("phar-path"),
Debug: viper.GetBool("debug"),
DisableInteraction: viper.GetBool("no-interaction"),
Expand Down
145 changes: 145 additions & 0 deletions commands/config_install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package commands

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"

"github.com/fatih/color"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"

"github.com/platformsh/cli/internal/config"
"github.com/platformsh/cli/internal/config/alt"
)

var configInstallCommand = &cobra.Command{
Use: "config:install [flags] [url]",
Short: "Installs an alternative CLI, downloading new configuration from a URL",
Args: cobra.ExactArgs(1),
RunE: runConfigInstall,
}

func runConfigInstall(cmd *cobra.Command, args []string) error {
cmd.PrintErrln("Downloading and validating new CLI configuration...")

urlStr := args[0]
if !strings.Contains(urlStr, "://") {
urlStr = "https://" + urlStr
}
newCnfNode, newCnfStruct, err := alt.FetchConfig(cmd.Context(), urlStr)
if err != nil {
return err
}
cnf := config.FromContext(cmd.Context())
newExecutable := newCnfStruct.Application.Executable
if newExecutable == cnf.Application.Executable {
return fmt.Errorf("cannot install config for same executable name as this program: %s", newExecutable)
}

// Find the directory and file paths.
executableDir, err := alt.FindBinDir()
if err != nil {
return err
}
configDir, err := alt.FindConfigDir()
if err != nil {
return err
}
configFilePath := filepath.Join(configDir, newExecutable+".yaml")
executableFilePath := filepath.Join(executableDir, newExecutable)

if path, err := exec.LookPath(newExecutable); err == nil && path != executableFilePath {
return fmt.Errorf(
"cannot install config: an executable with the same name already exists at another location: %s",
path,
)
}

// Generate the file content.
executableContent := fmt.Sprintf(
"#!/bin/sh\n"+
"# This file is automatically generated by the %s.\n"+
"export CLI_CONFIG_FILE=%s\n"+
`[ "$#" -gt 1 ] && shift # Skip first argument`+"\n"+
`%s "$@"`+"\n",
cnf.Application.Name,
configFilePath,
cnf.Application.Executable,
)
configContent, err := yaml.Marshal(newCnfNode)
if err != nil {
return fmt.Errorf("failed to marshal new config: %w", err)
}

// Start writing the files.
if err := os.MkdirAll(configDir, 0o755); err != nil {
return err
}
if err := os.MkdirAll(executableDir, 0o755); err != nil {
return err
}
if err := os.WriteFile(configFilePath, configContent, 0o600); err != nil {
return err
}
if err := os.WriteFile(executableFilePath, []byte(executableContent), 0o700); err != nil { //nolint:gosec
return err
}

// Make a formatter to replace the home directory with ~ in filenames.
replaceHomeDir := func() func(string) string {
hd, err := os.UserHomeDir()
return func(p string) string {
if err == nil && strings.HasPrefix(p, hd) {
return "~" + strings.TrimPrefix(p, hd)
}
return p
}
}()

cmd.PrintErrf(
"\nThe following files have been created:\n"+
" - Configuration: %s\n"+
" - Executable: %s\n",
color.CyanString(replaceHomeDir(configFilePath)),
color.CyanString(replaceHomeDir(executableFilePath)),
)

if newCnfStruct.Updates.Check {
cmd.PrintErrln("\nThe configuration file will be auto-updated periodically in the background.")
} else {
cmd.PrintErrln("\nEnable automatic updates to keep the configuration file up to date.")
}

isInPath, err := alt.InPath(executableDir)
if err != nil {
return fmt.Errorf("could not determine if the executable directory is in the PATH: %w", err)
}

envVarName := "PATH"
if runtime.GOOS == "windows" {
envVarName = "Path"
}

cmd.PrintErrln()

if isInPath {
cmd.PrintErrln("Run the new CLI with:", color.GreenString(filepath.Base(executableFilePath)))
} else {
cmd.PrintErrf(
"Add the following directory to your %s: %s\n",
envVarName,
color.YellowString(replaceHomeDir(executableDir)),
)
cmd.PrintErrln()
cmd.PrintErrln(
"Then you will be able to run the new CLI with:",
color.YellowString(filepath.Base(executableFilePath)),
)
}

return nil
}
87 changes: 87 additions & 0 deletions commands/config_install_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package commands

import (
"bytes"
"context"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"

"github.com/platformsh/cli/internal/config"
"github.com/platformsh/cli/internal/config/alt"
)

func TestConfigInstallCmd(t *testing.T) {
tempDir := t.TempDir()

// Ensure filesystem functions looking for UserHomeDir or UserConfigDir return the test directory.
homeEnv := os.Getenv("HOME")
require.NoError(t, os.Setenv("HOME", tempDir))
require.NoError(t, os.Unsetenv("XDG_CONFIG_HOME"))
require.NoError(t, os.Unsetenv("TEST_HOME"))
t.Cleanup(func() {
_ = os.Setenv("HOME", homeEnv)
})

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/test-config.yaml" {
cnf := testConfig()
_ = yaml.NewEncoder(w).Encode(cnf)
}
}))
defer server.Close()
testConfigURL := server.URL + "/test-config.yaml"

cnf := testConfig()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx = config.ToContext(ctx, cnf)

cmd := configInstallCommand
cmd.SetContext(ctx)
cmd.SetOut(io.Discard)

args := []string{testConfigURL}

stdErrBuf := &bytes.Buffer{}
cmd.SetErr(stdErrBuf)
err := cmd.RunE(cmd, args)
assert.ErrorContains(t, err, "cannot install config for same executable name as this program: test")

cnf.Application.Executable = "test-cli-executable-host"
err = cmd.RunE(cmd, args)
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(tempDir, alt.HomeSubDir, "test-cli-executable.yaml"))
assert.FileExists(t, filepath.Join(tempDir, alt.HomeSubDir, "bin", "test-cli-executable"))
assert.Contains(t, stdErrBuf.String(), filepath.Join("~", alt.HomeSubDir, "test-cli-executable.yaml"))
assert.Contains(t, stdErrBuf.String(), filepath.Join("~", alt.HomeSubDir, "bin", "test-cli-executable"))

b, err := os.ReadFile(filepath.Join(tempDir, alt.HomeSubDir, "bin", "test-cli-executable"))
require.NoError(t, err)
assert.Contains(t, string(b), filepath.Join(tempDir, alt.HomeSubDir, "test-cli-executable.yaml"))
assert.Contains(t, string(b), `test-cli-executable-host "$@"`)
}

func testConfig() *config.Config {
cnf := &config.Config{}
cnf.Application.Name = "Test CLI"
cnf.Application.Executable = "test-cli-executable" // Not "test" as that is usually a real binary
cnf.Application.EnvPrefix = "TEST_"
cnf.Application.Slug = "test-cli"
cnf.Application.UserConfigDir = ".test-cli"
cnf.API.BaseURL = "https://localhost"
cnf.API.AuthURL = "https://localhost"
cnf.Detection.GitRemoteName = "platform"
cnf.Service.Name = "Test"
cnf.Service.EnvPrefix = "TEST_"
cnf.Service.ProjectConfigDir = ".test"
cnf.SSH.DomainWildcards = []string{"*"}
return cnf
}
2 changes: 1 addition & 1 deletion commands/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func newListCommand(cnf *config.Config) *cobra.Command {
var b bytes.Buffer
c := &legacy.CLIWrapper{
Config: cnf,
Version: version,
Version: config.Version,
CustomPharPath: viper.GetString("phar-path"),
Debug: viper.GetBool("debug"),
DisableInteraction: viper.GetBool("no-interaction"),
Expand Down
15 changes: 12 additions & 3 deletions commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

"github.com/platformsh/cli/internal"
"github.com/platformsh/cli/internal/config"
"github.com/platformsh/cli/internal/config/alt"
"github.com/platformsh/cli/internal/legacy"
)

Expand Down Expand Up @@ -63,15 +64,22 @@ func newRootCommand(cnf *config.Config, assets *vendorization.VendorAssets) *cob
}
if cnf.Wrapper.GitHubRepo != "" {
go func() {
rel, _ := internal.CheckForUpdate(cnf, version)
rel, _ := internal.CheckForUpdate(cnf, config.Version)
updateMessageChan <- rel
}()
}
if alt.ShouldUpdate(cnf) {
go func() {
if err := alt.Update(cmd.Context(), cnf, debugLog); err != nil {
cmd.PrintErrln("Error updating config:", color.RedString(err.Error()))
}
}()
}
},
Run: func(cmd *cobra.Command, _ []string) {
c := &legacy.CLIWrapper{
Config: cnf,
Version: version,
Version: config.Version,
CustomPharPath: viper.GetString("phar-path"),
Debug: viper.GetBool("debug"),
DisableInteraction: viper.GetBool("no-interaction"),
Expand Down Expand Up @@ -145,6 +153,7 @@ func newRootCommand(cnf *config.Config, assets *vendorization.VendorAssets) *cob

// Add subcommands.
cmd.AddCommand(
configInstallCommand,
newCompletionCommand(cnf),
newHelpCommand(cnf),
newListCommand(cnf),
Expand Down Expand Up @@ -204,7 +213,7 @@ func printUpdateMessage(newRelease *internal.ReleaseInfo, cnf *config.Config) {

fmt.Fprintf(color.Error, "\n\n%s %s → %s\n",
color.YellowString(fmt.Sprintf("A new release of the %s is available:", cnf.Application.Name)),
color.CyanString(version),
color.CyanString(config.Version),
color.CyanString(newRelease.Version),
)

Expand Down
15 changes: 4 additions & 11 deletions commands/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,13 @@ import (
"github.com/platformsh/cli/internal/legacy"
)

var (
version = "0.0.0"
commit = "local"
date = ""
builtBy = "local"
)

func newVersionCommand(cnf *config.Config) *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Print the version number of the " + cnf.Application.Name,
FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true},
Run: func(_ *cobra.Command, _ []string) {
fmt.Fprintf(color.Output, "%s %s\n", cnf.Application.Name, color.CyanString(version))
fmt.Fprintf(color.Output, "%s %s\n", cnf.Application.Name, color.CyanString(config.Version))

if viper.GetBool("verbose") {
fmt.Fprintf(
Expand All @@ -40,9 +33,9 @@ func newVersionCommand(cnf *config.Config) *cobra.Command {
fmt.Fprintf(
color.Output,
"Commit %s (built %s by %s)\n",
color.CyanString(commit),
color.CyanString(date),
color.CyanString(builtBy),
color.CyanString(config.Commit),
color.CyanString(config.Date),
color.CyanString(config.BuiltBy),
)
}
},
Expand Down
Loading

0 comments on commit d0bb77a

Please sign in to comment.