diff --git a/README.md b/README.md index 61962a9..5ab1356 100644 --- a/README.md +++ b/README.md @@ -92,13 +92,15 @@ The hybrid export combines: | `--redact-secrets` | Opt-in: mask sensitive keys in JSON/YAML artifacts (see [Redaction](#redaction) below) | | `--per-page` | Admin API page size (max 500) | | `--concurrency` | Concurrent requests (default 4) | -| `--insecure` | Skip TLS verification on Admin API | +| `--insecure` | Skip TLS verification on Admin API, Analytics API, and toolbox (`-k`) | | `--toolbox-image` | Toolbox image (default Red Hat 2.16) | | `--toolbox-runtime` | `docker` or `podman` (auto-detect if empty) | | `--toolbox-tls-cert` | CA certificate mounted in the toolbox container | Self-signed TLS (Admin Portal or toolbox): +`--insecure` passes `-k` to the 3scale toolbox container/native binary so product YAML export also skips certificate verification. Alternatively, mount a custom CA with `--toolbox-tls-cert` without disabling verification. + ```bash ./threescale-export \ --toolbox-tls-cert ./ca.pem \ diff --git a/docs/TEST_CASES.md b/docs/TEST_CASES.md index 0ef6c9f..1306208 100644 --- a/docs/TEST_CASES.md +++ b/docs/TEST_CASES.md @@ -188,7 +188,7 @@ Automation references are verified against the repository at the time of writing |-------|-------| | Priority | P2 | | CLI | `threescale-export` | -| Automation | **manual** | +| Automation | `TestRunContainerArgsInsecure`, `TestExportProductNativeInsecure` (`internal/export/toolbox_test.go`); lab validation **manual** | **Preconditions** @@ -197,7 +197,7 @@ Automation references are verified against the repository at the time of writing **Steps** -1. Export with `--insecure` for Admin API TLS, and/or set `THREESCALE_TOOLBOX_TLS_CERT` for toolbox container TLS +1. Export with `--insecure` (Admin API, Analytics API, and toolbox `-k`), and/or set `THREESCALE_TOOLBOX_TLS_CERT` for CA pinning without full skip 2. For integration CI: set repository secret `THREESCALE_INSECURE_TLS=true` **Expected results** diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 7ce565e..104d5e6 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -68,6 +68,7 @@ func RunExport(ctx context.Context, cfg config.ExportConfig) error { Image: cfg.ToolboxImage, NativeBinary: cfg.ToolboxNativeBinary, CertFile: cfg.ToolboxCertFile, + Insecure: cfg.InsecureTLS, }) if err != nil { return err diff --git a/internal/export/toolbox.go b/internal/export/toolbox.go index 228a1a9..1f2ec80 100644 --- a/internal/export/toolbox.go +++ b/internal/export/toolbox.go @@ -46,6 +46,8 @@ type ToolboxOptions struct { NativeBinary string // CertFile mounts a CA/cert for toolbox TLS (SSL_CERT_FILE in container). CertFile string + // Insecure passes -k to toolbox to skip TLS verification (lab tenants). + Insecure bool // CommandRunner overrides process execution (defaults to os/exec). CommandRunner CommandRunner } @@ -55,6 +57,7 @@ type Toolbox struct { image string nativeBinary string certFile string + insecure bool runner CommandRunner } @@ -64,6 +67,7 @@ func NewToolbox(opts ToolboxOptions) (*Toolbox, error) { image: strings.TrimSpace(opts.Image), nativeBinary: strings.TrimSpace(opts.NativeBinary), certFile: strings.TrimSpace(opts.CertFile), + insecure: opts.Insecure, runner: opts.CommandRunner, } if t.runner == nil { @@ -128,8 +132,7 @@ func buildRemoteURL(adminURL, token string) (string, error) { } func (t *Toolbox) runNative(ctx context.Context, remoteURL, systemName string) ([]byte, error) { - args := []string{"product", "export", remoteURL, systemName} - return t.runCommand(ctx, t.nativeBinary, args) + return t.runCommand(ctx, t.nativeBinary, t.toolboxProductArgs(remoteURL, systemName)) } func (t *Toolbox) runContainer(ctx context.Context, remoteURL, systemName string) ([]byte, error) { @@ -140,10 +143,20 @@ func (t *Toolbox) runContainer(ctx context.Context, remoteURL, systemName string "-v", t.certFile+":/tmp/3scale-toolbox-cert.pem:ro", ) } - args = append(args, t.image, "3scale", "product", "export", remoteURL, systemName) + args = append(args, t.image, "3scale") + args = append(args, t.toolboxProductArgs(remoteURL, systemName)...) return t.runCommand(ctx, t.runtime, args) } +func (t *Toolbox) toolboxProductArgs(remoteURL, systemName string) []string { + args := make([]string, 0, 5) + if t.insecure { + args = append(args, "-k") + } + args = append(args, "product", "export", remoteURL, systemName) + return args +} + func (t *Toolbox) runCommand(ctx context.Context, command string, args []string) ([]byte, error) { stdout, stderr, err := t.runner.Run(ctx, command, args) if err != nil { diff --git a/internal/export/toolbox_test.go b/internal/export/toolbox_test.go index f2d37bd..3a3bc62 100644 --- a/internal/export/toolbox_test.go +++ b/internal/export/toolbox_test.go @@ -151,6 +151,34 @@ func TestRunContainerArgs(t *testing.T) { } } +func TestRunContainerArgsInsecure(t *testing.T) { + var captured []string + runner := &mockCommandRunner{ + fn: func(_ string, args []string) ([]byte, []byte, error) { + captured = append([]string(nil), args...) + return []byte("apiVersion: v1\n"), nil, nil + }, + } + tb := &Toolbox{ + runtime: "podman", + image: DefaultToolboxImage, + insecure: true, + runner: runner, + } + if _, err := tb.ExportProduct(context.Background(), "https://admin.example.com", "tok", "payments"); err != nil { + t.Fatal(err) + } + joined := strings.Join(captured, " ") + if !strings.Contains(joined, " -k ") && !strings.HasSuffix(joined, " -k") { + for i, arg := range captured { + if arg == "-k" && i+1 < len(captured) && captured[i+1] == "product" { + return + } + } + t.Fatalf("args missing -k before product: %v", captured) + } +} + func TestExportProductNativeUsesRunner(t *testing.T) { var captured struct { command string @@ -178,6 +206,27 @@ func TestExportProductNativeUsesRunner(t *testing.T) { } } +func TestExportProductNativeInsecure(t *testing.T) { + var captured []string + runner := &mockCommandRunner{ + fn: func(_ string, args []string) ([]byte, []byte, error) { + captured = append([]string(nil), args...) + return []byte("kind: Product\n"), nil, nil + }, + } + tb := &Toolbox{ + nativeBinary: "/usr/bin/3scale", + insecure: true, + runner: runner, + } + if _, err := tb.ExportProduct(context.Background(), "https://tenant.example.com", "secret", "demo_api"); err != nil { + t.Fatal(err) + } + if len(captured) != 5 || captured[0] != "-k" || captured[1] != "product" || captured[4] != "demo_api" { + t.Fatalf("args = %v", captured) + } +} + func TestRunCommandEmptyOutput(t *testing.T) { tb := &Toolbox{ nativeBinary: "/usr/bin/3scale",