From 8bcb3f0cab18b52157d91d838fd2c4da19715e99 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 8 Sep 2026 16:33:44 +0500 Subject: [PATCH 1/3] test(placement): execute the Starlark scriptlet against a mock Incus cluster String asserts cannot prove packing. Run the rendered script with Incus call shapes so an 8GiB worker still fails on four hosts each holding two 4GiB jobs and lands on the empty member of a packed occupancy. Signed-off-by: rldyourmnd --- go.mod | 1 + go.sum | 2 + internal/incusplacement/execute.go | 266 ++++++++++++++++++++++++ internal/incusplacement/execute_test.go | 163 +++++++++++++++ 4 files changed, 432 insertions(+) create mode 100644 internal/incusplacement/execute.go create mode 100644 internal/incusplacement/execute_test.go diff --git a/go.mod b/go.mod index 10cd742e..dbe041db 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.46.0 go.opentelemetry.io/otel/sdk v1.46.0 go.opentelemetry.io/otel/trace v1.46.0 + go.starlark.net v0.0.0-20260708150628-5395d018f003 golang.org/x/crypto v0.55.0 golang.org/x/sys v0.47.0 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 14fea433..86d9631b 100644 --- a/go.sum +++ b/go.sum @@ -249,6 +249,8 @@ go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu6 go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI= go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= +go.starlark.net v0.0.0-20260708150628-5395d018f003 h1:cAxcqHgW8fnmT0cEBU3TzvVYHIFt8IIGDMWUF6rImk4= +go.starlark.net v0.0.0-20260708150628-5395d018f003/go.mod h1:Iue6g6iirlfLoVi/DYCi5/x0h/bAOuWF3dULTKpt2Vo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= diff --git a/internal/incusplacement/execute.go b/internal/incusplacement/execute.go new file mode 100644 index 00000000..494ff94e --- /dev/null +++ b/internal/incusplacement/execute.go @@ -0,0 +1,266 @@ +package incusplacement + +import ( + "errors" + "fmt" + "strconv" + "strings" + + "github.com/lxc/incus/v7/shared/api" + apiScriptlet "github.com/lxc/incus/v7/shared/api/scriptlet" + "github.com/lxc/incus/v7/shared/scriptlet" + "go.starlark.net/starlark" +) + +// The compile-time names Incus v6.0.6 / v7 InstancePlacementCompile injects. +// get_instances does not take pending; get_instances_count does. +var placementPredeclared = []string{ + "log_info", + "log_warn", + "log_error", + "set_target", + "get_cluster_member_resources", + "get_cluster_member_state", + "get_instance_resources", + "get_instances", + "get_instances_count", + "get_cluster_members", + "get_project", +} + +// ClusterSnapshot is the Incus view the scriptlet reads. MemoryTotalBytes is +// RAM only; swap is not a schedulable placement input. +type ClusterSnapshot struct { + PoolName string + Members []MemberSnapshot +} + +type MemberSnapshot struct { + Name string + Config map[string]string + MemoryTotalBytes uint64 + CPUTotal uint64 + LoadAverage float64 + PoolTotalBytes uint64 + PoolUsedBytes uint64 + Instances []InstanceSnapshot + PendingCount int +} + +type InstanceSnapshot struct { + Name string + MemoryLimitMiB int +} + +type PlacementRequest struct { + Project string + Name string + MemorySize uint64 + RootDiskSize uint64 +} + +type PlacementOutcome struct { + Target string + Failed bool + FailMessage string + Logs []string +} + +// Execute compiles and runs a rendered placement scriptlet against a mocked +// Incus cluster. Builtins match Incus instance-placement signatures so a +// strings.Contains test cannot hide a live call-shape or packing bug. +func Execute(script string, request PlacementRequest, cluster ClusterSnapshot) (PlacementOutcome, error) { + membersByName := make(map[string]MemberSnapshot, len(cluster.Members)) + candidates := make([]*api.ClusterMember, 0, len(cluster.Members)) + for _, member := range cluster.Members { + if member.Name == "" { + return PlacementOutcome{}, fmt.Errorf("cluster snapshot member is missing a name") + } + if _, duplicate := membersByName[member.Name]; duplicate { + return PlacementOutcome{}, fmt.Errorf("cluster snapshot repeats member %q", member.Name) + } + membersByName[member.Name] = member + config := api.ConfigMap{} + for key, value := range member.Config { + config[key] = value + } + candidates = append(candidates, &api.ClusterMember{ + ServerName: member.Name, + ClusterMemberPut: api.ClusterMemberPut{Config: config}, + }) + } + + var outcome PlacementOutcome + setTarget := func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var memberName string + if err := starlark.UnpackArgs(b.Name(), args, kwargs, "member_name", &memberName); err != nil { + return nil, err + } + if _, found := membersByName[memberName]; !found { + return nil, fmt.Errorf("Invalid member name: %s", memberName) + } + outcome.Target = memberName + return starlark.None, nil + } + getResources := func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var memberName string + if err := starlark.UnpackArgs(b.Name(), args, kwargs, "member_name", &memberName); err != nil { + return nil, err + } + member, err := lookupMember(membersByName, memberName) + if err != nil { + return nil, err + } + return scriptlet.StarlarkMarshal(api.Resources{ + CPU: api.ResourcesCPU{Total: member.CPUTotal}, + Memory: api.ResourcesMemory{Total: member.MemoryTotalBytes}, + }) + } + getState := func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var memberName string + if err := starlark.UnpackArgs(b.Name(), args, kwargs, "member_name", &memberName); err != nil { + return nil, err + } + member, err := lookupMember(membersByName, memberName) + if err != nil { + return nil, err + } + poolName := cluster.PoolName + if poolName == "" { + poolName = "gha-lvm" + } + return scriptlet.StarlarkMarshal(api.ClusterMemberState{ + SysInfo: api.ClusterMemberSysInfo{LoadAverages: []float64{member.LoadAverage}}, + StoragePools: map[string]api.StoragePoolState{ + poolName: {ResourcesStoragePool: api.ResourcesStoragePool{ + Space: api.ResourcesStoragePoolSpace{ + Used: member.PoolUsedBytes, Total: member.PoolTotalBytes, + }, + }}, + }, + }) + } + getInstanceResources := func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + if err := starlark.UnpackArgs(b.Name(), args, kwargs); err != nil { + return nil, err + } + return scriptlet.StarlarkMarshal(apiScriptlet.InstanceResources{ + MemorySize: request.MemorySize, RootDiskSize: request.RootDiskSize, + }) + } + getInstances := func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var project, location string + if err := starlark.UnpackArgs(b.Name(), args, kwargs, "project??", &project, "location??", &location); err != nil { + return nil, err + } + member, err := lookupMember(membersByName, location) + if err != nil { + return nil, err + } + instances := make([]api.Instance, 0, len(member.Instances)) + for _, instance := range member.Instances { + instances = append(instances, api.Instance{ + Name: instance.Name, Project: project, Location: location, + ExpandedConfig: api.ConfigMap{ + "limits.memory": strconv.Itoa(instance.MemoryLimitMiB) + "MiB", + }, + }) + } + return scriptlet.StarlarkMarshal(instances) + } + getInstancesCount := func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var project, location string + var includePending bool + if err := starlark.UnpackArgs( + b.Name(), args, kwargs, "project??", &project, "location??", &location, "pending??", &includePending, + ); err != nil { + return nil, err + } + member, err := lookupMember(membersByName, location) + if err != nil { + return nil, err + } + count := len(member.Instances) + if includePending { + count = member.PendingCount + } + return scriptlet.StarlarkMarshal(count) + } + logger := func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) { + var builder strings.Builder + for _, arg := range args { + text, err := strconv.Unquote(arg.String()) + if err != nil { + text = arg.String() + } + builder.WriteString(text) + } + outcome.Logs = append(outcome.Logs, b.Name()+": "+builder.String()) + return starlark.None, nil + } + unsupported := func(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + return nil, fmt.Errorf("%s is not part of the fleet placement harness", b.Name()) + } + + program, err := scriptlet.Compile("instance_placement", script, placementPredeclared) + if err != nil { + return PlacementOutcome{}, err + } + thread := &starlark.Thread{Name: "instance_placement"} + globals, err := program.Init(thread, starlark.StringDict{ + "log_info": starlark.NewBuiltin("log_info", logger), + "log_warn": starlark.NewBuiltin("log_warn", logger), + "log_error": starlark.NewBuiltin("log_error", logger), + "set_target": starlark.NewBuiltin("set_target", setTarget), + "get_cluster_member_resources": starlark.NewBuiltin("get_cluster_member_resources", getResources), + "get_cluster_member_state": starlark.NewBuiltin("get_cluster_member_state", getState), + "get_instance_resources": starlark.NewBuiltin("get_instance_resources", getInstanceResources), + "get_instances": starlark.NewBuiltin("get_instances", getInstances), + "get_instances_count": starlark.NewBuiltin("get_instances_count", getInstancesCount), + "get_cluster_members": starlark.NewBuiltin("get_cluster_members", unsupported), + "get_project": starlark.NewBuiltin("get_project", unsupported), + }) + if err != nil { + return PlacementOutcome{}, fmt.Errorf("Failed initializing: %w", err) + } + placement := globals["instance_placement"] + if placement == nil { + return PlacementOutcome{}, errors.New("Scriptlet missing instance_placement function") + } + requestValue, err := scriptlet.StarlarkMarshal(apiScriptlet.InstancePlacement{ + InstancesPost: api.InstancesPost{Name: request.Name}, + Project: request.Project, + }) + if err != nil { + return PlacementOutcome{}, err + } + candidatesValue, err := scriptlet.StarlarkMarshal(candidates) + if err != nil { + return PlacementOutcome{}, err + } + returned, err := starlark.Call(thread, placement, nil, []starlark.Tuple{ + {starlark.String("request"), requestValue}, + {starlark.String("candidate_members"), candidatesValue}, + }) + if err != nil { + var evalErr *starlark.EvalError + if errors.As(err, &evalErr) { + outcome.Failed = true + outcome.FailMessage = evalErr.Error() + return outcome, nil + } + return outcome, err + } + if returned.Type() != "NoneType" { + return PlacementOutcome{}, fmt.Errorf("Failed with unexpected return value: %v", returned) + } + return outcome, nil +} + +func lookupMember(members map[string]MemberSnapshot, name string) (MemberSnapshot, error) { + member, found := members[name] + if !found { + return MemberSnapshot{}, fmt.Errorf("Invalid member name: %s", name) + } + return member, nil +} diff --git a/internal/incusplacement/execute_test.go b/internal/incusplacement/execute_test.go new file mode 100644 index 00000000..acdc1105 --- /dev/null +++ b/internal/incusplacement/execute_test.go @@ -0,0 +1,163 @@ +package incusplacement + +import ( + "fmt" + "strings" + "testing" + + "github.com/NDDev-OpenNetwork/github-actions/internal/config" +) + +const ( + hostRAMBytes = 16 * 1024 * 1024 * 1024 + eightGiBBytes = 8192 * 1024 * 1024 + rootDiskBytes = 50 * 1024 * 1024 * 1024 + poolBytes = 200 * 1024 * 1024 * 1024 +) + +func TestRenderedPlacementCompilesAgainstIncusBuiltinNames(t *testing.T) { + script := renderExample(t) + outcome, err := Execute(script, eightGiBRequest(), fourEmptyMembers()) + if err != nil { + t.Fatal(err) + } + if outcome.Failed || outcome.Target != "gha-runner-1" { + t.Fatalf("empty cluster refused an 8GiB worker: %#v", outcome) + } +} + +func TestSpreadFourGiBWorkersLeaveNoEightGiBSlot(t *testing.T) { + script := renderExample(t) + // Four hosts, each holding 8 GiB of 4 GiB workers. Packing onto remaining + // RAM is the live policy; spreading that occupancy is the counterexample + // that used to starve Almaty 8 GiB jobs. + outcome, err := Execute(script, eightGiBRequest(), membersWithFourGiBOccupancy([]int{8192, 8192, 8192, 8192})) + if err != nil { + t.Fatal(err) + } + if !outcome.Failed || !strings.Contains(outcome.FailMessage, "insufficient-memory") { + t.Fatalf("spread 4GiB occupancy still placed an 8GiB worker: %#v", outcome) + } +} + +func TestPackedFourGiBWorkersLeaveAnEightGiBSlot(t *testing.T) { + script := renderExample(t) + outcome, err := Execute(script, eightGiBRequest(), membersWithFourGiBOccupancy([]int{12288, 12288, 8192, 0})) + if err != nil { + t.Fatal(err) + } + if outcome.Failed || outcome.Target != "gha-runner-4" { + t.Fatalf("packed occupancy did not land the 8GiB worker on the empty member: %#v", outcome) + } +} + +func TestSysinfoSwapDoesNotBecomeSchedulableRAM(t *testing.T) { + script := renderExample(t) + cluster := membersWithFourGiBOccupancy([]int{8192, 8192, 8192, 8192}) + for i := range cluster.Members { + cluster.Members[i].LoadAverage = 0 + } + outcome, err := Execute(script, eightGiBRequest(), cluster) + if err != nil { + t.Fatal(err) + } + if !outcome.Failed { + t.Fatalf("placement treated something other than resources.memory.total as RAM: %#v", outcome) + } +} + +func TestMaintenancePlacesOnClosedEmptyMember(t *testing.T) { + script := renderExample(t) + cluster := fourEmptyMembers() + for i := range cluster.Members { + cluster.Members[i].Config["user.gha_pressure.state"] = "closed" + } + job, err := Execute(script, eightGiBRequest(), cluster) + if err != nil { + t.Fatal(err) + } + if !job.Failed { + t.Fatalf("closed gate still placed a job worker: %#v", job) + } + build, err := Execute(script, PlacementRequest{ + Project: "gha-fleet", Name: "gha-image-builder-stage", + MemorySize: eightGiBBytes, RootDiskSize: rootDiskBytes, + }, cluster) + if err != nil { + t.Fatal(err) + } + if build.Failed || build.Target == "" { + t.Fatalf("maintenance placement did not use the drained closed member: %#v", build) + } +} + +func TestWrongProjectIsANoOp(t *testing.T) { + script := renderExample(t) + outcome, err := Execute(script, PlacementRequest{ + Project: "not-fleet", Name: "worker-1", + MemorySize: eightGiBBytes, RootDiskSize: rootDiskBytes, + }, fourEmptyMembers()) + if err != nil { + t.Fatal(err) + } + if outcome.Failed || outcome.Target != "" { + t.Fatalf("foreign project must return without set_target: %#v", outcome) + } +} + +func renderExample(t *testing.T) string { + t.Helper() + cfg, err := config.Load("../../config/example-runner-1.yaml") + if err != nil { + t.Fatal(err) + } + script, err := Render(cfg) + if err != nil { + t.Fatal(err) + } + return script +} + +func eightGiBRequest() PlacementRequest { + return PlacementRequest{ + Project: "gha-fleet", Name: "gha-job-almaty", + MemorySize: eightGiBBytes, RootDiskSize: rootDiskBytes, + } +} + +func fourEmptyMembers() ClusterSnapshot { + return membersWithFourGiBOccupancy([]int{0, 0, 0, 0}) +} + +func membersWithFourGiBOccupancy(committedMiB []int) ClusterSnapshot { + members := make([]MemberSnapshot, len(committedMiB)) + for index, committed := range committedMiB { + if committed%4096 != 0 { + panic("occupancy must be a multiple of the 4GiB worker class") + } + count := committed / 4096 + instances := make([]InstanceSnapshot, 0, count) + for n := 0; n < count; n++ { + instances = append(instances, InstanceSnapshot{ + Name: fmt.Sprintf("gha-worker-%d-%d", index+1, n+1), MemoryLimitMiB: 4096, + }) + } + members[index] = MemberSnapshot{ + Name: fmt.Sprintf("gha-runner-%d", index+1), + Config: pressureOpen(), + MemoryTotalBytes: hostRAMBytes, + CPUTotal: 8, + PoolTotalBytes: poolBytes, + Instances: instances, + PendingCount: count, + } + } + return ClusterSnapshot{PoolName: "gha-lvm", Members: members} +} + +func pressureOpen() map[string]string { + return map[string]string{ + "user.gha_pressure.schema": "1", + "user.gha_pressure.state": "open", + } +} From e6f0554abc9b31c5ea1a82dd4f97d6bd38bd2549 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 8 Sep 2026 17:20:16 +0500 Subject: [PATCH 2/3] test(placement): isolate Starlark harness from provider go.mod Execute the rendered scriptlet in a nested module so packing tests keep the real Incus interpreter without invalidating the pinned provider derivative. Signed-off-by: rldyourmnd Co-authored-by: Cursor --- .github/workflows/ci.yml | 1 + go.mod | 1 - go.sum | 2 - internal/incusplacement/race_off.go | 5 ++ internal/incusplacement/race_on.go | 5 ++ .../{ => starlarkexec}/execute.go | 4 +- .../{ => starlarkexec}/execute_test.go | 13 ++++-- internal/incusplacement/starlarkexec/go.mod | 19 ++++++++ internal/incusplacement/starlarkexec/go.sum | 20 ++++++++ .../incusplacement/starlarkexec_hook_test.go | 46 +++++++++++++++++++ .../derivative_boundaries_test.go | 20 ++++++++ 11 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 internal/incusplacement/race_off.go create mode 100644 internal/incusplacement/race_on.go rename internal/incusplacement/{ => starlarkexec}/execute.go (98%) rename internal/incusplacement/{ => starlarkexec}/execute_test.go (92%) create mode 100644 internal/incusplacement/starlarkexec/go.mod create mode 100644 internal/incusplacement/starlarkexec/go.sum create mode 100644 internal/incusplacement/starlarkexec_hook_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26387899..a155bdd5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,6 +138,7 @@ jobs: cache: true - run: test -z "$(gofmt -l cmd internal third_party)" - run: go vet ./... + - run: go vet -C internal/incusplacement/starlarkexec ./... - run: go build -trimpath ./... derivative: diff --git a/go.mod b/go.mod index dbe041db..10cd742e 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,6 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.46.0 go.opentelemetry.io/otel/sdk v1.46.0 go.opentelemetry.io/otel/trace v1.46.0 - go.starlark.net v0.0.0-20260708150628-5395d018f003 golang.org/x/crypto v0.55.0 golang.org/x/sys v0.47.0 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 86d9631b..14fea433 100644 --- a/go.sum +++ b/go.sum @@ -249,8 +249,6 @@ go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu6 go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI= go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= -go.starlark.net v0.0.0-20260708150628-5395d018f003 h1:cAxcqHgW8fnmT0cEBU3TzvVYHIFt8IIGDMWUF6rImk4= -go.starlark.net v0.0.0-20260708150628-5395d018f003/go.mod h1:Iue6g6iirlfLoVi/DYCi5/x0h/bAOuWF3dULTKpt2Vo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= diff --git a/internal/incusplacement/race_off.go b/internal/incusplacement/race_off.go new file mode 100644 index 00000000..f16a8aab --- /dev/null +++ b/internal/incusplacement/race_off.go @@ -0,0 +1,5 @@ +//go:build !race + +package incusplacement + +const raceEnabled = false diff --git a/internal/incusplacement/race_on.go b/internal/incusplacement/race_on.go new file mode 100644 index 00000000..316879fc --- /dev/null +++ b/internal/incusplacement/race_on.go @@ -0,0 +1,5 @@ +//go:build race + +package incusplacement + +const raceEnabled = true diff --git a/internal/incusplacement/execute.go b/internal/incusplacement/starlarkexec/execute.go similarity index 98% rename from internal/incusplacement/execute.go rename to internal/incusplacement/starlarkexec/execute.go index 494ff94e..567de25a 100644 --- a/internal/incusplacement/execute.go +++ b/internal/incusplacement/starlarkexec/execute.go @@ -1,4 +1,6 @@ -package incusplacement +// Nested module so packing tests can import go.starlark.net without +// changing the root go.mod that pins the provider derivative. +package starlarkexec import ( "errors" diff --git a/internal/incusplacement/execute_test.go b/internal/incusplacement/starlarkexec/execute_test.go similarity index 92% rename from internal/incusplacement/execute_test.go rename to internal/incusplacement/starlarkexec/execute_test.go index acdc1105..d9fb362d 100644 --- a/internal/incusplacement/execute_test.go +++ b/internal/incusplacement/starlarkexec/execute_test.go @@ -1,11 +1,14 @@ -package incusplacement +package starlarkexec import ( "fmt" + "path/filepath" + "runtime" "strings" "testing" "github.com/NDDev-OpenNetwork/github-actions/internal/config" + "github.com/NDDev-OpenNetwork/github-actions/internal/incusplacement" ) const ( @@ -107,11 +110,15 @@ func TestWrongProjectIsANoOp(t *testing.T) { func renderExample(t *testing.T) string { t.Helper() - cfg, err := config.Load("../../config/example-runner-1.yaml") + _, current, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("runtime.Caller failed") + } + cfg, err := config.Load(filepath.Join(filepath.Dir(current), "../../../config/example-runner-1.yaml")) if err != nil { t.Fatal(err) } - script, err := Render(cfg) + script, err := incusplacement.Render(cfg) if err != nil { t.Fatal(err) } diff --git a/internal/incusplacement/starlarkexec/go.mod b/internal/incusplacement/starlarkexec/go.mod new file mode 100644 index 00000000..bc35aaea --- /dev/null +++ b/internal/incusplacement/starlarkexec/go.mod @@ -0,0 +1,19 @@ +// Nested module: packing tests import go.starlark.net here so the root +// go.mod/go.sum stay identical to the pinned provider derivative. +module github.com/NDDev-OpenNetwork/github-actions/internal/incusplacement/starlarkexec + +go 1.26.7 + +require ( + github.com/NDDev-OpenNetwork/github-actions v0.0.0 + github.com/lxc/incus/v7 v7.4.0 + go.starlark.net v0.0.0-20260708150628-5395d018f003 +) + +require ( + go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect + golang.org/x/sys v0.47.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/NDDev-OpenNetwork/github-actions => ../../.. diff --git a/internal/incusplacement/starlarkexec/go.sum b/internal/incusplacement/starlarkexec/go.sum new file mode 100644 index 00000000..3d82257e --- /dev/null +++ b/internal/incusplacement/starlarkexec/go.sum @@ -0,0 +1,20 @@ +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/lxc/incus/v7 v7.4.0 h1:gZPEQvFVOeeSVhsnZVObcrSCLVfnowBP3VM8Fr5QaAQ= +github.com/lxc/incus/v7 v7.4.0/go.mod h1:drJHddxdr1men9MS1VYrdjDWBeiI92yL//Aq4oAIPCY= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +go.starlark.net v0.0.0-20260708150628-5395d018f003 h1:cAxcqHgW8fnmT0cEBU3TzvVYHIFt8IIGDMWUF6rImk4= +go.starlark.net v0.0.0-20260708150628-5395d018f003/go.mod h1:Iue6g6iirlfLoVi/DYCi5/x0h/bAOuWF3dULTKpt2Vo= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4= +go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/incusplacement/starlarkexec_hook_test.go b/internal/incusplacement/starlarkexec_hook_test.go new file mode 100644 index 00000000..57fd2b06 --- /dev/null +++ b/internal/incusplacement/starlarkexec_hook_test.go @@ -0,0 +1,46 @@ +package incusplacement + +import ( + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" +) + +// TestStarlarkExecutionHarness runs the nested module. `go test ./...` from +// the repository root skips nested modules, and that skip is what would let a +// packing regression land without executing Starlark. +func TestStarlarkExecutionHarness(t *testing.T) { + _, file, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("runtime.Caller failed") + } + dir := filepath.Join(filepath.Dir(file), "starlarkexec") + args := []string{"test", "-count=1", "."} + if raceEnabled { + args = []string{"test", "-race", "-count=1", "."} + } + cmd := exec.Command("go", args...) + cmd.Dir = dir + cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local") + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("starlarkexec tests: %v\n%s", err, output) + } +} + +func TestStarlarkExecutionHarnessVet(t *testing.T) { + _, file, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("runtime.Caller failed") + } + dir := filepath.Join(filepath.Dir(file), "starlarkexec") + cmd := exec.Command("go", "vet", ".") + cmd.Dir = dir + cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local") + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("starlarkexec vet: %v\n%s", err, output) + } +} diff --git a/internal/repositorycontract/derivative_boundaries_test.go b/internal/repositorycontract/derivative_boundaries_test.go index 88cefce0..db0ebbe0 100644 --- a/internal/repositorycontract/derivative_boundaries_test.go +++ b/internal/repositorycontract/derivative_boundaries_test.go @@ -65,3 +65,23 @@ func TestWorkflowResolvesTheProviderBoundaryItEnforces(t *testing.T) { t.Fatal("observer-only providerretry package still invalidates the external provider artifact") } } + +func TestRootModuleDoesNotRequireStarlark(t *testing.T) { + content, err := os.ReadFile(filepath.Join(repositoryRoot, "go.mod")) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(content), "go.starlark.net") { + t.Fatal("root go.mod requires go.starlark.net; that is provider-derivative drift") + } +} + +func TestWorkflowVetsTheNestedStarlarkHarness(t *testing.T) { + content, err := os.ReadFile(filepath.Join(repositoryRoot, ".github", "workflows", "ci.yml")) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(content), "go vet -C internal/incusplacement/starlarkexec") { + t.Fatal("static CI no longer vets the nested Starlark placement harness") + } +} From d9ac66ca198429d5509e020a9f07ee2b62836c1e Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 8 Sep 2026 17:46:49 +0500 Subject: [PATCH 3/3] test(placement): treat pending creates as max-worker occupancy get_instances_count(..., true) is running instances plus InstanceCreate operations, not extras-only. A create with no record must still pack. Signed-off-by: rldyourmnd Co-authored-by: Cursor --- internal/incusplacement/starlarkexec/execute.go | 5 ++++- .../incusplacement/starlarkexec/execute_test.go | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/internal/incusplacement/starlarkexec/execute.go b/internal/incusplacement/starlarkexec/execute.go index 567de25a..4980a59b 100644 --- a/internal/incusplacement/starlarkexec/execute.go +++ b/internal/incusplacement/starlarkexec/execute.go @@ -46,7 +46,10 @@ type MemberSnapshot struct { PoolTotalBytes uint64 PoolUsedBytes uint64 Instances []InstanceSnapshot - PendingCount int + // PendingCount is get_instances_count(project, location, true): current + // instances plus InstanceCreate operations. It is not extras-only; the + // script subtracts len(instances) to size creates that have no record yet. + PendingCount int } type InstanceSnapshot struct { diff --git a/internal/incusplacement/starlarkexec/execute_test.go b/internal/incusplacement/starlarkexec/execute_test.go index d9fb362d..fae5e060 100644 --- a/internal/incusplacement/starlarkexec/execute_test.go +++ b/internal/incusplacement/starlarkexec/execute_test.go @@ -94,6 +94,19 @@ func TestMaintenancePlacesOnClosedEmptyMember(t *testing.T) { } } +func TestPendingCreateWithoutRecordReservesMaxWorker(t *testing.T) { + script := renderExample(t) + cluster := fourEmptyMembers() + cluster.Members[0].PendingCount = 1 + outcome, err := Execute(script, eightGiBRequest(), cluster) + if err != nil { + t.Fatal(err) + } + if outcome.Failed || outcome.Target == "gha-runner-1" { + t.Fatalf("pending create on runner-1 still took the 8GiB worker: %#v", outcome) + } +} + func TestWrongProjectIsANoOp(t *testing.T) { script := renderExample(t) outcome, err := Execute(script, PlacementRequest{