Skip to content
Open
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
156 changes: 155 additions & 1 deletion cmd/ateapi/internal/apivalidation/zz_generated.validation.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 25 additions & 3 deletions cmd/ateapi/internal/controlapi/actor.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,21 @@ func (s *ServiceImpl) CreateActor(ctx context.Context, inActor *ateapipb.Actor)

// Resolve the explicit tag, or freeze the template's current golden default.
tagRef := inActor.GetSourceTag()
if tagRef == nil {
tagRef = template.GetStatus().GetGoldenSnapshotStatus().GetGoldenTag()
} else {
switch {
case tagRef != nil:
for _, volume := range template.GetVolumes() {
if volume.GetExternalVolumeTemplate() != nil {
// TODO: Permit cloning after CSI volume snapshots are supported.
return nil, apierror.FailedPrecondition("Tag cloning does not support ActorTemplates with external volumes")
}
if volume.GetExternalVolumeRef() != nil {
return nil, apierror.FailedPrecondition("Tag cloning does not support ActorTemplates that reference an ExternalVolume")
}
}
case templateHasExternalVolumeRef(template):
// TODO(shared-volumes): restore from the golden once checkpoint and restore mount options agree for external volume refs; see docs/csi-volumes.md.
default:
tagRef = template.GetStatus().GetGoldenSnapshotStatus().GetGoldenTag()
}
var sourceTag *ateapipb.Tag
if tagRef != nil {
Expand All @@ -120,6 +126,13 @@ func (s *ServiceImpl) CreateActor(ctx context.Context, inActor *ateapipb.Actor)
if err != nil {
return nil, err
}
if !isGoldenActor(inActor) {
refVols, err := resolveExternalVolumeRefs(ctx, s.store, inActor, template)
if err != nil {
return nil, err
}
initVols = append(initVols, refVols...)
}

// Verify that the result is properly valid before storing it.
outActor := proto.CloneOf(inActor)
Expand Down Expand Up @@ -158,13 +171,22 @@ func (s *ServiceImpl) CreateActor(ctx context.Context, inActor *ateapipb.Actor)
return nil, fmt.Errorf("while recording actor: %w", err)
}

// Resume re-takes any reference missed here, so this does not fail the create.
if err := bindExternalVolumes(ctx, s.store, stored); err != nil {
slog.WarnContext(ctx, "Failed to bind external volumes to new actor", slog.Any("actor", resources.ActorRefFromActor(stored)), slog.Any("err", err))
}

// Without this an actor that is created and never resumed has no record at
// all, at any retention.
logActorStateChanged(ctx, stored, ateattr.OperationCreate)

return stored, nil
}

func isGoldenActor(actor *ateapipb.Actor) bool {
return actor.GetMetadata().GetAtespace() == resources.GoldenActorAtespace
}

// resolveTagSource resolves a CreateActor request's source tag
// and checks that the tag is usable for creating an Actor in actorAtespace
// from template.
Expand Down
45 changes: 45 additions & 0 deletions cmd/ateapi/internal/controlapi/external_volume_golden_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package controlapi

import (
"testing"

"github.com/agent-substrate/substrate/pkg/proto/ateapipb"
)

func TestTemplateHasExternalVolumeRef(t *testing.T) {
tests := []struct {
name string
volumes []*ateapipb.Volume
want bool
}{
{name: "no volumes"},
{name: "durable dir only", volumes: []*ateapipb.Volume{{Name: "home", DurableDir: &ateapipb.DurableDirVolumeSource{}}}},
{name: "external volume template", volumes: []*ateapipb.Volume{{Name: "owned", ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{StorageClassName: "standard"}}}},
{name: "named ref", volumes: []*ateapipb.Volume{{Name: "shared", ExternalVolumeRef: &ateapipb.ExternalVolumeRef{Name: "ctx"}}}, want: true},
{name: "unnamed ref after other volumes", volumes: []*ateapipb.Volume{
{Name: "home", DurableDir: &ateapipb.DurableDirVolumeSource{}},
{Name: "shared", ExternalVolumeRef: &ateapipb.ExternalVolumeRef{}},
}, want: true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := templateHasExternalVolumeRef(&ateapipb.ActorTemplate{Volumes: tc.volumes}); got != tc.want {
t.Errorf("templateHasExternalVolumeRef() = %v, want %v", got, tc.want)
}
})
}
}
Loading
Loading