Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions cmd/compiler/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
48 changes: 47 additions & 1 deletion cmd/compiler/compile_bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down
Loading