fix(provisioner): wire collectCPConfigFiles into CPProvisioner.Start (OFFSEC-010) #1079

Closed
fullstack-engineer wants to merge 1 commits from fix/offsec-010-wiring into staging
2 changed files with 57 additions and 0 deletions
@@ -158,6 +158,7 @@ type cpProvisionRequest struct {
Tier int `json:"tier"`
PlatformURL string `json:"platform_url"`
Env map[string]string `json:"env"`
ConfigFiles map[string]string `json:"config_files,omitempty"`
}
type cpProvisionResponse struct {
@@ -181,6 +182,10 @@ func (p *CPProvisioner) Start(ctx context.Context, cfg WorkspaceConfig) (string,
}
env["ADMIN_TOKEN"] = p.adminToken
}
configFiles, err := collectCPConfigFiles(cfg)
if err != nil {
return "", fmt.Errorf("cp provisioner: collect config files: %w", err)
}
req := cpProvisionRequest{
OrgID: p.orgID,
WorkspaceID: cfg.WorkspaceID,
@@ -188,6 +193,7 @@ func (p *CPProvisioner) Start(ctx context.Context, cfg WorkspaceConfig) (string,
Tier: cfg.Tier,
PlatformURL: cfg.PlatformURL,
Env: env,
ConfigFiles: configFiles,
}
body, err := json.Marshal(req)
@@ -6,6 +6,8 @@ import (
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
@@ -187,6 +189,10 @@ func TestStart_HappyPath(t *testing.T) {
if body.WorkspaceID != "ws-1" || body.Runtime != "python" {
t.Errorf("body mismatch: %+v", body)
}
// ConfigFiles should be empty when neither TemplatePath nor ConfigFiles is set
if body.ConfigFiles != nil {
t.Errorf("ConfigFiles = %v, want nil", body.ConfigFiles)
}
w.WriteHeader(http.StatusCreated)
_, _ = io.WriteString(w, `{"instance_id":"i-abc123","state":"pending"}`)
}))
@@ -213,6 +219,51 @@ func TestStart_HappyPath(t *testing.T) {
}
}
// TestStart_CollectsConfigFiles wires collectCPConfigFiles into the provision request.
// Verifies the OFFSEC-010 fix is actually reachable (issue #1077: collectCPConfigFiles
// was dead code after PR #1075).
func TestStart_CollectsConfigFiles(t *testing.T) {
tmpl := t.TempDir()
if err := os.WriteFile(filepath.Join(tmpl, "config.yaml"), []byte("name: test\n"), 0o600); err != nil {
t.Fatal(err)
}
var gotBody cpProvisionRequest
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = json.NewDecoder(r.Body).Decode(&gotBody)
w.WriteHeader(http.StatusCreated)
_, _ = io.WriteString(w, `{"instance_id":"i-xyz","state":"pending"}`)
}))
defer srv.Close()
p := &CPProvisioner{
baseURL: srv.URL,
orgID: "org-1",
sharedSecret: "s3cret",
httpClient: srv.Client(),
}
id, err := p.Start(context.Background(), WorkspaceConfig{
WorkspaceID: "ws-2",
Runtime: "python",
Tier: 1,
PlatformURL: "http://tenant",
TemplatePath: tmpl,
})
if err != nil {
t.Fatalf("Start: %v", err)
}
if id != "i-xyz" {
t.Errorf("instance id = %q, want i-xyz", id)
}
// config.yaml must appear as a base64-encoded entry
if gotBody.ConfigFiles == nil {
t.Fatal("ConfigFiles is nil, expected at least config.yaml")
}
if _, ok := gotBody.ConfigFiles["config.yaml"]; !ok {
t.Errorf("ConfigFiles missing config.yaml; got: %v", gotBody.ConfigFiles)
}
}
// TestStart_Non201ReturnsStructuredError — when CP returns 401 with a
// structured {"error":"..."} body, Start surfaces that error message.
// Verifies the defense against log-leaking raw upstream bodies.