Add UNSPECIFIED zero value for ActorSnapshotTagScope (#898)

Fixes #897


- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
This commit is contained in:
Luiz Oliveira
2026-08-13 16:50:21 -04:00
committed by GitHub
parent dc6aa247e2
commit 5fbfd774e3
7 changed files with 714 additions and 177 deletions
File diff suppressed because one or more lines are too long
+408 -38
View File
@@ -139,6 +139,51 @@ class ControlStub:
request_serializer=ateapi__pb2.DeleteAtespaceRequest.SerializeToString,
response_deserializer=ateapi__pb2.Atespace.FromString,
_registered_method=True)
self.CreateActorTemplate = channel.unary_unary(
'/ateapi.Control/CreateActorTemplate',
request_serializer=ateapi__pb2.CreateActorTemplateRequest.SerializeToString,
response_deserializer=ateapi__pb2.ActorTemplate.FromString,
_registered_method=True)
self.GetActorTemplate = channel.unary_unary(
'/ateapi.Control/GetActorTemplate',
request_serializer=ateapi__pb2.GetActorTemplateRequest.SerializeToString,
response_deserializer=ateapi__pb2.ActorTemplate.FromString,
_registered_method=True)
self.UpdateActorTemplate = channel.unary_unary(
'/ateapi.Control/UpdateActorTemplate',
request_serializer=ateapi__pb2.UpdateActorTemplateRequest.SerializeToString,
response_deserializer=ateapi__pb2.ActorTemplate.FromString,
_registered_method=True)
self.ListActorTemplates = channel.unary_unary(
'/ateapi.Control/ListActorTemplates',
request_serializer=ateapi__pb2.ListActorTemplatesRequest.SerializeToString,
response_deserializer=ateapi__pb2.ListActorTemplatesResponse.FromString,
_registered_method=True)
self.DeleteActorTemplate = channel.unary_unary(
'/ateapi.Control/DeleteActorTemplate',
request_serializer=ateapi__pb2.DeleteActorTemplateRequest.SerializeToString,
response_deserializer=ateapi__pb2.ActorTemplate.FromString,
_registered_method=True)
self.CreateActorTemplateVersion = channel.unary_unary(
'/ateapi.Control/CreateActorTemplateVersion',
request_serializer=ateapi__pb2.CreateActorTemplateVersionRequest.SerializeToString,
response_deserializer=ateapi__pb2.ActorTemplateVersion.FromString,
_registered_method=True)
self.GetActorTemplateVersion = channel.unary_unary(
'/ateapi.Control/GetActorTemplateVersion',
request_serializer=ateapi__pb2.GetActorTemplateVersionRequest.SerializeToString,
response_deserializer=ateapi__pb2.ActorTemplateVersion.FromString,
_registered_method=True)
self.ListActorTemplateVersions = channel.unary_unary(
'/ateapi.Control/ListActorTemplateVersions',
request_serializer=ateapi__pb2.ListActorTemplateVersionsRequest.SerializeToString,
response_deserializer=ateapi__pb2.ListActorTemplateVersionsResponse.FromString,
_registered_method=True)
self.DeleteActorTemplateVersion = channel.unary_unary(
'/ateapi.Control/DeleteActorTemplateVersion',
request_serializer=ateapi__pb2.DeleteActorTemplateVersionRequest.SerializeToString,
response_deserializer=ateapi__pb2.ActorTemplateVersion.FromString,
_registered_method=True)
class ControlServicer:
@@ -167,7 +212,9 @@ class ControlServicer:
raise NotImplementedError('Method not implemented!')
def SuspendActor(self, request, context):
"""Suspend a given actor to a new snapshot.
"""Suspend a given actor to a new snapshot. A running actor is checkpointed
on its worker; a paused actor's node-local snapshot is uploaded, narrowed
to the template's commit scope where required (Full capture, Data commit).
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
@@ -273,6 +320,72 @@ class ControlServicer:
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def CreateActorTemplate(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def GetActorTemplate(self, request, context):
"""Get an ActorTemplate by name.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def UpdateActorTemplate(self, request, context):
"""Update mutable fields on an existing ActorTemplate.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def ListActorTemplates(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def DeleteActorTemplate(self, request, context):
"""Delete an ActorTemplate. Rejects (FailedPrecondition) while any of its
ActorTemplateVersions exist.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def CreateActorTemplateVersion(self, request, context):
"""Create a new ActorTemplateVersion under an existing ActorTemplate
(FailedPrecondition if the parent does not exist).
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def GetActorTemplateVersion(self, request, context):
"""Get an ActorTemplateVersion by name.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def ListActorTemplateVersions(self, request, context):
"""List ActorTemplateVersions, optionally filtered to one ActorTemplate.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def DeleteActorTemplateVersion(self, request, context):
"""Delete an ActorTemplateVersion together with its golden actor and golden
snapshot in the reserved ate-golden atespace. Rejects (FailedPrecondition)
while the version is its parent's default_version_on_create or while any
Actor pins it.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_ControlServicer_to_server(servicer, server):
rpc_method_handlers = {
@@ -366,6 +479,51 @@ def add_ControlServicer_to_server(servicer, server):
request_deserializer=ateapi__pb2.DeleteAtespaceRequest.FromString,
response_serializer=ateapi__pb2.Atespace.SerializeToString,
),
'CreateActorTemplate': grpc.unary_unary_rpc_method_handler(
servicer.CreateActorTemplate,
request_deserializer=ateapi__pb2.CreateActorTemplateRequest.FromString,
response_serializer=ateapi__pb2.ActorTemplate.SerializeToString,
),
'GetActorTemplate': grpc.unary_unary_rpc_method_handler(
servicer.GetActorTemplate,
request_deserializer=ateapi__pb2.GetActorTemplateRequest.FromString,
response_serializer=ateapi__pb2.ActorTemplate.SerializeToString,
),
'UpdateActorTemplate': grpc.unary_unary_rpc_method_handler(
servicer.UpdateActorTemplate,
request_deserializer=ateapi__pb2.UpdateActorTemplateRequest.FromString,
response_serializer=ateapi__pb2.ActorTemplate.SerializeToString,
),
'ListActorTemplates': grpc.unary_unary_rpc_method_handler(
servicer.ListActorTemplates,
request_deserializer=ateapi__pb2.ListActorTemplatesRequest.FromString,
response_serializer=ateapi__pb2.ListActorTemplatesResponse.SerializeToString,
),
'DeleteActorTemplate': grpc.unary_unary_rpc_method_handler(
servicer.DeleteActorTemplate,
request_deserializer=ateapi__pb2.DeleteActorTemplateRequest.FromString,
response_serializer=ateapi__pb2.ActorTemplate.SerializeToString,
),
'CreateActorTemplateVersion': grpc.unary_unary_rpc_method_handler(
servicer.CreateActorTemplateVersion,
request_deserializer=ateapi__pb2.CreateActorTemplateVersionRequest.FromString,
response_serializer=ateapi__pb2.ActorTemplateVersion.SerializeToString,
),
'GetActorTemplateVersion': grpc.unary_unary_rpc_method_handler(
servicer.GetActorTemplateVersion,
request_deserializer=ateapi__pb2.GetActorTemplateVersionRequest.FromString,
response_serializer=ateapi__pb2.ActorTemplateVersion.SerializeToString,
),
'ListActorTemplateVersions': grpc.unary_unary_rpc_method_handler(
servicer.ListActorTemplateVersions,
request_deserializer=ateapi__pb2.ListActorTemplateVersionsRequest.FromString,
response_serializer=ateapi__pb2.ListActorTemplateVersionsResponse.SerializeToString,
),
'DeleteActorTemplateVersion': grpc.unary_unary_rpc_method_handler(
servicer.DeleteActorTemplateVersion,
request_deserializer=ateapi__pb2.DeleteActorTemplateVersionRequest.FromString,
response_serializer=ateapi__pb2.ActorTemplateVersion.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'ateapi.Control', rpc_method_handlers)
@@ -864,6 +1022,249 @@ class Control:
metadata,
_registered_method=True)
@staticmethod
def CreateActorTemplate(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/ateapi.Control/CreateActorTemplate',
ateapi__pb2.CreateActorTemplateRequest.SerializeToString,
ateapi__pb2.ActorTemplate.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def GetActorTemplate(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/ateapi.Control/GetActorTemplate',
ateapi__pb2.GetActorTemplateRequest.SerializeToString,
ateapi__pb2.ActorTemplate.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def UpdateActorTemplate(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/ateapi.Control/UpdateActorTemplate',
ateapi__pb2.UpdateActorTemplateRequest.SerializeToString,
ateapi__pb2.ActorTemplate.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def ListActorTemplates(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/ateapi.Control/ListActorTemplates',
ateapi__pb2.ListActorTemplatesRequest.SerializeToString,
ateapi__pb2.ListActorTemplatesResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def DeleteActorTemplate(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/ateapi.Control/DeleteActorTemplate',
ateapi__pb2.DeleteActorTemplateRequest.SerializeToString,
ateapi__pb2.ActorTemplate.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def CreateActorTemplateVersion(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/ateapi.Control/CreateActorTemplateVersion',
ateapi__pb2.CreateActorTemplateVersionRequest.SerializeToString,
ateapi__pb2.ActorTemplateVersion.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def GetActorTemplateVersion(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/ateapi.Control/GetActorTemplateVersion',
ateapi__pb2.GetActorTemplateVersionRequest.SerializeToString,
ateapi__pb2.ActorTemplateVersion.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def ListActorTemplateVersions(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/ateapi.Control/ListActorTemplateVersions',
ateapi__pb2.ListActorTemplateVersionsRequest.SerializeToString,
ateapi__pb2.ListActorTemplateVersionsResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def DeleteActorTemplateVersion(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/ateapi.Control/DeleteActorTemplateVersion',
ateapi__pb2.DeleteActorTemplateVersionRequest.SerializeToString,
ateapi__pb2.ActorTemplateVersion.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
class DebugStub:
"""Debug is the RPC interface for administrative and debugging operations
@@ -947,20 +1348,10 @@ class Debug:
class ActorIdentityStub:
"""ActorIdentity allows substrate workloads to exchange their
infrastructure-level credentials (k8s service account token, etc.) for a
substrate actor-level credential. A given substrate actor might migrate
substrate actor-level credential. A given substrate actor might migrate
between many different physical workers over the course of its lifecycle,
whereas the actor credential's identity will be stable for the life of the
actor.
This service requires authentication. You can authenticate with a Kubernetes
service account token in an `Authorization: Bearer` header, or you can
authenticate with a Kubernetes service account certificate as an mTLS
certificate. (Kubernetes service account certificates do not currently exist
upstream, but we will provide a polyfill based on Pod Certificates).
The broker will check that the service credentials you authenticated with
belong to a Pod that is currently mapped to the requested actor in the
actor database.
"""
def __init__(self, channel):
@@ -984,20 +1375,10 @@ class ActorIdentityStub:
class ActorIdentityServicer:
"""ActorIdentity allows substrate workloads to exchange their
infrastructure-level credentials (k8s service account token, etc.) for a
substrate actor-level credential. A given substrate actor might migrate
substrate actor-level credential. A given substrate actor might migrate
between many different physical workers over the course of its lifecycle,
whereas the actor credential's identity will be stable for the life of the
actor.
This service requires authentication. You can authenticate with a Kubernetes
service account token in an `Authorization: Bearer` header, or you can
authenticate with a Kubernetes service account certificate as an mTLS
certificate. (Kubernetes service account certificates do not currently exist
upstream, but we will provide a polyfill based on Pod Certificates).
The broker will check that the service credentials you authenticated with
belong to a Pod that is currently mapped to the requested actor in the
actor database.
"""
def MintJWT(self, request, context):
@@ -1017,10 +1398,9 @@ class ActorIdentityServicer:
it on the actor's behalf, authenticating with its own client certificate
rather than a bearer token.
Authorization is decided on that client certificate: it must identify the
atelet running on the same node as the worker Pod that currently hosts the
requested actor, and the actor must still be running. Any other caller is
rejected with PERMISSION_DENIED.
Authorization is decided on that client certificate and the worker
identity attested by atelet. Ateapi verifies that the worker is assigned to
the actor and that the actor points back to that exact worker before signing.
The certificate in the response is the actor's identity, not the atelet's.
"""
@@ -1052,20 +1432,10 @@ def add_ActorIdentityServicer_to_server(servicer, server):
class ActorIdentity:
"""ActorIdentity allows substrate workloads to exchange their
infrastructure-level credentials (k8s service account token, etc.) for a
substrate actor-level credential. A given substrate actor might migrate
substrate actor-level credential. A given substrate actor might migrate
between many different physical workers over the course of its lifecycle,
whereas the actor credential's identity will be stable for the life of the
actor.
This service requires authentication. You can authenticate with a Kubernetes
service account token in an `Authorization: Bearer` header, or you can
authenticate with a Kubernetes service account certificate as an mTLS
certificate. (Kubernetes service account certificates do not currently exist
upstream, but we will provide a polyfill based on Pod Certificates).
The broker will check that the service credentials you authenticated with
belong to a Pod that is currently mapped to the requested actor in the
actor database.
"""
@staticmethod
@@ -19,6 +19,7 @@ import (
"errors"
"fmt"
"slices"
"strings"
"github.com/agent-substrate/substrate/cmd/ateapi/internal/store"
"github.com/agent-substrate/substrate/internal/fieldmask"
@@ -30,6 +31,8 @@ import (
)
// actorSnapshotTagScopes lists the scopes a client may set on an ActorSnapshotTag.
// ACTOR_SNAPSHOT_TAG_SCOPE_UNSPECIFIED is deliberately absent: scope is required
// on the wire, not defaulted. See validateActorSnapshotTagScope.
var actorSnapshotTagScopes = []ateapipb.ActorSnapshotTagScope{
ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE,
ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED,
@@ -184,9 +187,7 @@ func validateUpdateActorSnapshotTagRequest(req *ateapipb.UpdateActorSnapshotTagR
errs = append(errs, fieldmask.Validate(req.GetUpdateMask(), actorSnapshotTagMutableFields, field.NewPath("update_mask"))...)
if scope, p := tag.GetScope(), tagPath.Child("scope"); validateActorSnapshotTagScope(scope) != nil {
errs = append(errs, field.NotSupported(p, scope.String(), actorSnapshotTagScopeNames))
}
errs = append(errs, validateActorSnapshotTagScope(tag.GetScope(), tagPath.Child("scope"))...)
return errs
}
@@ -271,12 +272,19 @@ func validateActorSnapshotTag(tag *ateapipb.ActorSnapshotTag, name string) error
if errs := resources.ValidateObjectRef(&ateapipb.ObjectRef{Atespace: tag.GetMetadata().GetAtespace(), Name: tag.GetMetadata().GetName()}, p.Child("metadata")); len(errs) > 0 {
return status.Error(codes.InvalidArgument, errs.ToAggregate().Error())
}
return validateActorSnapshotTagScope(tag.GetScope())
if errs := validateActorSnapshotTagScope(tag.GetScope(), p.Child("scope")); len(errs) > 0 {
return status.Error(codes.InvalidArgument, errs.ToAggregate().Error())
}
return nil
}
func validateActorSnapshotTagScope(scope ateapipb.ActorSnapshotTagScope) error {
if slices.Contains(actorSnapshotTagScopes, scope) {
return nil
// validateActorSnapshotTagScope checks that scope is one a client may set.
func validateActorSnapshotTagScope(scope ateapipb.ActorSnapshotTagScope, p *field.Path) field.ErrorList {
switch {
case scope == ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_UNSPECIFIED:
return field.ErrorList{field.Required(p, "must be one of: "+strings.Join(actorSnapshotTagScopeNames, ", "))}
case !slices.Contains(actorSnapshotTagScopes, scope):
return field.ErrorList{field.NotSupported(p, scope.String(), actorSnapshotTagScopeNames)}
}
return status.Error(codes.InvalidArgument, "invalid ActorSnapshot tag scope")
return nil
}
@@ -202,7 +202,6 @@ func TestValidateUpdateActorSnapshotTagRequest(t *testing.T) {
wantError: field.ErrorList{field.NotSupported(field.NewPath("update_mask"), "snapshot", mutableFields)},
},
{
// The zero value is ATESPACE, so leaving scope unset unpublishes the tag.
name: "unset tag.scope",
req: &ateapipb.UpdateActorSnapshotTagRequest{
Tag: &ateapipb.ActorSnapshotTag{
@@ -210,6 +209,28 @@ func TestValidateUpdateActorSnapshotTagRequest(t *testing.T) {
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"scope"}},
},
wantError: field.ErrorList{field.Required(field.NewPath("tag", "scope"), "")},
},
{
name: "explicit tag.scope UNSPECIFIED",
req: &ateapipb.UpdateActorSnapshotTagRequest{
Tag: &ateapipb.ActorSnapshotTag{
Metadata: &ateapipb.ResourceMetadata{Atespace: "ns1", Name: "tag1"},
Scope: ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_UNSPECIFIED,
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"scope"}},
},
wantError: field.ErrorList{field.Required(field.NewPath("tag", "scope"), "")},
},
{
name: "tag.scope ATESPACE explicitly unpublishes",
req: &ateapipb.UpdateActorSnapshotTagRequest{
Tag: &ateapipb.ActorSnapshotTag{
Metadata: &ateapipb.ResourceMetadata{Atespace: "ns1", Name: "tag1"},
Scope: ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE,
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"scope"}},
},
wantError: nil,
},
{
@@ -240,18 +261,18 @@ func TestUpdateActorSnapshotTag_FieldMasks(t *testing.T) {
want *ateapipb.ActorSnapshotTag
}{
{
name: "mask sets scope",
stored: &ateapipb.ActorSnapshotTag{},
name: "mask publishes an atespace-scoped tag",
stored: &ateapipb.ActorSnapshotTag{Scope: ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE},
req: &ateapipb.ActorSnapshotTag{Scope: ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED},
maskPaths: []string{"scope"},
want: &ateapipb.ActorSnapshotTag{Scope: ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED},
},
{
name: "mask clears scope left unset on request, resetting to the zero value",
name: "mask unpublishes a published tag",
stored: &ateapipb.ActorSnapshotTag{Scope: ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED},
req: &ateapipb.ActorSnapshotTag{},
req: &ateapipb.ActorSnapshotTag{Scope: ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE},
maskPaths: []string{"scope"},
want: &ateapipb.ActorSnapshotTag{},
want: &ateapipb.ActorSnapshotTag{Scope: ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE},
},
}
for _, tt := range tests {
@@ -280,6 +301,59 @@ func TestUpdateActorSnapshotTag_FieldMasks(t *testing.T) {
}
}
// TestUpdateActorSnapshotTag_UnsetScopeDoesNotUnpublish checks that masking
// scope without populating it is rejected.
func TestUpdateActorSnapshotTag_UnsetScopeDoesNotUnpublish(t *testing.T) {
ctx := context.Background()
svc, stored := serviceWithActorSnapshotTag(t, &ateapipb.ActorSnapshotTag{
Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "tag1"},
Scope: ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED,
})
_, err := svc.UpdateActorSnapshotTag(ctx, &ateapipb.UpdateActorSnapshotTagRequest{
Tag: &ateapipb.ActorSnapshotTag{
Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "tag1"},
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"scope"}},
})
if code := status.Code(err); code != codes.InvalidArgument {
t.Errorf("UpdateActorSnapshotTag error = %v (code %v), want code InvalidArgument", err, code)
}
_, current, err := svc.persistence.GetActorSnapshotByTag(ctx, testAtespace, "tag1")
if err != nil {
t.Fatalf("GetActorSnapshotByTag: %v", err)
}
if got, want := current.GetScope(), ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED; got != want {
t.Errorf("stored scope = %v, want %v: the rejected update must not have unpublished the tag", got, want)
}
if got, want := current.GetMetadata().GetVersion(), stored.GetMetadata().GetVersion(); got != want {
t.Errorf("stored version = %d, want %d: the rejected update must not have written", got, want)
}
}
// TestTagActorSnapshot_RejectsUnsetScope checks that scope is required at
// creation.
func TestTagActorSnapshot_RejectsUnsetScope(t *testing.T) {
ctx := context.Background()
svc, stored := serviceWithActorSnapshotTag(t, &ateapipb.ActorSnapshotTag{
Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "tag1"},
Scope: ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE,
})
_, err := svc.TagActorSnapshot(ctx, &ateapipb.TagActorSnapshotRequest{
Snapshot: &ateapipb.ActorSnapshotRef{
Reference: &ateapipb.ActorSnapshotRef_Snapshot{Snapshot: stored.GetSnapshot()},
},
Tag: &ateapipb.ActorSnapshotTag{
Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "tag2"},
},
})
if code := status.Code(err); code != codes.InvalidArgument {
t.Errorf("TagActorSnapshot error = %v (code %v), want code InvalidArgument", err, code)
}
}
// serviceWithActorSnapshotTag seeds an ActorSnapshot and a tag pointing at it
// in a miniredis-backed store, and returns a Service over it.
func serviceWithActorSnapshotTag(t *testing.T, tag *ateapipb.ActorSnapshotTag) (*Service, *ateapipb.ActorSnapshotTag) {
@@ -190,7 +190,7 @@ func parseActorSnapshotTagScope(value string) (ateapipb.ActorSnapshotTagScope, e
case "published":
return ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED, nil
default:
return 0, fmt.Errorf("invalid scope %q; must be atespace or published", value)
return ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_UNSPECIFIED, fmt.Errorf("invalid scope %q; must be atespace or published", value)
}
}
+16 -11
View File
@@ -92,22 +92,26 @@ func (SnapshotContentScope) EnumDescriptor() ([]byte, []int) {
type ActorSnapshotTagScope int32
const (
// Not set and rejected wherever a client supplies a scope
ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_UNSPECIFIED ActorSnapshotTagScope = 0
// May initialize Actors only in the tag's owning Atespace.
ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE ActorSnapshotTagScope = 0
ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE ActorSnapshotTagScope = 1
// Published for use by Actors in any Atespace. The tag remains addressed
// through its owning Atespace.
ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED ActorSnapshotTagScope = 1
ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED ActorSnapshotTagScope = 2
)
// Enum value maps for ActorSnapshotTagScope.
var (
ActorSnapshotTagScope_name = map[int32]string{
0: "ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE",
1: "ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED",
0: "ACTOR_SNAPSHOT_TAG_SCOPE_UNSPECIFIED",
1: "ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE",
2: "ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED",
}
ActorSnapshotTagScope_value = map[string]int32{
"ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE": 0,
"ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED": 1,
"ACTOR_SNAPSHOT_TAG_SCOPE_UNSPECIFIED": 0,
"ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE": 1,
"ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED": 2,
}
)
@@ -1229,7 +1233,7 @@ func (x *ActorSnapshotTag) GetScope() ActorSnapshotTagScope {
if x != nil {
return x.Scope
}
return ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE
return ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_UNSPECIFIED
}
// Atespace is the isolation boundary an Actor is created into. Global-scoped:
@@ -5450,10 +5454,11 @@ const file_ateapi_proto_rawDesc = "" +
"\x14SnapshotContentScope\x12&\n" +
"\"SNAPSHOT_CONTENT_SCOPE_UNSPECIFIED\x10\x00\x12\x1f\n" +
"\x1bSNAPSHOT_CONTENT_SCOPE_FULL\x10\x01\x12\x1f\n" +
"\x1bSNAPSHOT_CONTENT_SCOPE_DATA\x10\x02*f\n" +
"\x15ActorSnapshotTagScope\x12%\n" +
"!ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE\x10\x00\x12&\n" +
"\"ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED\x10\x01*b\n" +
"\x1bSNAPSHOT_CONTENT_SCOPE_DATA\x10\x02*\x90\x01\n" +
"\x15ActorSnapshotTagScope\x12(\n" +
"$ACTOR_SNAPSHOT_TAG_SCOPE_UNSPECIFIED\x10\x00\x12%\n" +
"!ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE\x10\x01\x12&\n" +
"\"ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED\x10\x02*b\n" +
"\fSandboxClass\x12\x1d\n" +
"\x19SANDBOX_CLASS_UNSPECIFIED\x10\x00\x12\x18\n" +
"\x14SANDBOX_CLASS_GVISOR\x10\x01\x12\x19\n" +
+4 -2
View File
@@ -135,11 +135,13 @@ enum SnapshotContentScope {
}
enum ActorSnapshotTagScope {
// Not set and rejected wherever a client supplies a scope
ACTOR_SNAPSHOT_TAG_SCOPE_UNSPECIFIED = 0;
// May initialize Actors only in the tag's owning Atespace.
ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE = 0;
ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE = 1;
// Published for use by Actors in any Atespace. The tag remains addressed
// through its owning Atespace.
ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED = 1;
ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED = 2;
}
// Selector matches worker pools by label.