Skip to content
This repository was archived by the owner on Feb 15, 2026. It is now read-only.

feat: add keyway docker command for Docker secrets injection#4

Open
Alan-221b wants to merge 2 commits intokeywaysh:mainfrom
Alan-221b:feat/docker-command
Open

feat: add keyway docker command for Docker secrets injection#4
Alan-221b wants to merge 2 commits intokeywaysh:mainfrom
Alan-221b:feat/docker-command

Conversation

@Alan-221b
Copy link
Copy Markdown
Contributor

@Alan-221b Alan-221b commented Jan 5, 2026

Closes #2

Summary

Add a new keyway docker command that injects vault secrets into Docker and Docker Compose commands.

Features

Command Injection Method
keyway docker run -e KEY=VALUE flags before image name
keyway docker compose run -e KEY=VALUE flags after run
keyway docker compose up/down/etc Temporary --env-file
  • User-provided -e flags take precedence over vault secrets
  • Interactive environment selection when --env flag not provided
  • Same authentication flow as other keyway commands

Usage

# Docker run - secrets injected as -e flags
keyway docker run --rm alpine env
keyway docker --env production run -p 8080:8080 myapp:latest

# Docker compose run - secrets injected as -e flags  
keyway docker compose run --rm test env

# Docker compose up - secrets injected via --env-file
keyway docker compose up -d

Files Changed

  • internal/cmd/docker.go - New command implementation
  • internal/cmd/docker_test.go - Comprehensive tests
  • internal/cmd/root.go - Register command + help text

Test Plan

  • Test keyway docker run --rm alpine env shows injected secrets
  • Test keyway docker compose run with secrets
  • Test keyway docker compose up with secrets
  • Test user -e flags override vault secrets
  • Test interactive environment selection
  • Run make test - all tests pass

Summary by CodeRabbit

  • New Features

    • Added docker command that automatically injects vault secrets into Docker and Docker Compose commands
    • Supports environment-based secret selection with interactive prompts
    • User-provided flags take precedence over vault-injected secrets
  • Tests

    • Comprehensive test coverage for Docker command functionality across multiple scenarios

✏️ Tip: You can customize this high-level summary in your review settings.

Alan-221b and others added 2 commits January 5, 2026 13:05
Closes keywaysh#2

Add a new `keyway docker` command that injects vault secrets into Docker
and Docker Compose commands.

Features:
- `keyway docker run` - injects secrets as -e KEY=VALUE flags
- `keyway docker compose run` - injects secrets as -e flags
- `keyway docker compose up` - injects secrets via --env-file
- User-provided -e flags take precedence over vault secrets
- Interactive environment selection when --env flag not provided

Usage:
  keyway docker run --rm alpine env
  keyway docker --env production run -p 8080:8080 myapp:latest
  keyway docker compose up -d
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 21, 2026

📝 Walkthrough

Walkthrough

The changes introduce a new keyway docker command that injects vault secrets into Docker and Docker Compose commands. The implementation includes environment selection via flags, secret retrieval from vault, and differential injection logic based on command type (docker run vs. docker compose), with user-provided environment variables taking precedence. Comprehensive test coverage validates all major code paths and error scenarios.

Changes

