Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cmd/topo/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ var configureCmd = &cobra.Command{
Short: "Configure project parameters",
Long: `Configure project parameters for the Topo project in the current directory.

The compose file (compose.yaml or compose.yml) must be in the current working directory.
By default, Topo uses compose.yaml in the current working directory, then compose.yml. Use -f to specify a different compose file.

Some projects require parameters. Supply them on the command line or answer
interactive prompts.`,
Expand All @@ -27,7 +27,7 @@ interactive prompts.`,
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true

composeFile, err := getComposeFileName()
composeFile, err := getComposeFileName(cmd)
if err != nil {
return err
}
Expand All @@ -51,5 +51,6 @@ interactive prompts.`,
}

func init() {
addComposeFileFlag(configureCmd)
rootCmd.AddCommand(configureCmd)
}
15 changes: 3 additions & 12 deletions cmd/topo/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ This command performs the following operations in sequence:
3. Transfer - Transfers built and pulled images and compose file to the target
4. Run - Runs docker compose up on the target

The compose file (compose.yaml) must be in the current working directory, as this is used to select the containers to be deployed.`,
By default, Topo uses compose.yaml in the current working directory, then compose.yml. Use -f to specify a different compose file.`,
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
Expand All @@ -53,7 +53,7 @@ The compose file (compose.yaml) must be in the current working directory, as thi
return err
}

composeFile, err := getComposeFileName()
composeFile, err := getComposeFileName(cmd)
if err != nil {
return err
}
Expand Down Expand Up @@ -106,16 +106,6 @@ The compose file (compose.yaml) must be in the current working directory, as thi
},
}

func getComposeFileName() (string, error) {
candidates := []string{"compose.yaml", "compose.yml"}
for _, fileName := range candidates {
if _, err := os.Stat(fileName); err == nil {
return fileName, nil
}
}
return "", fmt.Errorf("compose file not found in current working directory: looking for compose.yaml or compose.yml")
}

