diff --git a/cmd/compiler/compile.go b/cmd/compiler/compile.go index de5cb96..1df5968 100644 --- a/cmd/compiler/compile.go +++ b/cmd/compiler/compile.go @@ -28,6 +28,7 @@ import ( dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" platformv1alpha1 "github.com/ontai-dev/platform/api/v1alpha1" + seamv1alpha1 "github.com/ontai-dev/seam/api/v1alpha1" ) // RegistryMirror configures one registry mirror entry in Talos machine config @@ -1021,10 +1022,44 @@ func compileBootstrap(input, output, kubeconfigPath, talosconfigPath string) err return fmt.Errorf("write TalosCluster CR: %w", err) } + // Produce OperatorContext CR in ont-system on the management cluster. + // Scoped to this cluster. Default autonomyLevel=observe-only so the admin + // must explicitly promote autonomy after validating the cluster. GAP-B1. + ocName := "ctx-" + in.Name + oc := buildOperatorContextCR(in.Name, ocName) + if err := writeCRYAML(output, ocName, oc); err != nil { + return fmt.Errorf("write OperatorContext CR: %w", err) + } + // Produce bootstrap-sequence.yaml documenting the apply order. return writeBootstrapSequence(output, in.Name, allResources, tcMode) } +// buildOperatorContextCR builds an OperatorContext CR scoped to clusterName with +// observe-only autonomy. Applied to ont-system on the management cluster alongside +// the TalosCluster CR. Admin must promote autonomyLevel after cluster validation. +// GAP-B1: compiler must emit OperatorContext so conductor has governance input on +// first boot instead of defaulting to full-delegation. conductor-schema.md ยง7. +func buildOperatorContextCR(clusterName, crName string) seamv1alpha1.OperatorContext { + return seamv1alpha1.OperatorContext{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "seam.ontai.dev/v1alpha1", + Kind: "OperatorContext", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: crName, + Namespace: "ont-system", + }, + Spec: seamv1alpha1.OperatorContextSpec{ + Scope: seamv1alpha1.OperatorContextScope{ + ClusterRefs: []string{clusterName}, + }, + Mode: seamv1alpha1.OperatorContextModeNormal, + AutonomyLevel: seamv1alpha1.AutonomyLevelObserveOnly, + }, + } +} + // buildMachineConfigCR converts a generated Talos machine config YAML into a // MachineConfig CR. The machine and cluster top-level sections are stored as // unstructured JSON in spec.machine and spec.cluster respectively so the CR diff --git a/cmd/compiler/compile_bootstrap_test.go b/cmd/compiler/compile_bootstrap_test.go index 618fd2c..c7a984a 100644 --- a/cmd/compiler/compile_bootstrap_test.go +++ b/cmd/compiler/compile_bootstrap_test.go @@ -52,13 +52,14 @@ func TestBootstrap_ProducesExpectedOutputFiles(t *testing.T) { t.Fatalf("compileBootstrap error: %v", err) } - // Expect: namespace manifest + 3 MachineConfig CRs + TalosCluster CR + bootstrap-sequence. + // Expect: namespace manifest + 3 MachineConfig CRs + TalosCluster CR + OperatorContext CR + bootstrap-sequence. expectedFiles := []string{ "seam-tenant-namespace.yaml", "seam-mc-ccs-mgmt-node1.yaml", "seam-mc-ccs-mgmt-node2.yaml", "seam-mc-ccs-mgmt-node3.yaml", "ccs-mgmt.yaml", + "ctx-ccs-mgmt.yaml", "bootstrap-sequence.yaml", } for _, name := range expectedFiles { @@ -378,6 +379,51 @@ func TestBootstrap_CAPIDisabled_NoCapiBlockInCR(t *testing.T) { } } +// TestBootstrap_EmitsOperatorContextCR verifies that compileBootstrap emits an +// OperatorContext CR (ctx-{cluster}.yaml) in ont-system with autonomyLevel=observe-only +// and scope.clusterRefs targeting the cluster. GAP-B1. +func TestBootstrap_EmitsOperatorContextCR(t *testing.T) { + outDir := t.TempDir() + inputPath := writeInputFile(t, bootstrapInputYAML) + + if err := compileBootstrap(inputPath, outDir, "", ""); err != nil { + t.Fatalf("compileBootstrap error: %v", err) + } + + data, err := os.ReadFile(filepath.Join(outDir, "ctx-ccs-mgmt.yaml")) + if err != nil { + t.Fatalf("OperatorContext CR ctx-ccs-mgmt.yaml not found: %v", err) + } + content := string(data) + + assertContainsStr(t, content, "apiVersion: seam.ontai.dev/v1alpha1") + assertContainsStr(t, content, "kind: OperatorContext") + assertContainsStr(t, content, "name: ctx-ccs-mgmt") + assertContainsStr(t, content, "namespace: ont-system") + assertContainsStr(t, content, "autonomyLevel: observe-only") + assertContainsStr(t, content, "ccs-mgmt") // clusterRefs entry + + var oc map[string]interface{} + if err := yaml.Unmarshal(data, &oc); err != nil { + t.Fatalf("parse OperatorContext YAML: %v", err) + } + spec, _ := oc["spec"].(map[string]interface{}) + if spec == nil { + t.Fatal("OperatorContext missing spec") + } + if got, _ := spec["autonomyLevel"].(string); got != "observe-only" { + t.Errorf("autonomyLevel = %q, want observe-only", got) + } + scope, _ := spec["scope"].(map[string]interface{}) + if scope == nil { + t.Fatal("OperatorContext missing spec.scope") + } + refs, _ := scope["clusterRefs"].([]interface{}) + if len(refs) != 1 || refs[0] != "ccs-mgmt" { + t.Errorf("spec.scope.clusterRefs = %v, want [ccs-mgmt]", refs) + } +} + // writeInputFile writes YAML content to a temp file and returns its path. func writeInputFile(t *testing.T, content string) string { t.Helper()