Cohort / File(s) Summary
Docker command implementation
internal/cmd/docker.go
New file introducing DockerOptions struct, runDockerCmd entry point, and runDockerWithDeps orchestrator. Implements secret injection for docker run (via -e flags before image) and docker compose (via -e flags for run subcommand, temporary --env-file for other subcommands). Includes helper functions findImagePosition and extractUserEnvVars to parse docker arguments and respect user overrides.
Docker command tests
internal/cmd/docker_test.go
New test file with comprehensive unit tests covering docker run and compose scenarios, environment injection, precedence rules, error handling, and helper function coverage via mock dependencies.
Root command registration
internal/cmd/root.go
Registers the new dockerCmd with rootCmd and adds help output entry for the keyway docker command.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as keyway docker
    participant Repo as Git/Repo
    participant Auth as Auth/Login
    participant UI as UI
    participant API as API Client
    participant Docker as Docker
    
    User->>CLI: keyway docker [--env NAME] DOCKER_ARGS
    CLI->>Repo: Detect git repo context
    Repo-->>CLI: Repo info
    CLI->>Auth: Ensure login
    Auth-->>CLI: Authenticated
    
    alt Env not provided and interactive
        CLI->>UI: Select environment
        UI-->>CLI: Selected env name
    else Env provided
        CLI->>CLI: Use provided env
    end
    
    CLI->>API: Fetch secrets for environment
    API-->>CLI: Vault secrets (key-value map)
    
    alt docker run detected
        CLI->>CLI: Parse user-provided -e flags
        CLI->>CLI: Find image position in args
        CLI->>Docker: Inject secrets before image, pass through docker args
    else docker compose detected
        alt compose run
            CLI->>Docker: Inject secrets as -e flags
        else compose up/down/etc
            CLI->>CLI: Create temp env file
            CLI->>Docker: Pass --env-file with secrets
        end
    end
    
    Docker-->>User: Command output
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A docker command now hops into place,
With secrets from vaults interspersed with grace!
The rabbits rejoice—no more manual fuss,
Inject and compose, it's magical plus! 🎩✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately and concisely describes the main feature addition: a new keyway docker command for Docker secrets injection.
Linked Issues check ✅ Passed The pull request implements all coding objectives from issue #2: docker run secret injection via -e flags, docker compose run/up support, user-supplied flag precedence, interactive environment selection, and authentication flow integration.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the docker command feature: the new docker.go implementation, corresponding docker_test.go tests, and root.go command registration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@internal/cmd/docker_test.go`:
- Around line 61-101: The test TestRunDockerWithDeps_DockerCompose_Success
should be updated to match runDockerWithDeps behavior: instead of asserting
LastArgs equals ["compose","up","-d"] and checking cmdRunner.LastSecrets, assert
that cmdRunner.LastArgs contains the "--env-file" flag followed by a non-empty
path, and verify that the referenced temp file exists and contains the expected
secret "API_KEY=secret123"; remove or replace the LastSecrets assertion. Locate
assertions in TestRunDockerWithDeps_DockerCompose_Success and change them to
search for "--env-file" within cmdRunner.LastArgs, validate the following arg is
a valid file path, read that file and assert it includes the secret, and no
longer expect secrets in cmdRunner.LastSecrets.

In `@internal/cmd/docker.go`:
- Around line 195-206: The compose-run branch injects all vault secrets into
newArgs without checking for user-supplied -e/--env flags, violating the
precedence rule; update the code in the block that handles opts.DockerArgs
starting with "run" to first scan opts.DockerArgs for user-provided environment
keys (handle "-e KEY=val", "-eKEY=val", "--env KEY=val", and "--env=KEY=val"
forms) and build a set of provided keys, then when appending vault secrets to
newArgs only add -e entries for keys not present in that set (mirror the
precedence logic used by runDockerRun), preserving the rest of opts.DockerArgs
and then call deps.CmdRunner.RunCommand("docker", newArgs, nil).
🧹 Nitpick comments (3)
internal/cmd/docker_test.go (1)

56-58: Simplify the nil check.

Per static analysis (gosimple S1009), len() for nil maps returns zero, so the nil check is redundant.

