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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,11 @@ Each command has full help via `imposter <command> --help`.

| Command | What it does |
| --- | --- |
| `imposter up [DIR]` | Start a live mock from Imposter config in `DIR` (defaults to current directory). Add `-s` to scaffold first. |
| `imposter up [DIR]` | Start a live mock from Imposter config in `DIR` (defaults to current directory). Add `-s` to scaffold first, or `-d` to background it. |
| `imposter scaffold [DIR]` | Generate Imposter config from any OpenAPI/Swagger or WSDL files in `DIR`. |
| `imposter proxy URL` | Forward traffic to `URL` and record each exchange to disk as a replayable mock. Add `--insecure` to skip TLS verification. |
| `imposter down` | Stop running mocks for the current engine type. Add `-a` to stop them all. |
| `imposter list` | List running mocks and their health. `-qx` makes a tidy healthcheck. |
| `imposter down ID` | Stop the mock with the given ID (see `imposter ls`). `-a` / `--all` stops every managed mock across all engine types. |
| `imposter list` | List running mocks and their health across all engine types. `-t` filters by engine type; `-qx` makes a tidy healthcheck. |
| `imposter bundle [DIR]` | Bundle config and engine into a Docker image or Lambda zip. |
| `imposter doctor` | Check that you have at least one engine ready to run. |
| `imposter engine pull` / `engine list` | Manage cached engine binaries and images. |
Expand Down
70 changes: 48 additions & 22 deletions cmd/down.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,45 @@ limitations under the License.
package cmd

import (
"github.com/imposter-project/imposter-cli/internal/engine"
"github.com/spf13/cobra"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/imposter-project/imposter-cli/internal/engine"
"github.com/spf13/cobra"
)

var downFlags = struct {
engineType string
all bool
all bool
}{}

// downCmd represents the down command
var downCmd = &cobra.Command{
Use: "down",
Short: "Stop running mocks",
Long: `Stops running Imposter mocks for the current engine type.`,
Use: "down [ID]",
Short: "Stop a running mock by ID, or all mocks with --all",
Long: `Stops a single running Imposter mock identified by ID, or all
managed mocks across every engine type with --all.

Use 'imposter ls' to discover the IDs of running mocks.`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if downFlags.all {
if len(args) > 0 {
logger.Fatal("cannot specify both --all and a mock ID")
}
stopAllEngines()
} else {
stopAll(engine.GetConfiguredType(downFlags.engineType))
return
}
if len(args) == 0 {
logger.Fatal("a mock ID is required (or use --all to stop all mocks); see 'imposter ls' for IDs")
}
stopMockByID(args[0])
},
}

func init() {
downCmd.Flags().StringVarP(&downFlags.engineType, "engine-type", "t", "", "Imposter engine type (valid: docker,native,jvm - default \"docker\")")
downCmd.Flags().BoolVarP(&downFlags.all, "all", "a", false, "Stop mocks for all engine types")
downCmd.MarkFlagsMutuallyExclusive("engine-type", "all")
registerEngineTypeCompletions(downCmd)
downCmd.Flags().BoolVarP(&downFlags.all, "all", "a", false, "Stop all managed mocks across all engine types")
rootCmd.AddCommand(downCmd)
}

Expand Down Expand Up @@ -76,17 +85,34 @@ func stopAllEngines() {
}
}

