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>
115 lines
3.3 KiB
Go
115 lines
3.3 KiB
Go
// Package cmd implements the CLI command tree.
|
|
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Template command group (PLATFORM-MANAGEMENT-API.md §5(b)):
|
|
// molecule template {list, import --name <n> --file <k>=<path>..., refresh}
|
|
// All go to the tenant host with the Org API Key (AdminAuth).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
var templateCmd = &cobra.Command{
|
|
Use: "template",
|
|
Short: "Manage workspace templates",
|
|
}
|
|
|
|
func init() {
|
|
templateCmd.AddCommand(templateListCmd, templateImportCmd, templateRefreshCmd)
|
|
}
|
|
|
|
var templateListCmd = &cobra.Command{
|
|
Use: "list",
|
|
Short: "List workspace templates",
|
|
RunE: runTemplateList,
|
|
}
|
|
|
|
func runTemplateList(_ *cobra.Command, _ []string) error {
|
|
raw, err := newClient().ListTemplates()
|
|
if err != nil {
|
|
return fmt.Errorf("template list: %w", err)
|
|
}
|
|
return printRaw(raw)
|
|
}
|
|
|
|
var templateImportFlags struct {
|
|
name string
|
|
files []string // KEY=PATH pairs
|
|
}
|
|
|
|
var templateImportCmd = &cobra.Command{
|
|
Use: "import --name <name> --file <relpath>=<localpath> [--file ...]",
|
|
Short: "Import a template from local files",
|
|
Long: `Imports a template (POST /templates/import). Each --file maps a path
|
|
inside the template to a local file whose contents are read and uploaded.
|
|
|
|
molecule template import --name my-tmpl \
|
|
--file org.yaml=./org.yaml \
|
|
--file config.yaml=./config.yaml`,
|
|
RunE: runTemplateImport,
|
|
}
|
|
|
|
func init() {
|
|
f := templateImportCmd.Flags()
|
|
f.StringVar(&templateImportFlags.name, "name", "", "Template name (required)")
|
|
f.StringArrayVar(&templateImportFlags.files, "file", nil, "Template file mapping relpath=localpath (repeatable, required)")
|
|
templateImportCmd.MarkFlagRequired("name")
|
|
}
|
|
|
|
func runTemplateImport(_ *cobra.Command, _ []string) error {
|
|
if len(templateImportFlags.files) == 0 {
|
|
return &exitError{code: 2, msg: "template import: at least one --file relpath=localpath is required"}
|
|
}
|
|
files, err := readFileMappings(templateImportFlags.files)
|
|
if err != nil {
|
|
return &exitError{code: 2, msg: "template import: " + err.Error()}
|
|
}
|
|
raw, err := newClient().ImportTemplate(templateImportFlags.name, files)
|
|
if err != nil {
|
|
return fmt.Errorf("template import: %w", err)
|
|
}
|
|
return printRaw(raw)
|
|
}
|
|
|
|
// readFileMappings parses "relpath=localpath" pairs and reads each local file
|
|
// into the returned map[relpath]contents.
|
|
func readFileMappings(pairs []string) (map[string]string, error) {
|
|
out := make(map[string]string, len(pairs))
|
|
for _, p := range pairs {
|
|
rel, local, ok := strings.Cut(p, "=")
|
|
if !ok || rel == "" || local == "" {
|
|
return nil, fmt.Errorf("invalid --file %q (want relpath=localpath)", p)
|
|
}
|
|
data, err := os.ReadFile(local)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read %s: %w", local, err)
|
|
}
|
|
out[rel] = string(data)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
var templateRefreshCmd = &cobra.Command{
|
|
Use: "refresh",
|
|
Short: "Refresh the template cache",
|
|
RunE: runTemplateRefresh,
|
|
}
|
|
|
|
func runTemplateRefresh(_ *cobra.Command, _ []string) error {
|
|
raw, err := newClient().RefreshTemplates()
|
|
if err != nil {
|
|
return fmt.Errorf("template refresh: %w", err)
|
|
}
|
|
if len(raw) == 0 {
|
|
fmt.Println("Template cache refresh triggered.")
|
|
return nil
|
|
}
|
|
return printRaw(raw)
|
|
}
|