Suggested fix
-	if cmdRunner.LastSecrets != nil && len(cmdRunner.LastSecrets) > 0 {
+	if len(cmdRunner.LastSecrets) > 0 {
		t.Errorf("expected no secrets in environment for docker run, got %v", cmdRunner.LastSecrets)
	}
internal/cmd/docker.go (2)

75-77: Consider adding a timeout to the context.

The context created at line 77 has no timeout. If the API is slow or unresponsive, GetVaultEnvironments and PullSecrets calls could block indefinitely.

Suggested fix
 	// 3. Setup Client
 	client := deps.APIFactory.NewClient(token)
-	ctx := context.Background()
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()

Don't forget to add "time" to the imports.


216-219: Consider handling write errors to the temp file.

fmt.Fprintf returns an error that is currently ignored. If the write fails (e.g., disk full), the command would run with an incomplete or empty env file.

Suggested fix
 	for k, v := range secrets {
-		fmt.Fprintf(envFile, "%s=%s\n", k, v)
+		if _, err := fmt.Fprintf(envFile, "%s=%s\n", k, v); err != nil {
+			return fmt.Errorf("failed to write to temp env file: %w", err)
+		}
 	}

Comment on lines +61 to +101
func TestRunDockerWithDeps_DockerCompose_Success(t *testing.T) {
deps, _, _, _, cmdRunner, apiClient := NewTestDepsWithRunner()

apiClient.PullResponse = &api.PullSecretsResponse{
Content: "API_KEY=secret123",
}

opts := DockerOptions{
EnvName: "production",
EnvFlagSet: true,
DockerCommand: "compose",
DockerArgs: []string{"up", "-d"},
}

err := runDockerWithDeps(opts, deps)

if err != nil {
t.Fatalf("unexpected error: %v", err)
}

// Verify docker was called
if cmdRunner.LastCommand != "docker" {
t.Errorf("expected command 'docker', got %q", cmdRunner.LastCommand)
}

// Verify args are "compose up -d"
expectedArgs := []string{"compose", "up", "-d"}
if len(cmdRunner.LastArgs) != len(expectedArgs) {
t.Errorf("expected args %v, got %v", expectedArgs, cmdRunner.LastArgs)
}
for i, expected := range expectedArgs {
if i < len(cmdRunner.LastArgs) && cmdRunner.LastArgs[i] != expected {
t.Errorf("expected arg[%d] = %q, got %q", i, expected, cmdRunner.LastArgs[i])
}
}

// Verify secrets were passed via environment (compose uses env injection)
if cmdRunner.LastSecrets["API_KEY"] != "secret123" {
t.Errorf("expected API_KEY in secrets, got %v", cmdRunner.LastSecrets)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Test expectations don't match implementation — causing CI failure.

The implementation in docker.go (lines 208-224) uses --env-file for docker compose up, writing secrets to a temp file rather than passing them via LastSecrets. This test expects:

  1. Args to be exactly [compose up -d] — but actual args include --env-file <path>
  2. Secrets in LastSecrets — but secrets are written to a file, not passed via environment

Update the test to verify --env-file is present in args and that the file path is valid:

Proposed fix
 	// Verify args are "compose up -d"
-	expectedArgs := []string{"compose", "up", "-d"}
-	if len(cmdRunner.LastArgs) != len(expectedArgs) {
-		t.Errorf("expected args %v, got %v", expectedArgs, cmdRunner.LastArgs)
-	}
-	for i, expected := range expectedArgs {
-		if i < len(cmdRunner.LastArgs) && cmdRunner.LastArgs[i] != expected {
-			t.Errorf("expected arg[%d] = %q, got %q", i, expected, cmdRunner.LastArgs[i])
-		}
+	// Verify compose and --env-file are present
+	if len(cmdRunner.LastArgs) < 4 {
+		t.Fatalf("expected at least 4 args, got %v", cmdRunner.LastArgs)
+	}
+	if cmdRunner.LastArgs[0] != "compose" {
+		t.Errorf("expected first arg 'compose', got %q", cmdRunner.LastArgs[0])
+	}
+	if cmdRunner.LastArgs[1] != "--env-file" {
+		t.Errorf("expected '--env-file' flag, got %q", cmdRunner.LastArgs[1])
 	}
 
-	// Verify secrets were passed via environment (compose uses env injection)
-	if cmdRunner.LastSecrets["API_KEY"] != "secret123" {
-		t.Errorf("expected API_KEY in secrets, got %v", cmdRunner.LastSecrets)
+	// Verify up -d are at the end
+	argsStr := strings.Join(cmdRunner.LastArgs, " ")
+	if !strings.Contains(argsStr, "up -d") {
+		t.Errorf("expected 'up -d' in args, got %v", cmdRunner.LastArgs)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestRunDockerWithDeps_DockerCompose_Success(t *testing.T) {
deps, _, _, _, cmdRunner, apiClient := NewTestDepsWithRunner()
apiClient.PullResponse = &api.PullSecretsResponse{
Content: "API_KEY=secret123",
}
opts := DockerOptions{
EnvName: "production",
EnvFlagSet: true,
DockerCommand: "compose",
DockerArgs: []string{"up", "-d"},
}
err := runDockerWithDeps(opts, deps)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify docker was called
if cmdRunner.LastCommand != "docker" {
t.Errorf("expected command 'docker', got %q", cmdRunner.LastCommand)
}
// Verify args are "compose up -d"
expectedArgs := []string{"compose", "up", "-d"}
if len(cmdRunner.LastArgs) != len(expectedArgs) {
t.Errorf("expected args %v, got %v", expectedArgs, cmdRunner.LastArgs)
}
for i, expected := range expectedArgs {
if i < len(cmdRunner.LastArgs) && cmdRunner.LastArgs[i] != expected {
t.Errorf("expected arg[%d] = %q, got %q", i, expected, cmdRunner.LastArgs[i])
}
}
// Verify secrets were passed via environment (compose uses env injection)
if cmdRunner.LastSecrets["API_KEY"] != "secret123" {
t.Errorf("expected API_KEY in secrets, got %v", cmdRunner.LastSecrets)
}
}
func TestRunDockerWithDeps_DockerCompose_Success(t *testing.T) {
deps, _, _, _, cmdRunner, apiClient := NewTestDepsWithRunner()
apiClient.PullResponse = &api.PullSecretsResponse{
Content: "API_KEY=secret123",
}
opts := DockerOptions{
EnvName: "production",
EnvFlagSet: true,
DockerCommand: "compose",
DockerArgs: []string{"up", "-d"},
}
err := runDockerWithDeps(opts, deps)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify docker was called
if cmdRunner.LastCommand != "docker" {
t.Errorf("expected command 'docker', got %q", cmdRunner.LastCommand)
}
// Verify compose and --env-file are present
if len(cmdRunner.LastArgs) < 4 {
t.Fatalf("expected at least 4 args, got %v", cmdRunner.LastArgs)
}
if cmdRunner.LastArgs[0] != "compose" {
t.Errorf("expected first arg 'compose', got %q", cmdRunner.LastArgs[0])
}
if cmdRunner.LastArgs[1] != "--env-file" {
t.Errorf("expected '--env-file' flag, got %q", cmdRunner.LastArgs[1])
}
// Verify up -d are at the end
argsStr := strings.Join(cmdRunner.LastArgs, " ")
if !strings.Contains(argsStr, "up -d") {
t.Errorf("expected 'up -d' in args, got %v", cmdRunner.LastArgs)
}
}
🧰 Tools
🪛 GitHub Actions: CI

[error] 89-99: TestRunDockerWithDeps_DockerCompose_Success failed: expected args [compose up -d], got [compose --env-file /tmp/keyway-env-2596303086.env up -d] (and subsequent argument misalignment). This indicates the docker compose command formatting logic or test expectations are out of sync.


[error] 99-99: API_KEY not present in secrets in Docker compose test; test expectations not met due to environment/file generation differences.

🤖 Prompt for AI Agents
In `@internal/cmd/docker_test.go` around lines 61 - 101, The test
TestRunDockerWithDeps_DockerCompose_Success should be updated to match
runDockerWithDeps behavior: instead of asserting LastArgs equals
["compose","up","-d"] and checking cmdRunner.LastSecrets, assert that
cmdRunner.LastArgs contains the "--env-file" flag followed by a non-empty path,
and verify that the referenced temp file exists and contains the expected secret
"API_KEY=secret123"; remove or replace the LastSecrets assertion. Locate
assertions in TestRunDockerWithDeps_DockerCompose_Success and change them to
search for "--env-file" within cmdRunner.LastArgs, validate the following arg is
a valid file path, read that file and assert it includes the secret, and no
longer expect secrets in cmdRunner.LastSecrets.

Comment thread internal/cmd/docker.go
Comment on lines +195 to +206
if len(opts.DockerArgs) > 0 && opts.DockerArgs[0] == "run" {
// Find position after "run" to inject -e flags
newArgs := []string{"compose", "run"}
for k, v := range secrets {
newArgs = append(newArgs, "-e", fmt.Sprintf("%s=%s", k, v))
}
// Append remaining args after "run"
if len(opts.DockerArgs) > 1 {
newArgs = append(newArgs, opts.DockerArgs[1:]...)
}
return deps.CmdRunner.RunCommand("docker", newArgs, nil)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing user env precedence check for compose run.

Unlike runDockerRun, this path injects all vault secrets without checking if the user already provided -e flags for those keys. This breaks the PR requirement that "user-provided -e flags take precedence over vault secrets."

Proposed fix
 	if len(opts.DockerArgs) > 0 && opts.DockerArgs[0] == "run" {
+		// Extract user's -e flags to ensure they take precedence
+		userEnvVars := extractUserEnvVars(opts.DockerArgs)
+
 		// Find position after "run" to inject -e flags
 		newArgs := []string{"compose", "run"}
 		for k, v := range secrets {
-			newArgs = append(newArgs, "-e", fmt.Sprintf("%s=%s", k, v))
+			if _, userSet := userEnvVars[k]; !userSet {
+				newArgs = append(newArgs, "-e", fmt.Sprintf("%s=%s", k, v))
+			}
 		}
 		// Append remaining args after "run"
 		if len(opts.DockerArgs) > 1 {
 			newArgs = append(newArgs, opts.DockerArgs[1:]...)
 		}
 		return deps.CmdRunner.RunCommand("docker", newArgs, nil)
 	}
🤖 Prompt for AI Agents
In `@internal/cmd/docker.go` around lines 195 - 206, The compose-run branch
injects all vault secrets into newArgs without checking for user-supplied
-e/--env flags, violating the precedence rule; update the code in the block that
handles opts.DockerArgs starting with "run" to first scan opts.DockerArgs for
user-provided environment keys (handle "-e KEY=val", "-eKEY=val", "--env
KEY=val", and "--env=KEY=val" forms) and build a set of provided keys, then when
appending vault secrets to newArgs only add -e entries for keys not present in
that set (mirror the precedence logic used by runDockerRun), preserving the rest
of opts.DockerArgs and then call deps.CmdRunner.RunCommand("docker", newArgs,
nil).

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Docker Feature

2 participants