func stopAll(engineType engine.EngineType) {
logger.Info("stopping all managed mocks...")
stopped, err := stopEngine(engineType)
if err != nil {
logger.Fatalf("failed to stop mocks: %s", err)
// stopMockByID searches every engine type for a managed mock with the
// given ID and stops it.
func stopMockByID(id string) {
var engineErrors []string
for _, engineType := range allEngineTypes {
var stopped bool
var stopErr error
err := runWithRecovery(func() {
mockEngine := engine.BuildEngine(engineType, filepath.Join(os.TempDir(), "imposter-down"), engine.StartOptions{})
stopped, stopErr = mockEngine.StopManaged(id)
})
if err != nil {
engineErrors = append(engineErrors, fmt.Sprintf("%s: %v", engineType, err))
continue
}
if stopErr != nil {
engineErrors = append(engineErrors, fmt.Sprintf("%s: %v", engineType, stopErr))
continue
}
if stopped {
logger.Infof("stopped mock %s (%s engine)", id, engineType)
return
}
}
if stopped > 0 {
logger.Infof("stopped %d managed mock(s)", stopped)
} else {
logger.Info("no managed mocks were found")
if len(engineErrors) == len(allEngineTypes) {
logger.Fatalf("failed to query any engine: %s", strings.Join(engineErrors, "; "))
}
logger.Fatalf("no managed mock found with ID %q (run 'imposter ls' to see running mocks)", id)
}

func stopEngine(engineType engine.EngineType) (int, error) {
Expand Down
26 changes: 22 additions & 4 deletions cmd/down_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,26 @@ func Test_stopEngine(t *testing.T) {
}
}

func Test_downCmd_mutual_exclusivity(t *testing.T) {
rootCmd.SetArgs([]string{"down", "-a", "-t", "docker"})
err := rootCmd.Execute()
require.Error(t, err, "should reject --all with --engine-type")
func Test_downCmd_requires_id_or_all(t *testing.T) {
// bare `imposter down` is fatal — capture via runWithRecovery
rootCmd.SetArgs([]string{"down"})
err := runWithRecovery(func() {
_ = rootCmd.Execute()
})
require.Error(t, err, "should fail without an ID or --all")
}

func Test_downCmd_rejects_id_with_all(t *testing.T) {
rootCmd.SetArgs([]string{"down", "--all", "abc123"})
err := runWithRecovery(func() {
_ = rootCmd.Execute()
})
require.Error(t, err, "should reject ID together with --all")
}

func Test_stopMockByID_unknown(t *testing.T) {
err := runWithRecovery(func() {
stopMockByID("definitely-not-a-real-mock-id")
})
require.Error(t, err, "should fail when no engine has a mock with the given id")
}
17 changes: 8 additions & 9 deletions cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import (

var listFlags = struct {
engineType string
all bool
healthExitCode bool
quiet bool
}{}
Expand All @@ -37,23 +36,23 @@ var listCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List running mocks",
Long: `Lists running Imposter mocks for the current engine type
and reports their health.`,
Long: `Lists running Imposter mocks and reports their health.

By default, mocks across all engine types are listed. Use --engine-type / -t
to filter to a single engine type.`,
Run: func(cmd *cobra.Command, args []string) {
if listFlags.all {
listAllMocks(listFlags.quiet)
} else {
if listFlags.engineType != "" {
listMocks(engine.GetConfiguredType(listFlags.engineType), listFlags.quiet, false)
} else {
listAllMocks(listFlags.quiet)
}
},
}

func init() {
listCmd.Flags().StringVarP(&listFlags.engineType, "engine-type", "t", "", "Imposter engine type (valid: docker,native,jvm - default \"docker\")")
listCmd.Flags().BoolVarP(&listFlags.all, "all", "a", false, "List mocks for all engine types")
listCmd.Flags().StringVarP(&listFlags.engineType, "engine-type", "t", "", "Filter mocks to this engine type (valid: docker,native,jvm)")
listCmd.Flags().BoolVarP(&listFlags.healthExitCode, "exit-code-health", "x", false, "Set exit code based on mock health")
listCmd.Flags().BoolVarP(&listFlags.quiet, "quiet", "q", false, "Quieten output; only print ID")
listCmd.MarkFlagsMutuallyExclusive("engine-type", "all")
registerEngineTypeCompletions(listCmd)
rootCmd.AddCommand(listCmd)
}
Expand Down
6 changes: 3 additions & 3 deletions cmd/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ func Test_listMocksForEngine(t *testing.T) {
}
}

func Test_listCmd_mutual_exclusivity(t *testing.T) {
rootCmd.SetArgs([]string{"list", "-a", "-t", "docker"})
func Test_listCmd_rejects_unknown_flag(t *testing.T) {
rootCmd.SetArgs([]string{"list", "--all"})
err := rootCmd.Execute()
require.Error(t, err, "should reject --all with --engine-type")
require.Error(t, err, "--all is no longer a flag; listing across engines is the default")
}
71 changes: 70 additions & 1 deletion cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ var upFlags = struct {
dirMounts []string
recursiveConfigScan bool
debugMode bool
detach string
logFile string
}{}

// upCmd represents the up command
Expand Down Expand Up @@ -111,10 +113,49 @@ If CONFIG_DIR is not specified, the current working directory is used.`,
DirMounts: upFlags.dirMounts,
DebugMode: upFlags.debugMode,
}
start(&lib, startOptions, configDir, upFlags.restartOnChange)
restartOnChange := applyDetachOptions(&startOptions, engineType, upFlags.detach, upFlags.logFile, upFlags.restartOnChange)

start(&lib, startOptions, configDir, restartOnChange)
},
}

// applyDetachOptions resolves the detach-related flags onto startOptions
// and returns the (possibly disabled) auto-restart setting. Auto-restart
// is incompatible with detaching because the config-dir watcher lives in
// the foreground CLI process, which exits once the mock is backgrounded.
func applyDetachOptions(startOptions *engine.StartOptions, engineType engine.EngineType, detach string, logFile string, restartOnChange bool) bool {
switch detach {
case "":
return restartOnChange
case "healthy":
startOptions.Detach = engine.DetachHealthy
case "now":
startOptions.Detach = engine.DetachNow
default:
logger.Fatalf("invalid --detach mode %q (valid: healthy, now)", detach)
}

if restartOnChange {
logger.Warn("--auto-restart is not supported with --detach; auto-restart disabled")
restartOnChange = false
}

if engine.IsDockerEngine(engineType) {
if logFile != "" {
logger.Warn("--log-file is ignored for the docker engine; use 'docker logs' instead")
}
} else if logFile != "" {
startOptions.DetachLog, _ = filepath.Abs(logFile)
} else {
detachLog, err := engine.DefaultDetachLogPath(startOptions.Port)
if err != nil {
logger.Fatal(err)
}
startOptions.DetachLog = detachLog
}
return restartOnChange
}

func init() {
upCmd.Flags().StringVarP(&upFlags.engineType, "engine-type", "t", "", "Imposter engine type (valid: docker,native,jvm - default \"docker\")")
upCmd.Flags().StringVarP(&upFlags.engineVersion, "version", "v", "", "Imposter engine version (default \"latest\")")
Expand All @@ -130,6 +171,9 @@ func init() {
upCmd.Flags().StringArrayVar(&upFlags.dirMounts, "mount-dir", []string{}, "(Docker engine type only) Extra directory bind-mounts in the form HOST_PATH:CONTAINER_PATH (e.g. $HOME/somedir:/opt/imposter/somedir) or simply HOST_PATH, which will mount the directory at /opt/imposter/<dir>")
upCmd.Flags().BoolVarP(&upFlags.recursiveConfigScan, "recursive-config-scan", "r", false, "Scan for config files in subdirectories")
upCmd.Flags().BoolVar(&upFlags.debugMode, "debug-mode", false, fmt.Sprintf("Enable JVM debug mode and listen on port %v", engine.DefaultDebugPort))
upCmd.Flags().StringVarP(&upFlags.detach, "detach", "d", "", "Run the mock in the background and return control to the terminal. Optional mode: 'healthy' (default, wait for the healthcheck before detaching) or 'now' (detach immediately)")
upCmd.Flags().Lookup("detach").NoOptDefVal = "healthy"
upCmd.Flags().StringVar(&upFlags.logFile, "log-file", "", "(Process engine types only) File to write detached mock logs to (default ~/.imposter/logs/imposter-<port>.log)")
registerEngineTypeCompletions(upCmd)
rootCmd.AddCommand(upCmd)
}
Expand Down Expand Up @@ -172,6 +216,23 @@ func start(lib *engine.EngineLibrary, startOptions engine.StartOptions, configDi
mockEngine := provider.Build(configDir, startOptions)

wg := &sync.WaitGroup{}

if startOptions.IsDetached() {
// DetachHealthy still traps Ctrl+C so an abort during the
// healthcheck wait stops the mock; DetachNow returns immediately
// so there is nothing to interrupt.
if startOptions.Detach == engine.DetachHealthy {
trapExit(mockEngine, wg)
}
if !mockEngine.Start(wg) {
// healthcheck timeout already calls logger.Fatalf; reaching
// here means the wait was aborted (e.g. Ctrl+C)
return
}
printDetachSummary(mockEngine, startOptions)
return
}

trapExit(mockEngine, wg)
success := mockEngine.Start(wg)

Expand All @@ -190,6 +251,14 @@ func start(lib *engine.EngineLibrary, startOptions engine.StartOptions, configDi
logger.Debug("shutting down")
}

func printDetachSummary(mockEngine engine.MockEngine, startOptions engine.StartOptions) {
logger.Infof("mock running in the background (id: %s, port: %d)", mockEngine.GetID(), startOptions.Port)
if startOptions.DetachLog != "" {
logger.Infof("logs: %s", startOptions.DetachLog)
}
logger.Info("use 'imposter ls' to list running mocks, or 'imposter down' to stop them")
}

// listen for an interrupt from the OS, then attempt engine cleanup
func trapExit(mockEngine engine.MockEngine, wg *sync.WaitGroup) {
c := make(chan os.Signal)
Expand Down
75 changes: 75 additions & 0 deletions cmd/up_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
Copyright © 2021 Pete Cornish <outofcoffee@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package cmd

import (
"path/filepath"
"testing"

"github.com/imposter-project/imposter-cli/internal/config"
"github.com/imposter-project/imposter-cli/internal/engine"
"github.com/stretchr/testify/assert"
)

func Test_applyDetachOptions(t *testing.T) {
tmpHome := t.TempDir()
config.DirPath = tmpHome
defer func() { config.DirPath = "" }()

t.Run("no detach leaves foreground mode and keeps auto-restart", func(t *testing.T) {
opts := engine.StartOptions{Port: 8080}
restart := applyDetachOptions(&opts, engine.EngineTypeJvmSingleJar, "", "", true)
assert.Equal(t, engine.DetachNone, opts.Detach)
assert.False(t, opts.IsDetached())
assert.True(t, restart)
assert.Empty(t, opts.DetachLog)
})

t.Run("detach=healthy waits for the healthcheck", func(t *testing.T) {
opts := engine.StartOptions{Port: 8080}
restart := applyDetachOptions(&opts, engine.EngineTypeJvmSingleJar, "healthy", "", true)
assert.Equal(t, engine.DetachHealthy, opts.Detach)
assert.False(t, restart, "auto-restart must be disabled when detached")
})

t.Run("detach=now returns immediately", func(t *testing.T) {
opts := engine.StartOptions{Port: 8080}
applyDetachOptions(&opts, engine.EngineTypeNative, "now", "", false)
assert.Equal(t, engine.DetachNow, opts.Detach)
})

t.Run("process engine resolves default log path", func(t *testing.T) {
opts := engine.StartOptions{Port: 1234}
applyDetachOptions(&opts, engine.EngineTypeJvmSingleJar, "healthy", "", false)
expected := filepath.Join(tmpHome, "logs", "imposter-1234.log")
assert.Equal(t, expected, opts.DetachLog)
})

t.Run("explicit log file is honoured and made absolute", func(t *testing.T) {
opts := engine.StartOptions{Port: 8080}
applyDetachOptions(&opts, engine.EngineTypeNative, "healthy", "relative/mock.log", false)
assert.True(t, filepath.IsAbs(opts.DetachLog))
assert.Equal(t, "mock.log", filepath.Base(opts.DetachLog))
})

t.Run("docker engine does not set a detach log", func(t *testing.T) {
opts := engine.StartOptions{Port: 8080}
applyDetachOptions(&opts, engine.EngineTypeDockerCore, "healthy", "", false)
assert.Equal(t, engine.DetachHealthy, opts.Detach)
assert.Empty(t, opts.DetachLog)
})
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ require (
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/sys v0.42.0
golang.org/x/text v0.35.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading
Loading