a002b412e9
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>
87 lines
2.4 KiB
Go
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)
|
|
}
|