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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
4 changes: 2 additions & 2 deletions docs/TEST_CASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand All @@ -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**
Expand Down
1 change: 1 addition & 0 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 16 additions & 3 deletions internal/export/toolbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -55,6 +57,7 @@ type Toolbox struct {
image string
nativeBinary string
certFile string
insecure bool
runner CommandRunner
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down
49 changes: 49 additions & 0 deletions internal/export/toolbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down
Loading