func validatePort(port string) error {
portNum, err := strconv.Atoi(port)
if err != nil {
Expand All @@ -141,6 +131,7 @@ func resolvePort(cmd *cobra.Command, flagValue string) (string, error) {

func init() {
addTargetFlag(deployCmd)
addComposeFileFlag(deployCmd)
deployCmd.Flags().StringVarP(&registryPort, "registry-port", "p", operation.DefaultRegistryPort, fmt.Sprintf("registry and SSH tunnel port (can also be set via %s env var)", portEnvVar))
deployCmd.Flags().BoolVar(&noRegistry, "no-registry", false, "disable private registry flow; use direct save/load transfer")
deployCmd.Flags().BoolVar(&forceRecreate, "force-recreate", false, "force recreation of containers even if their configuration and image haven't changed")
Expand Down
5 changes: 3 additions & 2 deletions cmd/topo/ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var topoPsCmd = &cobra.Command{
Short: "List containers on the target for the current Compose project.",
Long: `List containers on the target for the current Compose project.

The compose.yaml must be in the current working directory, as this is used to select containers to be viewed.
By default, Topo uses compose.yaml in the current working directory, then compose.yml. Use -f to specify a different compose file.
`,
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, _ []string) error {
Expand All @@ -27,7 +27,7 @@ The compose.yaml must be in the current working directory, as this is used to se
return err
}

composeFile, err := getComposeFileName()
composeFile, err := getComposeFileName(cmd)
if err != nil {
return err
}
Expand All @@ -51,6 +51,7 @@ The compose.yaml must be in the current working directory, as this is used to se

func init() {
addTargetFlag(topoPsCmd)
addComposeFileFlag(topoPsCmd)
topoPsCmd.Flags().BoolP("all", "a", false, "show all containers, including stopped")
rootCmd.AddCommand(topoPsCmd)
}
29 changes: 29 additions & 0 deletions cmd/topo/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"
"time"

"github.com/arm/topo/internal/compose"
"github.com/arm/topo/internal/env"
"github.com/arm/topo/internal/output/logger"
"github.com/arm/topo/internal/output/term"
Expand Down Expand Up @@ -35,6 +36,34 @@ func init() {
)
}

const composeFileFlag = "file"

func addComposeFileFlag(cmd *cobra.Command) {
cmd.Flags().StringP(
composeFileFlag,
"f",
"",
"compose file to use (default: compose.yaml, then compose.yml)",
)
}

func getComposeFileName(cmd *cobra.Command) (string, error) {
flag := cmd.Flag(composeFileFlag)
if flag == nil {
panic(fmt.Sprintf("internal error: compose file flag not registered: %s", composeFileFlag))
}

if flag.Changed {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL.

flag.Changed is a bool; where if it's set (to anything other than it's default) it's true?

composeFile := strings.TrimSpace(flag.Value.String())
if composeFile == "" {
return "", fmt.Errorf("compose file path must not be empty")
}
return compose.RequireFile(composeFile)
}

return compose.FindDefaultFile()
}

const targetEnvVar = "TOPO_TARGET"

func addTargetFlag(cmd *cobra.Command) {
Expand Down
5 changes: 3 additions & 2 deletions cmd/topo/stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ var topoStopCmd = &cobra.Command{

Executing this command does not remove the containers.

The compose file (compose.yaml) must be in the current working directory, as this is used to select the containers to be stopped.`,
By default, Topo uses compose.yaml in the current working directory, then compose.yml. Use -f to specify a different compose file.`,
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, _ []string) error {
cmd.SilenceUsage = true
Expand All @@ -26,7 +26,7 @@ The compose file (compose.yaml) must be in the current working directory, as thi
return err
}

composeFile, err := getComposeFileName()
composeFile, err := getComposeFileName(cmd)
if err != nil {
return err
}
Expand All @@ -41,5 +41,6 @@ The compose file (compose.yaml) must be in the current working directory, as thi

func init() {
addTargetFlag(topoStopCmd)
addComposeFileFlag(topoStopCmd)
rootCmd.AddCommand(topoStopCmd)
}
20 changes: 20 additions & 0 deletions e2e/configure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,26 @@ func TestConfigure(t *testing.T) {
assert.YAMLEq(t, want, got)
})

t.Run("respect compose file flag", func(t *testing.T) {
projectDir := t.TempDir()
composePath := filepath.Join(projectDir, "compose.yaml")
customComposePath := filepath.Join(projectDir, "custom-compose.yaml")
originalCompose := configurableCompose("Original")
testutil.RequireWriteFile(t, composePath, originalCompose)
testutil.RequireWriteFile(t, customComposePath, configurableCompose("CustomOriginal"))

cmd := exec.Command(topo, "configure", "-f", "custom-compose.yaml", "GREETING_NAME=Custom")
cmd.Dir = projectDir
out, err := cmd.CombinedOutput()

require.NoErrorf(t, err, "configure failed: %s", out)
assert.Empty(t, string(out))
want := configurableCompose("Custom")
got := testutil.RequireReadFile(t, customComposePath)
assert.YAMLEq(t, want, got)
assert.Equal(t, originalCompose, testutil.RequireReadFile(t, composePath))
})

t.Run("rejects undeclared parameters without changing the compose file", func(t *testing.T) {
projectDir := t.TempDir()
composePath := filepath.Join(projectDir, "compose.yaml")
Expand Down
73 changes: 73 additions & 0 deletions internal/compose/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package compose

import (
"errors"
"fmt"
"os"
"strings"

"github.com/arm/topo/internal/output/logger"
)

var errFileNotFound = errors.New("compose file not found")

var defaultFileNames = []string{"compose.yaml", "compose.yml"}

// RequireFile returns the path when it exists and is not a directory.
func RequireFile(composeFile string) (string, error) {
info, err := os.Stat(composeFile)
if err != nil {
if os.IsNotExist(err) {
return "", fmt.Errorf("%w: %s", errFileNotFound, composeFile)
}
return "", fmt.Errorf("failed to access compose file %s: %w", composeFile, err)
}
if info.IsDir() {
return "", fmt.Errorf("compose file path is a directory: %s", composeFile)
}
return composeFile, nil
}

// FindDefaultFile returns the first existing default compose file.
func FindDefaultFile() (string, error) {
type defaultFileCandidate struct {
name string
info os.FileInfo
}
candidates := []defaultFileCandidate{}

for _, fileName := range defaultFileNames {
info, err := os.Stat(fileName)
if err != nil {
if os.IsNotExist(err) {
continue
}
return "", fmt.Errorf("failed to access compose file %s: %w", fileName, err)
}

candidates = append(candidates, defaultFileCandidate{
name: fileName,
info: info,
})
}

if len(candidates) == 0 {
return "", fmt.Errorf(
"compose file not found in current working directory: looking for %s",
strings.Join(defaultFileNames, " or "),
)
}

defaultCandidate := candidates[0]
if len(candidates) > 1 {
logger.Warn(fmt.Sprintf(
"found multiple compose files: %s; using %s",
strings.Join(defaultFileNames, ", "),
defaultCandidate.name,
))
}
if defaultCandidate.info.IsDir() {
return "", fmt.Errorf("compose file path is a directory: %s", defaultCandidate.name)
}
return defaultCandidate.name, nil
}
99 changes: 99 additions & 0 deletions internal/compose/file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package compose_test

import (
"bytes"
"os"
"path/filepath"
"testing"

"github.com/arm/topo/internal/compose"
"github.com/arm/topo/internal/output/logger"
"github.com/arm/topo/internal/output/term"
"github.com/arm/topo/internal/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestRequireFile(t *testing.T) {
t.Run("returns path when compose file exists", func(t *testing.T) {
composeFile := filepath.Join(t.TempDir(), "compose.yaml")
testutil.RequireWriteFile(t, composeFile, "")

got, err := compose.RequireFile(composeFile)

require.NoError(t, err)
assert.Equal(t, composeFile, got)
})

t.Run("returns error when compose file does not exist", func(t *testing.T) {
composeFile := filepath.Join(t.TempDir(), "compose.yaml")

got, err := compose.RequireFile(composeFile)

require.Error(t, err)
assert.Empty(t, got)
assert.ErrorContains(t, err, "compose file not found")
assert.ErrorContains(t, err, composeFile)
})

t.Run("returns error when compose file path is a directory", func(t *testing.T) {
composeFile := filepath.Join(t.TempDir(), "compose.yaml")
require.NoError(t, os.Mkdir(composeFile, 0o755))

got, err := compose.RequireFile(composeFile)

require.Error(t, err)
assert.Empty(t, got)
assert.ErrorContains(t, err, "compose file path is a directory")
assert.ErrorContains(t, err, composeFile)
})
}

func TestFindDefaultFile(t *testing.T) {
t.Run("returns compose.yaml when it exists", func(t *testing.T) {
t.Chdir(t.TempDir())
testutil.RequireWriteFile(t, "compose.yaml", "")

got, err := compose.FindDefaultFile()

require.NoError(t, err)
assert.Equal(t, "compose.yaml", got)
})

t.Run("falls back to compose.yml", func(t *testing.T) {
t.Chdir(t.TempDir())
testutil.RequireWriteFile(t, "compose.yml", "")

got, err := compose.FindDefaultFile()

require.NoError(t, err)
assert.Equal(t, "compose.yml", got)
})

t.Run("warns when multiple default compose files exist", func(t *testing.T) {
t.Chdir(t.TempDir())
testutil.RequireWriteFile(t, "compose.yaml", "")
testutil.RequireWriteFile(t, "compose.yml", "")
var logOutput bytes.Buffer
logger.SetOptions(logger.Options{Output: &logOutput, Format: term.Plain})
t.Cleanup(func() {
logger.SetOptions(logger.Options{})
})

got, err := compose.FindDefaultFile()

require.NoError(t, err)
assert.Equal(t, "compose.yaml", got)
assert.Contains(t, logOutput.String(), "found multiple compose files: compose.yaml, compose.yml; using compose.yaml")
})

t.Run("returns error when no default compose file exists", func(t *testing.T) {
t.Chdir(t.TempDir())

got, err := compose.FindDefaultFile()

require.Error(t, err)
assert.Empty(t, got)
assert.EqualError(t, err, "compose file not found in current working directory: looking for compose.yaml or compose.yml")
})
}