Files
molecule-cli/internal/cmd/bundle.go
sdk-dev a002b412e9
Release Go binaries / release (pull_request) Blocked by required conditions
CI / Test / test (pull_request) Successful in 1m51s
Release Go binaries / test (pull_request) Successful in 1m11s
feat(cli): fix runHTTP auth bug + add management verbs
Fix the auth bug FIRST: internal/cmd/http.go runHTTP (and the
internal/client Platform HTTP helpers, which had the same gap) sent NO
Authorization header, so management calls (workspace create/delete,
secrets, tokens, …) 401'd a hardened tenant. Now every request attaches
`Authorization: Bearer $MOLECULE_API_KEY` and, when set,
`X-Molecule-Org-Id: $MOLECULE_ORG_ID` (the tenant TenantGuard routing
gate). Headers are omitted when the env vars are unset so fresh
self-host/dev tenants keep working. Regression test
TestRunHTTP_SetsAuthHeader asserts the header is set and is proven
load-bearing (fails with `Authorization header = ""` when the fix is
reverted).

Add the management verbs (PLATFORM-MANAGEMENT-API.md §5(b)), each wired
to the OpenAPI-documented endpoint at the correct auth tier (verified
against the live workspace-server router.go + handlers and controlplane
orgs handler, since the parallel feat/openapi-management-spec branch
does not exist in molecule-core — reconciled to the actual handler
source instead):

  org   list|get|create --slug/--name|create --template|export
        token list|create|revoke | allowlist
  workspace list|get|create|delete|restart|pause|resume
            budget|billing-mode|token mint
  secret  ws  list|set|delete
          org list|set|delete
  template list|import|refresh
  bundle  export|import
  events
  approvals

Org-lifecycle verbs target the control plane (MOLECULE_CP_URL, default
= api-url); tenant verbs target the tenant host with the Org API Key.
All verbs honor --json (and existing -o table|json|yaml). Request/
response shapes match the handler structs (budget USD-cents
budget_limits; billing-mode {mode}; org import {dir,mode}; secrets
{key,value}; template import {name,files}; etc.).

Tests: table-driven request-construction tests (method/path/body/auth)
for all 30 management methods against an httptest mock, plus
cmd-layer branch tests (budget flag→limits, billing-mode validation,
template file mapping, --json resolution, CP-url fallback). Existing
workspace/agent/platform commands switched to the authenticated client.

go build ./..., go vet ./..., go test ./... all green; gofmt clean on
edited files. Binary smoke-tested end-to-end: auth headers reach the
server and --json output renders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 20:53:04 -07:00

87 lines
2.4 KiB
Go

// Package cmd implements the CLI command tree.
package cmd
import (
"encoding/json"
"fmt"
"io"
"os"
"github.com/spf13/cobra"
)
// ---------------------------------------------------------------------------
// Bundle command group (PLATFORM-MANAGEMENT-API.md §5(b)):
// molecule bundle export <workspace-id> [--file out.json]
// molecule bundle import --file bundle.json (or - for stdin)
// Tenant host, Org API Key (AdminAuth).
// ---------------------------------------------------------------------------
var bundleCmd = &cobra.Command{
Use: "bundle",
Short: "Export and import workspace bundles",
}
func init() {
bundleCmd.AddCommand(bundleExportCmd, bundleImportCmd)
bundleExportCmd.Flags().StringVar(&bundleExportFile, "file", "", "Write bundle JSON to this file instead of stdout")
bundleImportCmd.Flags().StringVar(&bundleImportFile, "file", "", "Read bundle JSON from this file (- for stdin)")
}
var (
bundleExportFile string
bundleImportFile string
)
var bundleExportCmd = &cobra.Command{
Use: "export <workspace-id>",
Short: "Export a workspace as a bundle",
Args: cobra.ExactArgs(1),
RunE: runBundleExport,
}
func runBundleExport(_ *cobra.Command, args []string) error {
raw, err := newClient().ExportBundle(args[0])
if err != nil {
return fmt.Errorf("bundle export: %w", err)
}
if bundleExportFile != "" {
if err := os.WriteFile(bundleExportFile, raw, 0o600); err != nil {
return fmt.Errorf("bundle export: write %s: %w", bundleExportFile, err)
}
fmt.Printf("Bundle written to %s\n", bundleExportFile)
return nil
}
return printRaw(raw)
}
var bundleImportCmd = &cobra.Command{
Use: "import --file <bundle.json|->",
Short: "Import a workspace bundle from a file or stdin",
RunE: runBundleImport,
}
func runBundleImport(_ *cobra.Command, _ []string) error {
if bundleImportFile == "" {
return &exitError{code: 2, msg: "bundle import: --file <path|-> is required"}
}
var data []byte
var err error
if bundleImportFile == "-" {
data, err = io.ReadAll(os.Stdin)
} else {
data, err = os.ReadFile(bundleImportFile)
}
if err != nil {
return fmt.Errorf("bundle import: read: %w", err)
}
if !json.Valid(data) {
return &exitError{code: 2, msg: "bundle import: input is not valid JSON"}
}
raw, err := newClient().ImportBundle(json.RawMessage(data))
if err != nil {
return fmt.Errorf("bundle import: %w", err)
}
return printRaw(raw)
}