From c4bb76a631aa52ba99e5d110051e5d756613a8a5 Mon Sep 17 00:00:00 2001 From: Max Thompson Date: Mon, 19 Oct 2026 19:26:21 -0700 Subject: [PATCH 1/2] kubectl ate: authenticate with ExecCredential plugins kubectl ate minted one hour-long token for the shared ate-client ServiceAccount when it connected and never refreshed it, so long-running clients failed after an hour. Every user also shared that one identity, and using another identity provider meant piping a raw JWT into --token-file on every command. --credential-plugin, or $ATE_CREDENTIAL_PLUGIN, names a kubectl credential plugin. A value that names an existing file runs with no arguments; any other value is split into a command and arguments, honoring shell quoting. kubectl ate builds the exec stanza in memory (client.authentication.k8s.io/v1, interactiveMode IfAvailable, provideClusterInfo) and passes its --kubeconfig and --context to the plugin as $ATE_KUBECONFIG and $ATE_KUBE_CONTEXT. Passing both --token-file and --credential-plugin is an error. ateclient runs the plugin through client-go's exec authenticator, caches the credential until its expirationTimestamp, and retries an RPC once with a fresh credential when ate-api returns Unauthenticated. If the plugin returns the token that was just rejected, the client runs it again with ATE_REFRESH_CREDENTIAL=1 so that a plugin with a cache returns a new one. The ate-client token minted when there is no plugin or token file is now refreshed a minute before it expires. A credential source that fails locally is reported as a local error rather than an Unauthenticated status. kubectl ate credential is a built-in plugin. It mints a token for ate-client, or for a ServiceAccount named with --service-account, and caches it on disk until shortly before it expires; --refresh mints a new one. -o token prints only the token, and credential wrap turns any command that prints a JWT into a plugin. kubectl ate whoami calls a new Authentication.WhoAmI RPC that returns the principal ate-api authenticated the caller as. It is a separate service because it operates on no resource. The docs cover the flag, a per-user ServiceAccount recipe with the RBAC a non-admin needs, what deleting a ServiceAccount does not revoke, and in-cluster workloads. --- api/ateapi/v1/ateapi.pb.go | 1245 ++++++++++------- api/ateapi/v1/ateapi.proto | 31 + api/ateapi/v1/ateapi_grpc.pb.go | 112 ++ cmd/ateapi/internal/apiauthn/whoami.go | 44 + cmd/ateapi/internal/apiauthn/whoami_test.go | 64 + cmd/ateapi/internal/authz/registry.go | 3 + cmd/ateapi/main.go | 1 + cmd/kubectl-ate/README.md | 21 + cmd/kubectl-ate/internal/cmd/credential.go | 213 +++ .../internal/cmd/credential_test.go | 208 +++ .../internal/cmd/credentialplugin.go | 140 ++ .../internal/cmd/credentialplugin_test.go | 121 ++ cmd/kubectl-ate/internal/cmd/root.go | 18 +- cmd/kubectl-ate/internal/cmd/root_test.go | 35 + cmd/kubectl-ate/internal/cmd/verbose.go | 37 +- cmd/kubectl-ate/internal/cmd/verbose_test.go | 16 +- cmd/kubectl-ate/internal/cmd/whoami.go | 43 + cmd/kubectl-ate/internal/credplugin/cache.go | 90 ++ .../internal/credplugin/credplugin_test.go | 139 ++ .../internal/credplugin/execinfo.go | 116 ++ cmd/kubectl-ate/internal/printer/printer.go | 12 + .../internal/printer/printer_test.go | 26 + docs/authentication.md | 120 +- docs/authorization.md | 11 +- internal/ateclient/builder.go | 272 ++-- internal/ateclient/credentials.go | 323 +++++ internal/ateclient/credentials_test.go | 652 +++++++++ 27 files changed, 3392 insertions(+), 721 deletions(-) create mode 100644 cmd/ateapi/internal/apiauthn/whoami.go create mode 100644 cmd/ateapi/internal/apiauthn/whoami_test.go create mode 100644 cmd/kubectl-ate/internal/cmd/credential.go create mode 100644 cmd/kubectl-ate/internal/cmd/credential_test.go create mode 100644 cmd/kubectl-ate/internal/cmd/credentialplugin.go create mode 100644 cmd/kubectl-ate/internal/cmd/credentialplugin_test.go create mode 100644 cmd/kubectl-ate/internal/cmd/whoami.go create mode 100644 cmd/kubectl-ate/internal/credplugin/cache.go create mode 100644 cmd/kubectl-ate/internal/credplugin/credplugin_test.go create mode 100644 cmd/kubectl-ate/internal/credplugin/execinfo.go create mode 100644 internal/ateclient/credentials.go create mode 100644 internal/ateclient/credentials_test.go diff --git a/api/ateapi/v1/ateapi.pb.go b/api/ateapi/v1/ateapi.pb.go index 250a2f5968..43f2621dbe 100644 --- a/api/ateapi/v1/ateapi.pb.go +++ b/api/ateapi/v1/ateapi.pb.go @@ -535,6 +535,58 @@ func (WorkerState) EnumDescriptor() ([]byte, []int) { return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{8} } +// PrincipalKind is the authentication method that established a principal. +type PrincipalKind int32 + +const ( + PrincipalKind_PRINCIPAL_KIND_UNSPECIFIED PrincipalKind = 0 + // A bearer JWT verified against a configured JWT provider. + PrincipalKind_PRINCIPAL_KIND_JWT PrincipalKind = 1 + // An mTLS client certificate. + PrincipalKind_PRINCIPAL_KIND_MTLS PrincipalKind = 2 +) + +// Enum value maps for PrincipalKind. +var ( + PrincipalKind_name = map[int32]string{ + 0: "PRINCIPAL_KIND_UNSPECIFIED", + 1: "PRINCIPAL_KIND_JWT", + 2: "PRINCIPAL_KIND_MTLS", + } + PrincipalKind_value = map[string]int32{ + "PRINCIPAL_KIND_UNSPECIFIED": 0, + "PRINCIPAL_KIND_JWT": 1, + "PRINCIPAL_KIND_MTLS": 2, + } +) + +func (x PrincipalKind) Enum() *PrincipalKind { + p := new(PrincipalKind) + *p = x + return p +} + +func (x PrincipalKind) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (PrincipalKind) Descriptor() protoreflect.EnumDescriptor { + return file_api_ateapi_v1_ateapi_proto_enumTypes[9].Descriptor() +} + +func (PrincipalKind) Type() protoreflect.EnumType { + return &file_api_ateapi_v1_ateapi_proto_enumTypes[9] +} + +func (x PrincipalKind) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use PrincipalKind.Descriptor instead. +func (PrincipalKind) EnumDescriptor() ([]byte, []int) { + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{9} +} + // Scope is whether a top-level resource belongs to an atespace. type Scope int32 @@ -572,11 +624,11 @@ func (x Scope) String() string { } func (Scope) Descriptor() protoreflect.EnumDescriptor { - return file_api_ateapi_v1_ateapi_proto_enumTypes[9].Descriptor() + return file_api_ateapi_v1_ateapi_proto_enumTypes[10].Descriptor() } func (Scope) Type() protoreflect.EnumType { - return &file_api_ateapi_v1_ateapi_proto_enumTypes[9] + return &file_api_ateapi_v1_ateapi_proto_enumTypes[10] } func (x Scope) Number() protoreflect.EnumNumber { @@ -585,7 +637,7 @@ func (x Scope) Number() protoreflect.EnumNumber { // Deprecated: Use Scope.Descriptor instead. func (Scope) EnumDescriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{9} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{10} } // ObjectSnapshot addresses a snapshot held in object storage. @@ -8131,6 +8183,106 @@ func (x *ActorAssignment) GetResources() *Resources { return nil } +type WhoAmIRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WhoAmIRequest) Reset() { + *x = WhoAmIRequest{} + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[112] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WhoAmIRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WhoAmIRequest) ProtoMessage() {} + +func (x *WhoAmIRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[112] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WhoAmIRequest.ProtoReflect.Descriptor instead. +func (*WhoAmIRequest) Descriptor() ([]byte, []int) { + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{112} +} + +type WhoAmIResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The principal ID that authorization policies name: the subject of a + // bearer JWT, or the first URI SAN of an mTLS client certificate. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // How the caller authenticated. + Kind PrincipalKind `protobuf:"varint,2,opt,name=kind,proto3,enum=substrate.ateapi.v1.PrincipalKind" json:"kind,omitempty"` + // The issuer of the caller's bearer JWT. Empty for mTLS callers. + Issuer string `protobuf:"bytes,3,opt,name=issuer,proto3" json:"issuer,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WhoAmIResponse) Reset() { + *x = WhoAmIResponse{} + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[113] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WhoAmIResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WhoAmIResponse) ProtoMessage() {} + +func (x *WhoAmIResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[113] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WhoAmIResponse.ProtoReflect.Descriptor instead. +func (*WhoAmIResponse) Descriptor() ([]byte, []int) { + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{113} +} + +func (x *WhoAmIResponse) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *WhoAmIResponse) GetKind() PrincipalKind { + if x != nil { + return x.Kind + } + return PrincipalKind_PRINCIPAL_KIND_UNSPECIFIED +} + +func (x *WhoAmIResponse) GetIssuer() string { + if x != nil { + return x.Issuer + } + return "" +} + type AcquireWorkerEpochRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The Worker whose worker process is starting. atespace is always empty; @@ -8145,7 +8297,7 @@ type AcquireWorkerEpochRequest struct { func (x *AcquireWorkerEpochRequest) Reset() { *x = AcquireWorkerEpochRequest{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[112] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[114] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8157,7 +8309,7 @@ func (x *AcquireWorkerEpochRequest) String() string { func (*AcquireWorkerEpochRequest) ProtoMessage() {} func (x *AcquireWorkerEpochRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[112] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[114] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8170,7 +8322,7 @@ func (x *AcquireWorkerEpochRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AcquireWorkerEpochRequest.ProtoReflect.Descriptor instead. func (*AcquireWorkerEpochRequest) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{112} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{114} } func (x *AcquireWorkerEpochRequest) GetWorker() *ObjectRef { @@ -8190,7 +8342,7 @@ type AcquireWorkerEpochResponse struct { func (x *AcquireWorkerEpochResponse) Reset() { *x = AcquireWorkerEpochResponse{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[113] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[115] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8202,7 +8354,7 @@ func (x *AcquireWorkerEpochResponse) String() string { func (*AcquireWorkerEpochResponse) ProtoMessage() {} func (x *AcquireWorkerEpochResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[113] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[115] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8215,7 +8367,7 @@ func (x *AcquireWorkerEpochResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AcquireWorkerEpochResponse.ProtoReflect.Descriptor instead. func (*AcquireWorkerEpochResponse) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{113} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{115} } func (x *AcquireWorkerEpochResponse) GetEpoch() int64 { @@ -8258,7 +8410,7 @@ type RegisterWorkerRequest struct { func (x *RegisterWorkerRequest) Reset() { *x = RegisterWorkerRequest{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[114] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[116] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8270,7 +8422,7 @@ func (x *RegisterWorkerRequest) String() string { func (*RegisterWorkerRequest) ProtoMessage() {} func (x *RegisterWorkerRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[114] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[116] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8283,7 +8435,7 @@ func (x *RegisterWorkerRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RegisterWorkerRequest.ProtoReflect.Descriptor instead. func (*RegisterWorkerRequest) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{114} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{116} } func (x *RegisterWorkerRequest) GetWorker() *ObjectRef { @@ -8324,7 +8476,7 @@ type RegisterWorkerResponse struct { func (x *RegisterWorkerResponse) Reset() { *x = RegisterWorkerResponse{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[115] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[117] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8336,7 +8488,7 @@ func (x *RegisterWorkerResponse) String() string { func (*RegisterWorkerResponse) ProtoMessage() {} func (x *RegisterWorkerResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[115] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[117] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8349,7 +8501,7 @@ func (x *RegisterWorkerResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RegisterWorkerResponse.ProtoReflect.Descriptor instead. func (*RegisterWorkerResponse) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{115} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{117} } func (x *RegisterWorkerResponse) GetWorker() *Worker { @@ -8389,7 +8541,7 @@ type SetWorkerSandboxRuntimesRequest struct { func (x *SetWorkerSandboxRuntimesRequest) Reset() { *x = SetWorkerSandboxRuntimesRequest{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[116] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[118] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8401,7 +8553,7 @@ func (x *SetWorkerSandboxRuntimesRequest) String() string { func (*SetWorkerSandboxRuntimesRequest) ProtoMessage() {} func (x *SetWorkerSandboxRuntimesRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[116] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[118] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8414,7 +8566,7 @@ func (x *SetWorkerSandboxRuntimesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetWorkerSandboxRuntimesRequest.ProtoReflect.Descriptor instead. func (*SetWorkerSandboxRuntimesRequest) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{116} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{118} } func (x *SetWorkerSandboxRuntimesRequest) GetWorker() *ObjectRef { @@ -8448,7 +8600,7 @@ type SetWorkerSandboxRuntimesResponse struct { func (x *SetWorkerSandboxRuntimesResponse) Reset() { *x = SetWorkerSandboxRuntimesResponse{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[117] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[119] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8460,7 +8612,7 @@ func (x *SetWorkerSandboxRuntimesResponse) String() string { func (*SetWorkerSandboxRuntimesResponse) ProtoMessage() {} func (x *SetWorkerSandboxRuntimesResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[117] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[119] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8473,7 +8625,7 @@ func (x *SetWorkerSandboxRuntimesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetWorkerSandboxRuntimesResponse.ProtoReflect.Descriptor instead. func (*SetWorkerSandboxRuntimesResponse) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{117} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{119} } func (x *SetWorkerSandboxRuntimesResponse) GetWorker() *Worker { @@ -8520,7 +8672,7 @@ type MintWorkerActorCertificateRequest struct { func (x *MintWorkerActorCertificateRequest) Reset() { *x = MintWorkerActorCertificateRequest{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[118] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[120] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8532,7 +8684,7 @@ func (x *MintWorkerActorCertificateRequest) String() string { func (*MintWorkerActorCertificateRequest) ProtoMessage() {} func (x *MintWorkerActorCertificateRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[118] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[120] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8545,7 +8697,7 @@ func (x *MintWorkerActorCertificateRequest) ProtoReflect() protoreflect.Message // Deprecated: Use MintWorkerActorCertificateRequest.ProtoReflect.Descriptor instead. func (*MintWorkerActorCertificateRequest) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{118} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{120} } func (x *MintWorkerActorCertificateRequest) GetActor() *ObjectRef { @@ -8589,7 +8741,7 @@ type MintWorkerActorCertificateResponse struct { func (x *MintWorkerActorCertificateResponse) Reset() { *x = MintWorkerActorCertificateResponse{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[119] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[121] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8601,7 +8753,7 @@ func (x *MintWorkerActorCertificateResponse) String() string { func (*MintWorkerActorCertificateResponse) ProtoMessage() {} func (x *MintWorkerActorCertificateResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[119] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[121] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8614,7 +8766,7 @@ func (x *MintWorkerActorCertificateResponse) ProtoReflect() protoreflect.Message // Deprecated: Use MintWorkerActorCertificateResponse.ProtoReflect.Descriptor instead. func (*MintWorkerActorCertificateResponse) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{119} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{121} } func (x *MintWorkerActorCertificateResponse) GetActorCertificates() [][]byte { @@ -8655,7 +8807,7 @@ type MintWorkerActorJwtRequest struct { func (x *MintWorkerActorJwtRequest) Reset() { *x = MintWorkerActorJwtRequest{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[120] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[122] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8667,7 +8819,7 @@ func (x *MintWorkerActorJwtRequest) String() string { func (*MintWorkerActorJwtRequest) ProtoMessage() {} func (x *MintWorkerActorJwtRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[120] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[122] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8680,7 +8832,7 @@ func (x *MintWorkerActorJwtRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MintWorkerActorJwtRequest.ProtoReflect.Descriptor instead. func (*MintWorkerActorJwtRequest) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{120} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{122} } func (x *MintWorkerActorJwtRequest) GetActor() *ObjectRef { @@ -8728,7 +8880,7 @@ type MintWorkerActorJwtResponse struct { func (x *MintWorkerActorJwtResponse) Reset() { *x = MintWorkerActorJwtResponse{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[121] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[123] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8740,7 +8892,7 @@ func (x *MintWorkerActorJwtResponse) String() string { func (*MintWorkerActorJwtResponse) ProtoMessage() {} func (x *MintWorkerActorJwtResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[121] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[123] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8753,7 +8905,7 @@ func (x *MintWorkerActorJwtResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MintWorkerActorJwtResponse.ProtoReflect.Descriptor instead. func (*MintWorkerActorJwtResponse) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{121} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{123} } func (x *MintWorkerActorJwtResponse) GetActorJwt() string { @@ -8801,7 +8953,7 @@ type RequestActorSuspendRequest struct { func (x *RequestActorSuspendRequest) Reset() { *x = RequestActorSuspendRequest{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[122] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[124] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8813,7 +8965,7 @@ func (x *RequestActorSuspendRequest) String() string { func (*RequestActorSuspendRequest) ProtoMessage() {} func (x *RequestActorSuspendRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[122] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[124] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8826,7 +8978,7 @@ func (x *RequestActorSuspendRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestActorSuspendRequest.ProtoReflect.Descriptor instead. func (*RequestActorSuspendRequest) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{122} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{124} } func (x *RequestActorSuspendRequest) GetWorker() *ObjectRef { @@ -8860,7 +9012,7 @@ type RequestActorSuspendResponse struct { func (x *RequestActorSuspendResponse) Reset() { *x = RequestActorSuspendResponse{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[123] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[125] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8872,7 +9024,7 @@ func (x *RequestActorSuspendResponse) String() string { func (*RequestActorSuspendResponse) ProtoMessage() {} func (x *RequestActorSuspendResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[123] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[125] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8885,7 +9037,7 @@ func (x *RequestActorSuspendResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestActorSuspendResponse.ProtoReflect.Descriptor instead. func (*RequestActorSuspendResponse) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{123} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{125} } func (x *RequestActorSuspendResponse) GetActor() *Actor { @@ -8925,7 +9077,7 @@ type ReportActorCrashedRequest struct { func (x *ReportActorCrashedRequest) Reset() { *x = ReportActorCrashedRequest{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[124] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[126] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8937,7 +9089,7 @@ func (x *ReportActorCrashedRequest) String() string { func (*ReportActorCrashedRequest) ProtoMessage() {} func (x *ReportActorCrashedRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[124] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[126] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8950,7 +9102,7 @@ func (x *ReportActorCrashedRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportActorCrashedRequest.ProtoReflect.Descriptor instead. func (*ReportActorCrashedRequest) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{124} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{126} } func (x *ReportActorCrashedRequest) GetWorker() *ObjectRef { @@ -8991,7 +9143,7 @@ type ReportActorCrashedResponse struct { func (x *ReportActorCrashedResponse) Reset() { *x = ReportActorCrashedResponse{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[125] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[127] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9003,7 +9155,7 @@ func (x *ReportActorCrashedResponse) String() string { func (*ReportActorCrashedResponse) ProtoMessage() {} func (x *ReportActorCrashedResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[125] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[127] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9016,7 +9168,7 @@ func (x *ReportActorCrashedResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportActorCrashedResponse.ProtoReflect.Descriptor instead. func (*ReportActorCrashedResponse) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{125} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{127} } func (x *ReportActorCrashedResponse) GetActor() *Actor { @@ -9047,7 +9199,7 @@ type ReportActorSnapshotStateRequest struct { func (x *ReportActorSnapshotStateRequest) Reset() { *x = ReportActorSnapshotStateRequest{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[126] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[128] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9059,7 +9211,7 @@ func (x *ReportActorSnapshotStateRequest) String() string { func (*ReportActorSnapshotStateRequest) ProtoMessage() {} func (x *ReportActorSnapshotStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[126] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[128] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9072,7 +9224,7 @@ func (x *ReportActorSnapshotStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportActorSnapshotStateRequest.ProtoReflect.Descriptor instead. func (*ReportActorSnapshotStateRequest) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{126} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{128} } func (x *ReportActorSnapshotStateRequest) GetActor() *ObjectRef { @@ -9104,7 +9256,7 @@ type ReportActorSnapshotStateResponse struct { func (x *ReportActorSnapshotStateResponse) Reset() { *x = ReportActorSnapshotStateResponse{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[127] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[129] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9116,7 +9268,7 @@ func (x *ReportActorSnapshotStateResponse) String() string { func (*ReportActorSnapshotStateResponse) ProtoMessage() {} func (x *ReportActorSnapshotStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[127] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[129] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9129,7 +9281,7 @@ func (x *ReportActorSnapshotStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportActorSnapshotStateResponse.ProtoReflect.Descriptor instead. func (*ReportActorSnapshotStateResponse) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{127} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{129} } // ActorSnapshotState is what atelet holds for one Actor on its node. It @@ -9163,7 +9315,7 @@ type ActorSnapshotState struct { func (x *ActorSnapshotState) Reset() { *x = ActorSnapshotState{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[128] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[130] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9175,7 +9327,7 @@ func (x *ActorSnapshotState) String() string { func (*ActorSnapshotState) ProtoMessage() {} func (x *ActorSnapshotState) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[128] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[130] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9188,7 +9340,7 @@ func (x *ActorSnapshotState) ProtoReflect() protoreflect.Message { // Deprecated: Use ActorSnapshotState.ProtoReflect.Descriptor instead. func (*ActorSnapshotState) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{128} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{130} } func (x *ActorSnapshotState) GetLatestLocal() *SnapshotReport { @@ -9260,7 +9412,7 @@ type SnapshotReport struct { func (x *SnapshotReport) Reset() { *x = SnapshotReport{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[129] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[131] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9272,7 +9424,7 @@ func (x *SnapshotReport) String() string { func (*SnapshotReport) ProtoMessage() {} func (x *SnapshotReport) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[129] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[131] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9285,7 +9437,7 @@ func (x *SnapshotReport) ProtoReflect() protoreflect.Message { // Deprecated: Use SnapshotReport.ProtoReflect.Descriptor instead. func (*SnapshotReport) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{129} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{131} } func (x *SnapshotReport) GetUuid() string { @@ -9355,7 +9507,7 @@ type AccessPolicy struct { func (x *AccessPolicy) Reset() { *x = AccessPolicy{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[130] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[132] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9367,7 +9519,7 @@ func (x *AccessPolicy) String() string { func (*AccessPolicy) ProtoMessage() {} func (x *AccessPolicy) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[130] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[132] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9380,7 +9532,7 @@ func (x *AccessPolicy) ProtoReflect() protoreflect.Message { // Deprecated: Use AccessPolicy.ProtoReflect.Descriptor instead. func (*AccessPolicy) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{130} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{132} } func (x *AccessPolicy) GetMetadata() *ResourceMetadata { @@ -9422,7 +9574,7 @@ type Binding struct { func (x *Binding) Reset() { *x = Binding{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[131] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[133] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9434,7 +9586,7 @@ func (x *Binding) String() string { func (*Binding) ProtoMessage() {} func (x *Binding) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[131] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[133] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9447,7 +9599,7 @@ func (x *Binding) ProtoReflect() protoreflect.Message { // Deprecated: Use Binding.ProtoReflect.Descriptor instead. func (*Binding) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{131} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{133} } func (x *Binding) GetRole() string { @@ -9479,7 +9631,7 @@ type GetGlobalAccessPolicyRequest struct { func (x *GetGlobalAccessPolicyRequest) Reset() { *x = GetGlobalAccessPolicyRequest{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[132] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[134] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9491,7 +9643,7 @@ func (x *GetGlobalAccessPolicyRequest) String() string { func (*GetGlobalAccessPolicyRequest) ProtoMessage() {} func (x *GetGlobalAccessPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[132] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[134] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9504,7 +9656,7 @@ func (x *GetGlobalAccessPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetGlobalAccessPolicyRequest.ProtoReflect.Descriptor instead. func (*GetGlobalAccessPolicyRequest) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{132} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{134} } func (x *GetGlobalAccessPolicyRequest) GetName() string { @@ -9528,7 +9680,7 @@ type CreateGlobalAccessPolicyRequest struct { func (x *CreateGlobalAccessPolicyRequest) Reset() { *x = CreateGlobalAccessPolicyRequest{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[133] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[135] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9540,7 +9692,7 @@ func (x *CreateGlobalAccessPolicyRequest) String() string { func (*CreateGlobalAccessPolicyRequest) ProtoMessage() {} func (x *CreateGlobalAccessPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[133] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[135] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9553,7 +9705,7 @@ func (x *CreateGlobalAccessPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateGlobalAccessPolicyRequest.ProtoReflect.Descriptor instead. func (*CreateGlobalAccessPolicyRequest) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{133} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{135} } func (x *CreateGlobalAccessPolicyRequest) GetAccessPolicy() *AccessPolicy { @@ -9577,7 +9729,7 @@ type UpdateGlobalAccessPolicyRequest struct { func (x *UpdateGlobalAccessPolicyRequest) Reset() { *x = UpdateGlobalAccessPolicyRequest{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[134] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[136] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9589,7 +9741,7 @@ func (x *UpdateGlobalAccessPolicyRequest) String() string { func (*UpdateGlobalAccessPolicyRequest) ProtoMessage() {} func (x *UpdateGlobalAccessPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[134] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[136] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9602,7 +9754,7 @@ func (x *UpdateGlobalAccessPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateGlobalAccessPolicyRequest.ProtoReflect.Descriptor instead. func (*UpdateGlobalAccessPolicyRequest) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{134} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{136} } func (x *UpdateGlobalAccessPolicyRequest) GetAccessPolicy() *AccessPolicy { @@ -9632,7 +9784,7 @@ type GetAtespaceAccessPolicyRequest struct { func (x *GetAtespaceAccessPolicyRequest) Reset() { *x = GetAtespaceAccessPolicyRequest{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[135] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[137] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9644,7 +9796,7 @@ func (x *GetAtespaceAccessPolicyRequest) String() string { func (*GetAtespaceAccessPolicyRequest) ProtoMessage() {} func (x *GetAtespaceAccessPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[135] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[137] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9657,7 +9809,7 @@ func (x *GetAtespaceAccessPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetAtespaceAccessPolicyRequest.ProtoReflect.Descriptor instead. func (*GetAtespaceAccessPolicyRequest) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{135} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{137} } func (x *GetAtespaceAccessPolicyRequest) GetAtespace() *ObjectRef { @@ -9693,7 +9845,7 @@ type CreateAtespaceAccessPolicyRequest struct { func (x *CreateAtespaceAccessPolicyRequest) Reset() { *x = CreateAtespaceAccessPolicyRequest{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[136] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[138] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9705,7 +9857,7 @@ func (x *CreateAtespaceAccessPolicyRequest) String() string { func (*CreateAtespaceAccessPolicyRequest) ProtoMessage() {} func (x *CreateAtespaceAccessPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[136] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[138] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9718,7 +9870,7 @@ func (x *CreateAtespaceAccessPolicyRequest) ProtoReflect() protoreflect.Message // Deprecated: Use CreateAtespaceAccessPolicyRequest.ProtoReflect.Descriptor instead. func (*CreateAtespaceAccessPolicyRequest) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{136} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{138} } func (x *CreateAtespaceAccessPolicyRequest) GetAtespace() *ObjectRef { @@ -9754,7 +9906,7 @@ type UpdateAtespaceAccessPolicyRequest struct { func (x *UpdateAtespaceAccessPolicyRequest) Reset() { *x = UpdateAtespaceAccessPolicyRequest{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[137] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[139] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9766,7 +9918,7 @@ func (x *UpdateAtespaceAccessPolicyRequest) String() string { func (*UpdateAtespaceAccessPolicyRequest) ProtoMessage() {} func (x *UpdateAtespaceAccessPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[137] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[139] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9779,7 +9931,7 @@ func (x *UpdateAtespaceAccessPolicyRequest) ProtoReflect() protoreflect.Message // Deprecated: Use UpdateAtespaceAccessPolicyRequest.ProtoReflect.Descriptor instead. func (*UpdateAtespaceAccessPolicyRequest) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{137} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{139} } func (x *UpdateAtespaceAccessPolicyRequest) GetAtespace() *ObjectRef { @@ -9820,7 +9972,7 @@ type DeleteAtespaceAccessPolicyRequest struct { func (x *DeleteAtespaceAccessPolicyRequest) Reset() { *x = DeleteAtespaceAccessPolicyRequest{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[138] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[140] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9832,7 +9984,7 @@ func (x *DeleteAtespaceAccessPolicyRequest) String() string { func (*DeleteAtespaceAccessPolicyRequest) ProtoMessage() {} func (x *DeleteAtespaceAccessPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[138] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[140] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9845,7 +9997,7 @@ func (x *DeleteAtespaceAccessPolicyRequest) ProtoReflect() protoreflect.Message // Deprecated: Use DeleteAtespaceAccessPolicyRequest.ProtoReflect.Descriptor instead. func (*DeleteAtespaceAccessPolicyRequest) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{138} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{140} } func (x *DeleteAtespaceAccessPolicyRequest) GetAtespace() *ObjectRef { @@ -9891,7 +10043,7 @@ type ResourceAnnotation struct { func (x *ResourceAnnotation) Reset() { *x = ResourceAnnotation{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[139] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[141] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9903,7 +10055,7 @@ func (x *ResourceAnnotation) String() string { func (*ResourceAnnotation) ProtoMessage() {} func (x *ResourceAnnotation) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[139] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[141] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9916,7 +10068,7 @@ func (x *ResourceAnnotation) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceAnnotation.ProtoReflect.Descriptor instead. func (*ResourceAnnotation) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{139} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{141} } func (x *ResourceAnnotation) GetParents() []string { @@ -9959,7 +10111,7 @@ type MethodAnnotation struct { func (x *MethodAnnotation) Reset() { *x = MethodAnnotation{} - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[140] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[142] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9971,7 +10123,7 @@ func (x *MethodAnnotation) String() string { func (*MethodAnnotation) ProtoMessage() {} func (x *MethodAnnotation) ProtoReflect() protoreflect.Message { - mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[140] + mi := &file_api_ateapi_v1_ateapi_proto_msgTypes[142] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9984,7 +10136,7 @@ func (x *MethodAnnotation) ProtoReflect() protoreflect.Message { // Deprecated: Use MethodAnnotation.ProtoReflect.Descriptor instead. func (*MethodAnnotation) Descriptor() ([]byte, []int) { - return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{140} + return file_api_ateapi_v1_ateapi_proto_rawDescGZIP(), []int{142} } func (x *MethodAnnotation) GetResource() string { @@ -10431,7 +10583,12 @@ const file_api_ateapi_v1_ateapi_proto_rawDesc = "" + "\x05actor\x18\x02 \x01(\v2\x1e.substrate.ateapi.v1.ObjectRefR\x05actor\x12\x1b\n" + "\tactor_uid\x18\x03 \x01(\tR\bactorUid\x12<\n" + "\tresources\x18\x04 \x01(\v2\x1e.substrate.ateapi.v1.ResourcesR\tresources:\x1e\x82\xb5\x18\x1a\n" + - "\x06Worker\"\x10ActorAssignments\"S\n" + + "\x06Worker\"\x10ActorAssignments\"\x0f\n" + + "\rWhoAmIRequest\"p\n" + + "\x0eWhoAmIResponse\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x126\n" + + "\x04kind\x18\x02 \x01(\x0e2\".substrate.ateapi.v1.PrincipalKindR\x04kind\x12\x16\n" + + "\x06issuer\x18\x03 \x01(\tR\x06issuer\"S\n" + "\x19AcquireWorkerEpochRequest\x126\n" + "\x06worker\x18\x01 \x01(\v2\x1e.substrate.ateapi.v1.ObjectRefR\x06worker\"2\n" + "\x1aAcquireWorkerEpochResponse\x12\x14\n" + @@ -10574,7 +10731,11 @@ const file_api_ateapi_v1_ateapi_proto_rawDesc = "" + "\x18WORKER_STATE_UNSPECIFIED\x10\x00\x12\x17\n" + "\x13WORKER_STATE_ACTIVE\x10\x01\x12\x19\n" + "\x15WORKER_STATE_DRAINING\x10\x02\x12\x1c\n" + - "\x18WORKER_STATE_UNAVAILABLE\x10\x03*D\n" + + "\x18WORKER_STATE_UNAVAILABLE\x10\x03*`\n" + + "\rPrincipalKind\x12\x1e\n" + + "\x1aPRINCIPAL_KIND_UNSPECIFIED\x10\x00\x12\x16\n" + + "\x12PRINCIPAL_KIND_JWT\x10\x01\x12\x17\n" + + "\x13PRINCIPAL_KIND_MTLS\x10\x02*D\n" + "\x05Scope\x12\x15\n" + "\x11SCOPE_UNSPECIFIED\x10\x00\x12\x12\n" + "\x0eSCOPE_ATESPACE\x10\x01\x12\x10\n" + @@ -10670,7 +10831,9 @@ const file_api_ateapi_v1_ateapi_proto_rawDesc = "" + "\x1aUpdateAtespaceAccessPolicy\x126.substrate.ateapi.v1.UpdateAtespaceAccessPolicyRequest\x1a!.substrate.ateapi.v1.AccessPolicy\"\x12\x82\xb5\x18\x0e\n" + "\fAccessPolicy\x12\x8b\x01\n" + "\x1aDeleteAtespaceAccessPolicy\x126.substrate.ateapi.v1.DeleteAtespaceAccessPolicyRequest\x1a!.substrate.ateapi.v1.AccessPolicy\"\x12\x82\xb5\x18\x0e\n" + - "\fAccessPolicy2\xfd\a\n" + + "\fAccessPolicy2c\n" + + "\x0eAuthentication\x12Q\n" + + "\x06WhoAmI\x12\".substrate.ateapi.v1.WhoAmIRequest\x1a#.substrate.ateapi.v1.WhoAmIResponse2\xfd\a\n" + "\rWorkerService\x12u\n" + "\x12AcquireWorkerEpoch\x12..substrate.ateapi.v1.AcquireWorkerEpochRequest\x1a/.substrate.ateapi.v1.AcquireWorkerEpochResponse\x12i\n" + "\x0eRegisterWorker\x12*.substrate.ateapi.v1.RegisterWorkerRequest\x1a+.substrate.ateapi.v1.RegisterWorkerResponse\x12\x87\x01\n" + @@ -10695,8 +10858,8 @@ func file_api_ateapi_v1_ateapi_proto_rawDescGZIP() []byte { return file_api_ateapi_v1_ateapi_proto_rawDescData } -var file_api_ateapi_v1_ateapi_proto_enumTypes = make([]protoimpl.EnumInfo, 10) -var file_api_ateapi_v1_ateapi_proto_msgTypes = make([]protoimpl.MessageInfo, 144) +var file_api_ateapi_v1_ateapi_proto_enumTypes = make([]protoimpl.EnumInfo, 11) +var file_api_ateapi_v1_ateapi_proto_msgTypes = make([]protoimpl.MessageInfo, 146) var file_api_ateapi_v1_ateapi_proto_goTypes = []any{ (SnapshotOwner)(0), // 0: substrate.ateapi.v1.SnapshotOwner (SnapshotStorageStatus)(0), // 1: substrate.ateapi.v1.SnapshotStorageStatus @@ -10707,458 +10870,464 @@ var file_api_ateapi_v1_ateapi_proto_goTypes = []any{ (ActorMetadataField)(0), // 6: substrate.ateapi.v1.ActorMetadataField (DurabilityMode)(0), // 7: substrate.ateapi.v1.DurabilityMode (WorkerState)(0), // 8: substrate.ateapi.v1.WorkerState - (Scope)(0), // 9: substrate.ateapi.v1.Scope - (*ObjectSnapshot)(nil), // 10: substrate.ateapi.v1.ObjectSnapshot - (*SnapshotStorage)(nil), // 11: substrate.ateapi.v1.SnapshotStorage - (*Snapshot)(nil), // 12: substrate.ateapi.v1.Snapshot - (*Selector)(nil), // 13: substrate.ateapi.v1.Selector - (*ResourceMetadata)(nil), // 14: substrate.ateapi.v1.ResourceMetadata - (*ExternalVolumes)(nil), // 15: substrate.ateapi.v1.ExternalVolumes - (*ExternalVolumeStatus)(nil), // 16: substrate.ateapi.v1.ExternalVolumeStatus - (*Actor)(nil), // 17: substrate.ateapi.v1.Actor - (*EgressPolicy)(nil), // 18: substrate.ateapi.v1.EgressPolicy - (*EgressPolicyTemplate)(nil), // 19: substrate.ateapi.v1.EgressPolicyTemplate - (*EgressRule)(nil), // 20: substrate.ateapi.v1.EgressRule - (*HttpRule)(nil), // 21: substrate.ateapi.v1.HttpRule - (*HttpsRule)(nil), // 22: substrate.ateapi.v1.HttpsRule - (*TlsPassthroughRule)(nil), // 23: substrate.ateapi.v1.TlsPassthroughRule - (*Ports)(nil), // 24: substrate.ateapi.v1.Ports - (*AllPorts)(nil), // 25: substrate.ateapi.v1.AllPorts - (*HttpRuleEffects)(nil), // 26: substrate.ateapi.v1.HttpRuleEffects - (*CredentialHeader)(nil), // 27: substrate.ateapi.v1.CredentialHeader - (*ActorJwtSource)(nil), // 28: substrate.ateapi.v1.ActorJwtSource - (*ActorStatus)(nil), // 29: substrate.ateapi.v1.ActorStatus - (*ActorCrash)(nil), // 30: substrate.ateapi.v1.ActorCrash - (*WorkerAssignment)(nil), // 31: substrate.ateapi.v1.WorkerAssignment - (*TagStatus)(nil), // 32: substrate.ateapi.v1.TagStatus - (*Tag)(nil), // 33: substrate.ateapi.v1.Tag - (*Atespace)(nil), // 34: substrate.ateapi.v1.Atespace - (*ObjectRef)(nil), // 35: substrate.ateapi.v1.ObjectRef - (*ActorTemplate)(nil), // 36: substrate.ateapi.v1.ActorTemplate - (*ResourceRequirements)(nil), // 37: substrate.ateapi.v1.ResourceRequirements - (*Resources)(nil), // 38: substrate.ateapi.v1.Resources - (*GoldenSnapshotStatus)(nil), // 39: substrate.ateapi.v1.GoldenSnapshotStatus - (*ActorTemplateStatus)(nil), // 40: substrate.ateapi.v1.ActorTemplateStatus - (*SnapshotConfig)(nil), // 41: substrate.ateapi.v1.SnapshotConfig - (*GoldenSnapshotConfig)(nil), // 42: substrate.ateapi.v1.GoldenSnapshotConfig - (*Container)(nil), // 43: substrate.ateapi.v1.Container - (*SecurityContext)(nil), // 44: substrate.ateapi.v1.SecurityContext - (*Capabilities)(nil), // 45: substrate.ateapi.v1.Capabilities - (*EnvVar)(nil), // 46: substrate.ateapi.v1.EnvVar - (*WakeupProbe)(nil), // 47: substrate.ateapi.v1.WakeupProbe - (*HttpGetAction)(nil), // 48: substrate.ateapi.v1.HttpGetAction - (*Volume)(nil), // 49: substrate.ateapi.v1.Volume - (*ExternalVolumeSource)(nil), // 50: substrate.ateapi.v1.ExternalVolumeSource - (*ImageVolumeSource)(nil), // 51: substrate.ateapi.v1.ImageVolumeSource - (*DurableDirVolumeSource)(nil), // 52: substrate.ateapi.v1.DurableDirVolumeSource - (*ExternalVolumeTemplate)(nil), // 53: substrate.ateapi.v1.ExternalVolumeTemplate - (*SystemInfoVolumeSource)(nil), // 54: substrate.ateapi.v1.SystemInfoVolumeSource - (*SystemInfoDataSource)(nil), // 55: substrate.ateapi.v1.SystemInfoDataSource - (*ActorMetadataDataSource)(nil), // 56: substrate.ateapi.v1.ActorMetadataDataSource - (*ActorMetadataItem)(nil), // 57: substrate.ateapi.v1.ActorMetadataItem - (*TrustBundleDataSource)(nil), // 58: substrate.ateapi.v1.TrustBundleDataSource - (*VolumeMount)(nil), // 59: substrate.ateapi.v1.VolumeMount - (*CreateAtespaceRequest)(nil), // 60: substrate.ateapi.v1.CreateAtespaceRequest - (*GetAtespaceRequest)(nil), // 61: substrate.ateapi.v1.GetAtespaceRequest - (*ListAtespacesRequest)(nil), // 62: substrate.ateapi.v1.ListAtespacesRequest - (*ListAtespacesResponse)(nil), // 63: substrate.ateapi.v1.ListAtespacesResponse - (*DeleteAtespaceRequest)(nil), // 64: substrate.ateapi.v1.DeleteAtespaceRequest - (*CreateActorTemplateRequest)(nil), // 65: substrate.ateapi.v1.CreateActorTemplateRequest - (*GetActorTemplateRequest)(nil), // 66: substrate.ateapi.v1.GetActorTemplateRequest - (*ListActorTemplatesRequest)(nil), // 67: substrate.ateapi.v1.ListActorTemplatesRequest - (*ListActorTemplatesResponse)(nil), // 68: substrate.ateapi.v1.ListActorTemplatesResponse - (*DeleteActorTemplateRequest)(nil), // 69: substrate.ateapi.v1.DeleteActorTemplateRequest - (*GetActorRequest)(nil), // 70: substrate.ateapi.v1.GetActorRequest - (*CreateActorRequest)(nil), // 71: substrate.ateapi.v1.CreateActorRequest - (*UpdateActorRequest)(nil), // 72: substrate.ateapi.v1.UpdateActorRequest - (*SuspendActorRequest)(nil), // 73: substrate.ateapi.v1.SuspendActorRequest - (*SuspendActorResponse)(nil), // 74: substrate.ateapi.v1.SuspendActorResponse - (*SuspendOptions)(nil), // 75: substrate.ateapi.v1.SuspendOptions - (*ResumeActorRequest)(nil), // 76: substrate.ateapi.v1.ResumeActorRequest - (*ResumeActorResponse)(nil), // 77: substrate.ateapi.v1.ResumeActorResponse - (*RevertActorRequest)(nil), // 78: substrate.ateapi.v1.RevertActorRequest - (*RevertActorResponse)(nil), // 79: substrate.ateapi.v1.RevertActorResponse - (*DeleteActorRequest)(nil), // 80: substrate.ateapi.v1.DeleteActorRequest - (*GetActorEgressPolicyRequest)(nil), // 81: substrate.ateapi.v1.GetActorEgressPolicyRequest - (*CreateActorEgressPolicyRequest)(nil), // 82: substrate.ateapi.v1.CreateActorEgressPolicyRequest - (*UpdateActorEgressPolicyRequest)(nil), // 83: substrate.ateapi.v1.UpdateActorEgressPolicyRequest - (*DeleteActorEgressPolicyRequest)(nil), // 84: substrate.ateapi.v1.DeleteActorEgressPolicyRequest - (*GetTagRequest)(nil), // 85: substrate.ateapi.v1.GetTagRequest - (*MintActorJwtRequest)(nil), // 86: substrate.ateapi.v1.MintActorJwtRequest - (*MintActorJwtResponse)(nil), // 87: substrate.ateapi.v1.MintActorJwtResponse - (*MintActorCertificateRequest)(nil), // 88: substrate.ateapi.v1.MintActorCertificateRequest - (*MintActorCertificateResponse)(nil), // 89: substrate.ateapi.v1.MintActorCertificateResponse - (*ListTagsRequest)(nil), // 90: substrate.ateapi.v1.ListTagsRequest - (*ListTagsResponse)(nil), // 91: substrate.ateapi.v1.ListTagsResponse - (*CreateTagRequest)(nil), // 92: substrate.ateapi.v1.CreateTagRequest - (*UpdateTagRequest)(nil), // 93: substrate.ateapi.v1.UpdateTagRequest - (*DeleteTagRequest)(nil), // 94: substrate.ateapi.v1.DeleteTagRequest - (*DeleteOptions)(nil), // 95: substrate.ateapi.v1.DeleteOptions - (*ListOptions)(nil), // 96: substrate.ateapi.v1.ListOptions - (*ListWorkerActorAssignmentsRequest)(nil), // 97: substrate.ateapi.v1.ListWorkerActorAssignmentsRequest - (*ListWorkerActorAssignmentsResponse)(nil), // 98: substrate.ateapi.v1.ListWorkerActorAssignmentsResponse - (*ListWorkersRequest)(nil), // 99: substrate.ateapi.v1.ListWorkersRequest - (*ListWorkersResponse)(nil), // 100: substrate.ateapi.v1.ListWorkersResponse - (*GetWorkerRequest)(nil), // 101: substrate.ateapi.v1.GetWorkerRequest - (*CreateWorkerRequest)(nil), // 102: substrate.ateapi.v1.CreateWorkerRequest - (*UpdateWorkerRequest)(nil), // 103: substrate.ateapi.v1.UpdateWorkerRequest - (*DeleteWorkerRequest)(nil), // 104: substrate.ateapi.v1.DeleteWorkerRequest - (*DrainWorkerRequest)(nil), // 105: substrate.ateapi.v1.DrainWorkerRequest - (*DrainWorkerResponse)(nil), // 106: substrate.ateapi.v1.DrainWorkerResponse - (*MarkWorkerAvailableRequest)(nil), // 107: substrate.ateapi.v1.MarkWorkerAvailableRequest - (*MarkWorkerAvailableResponse)(nil), // 108: substrate.ateapi.v1.MarkWorkerAvailableResponse - (*MarkWorkerUnavailableRequest)(nil), // 109: substrate.ateapi.v1.MarkWorkerUnavailableRequest - (*MarkWorkerUnavailableResponse)(nil), // 110: substrate.ateapi.v1.MarkWorkerUnavailableResponse - (*ListActorsRequest)(nil), // 111: substrate.ateapi.v1.ListActorsRequest - (*ListActorsResponse)(nil), // 112: substrate.ateapi.v1.ListActorsResponse - (*Worker)(nil), // 113: substrate.ateapi.v1.Worker - (*WorkerStatus)(nil), // 114: substrate.ateapi.v1.WorkerStatus - (*WorkerSandboxClass)(nil), // 115: substrate.ateapi.v1.WorkerSandboxClass - (*WorkerSandboxRuntime)(nil), // 116: substrate.ateapi.v1.WorkerSandboxRuntime - (*ActorSandboxRuntime)(nil), // 117: substrate.ateapi.v1.ActorSandboxRuntime - (*VersionedSandboxCompat)(nil), // 118: substrate.ateapi.v1.VersionedSandboxCompat - (*AttributeEntry)(nil), // 119: substrate.ateapi.v1.AttributeEntry - (*WorkerResources)(nil), // 120: substrate.ateapi.v1.WorkerResources - (*ActorAssignment)(nil), // 121: substrate.ateapi.v1.ActorAssignment - (*AcquireWorkerEpochRequest)(nil), // 122: substrate.ateapi.v1.AcquireWorkerEpochRequest - (*AcquireWorkerEpochResponse)(nil), // 123: substrate.ateapi.v1.AcquireWorkerEpochResponse - (*RegisterWorkerRequest)(nil), // 124: substrate.ateapi.v1.RegisterWorkerRequest - (*RegisterWorkerResponse)(nil), // 125: substrate.ateapi.v1.RegisterWorkerResponse - (*SetWorkerSandboxRuntimesRequest)(nil), // 126: substrate.ateapi.v1.SetWorkerSandboxRuntimesRequest - (*SetWorkerSandboxRuntimesResponse)(nil), // 127: substrate.ateapi.v1.SetWorkerSandboxRuntimesResponse - (*MintWorkerActorCertificateRequest)(nil), // 128: substrate.ateapi.v1.MintWorkerActorCertificateRequest - (*MintWorkerActorCertificateResponse)(nil), // 129: substrate.ateapi.v1.MintWorkerActorCertificateResponse - (*MintWorkerActorJwtRequest)(nil), // 130: substrate.ateapi.v1.MintWorkerActorJwtRequest - (*MintWorkerActorJwtResponse)(nil), // 131: substrate.ateapi.v1.MintWorkerActorJwtResponse - (*RequestActorSuspendRequest)(nil), // 132: substrate.ateapi.v1.RequestActorSuspendRequest - (*RequestActorSuspendResponse)(nil), // 133: substrate.ateapi.v1.RequestActorSuspendResponse - (*ReportActorCrashedRequest)(nil), // 134: substrate.ateapi.v1.ReportActorCrashedRequest - (*ReportActorCrashedResponse)(nil), // 135: substrate.ateapi.v1.ReportActorCrashedResponse - (*ReportActorSnapshotStateRequest)(nil), // 136: substrate.ateapi.v1.ReportActorSnapshotStateRequest - (*ReportActorSnapshotStateResponse)(nil), // 137: substrate.ateapi.v1.ReportActorSnapshotStateResponse - (*ActorSnapshotState)(nil), // 138: substrate.ateapi.v1.ActorSnapshotState - (*SnapshotReport)(nil), // 139: substrate.ateapi.v1.SnapshotReport - (*AccessPolicy)(nil), // 140: substrate.ateapi.v1.AccessPolicy - (*Binding)(nil), // 141: substrate.ateapi.v1.Binding - (*GetGlobalAccessPolicyRequest)(nil), // 142: substrate.ateapi.v1.GetGlobalAccessPolicyRequest - (*CreateGlobalAccessPolicyRequest)(nil), // 143: substrate.ateapi.v1.CreateGlobalAccessPolicyRequest - (*UpdateGlobalAccessPolicyRequest)(nil), // 144: substrate.ateapi.v1.UpdateGlobalAccessPolicyRequest - (*GetAtespaceAccessPolicyRequest)(nil), // 145: substrate.ateapi.v1.GetAtespaceAccessPolicyRequest - (*CreateAtespaceAccessPolicyRequest)(nil), // 146: substrate.ateapi.v1.CreateAtespaceAccessPolicyRequest - (*UpdateAtespaceAccessPolicyRequest)(nil), // 147: substrate.ateapi.v1.UpdateAtespaceAccessPolicyRequest - (*DeleteAtespaceAccessPolicyRequest)(nil), // 148: substrate.ateapi.v1.DeleteAtespaceAccessPolicyRequest - (*ResourceAnnotation)(nil), // 149: substrate.ateapi.v1.ResourceAnnotation - (*MethodAnnotation)(nil), // 150: substrate.ateapi.v1.MethodAnnotation - nil, // 151: substrate.ateapi.v1.Selector.MatchLabelsEntry - nil, // 152: substrate.ateapi.v1.ExternalVolumeStatus.VolumeContextEntry - nil, // 153: substrate.ateapi.v1.Worker.LabelsEntry - (*timestamppb.Timestamp)(nil), // 154: google.protobuf.Timestamp - (*descriptorpb.MessageOptions)(nil), // 155: google.protobuf.MessageOptions - (*descriptorpb.MethodOptions)(nil), // 156: google.protobuf.MethodOptions + (PrincipalKind)(0), // 9: substrate.ateapi.v1.PrincipalKind + (Scope)(0), // 10: substrate.ateapi.v1.Scope + (*ObjectSnapshot)(nil), // 11: substrate.ateapi.v1.ObjectSnapshot + (*SnapshotStorage)(nil), // 12: substrate.ateapi.v1.SnapshotStorage + (*Snapshot)(nil), // 13: substrate.ateapi.v1.Snapshot + (*Selector)(nil), // 14: substrate.ateapi.v1.Selector + (*ResourceMetadata)(nil), // 15: substrate.ateapi.v1.ResourceMetadata + (*ExternalVolumes)(nil), // 16: substrate.ateapi.v1.ExternalVolumes + (*ExternalVolumeStatus)(nil), // 17: substrate.ateapi.v1.ExternalVolumeStatus + (*Actor)(nil), // 18: substrate.ateapi.v1.Actor + (*EgressPolicy)(nil), // 19: substrate.ateapi.v1.EgressPolicy + (*EgressPolicyTemplate)(nil), // 20: substrate.ateapi.v1.EgressPolicyTemplate + (*EgressRule)(nil), // 21: substrate.ateapi.v1.EgressRule + (*HttpRule)(nil), // 22: substrate.ateapi.v1.HttpRule + (*HttpsRule)(nil), // 23: substrate.ateapi.v1.HttpsRule + (*TlsPassthroughRule)(nil), // 24: substrate.ateapi.v1.TlsPassthroughRule + (*Ports)(nil), // 25: substrate.ateapi.v1.Ports + (*AllPorts)(nil), // 26: substrate.ateapi.v1.AllPorts + (*HttpRuleEffects)(nil), // 27: substrate.ateapi.v1.HttpRuleEffects + (*CredentialHeader)(nil), // 28: substrate.ateapi.v1.CredentialHeader + (*ActorJwtSource)(nil), // 29: substrate.ateapi.v1.ActorJwtSource + (*ActorStatus)(nil), // 30: substrate.ateapi.v1.ActorStatus + (*ActorCrash)(nil), // 31: substrate.ateapi.v1.ActorCrash + (*WorkerAssignment)(nil), // 32: substrate.ateapi.v1.WorkerAssignment + (*TagStatus)(nil), // 33: substrate.ateapi.v1.TagStatus + (*Tag)(nil), // 34: substrate.ateapi.v1.Tag + (*Atespace)(nil), // 35: substrate.ateapi.v1.Atespace + (*ObjectRef)(nil), // 36: substrate.ateapi.v1.ObjectRef + (*ActorTemplate)(nil), // 37: substrate.ateapi.v1.ActorTemplate + (*ResourceRequirements)(nil), // 38: substrate.ateapi.v1.ResourceRequirements + (*Resources)(nil), // 39: substrate.ateapi.v1.Resources + (*GoldenSnapshotStatus)(nil), // 40: substrate.ateapi.v1.GoldenSnapshotStatus + (*ActorTemplateStatus)(nil), // 41: substrate.ateapi.v1.ActorTemplateStatus + (*SnapshotConfig)(nil), // 42: substrate.ateapi.v1.SnapshotConfig + (*GoldenSnapshotConfig)(nil), // 43: substrate.ateapi.v1.GoldenSnapshotConfig + (*Container)(nil), // 44: substrate.ateapi.v1.Container + (*SecurityContext)(nil), // 45: substrate.ateapi.v1.SecurityContext + (*Capabilities)(nil), // 46: substrate.ateapi.v1.Capabilities + (*EnvVar)(nil), // 47: substrate.ateapi.v1.EnvVar + (*WakeupProbe)(nil), // 48: substrate.ateapi.v1.WakeupProbe + (*HttpGetAction)(nil), // 49: substrate.ateapi.v1.HttpGetAction + (*Volume)(nil), // 50: substrate.ateapi.v1.Volume + (*ExternalVolumeSource)(nil), // 51: substrate.ateapi.v1.ExternalVolumeSource + (*ImageVolumeSource)(nil), // 52: substrate.ateapi.v1.ImageVolumeSource + (*DurableDirVolumeSource)(nil), // 53: substrate.ateapi.v1.DurableDirVolumeSource + (*ExternalVolumeTemplate)(nil), // 54: substrate.ateapi.v1.ExternalVolumeTemplate + (*SystemInfoVolumeSource)(nil), // 55: substrate.ateapi.v1.SystemInfoVolumeSource + (*SystemInfoDataSource)(nil), // 56: substrate.ateapi.v1.SystemInfoDataSource + (*ActorMetadataDataSource)(nil), // 57: substrate.ateapi.v1.ActorMetadataDataSource + (*ActorMetadataItem)(nil), // 58: substrate.ateapi.v1.ActorMetadataItem + (*TrustBundleDataSource)(nil), // 59: substrate.ateapi.v1.TrustBundleDataSource + (*VolumeMount)(nil), // 60: substrate.ateapi.v1.VolumeMount + (*CreateAtespaceRequest)(nil), // 61: substrate.ateapi.v1.CreateAtespaceRequest + (*GetAtespaceRequest)(nil), // 62: substrate.ateapi.v1.GetAtespaceRequest + (*ListAtespacesRequest)(nil), // 63: substrate.ateapi.v1.ListAtespacesRequest + (*ListAtespacesResponse)(nil), // 64: substrate.ateapi.v1.ListAtespacesResponse + (*DeleteAtespaceRequest)(nil), // 65: substrate.ateapi.v1.DeleteAtespaceRequest + (*CreateActorTemplateRequest)(nil), // 66: substrate.ateapi.v1.CreateActorTemplateRequest + (*GetActorTemplateRequest)(nil), // 67: substrate.ateapi.v1.GetActorTemplateRequest + (*ListActorTemplatesRequest)(nil), // 68: substrate.ateapi.v1.ListActorTemplatesRequest + (*ListActorTemplatesResponse)(nil), // 69: substrate.ateapi.v1.ListActorTemplatesResponse + (*DeleteActorTemplateRequest)(nil), // 70: substrate.ateapi.v1.DeleteActorTemplateRequest + (*GetActorRequest)(nil), // 71: substrate.ateapi.v1.GetActorRequest + (*CreateActorRequest)(nil), // 72: substrate.ateapi.v1.CreateActorRequest + (*UpdateActorRequest)(nil), // 73: substrate.ateapi.v1.UpdateActorRequest + (*SuspendActorRequest)(nil), // 74: substrate.ateapi.v1.SuspendActorRequest + (*SuspendActorResponse)(nil), // 75: substrate.ateapi.v1.SuspendActorResponse + (*SuspendOptions)(nil), // 76: substrate.ateapi.v1.SuspendOptions + (*ResumeActorRequest)(nil), // 77: substrate.ateapi.v1.ResumeActorRequest + (*ResumeActorResponse)(nil), // 78: substrate.ateapi.v1.ResumeActorResponse + (*RevertActorRequest)(nil), // 79: substrate.ateapi.v1.RevertActorRequest + (*RevertActorResponse)(nil), // 80: substrate.ateapi.v1.RevertActorResponse + (*DeleteActorRequest)(nil), // 81: substrate.ateapi.v1.DeleteActorRequest + (*GetActorEgressPolicyRequest)(nil), // 82: substrate.ateapi.v1.GetActorEgressPolicyRequest + (*CreateActorEgressPolicyRequest)(nil), // 83: substrate.ateapi.v1.CreateActorEgressPolicyRequest + (*UpdateActorEgressPolicyRequest)(nil), // 84: substrate.ateapi.v1.UpdateActorEgressPolicyRequest + (*DeleteActorEgressPolicyRequest)(nil), // 85: substrate.ateapi.v1.DeleteActorEgressPolicyRequest + (*GetTagRequest)(nil), // 86: substrate.ateapi.v1.GetTagRequest + (*MintActorJwtRequest)(nil), // 87: substrate.ateapi.v1.MintActorJwtRequest + (*MintActorJwtResponse)(nil), // 88: substrate.ateapi.v1.MintActorJwtResponse + (*MintActorCertificateRequest)(nil), // 89: substrate.ateapi.v1.MintActorCertificateRequest + (*MintActorCertificateResponse)(nil), // 90: substrate.ateapi.v1.MintActorCertificateResponse + (*ListTagsRequest)(nil), // 91: substrate.ateapi.v1.ListTagsRequest + (*ListTagsResponse)(nil), // 92: substrate.ateapi.v1.ListTagsResponse + (*CreateTagRequest)(nil), // 93: substrate.ateapi.v1.CreateTagRequest + (*UpdateTagRequest)(nil), // 94: substrate.ateapi.v1.UpdateTagRequest + (*DeleteTagRequest)(nil), // 95: substrate.ateapi.v1.DeleteTagRequest + (*DeleteOptions)(nil), // 96: substrate.ateapi.v1.DeleteOptions + (*ListOptions)(nil), // 97: substrate.ateapi.v1.ListOptions + (*ListWorkerActorAssignmentsRequest)(nil), // 98: substrate.ateapi.v1.ListWorkerActorAssignmentsRequest + (*ListWorkerActorAssignmentsResponse)(nil), // 99: substrate.ateapi.v1.ListWorkerActorAssignmentsResponse + (*ListWorkersRequest)(nil), // 100: substrate.ateapi.v1.ListWorkersRequest + (*ListWorkersResponse)(nil), // 101: substrate.ateapi.v1.ListWorkersResponse + (*GetWorkerRequest)(nil), // 102: substrate.ateapi.v1.GetWorkerRequest + (*CreateWorkerRequest)(nil), // 103: substrate.ateapi.v1.CreateWorkerRequest + (*UpdateWorkerRequest)(nil), // 104: substrate.ateapi.v1.UpdateWorkerRequest + (*DeleteWorkerRequest)(nil), // 105: substrate.ateapi.v1.DeleteWorkerRequest + (*DrainWorkerRequest)(nil), // 106: substrate.ateapi.v1.DrainWorkerRequest + (*DrainWorkerResponse)(nil), // 107: substrate.ateapi.v1.DrainWorkerResponse + (*MarkWorkerAvailableRequest)(nil), // 108: substrate.ateapi.v1.MarkWorkerAvailableRequest + (*MarkWorkerAvailableResponse)(nil), // 109: substrate.ateapi.v1.MarkWorkerAvailableResponse + (*MarkWorkerUnavailableRequest)(nil), // 110: substrate.ateapi.v1.MarkWorkerUnavailableRequest + (*MarkWorkerUnavailableResponse)(nil), // 111: substrate.ateapi.v1.MarkWorkerUnavailableResponse + (*ListActorsRequest)(nil), // 112: substrate.ateapi.v1.ListActorsRequest + (*ListActorsResponse)(nil), // 113: substrate.ateapi.v1.ListActorsResponse + (*Worker)(nil), // 114: substrate.ateapi.v1.Worker + (*WorkerStatus)(nil), // 115: substrate.ateapi.v1.WorkerStatus + (*WorkerSandboxClass)(nil), // 116: substrate.ateapi.v1.WorkerSandboxClass + (*WorkerSandboxRuntime)(nil), // 117: substrate.ateapi.v1.WorkerSandboxRuntime + (*ActorSandboxRuntime)(nil), // 118: substrate.ateapi.v1.ActorSandboxRuntime + (*VersionedSandboxCompat)(nil), // 119: substrate.ateapi.v1.VersionedSandboxCompat + (*AttributeEntry)(nil), // 120: substrate.ateapi.v1.AttributeEntry + (*WorkerResources)(nil), // 121: substrate.ateapi.v1.WorkerResources + (*ActorAssignment)(nil), // 122: substrate.ateapi.v1.ActorAssignment + (*WhoAmIRequest)(nil), // 123: substrate.ateapi.v1.WhoAmIRequest + (*WhoAmIResponse)(nil), // 124: substrate.ateapi.v1.WhoAmIResponse + (*AcquireWorkerEpochRequest)(nil), // 125: substrate.ateapi.v1.AcquireWorkerEpochRequest + (*AcquireWorkerEpochResponse)(nil), // 126: substrate.ateapi.v1.AcquireWorkerEpochResponse + (*RegisterWorkerRequest)(nil), // 127: substrate.ateapi.v1.RegisterWorkerRequest + (*RegisterWorkerResponse)(nil), // 128: substrate.ateapi.v1.RegisterWorkerResponse + (*SetWorkerSandboxRuntimesRequest)(nil), // 129: substrate.ateapi.v1.SetWorkerSandboxRuntimesRequest + (*SetWorkerSandboxRuntimesResponse)(nil), // 130: substrate.ateapi.v1.SetWorkerSandboxRuntimesResponse + (*MintWorkerActorCertificateRequest)(nil), // 131: substrate.ateapi.v1.MintWorkerActorCertificateRequest + (*MintWorkerActorCertificateResponse)(nil), // 132: substrate.ateapi.v1.MintWorkerActorCertificateResponse + (*MintWorkerActorJwtRequest)(nil), // 133: substrate.ateapi.v1.MintWorkerActorJwtRequest + (*MintWorkerActorJwtResponse)(nil), // 134: substrate.ateapi.v1.MintWorkerActorJwtResponse + (*RequestActorSuspendRequest)(nil), // 135: substrate.ateapi.v1.RequestActorSuspendRequest + (*RequestActorSuspendResponse)(nil), // 136: substrate.ateapi.v1.RequestActorSuspendResponse + (*ReportActorCrashedRequest)(nil), // 137: substrate.ateapi.v1.ReportActorCrashedRequest + (*ReportActorCrashedResponse)(nil), // 138: substrate.ateapi.v1.ReportActorCrashedResponse + (*ReportActorSnapshotStateRequest)(nil), // 139: substrate.ateapi.v1.ReportActorSnapshotStateRequest + (*ReportActorSnapshotStateResponse)(nil), // 140: substrate.ateapi.v1.ReportActorSnapshotStateResponse + (*ActorSnapshotState)(nil), // 141: substrate.ateapi.v1.ActorSnapshotState + (*SnapshotReport)(nil), // 142: substrate.ateapi.v1.SnapshotReport + (*AccessPolicy)(nil), // 143: substrate.ateapi.v1.AccessPolicy + (*Binding)(nil), // 144: substrate.ateapi.v1.Binding + (*GetGlobalAccessPolicyRequest)(nil), // 145: substrate.ateapi.v1.GetGlobalAccessPolicyRequest + (*CreateGlobalAccessPolicyRequest)(nil), // 146: substrate.ateapi.v1.CreateGlobalAccessPolicyRequest + (*UpdateGlobalAccessPolicyRequest)(nil), // 147: substrate.ateapi.v1.UpdateGlobalAccessPolicyRequest + (*GetAtespaceAccessPolicyRequest)(nil), // 148: substrate.ateapi.v1.GetAtespaceAccessPolicyRequest + (*CreateAtespaceAccessPolicyRequest)(nil), // 149: substrate.ateapi.v1.CreateAtespaceAccessPolicyRequest + (*UpdateAtespaceAccessPolicyRequest)(nil), // 150: substrate.ateapi.v1.UpdateAtespaceAccessPolicyRequest + (*DeleteAtespaceAccessPolicyRequest)(nil), // 151: substrate.ateapi.v1.DeleteAtespaceAccessPolicyRequest + (*ResourceAnnotation)(nil), // 152: substrate.ateapi.v1.ResourceAnnotation + (*MethodAnnotation)(nil), // 153: substrate.ateapi.v1.MethodAnnotation + nil, // 154: substrate.ateapi.v1.Selector.MatchLabelsEntry + nil, // 155: substrate.ateapi.v1.ExternalVolumeStatus.VolumeContextEntry + nil, // 156: substrate.ateapi.v1.Worker.LabelsEntry + (*timestamppb.Timestamp)(nil), // 157: google.protobuf.Timestamp + (*descriptorpb.MessageOptions)(nil), // 158: google.protobuf.MessageOptions + (*descriptorpb.MethodOptions)(nil), // 159: google.protobuf.MethodOptions } var file_api_ateapi_v1_ateapi_proto_depIdxs = []int32{ 1, // 0: substrate.ateapi.v1.SnapshotStorage.status:type_name -> substrate.ateapi.v1.SnapshotStorageStatus - 10, // 1: substrate.ateapi.v1.SnapshotStorage.object:type_name -> substrate.ateapi.v1.ObjectSnapshot + 11, // 1: substrate.ateapi.v1.SnapshotStorage.object:type_name -> substrate.ateapi.v1.ObjectSnapshot 0, // 2: substrate.ateapi.v1.Snapshot.owner:type_name -> substrate.ateapi.v1.SnapshotOwner 2, // 3: substrate.ateapi.v1.Snapshot.fidelity:type_name -> substrate.ateapi.v1.SnapshotFidelity - 11, // 4: substrate.ateapi.v1.Snapshot.durable_snapshot:type_name -> substrate.ateapi.v1.SnapshotStorage - 117, // 5: substrate.ateapi.v1.Snapshot.sandbox_runtime:type_name -> substrate.ateapi.v1.ActorSandboxRuntime - 151, // 6: substrate.ateapi.v1.Selector.match_labels:type_name -> substrate.ateapi.v1.Selector.MatchLabelsEntry - 154, // 7: substrate.ateapi.v1.ResourceMetadata.create_time:type_name -> google.protobuf.Timestamp - 154, // 8: substrate.ateapi.v1.ResourceMetadata.update_time:type_name -> google.protobuf.Timestamp - 154, // 9: substrate.ateapi.v1.ResourceMetadata.delete_time:type_name -> google.protobuf.Timestamp - 16, // 10: substrate.ateapi.v1.ExternalVolumes.volumes:type_name -> substrate.ateapi.v1.ExternalVolumeStatus - 152, // 11: substrate.ateapi.v1.ExternalVolumeStatus.volume_context:type_name -> substrate.ateapi.v1.ExternalVolumeStatus.VolumeContextEntry - 14, // 12: substrate.ateapi.v1.Actor.metadata:type_name -> substrate.ateapi.v1.ResourceMetadata - 35, // 13: substrate.ateapi.v1.Actor.actor_template:type_name -> substrate.ateapi.v1.ObjectRef - 13, // 14: substrate.ateapi.v1.Actor.worker_selector:type_name -> substrate.ateapi.v1.Selector - 35, // 15: substrate.ateapi.v1.Actor.source_tag:type_name -> substrate.ateapi.v1.ObjectRef - 29, // 16: substrate.ateapi.v1.Actor.status:type_name -> substrate.ateapi.v1.ActorStatus - 14, // 17: substrate.ateapi.v1.EgressPolicy.metadata:type_name -> substrate.ateapi.v1.ResourceMetadata - 20, // 18: substrate.ateapi.v1.EgressPolicy.rules:type_name -> substrate.ateapi.v1.EgressRule - 20, // 19: substrate.ateapi.v1.EgressPolicyTemplate.rules:type_name -> substrate.ateapi.v1.EgressRule - 21, // 20: substrate.ateapi.v1.EgressRule.http:type_name -> substrate.ateapi.v1.HttpRule - 22, // 21: substrate.ateapi.v1.EgressRule.https:type_name -> substrate.ateapi.v1.HttpsRule - 23, // 22: substrate.ateapi.v1.EgressRule.tls_passthrough:type_name -> substrate.ateapi.v1.TlsPassthroughRule - 24, // 23: substrate.ateapi.v1.HttpRule.ports:type_name -> substrate.ateapi.v1.Ports - 26, // 24: substrate.ateapi.v1.HttpRule.effects:type_name -> substrate.ateapi.v1.HttpRuleEffects - 24, // 25: substrate.ateapi.v1.HttpsRule.ports:type_name -> substrate.ateapi.v1.Ports - 26, // 26: substrate.ateapi.v1.HttpsRule.effects:type_name -> substrate.ateapi.v1.HttpRuleEffects - 24, // 27: substrate.ateapi.v1.TlsPassthroughRule.ports:type_name -> substrate.ateapi.v1.Ports - 25, // 28: substrate.ateapi.v1.Ports.all:type_name -> substrate.ateapi.v1.AllPorts - 27, // 29: substrate.ateapi.v1.HttpRuleEffects.replace_headers:type_name -> substrate.ateapi.v1.CredentialHeader - 28, // 30: substrate.ateapi.v1.CredentialHeader.actor_jwt:type_name -> substrate.ateapi.v1.ActorJwtSource + 12, // 4: substrate.ateapi.v1.Snapshot.durable_snapshot:type_name -> substrate.ateapi.v1.SnapshotStorage + 118, // 5: substrate.ateapi.v1.Snapshot.sandbox_runtime:type_name -> substrate.ateapi.v1.ActorSandboxRuntime + 154, // 6: substrate.ateapi.v1.Selector.match_labels:type_name -> substrate.ateapi.v1.Selector.MatchLabelsEntry + 157, // 7: substrate.ateapi.v1.ResourceMetadata.create_time:type_name -> google.protobuf.Timestamp + 157, // 8: substrate.ateapi.v1.ResourceMetadata.update_time:type_name -> google.protobuf.Timestamp + 157, // 9: substrate.ateapi.v1.ResourceMetadata.delete_time:type_name -> google.protobuf.Timestamp + 17, // 10: substrate.ateapi.v1.ExternalVolumes.volumes:type_name -> substrate.ateapi.v1.ExternalVolumeStatus + 155, // 11: substrate.ateapi.v1.ExternalVolumeStatus.volume_context:type_name -> substrate.ateapi.v1.ExternalVolumeStatus.VolumeContextEntry + 15, // 12: substrate.ateapi.v1.Actor.metadata:type_name -> substrate.ateapi.v1.ResourceMetadata + 36, // 13: substrate.ateapi.v1.Actor.actor_template:type_name -> substrate.ateapi.v1.ObjectRef + 14, // 14: substrate.ateapi.v1.Actor.worker_selector:type_name -> substrate.ateapi.v1.Selector + 36, // 15: substrate.ateapi.v1.Actor.source_tag:type_name -> substrate.ateapi.v1.ObjectRef + 30, // 16: substrate.ateapi.v1.Actor.status:type_name -> substrate.ateapi.v1.ActorStatus + 15, // 17: substrate.ateapi.v1.EgressPolicy.metadata:type_name -> substrate.ateapi.v1.ResourceMetadata + 21, // 18: substrate.ateapi.v1.EgressPolicy.rules:type_name -> substrate.ateapi.v1.EgressRule + 21, // 19: substrate.ateapi.v1.EgressPolicyTemplate.rules:type_name -> substrate.ateapi.v1.EgressRule + 22, // 20: substrate.ateapi.v1.EgressRule.http:type_name -> substrate.ateapi.v1.HttpRule + 23, // 21: substrate.ateapi.v1.EgressRule.https:type_name -> substrate.ateapi.v1.HttpsRule + 24, // 22: substrate.ateapi.v1.EgressRule.tls_passthrough:type_name -> substrate.ateapi.v1.TlsPassthroughRule + 25, // 23: substrate.ateapi.v1.HttpRule.ports:type_name -> substrate.ateapi.v1.Ports + 27, // 24: substrate.ateapi.v1.HttpRule.effects:type_name -> substrate.ateapi.v1.HttpRuleEffects + 25, // 25: substrate.ateapi.v1.HttpsRule.ports:type_name -> substrate.ateapi.v1.Ports + 27, // 26: substrate.ateapi.v1.HttpsRule.effects:type_name -> substrate.ateapi.v1.HttpRuleEffects + 25, // 27: substrate.ateapi.v1.TlsPassthroughRule.ports:type_name -> substrate.ateapi.v1.Ports + 26, // 28: substrate.ateapi.v1.Ports.all:type_name -> substrate.ateapi.v1.AllPorts + 28, // 29: substrate.ateapi.v1.HttpRuleEffects.replace_headers:type_name -> substrate.ateapi.v1.CredentialHeader + 29, // 30: substrate.ateapi.v1.CredentialHeader.actor_jwt:type_name -> substrate.ateapi.v1.ActorJwtSource 4, // 31: substrate.ateapi.v1.ActorStatus.state:type_name -> substrate.ateapi.v1.ActorState - 31, // 32: substrate.ateapi.v1.ActorStatus.worker_assignment:type_name -> substrate.ateapi.v1.WorkerAssignment - 15, // 33: substrate.ateapi.v1.ActorStatus.external_volumes:type_name -> substrate.ateapi.v1.ExternalVolumes - 30, // 34: substrate.ateapi.v1.ActorStatus.crash:type_name -> substrate.ateapi.v1.ActorCrash - 12, // 35: substrate.ateapi.v1.ActorStatus.snapshots:type_name -> substrate.ateapi.v1.Snapshot - 154, // 36: substrate.ateapi.v1.ActorCrash.crash_time:type_name -> google.protobuf.Timestamp - 35, // 37: substrate.ateapi.v1.WorkerAssignment.worker:type_name -> substrate.ateapi.v1.ObjectRef - 117, // 38: substrate.ateapi.v1.WorkerAssignment.sandbox_runtime:type_name -> substrate.ateapi.v1.ActorSandboxRuntime - 12, // 39: substrate.ateapi.v1.TagStatus.snapshot:type_name -> substrate.ateapi.v1.Snapshot - 14, // 40: substrate.ateapi.v1.Tag.metadata:type_name -> substrate.ateapi.v1.ResourceMetadata - 32, // 41: substrate.ateapi.v1.Tag.status:type_name -> substrate.ateapi.v1.TagStatus + 32, // 32: substrate.ateapi.v1.ActorStatus.worker_assignment:type_name -> substrate.ateapi.v1.WorkerAssignment + 16, // 33: substrate.ateapi.v1.ActorStatus.external_volumes:type_name -> substrate.ateapi.v1.ExternalVolumes + 31, // 34: substrate.ateapi.v1.ActorStatus.crash:type_name -> substrate.ateapi.v1.ActorCrash + 13, // 35: substrate.ateapi.v1.ActorStatus.snapshots:type_name -> substrate.ateapi.v1.Snapshot + 157, // 36: substrate.ateapi.v1.ActorCrash.crash_time:type_name -> google.protobuf.Timestamp + 36, // 37: substrate.ateapi.v1.WorkerAssignment.worker:type_name -> substrate.ateapi.v1.ObjectRef + 118, // 38: substrate.ateapi.v1.WorkerAssignment.sandbox_runtime:type_name -> substrate.ateapi.v1.ActorSandboxRuntime + 13, // 39: substrate.ateapi.v1.TagStatus.snapshot:type_name -> substrate.ateapi.v1.Snapshot + 15, // 40: substrate.ateapi.v1.Tag.metadata:type_name -> substrate.ateapi.v1.ResourceMetadata + 33, // 41: substrate.ateapi.v1.Tag.status:type_name -> substrate.ateapi.v1.TagStatus 3, // 42: substrate.ateapi.v1.Tag.scope:type_name -> substrate.ateapi.v1.TagScope - 35, // 43: substrate.ateapi.v1.Tag.source_actor:type_name -> substrate.ateapi.v1.ObjectRef - 14, // 44: substrate.ateapi.v1.Atespace.metadata:type_name -> substrate.ateapi.v1.ResourceMetadata - 14, // 45: substrate.ateapi.v1.ActorTemplate.metadata:type_name -> substrate.ateapi.v1.ResourceMetadata - 13, // 46: substrate.ateapi.v1.ActorTemplate.worker_selector:type_name -> substrate.ateapi.v1.Selector - 43, // 47: substrate.ateapi.v1.ActorTemplate.containers:type_name -> substrate.ateapi.v1.Container - 49, // 48: substrate.ateapi.v1.ActorTemplate.volumes:type_name -> substrate.ateapi.v1.Volume - 41, // 49: substrate.ateapi.v1.ActorTemplate.snapshot_config:type_name -> substrate.ateapi.v1.SnapshotConfig - 37, // 50: substrate.ateapi.v1.ActorTemplate.resources:type_name -> substrate.ateapi.v1.ResourceRequirements - 19, // 51: substrate.ateapi.v1.ActorTemplate.golden_egress_policy:type_name -> substrate.ateapi.v1.EgressPolicyTemplate - 40, // 52: substrate.ateapi.v1.ActorTemplate.status:type_name -> substrate.ateapi.v1.ActorTemplateStatus - 38, // 53: substrate.ateapi.v1.ResourceRequirements.limits:type_name -> substrate.ateapi.v1.Resources - 35, // 54: substrate.ateapi.v1.GoldenSnapshotStatus.golden_tag:type_name -> substrate.ateapi.v1.ObjectRef - 154, // 55: substrate.ateapi.v1.GoldenSnapshotStatus.snapshot_time:type_name -> google.protobuf.Timestamp - 39, // 56: substrate.ateapi.v1.ActorTemplateStatus.golden_snapshot_status:type_name -> substrate.ateapi.v1.GoldenSnapshotStatus + 36, // 43: substrate.ateapi.v1.Tag.source_actor:type_name -> substrate.ateapi.v1.ObjectRef + 15, // 44: substrate.ateapi.v1.Atespace.metadata:type_name -> substrate.ateapi.v1.ResourceMetadata + 15, // 45: substrate.ateapi.v1.ActorTemplate.metadata:type_name -> substrate.ateapi.v1.ResourceMetadata + 14, // 46: substrate.ateapi.v1.ActorTemplate.worker_selector:type_name -> substrate.ateapi.v1.Selector + 44, // 47: substrate.ateapi.v1.ActorTemplate.containers:type_name -> substrate.ateapi.v1.Container + 50, // 48: substrate.ateapi.v1.ActorTemplate.volumes:type_name -> substrate.ateapi.v1.Volume + 42, // 49: substrate.ateapi.v1.ActorTemplate.snapshot_config:type_name -> substrate.ateapi.v1.SnapshotConfig + 38, // 50: substrate.ateapi.v1.ActorTemplate.resources:type_name -> substrate.ateapi.v1.ResourceRequirements + 20, // 51: substrate.ateapi.v1.ActorTemplate.golden_egress_policy:type_name -> substrate.ateapi.v1.EgressPolicyTemplate + 41, // 52: substrate.ateapi.v1.ActorTemplate.status:type_name -> substrate.ateapi.v1.ActorTemplateStatus + 39, // 53: substrate.ateapi.v1.ResourceRequirements.limits:type_name -> substrate.ateapi.v1.Resources + 36, // 54: substrate.ateapi.v1.GoldenSnapshotStatus.golden_tag:type_name -> substrate.ateapi.v1.ObjectRef + 157, // 55: substrate.ateapi.v1.GoldenSnapshotStatus.snapshot_time:type_name -> google.protobuf.Timestamp + 40, // 56: substrate.ateapi.v1.ActorTemplateStatus.golden_snapshot_status:type_name -> substrate.ateapi.v1.GoldenSnapshotStatus 2, // 57: substrate.ateapi.v1.SnapshotConfig.preferred_fidelity:type_name -> substrate.ateapi.v1.SnapshotFidelity - 42, // 58: substrate.ateapi.v1.SnapshotConfig.golden_snapshot_config:type_name -> substrate.ateapi.v1.GoldenSnapshotConfig + 43, // 58: substrate.ateapi.v1.SnapshotConfig.golden_snapshot_config:type_name -> substrate.ateapi.v1.GoldenSnapshotConfig 5, // 59: substrate.ateapi.v1.GoldenSnapshotConfig.mode:type_name -> substrate.ateapi.v1.GoldenSnapshotMode - 46, // 60: substrate.ateapi.v1.Container.env:type_name -> substrate.ateapi.v1.EnvVar - 47, // 61: substrate.ateapi.v1.Container.wakeup_probe:type_name -> substrate.ateapi.v1.WakeupProbe - 59, // 62: substrate.ateapi.v1.Container.volume_mounts:type_name -> substrate.ateapi.v1.VolumeMount - 44, // 63: substrate.ateapi.v1.Container.security_context:type_name -> substrate.ateapi.v1.SecurityContext - 37, // 64: substrate.ateapi.v1.Container.resources:type_name -> substrate.ateapi.v1.ResourceRequirements - 45, // 65: substrate.ateapi.v1.SecurityContext.capabilities:type_name -> substrate.ateapi.v1.Capabilities - 48, // 66: substrate.ateapi.v1.WakeupProbe.http_get:type_name -> substrate.ateapi.v1.HttpGetAction - 52, // 67: substrate.ateapi.v1.Volume.durable_dir:type_name -> substrate.ateapi.v1.DurableDirVolumeSource - 50, // 68: substrate.ateapi.v1.Volume.external:type_name -> substrate.ateapi.v1.ExternalVolumeSource - 54, // 69: substrate.ateapi.v1.Volume.system_info:type_name -> substrate.ateapi.v1.SystemInfoVolumeSource - 51, // 70: substrate.ateapi.v1.Volume.image:type_name -> substrate.ateapi.v1.ImageVolumeSource - 53, // 71: substrate.ateapi.v1.ExternalVolumeSource.template:type_name -> substrate.ateapi.v1.ExternalVolumeTemplate - 55, // 72: substrate.ateapi.v1.SystemInfoVolumeSource.data_sources:type_name -> substrate.ateapi.v1.SystemInfoDataSource - 56, // 73: substrate.ateapi.v1.SystemInfoDataSource.actor_metadata:type_name -> substrate.ateapi.v1.ActorMetadataDataSource - 58, // 74: substrate.ateapi.v1.SystemInfoDataSource.trust_bundle:type_name -> substrate.ateapi.v1.TrustBundleDataSource - 57, // 75: substrate.ateapi.v1.ActorMetadataDataSource.items:type_name -> substrate.ateapi.v1.ActorMetadataItem + 47, // 60: substrate.ateapi.v1.Container.env:type_name -> substrate.ateapi.v1.EnvVar + 48, // 61: substrate.ateapi.v1.Container.wakeup_probe:type_name -> substrate.ateapi.v1.WakeupProbe + 60, // 62: substrate.ateapi.v1.Container.volume_mounts:type_name -> substrate.ateapi.v1.VolumeMount + 45, // 63: substrate.ateapi.v1.Container.security_context:type_name -> substrate.ateapi.v1.SecurityContext + 38, // 64: substrate.ateapi.v1.Container.resources:type_name -> substrate.ateapi.v1.ResourceRequirements + 46, // 65: substrate.ateapi.v1.SecurityContext.capabilities:type_name -> substrate.ateapi.v1.Capabilities + 49, // 66: substrate.ateapi.v1.WakeupProbe.http_get:type_name -> substrate.ateapi.v1.HttpGetAction + 53, // 67: substrate.ateapi.v1.Volume.durable_dir:type_name -> substrate.ateapi.v1.DurableDirVolumeSource + 51, // 68: substrate.ateapi.v1.Volume.external:type_name -> substrate.ateapi.v1.ExternalVolumeSource + 55, // 69: substrate.ateapi.v1.Volume.system_info:type_name -> substrate.ateapi.v1.SystemInfoVolumeSource + 52, // 70: substrate.ateapi.v1.Volume.image:type_name -> substrate.ateapi.v1.ImageVolumeSource + 54, // 71: substrate.ateapi.v1.ExternalVolumeSource.template:type_name -> substrate.ateapi.v1.ExternalVolumeTemplate + 56, // 72: substrate.ateapi.v1.SystemInfoVolumeSource.data_sources:type_name -> substrate.ateapi.v1.SystemInfoDataSource + 57, // 73: substrate.ateapi.v1.SystemInfoDataSource.actor_metadata:type_name -> substrate.ateapi.v1.ActorMetadataDataSource + 59, // 74: substrate.ateapi.v1.SystemInfoDataSource.trust_bundle:type_name -> substrate.ateapi.v1.TrustBundleDataSource + 58, // 75: substrate.ateapi.v1.ActorMetadataDataSource.items:type_name -> substrate.ateapi.v1.ActorMetadataItem 6, // 76: substrate.ateapi.v1.ActorMetadataItem.field:type_name -> substrate.ateapi.v1.ActorMetadataField - 34, // 77: substrate.ateapi.v1.CreateAtespaceRequest.atespace:type_name -> substrate.ateapi.v1.Atespace - 35, // 78: substrate.ateapi.v1.GetAtespaceRequest.atespace:type_name -> substrate.ateapi.v1.ObjectRef - 96, // 79: substrate.ateapi.v1.ListAtespacesRequest.options:type_name -> substrate.ateapi.v1.ListOptions - 34, // 80: substrate.ateapi.v1.ListAtespacesResponse.atespaces:type_name -> substrate.ateapi.v1.Atespace - 35, // 81: substrate.ateapi.v1.DeleteAtespaceRequest.atespace:type_name -> substrate.ateapi.v1.ObjectRef - 95, // 82: substrate.ateapi.v1.DeleteAtespaceRequest.options:type_name -> substrate.ateapi.v1.DeleteOptions - 36, // 83: substrate.ateapi.v1.CreateActorTemplateRequest.actor_template:type_name -> substrate.ateapi.v1.ActorTemplate - 35, // 84: substrate.ateapi.v1.GetActorTemplateRequest.actor_template:type_name -> substrate.ateapi.v1.ObjectRef - 96, // 85: substrate.ateapi.v1.ListActorTemplatesRequest.options:type_name -> substrate.ateapi.v1.ListOptions - 36, // 86: substrate.ateapi.v1.ListActorTemplatesResponse.actor_templates:type_name -> substrate.ateapi.v1.ActorTemplate - 35, // 87: substrate.ateapi.v1.DeleteActorTemplateRequest.actor_template:type_name -> substrate.ateapi.v1.ObjectRef - 95, // 88: substrate.ateapi.v1.DeleteActorTemplateRequest.options:type_name -> substrate.ateapi.v1.DeleteOptions - 35, // 89: substrate.ateapi.v1.GetActorRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef - 17, // 90: substrate.ateapi.v1.CreateActorRequest.actor:type_name -> substrate.ateapi.v1.Actor - 17, // 91: substrate.ateapi.v1.UpdateActorRequest.actor:type_name -> substrate.ateapi.v1.Actor - 35, // 92: substrate.ateapi.v1.SuspendActorRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef - 75, // 93: substrate.ateapi.v1.SuspendActorRequest.options:type_name -> substrate.ateapi.v1.SuspendOptions - 17, // 94: substrate.ateapi.v1.SuspendActorResponse.actor:type_name -> substrate.ateapi.v1.Actor + 35, // 77: substrate.ateapi.v1.CreateAtespaceRequest.atespace:type_name -> substrate.ateapi.v1.Atespace + 36, // 78: substrate.ateapi.v1.GetAtespaceRequest.atespace:type_name -> substrate.ateapi.v1.ObjectRef + 97, // 79: substrate.ateapi.v1.ListAtespacesRequest.options:type_name -> substrate.ateapi.v1.ListOptions + 35, // 80: substrate.ateapi.v1.ListAtespacesResponse.atespaces:type_name -> substrate.ateapi.v1.Atespace + 36, // 81: substrate.ateapi.v1.DeleteAtespaceRequest.atespace:type_name -> substrate.ateapi.v1.ObjectRef + 96, // 82: substrate.ateapi.v1.DeleteAtespaceRequest.options:type_name -> substrate.ateapi.v1.DeleteOptions + 37, // 83: substrate.ateapi.v1.CreateActorTemplateRequest.actor_template:type_name -> substrate.ateapi.v1.ActorTemplate + 36, // 84: substrate.ateapi.v1.GetActorTemplateRequest.actor_template:type_name -> substrate.ateapi.v1.ObjectRef + 97, // 85: substrate.ateapi.v1.ListActorTemplatesRequest.options:type_name -> substrate.ateapi.v1.ListOptions + 37, // 86: substrate.ateapi.v1.ListActorTemplatesResponse.actor_templates:type_name -> substrate.ateapi.v1.ActorTemplate + 36, // 87: substrate.ateapi.v1.DeleteActorTemplateRequest.actor_template:type_name -> substrate.ateapi.v1.ObjectRef + 96, // 88: substrate.ateapi.v1.DeleteActorTemplateRequest.options:type_name -> substrate.ateapi.v1.DeleteOptions + 36, // 89: substrate.ateapi.v1.GetActorRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef + 18, // 90: substrate.ateapi.v1.CreateActorRequest.actor:type_name -> substrate.ateapi.v1.Actor + 18, // 91: substrate.ateapi.v1.UpdateActorRequest.actor:type_name -> substrate.ateapi.v1.Actor + 36, // 92: substrate.ateapi.v1.SuspendActorRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef + 76, // 93: substrate.ateapi.v1.SuspendActorRequest.options:type_name -> substrate.ateapi.v1.SuspendOptions + 18, // 94: substrate.ateapi.v1.SuspendActorResponse.actor:type_name -> substrate.ateapi.v1.Actor 7, // 95: substrate.ateapi.v1.SuspendOptions.mode:type_name -> substrate.ateapi.v1.DurabilityMode - 35, // 96: substrate.ateapi.v1.ResumeActorRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef - 17, // 97: substrate.ateapi.v1.ResumeActorResponse.actor:type_name -> substrate.ateapi.v1.Actor - 35, // 98: substrate.ateapi.v1.RevertActorRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef - 17, // 99: substrate.ateapi.v1.RevertActorResponse.actor:type_name -> substrate.ateapi.v1.Actor - 35, // 100: substrate.ateapi.v1.DeleteActorRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef - 95, // 101: substrate.ateapi.v1.DeleteActorRequest.options:type_name -> substrate.ateapi.v1.DeleteOptions - 35, // 102: substrate.ateapi.v1.GetActorEgressPolicyRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef - 35, // 103: substrate.ateapi.v1.CreateActorEgressPolicyRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef - 18, // 104: substrate.ateapi.v1.CreateActorEgressPolicyRequest.egress_policy:type_name -> substrate.ateapi.v1.EgressPolicy - 35, // 105: substrate.ateapi.v1.UpdateActorEgressPolicyRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef - 18, // 106: substrate.ateapi.v1.UpdateActorEgressPolicyRequest.egress_policy:type_name -> substrate.ateapi.v1.EgressPolicy - 35, // 107: substrate.ateapi.v1.DeleteActorEgressPolicyRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef - 95, // 108: substrate.ateapi.v1.DeleteActorEgressPolicyRequest.options:type_name -> substrate.ateapi.v1.DeleteOptions - 35, // 109: substrate.ateapi.v1.GetTagRequest.tag:type_name -> substrate.ateapi.v1.ObjectRef - 35, // 110: substrate.ateapi.v1.MintActorJwtRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef - 154, // 111: substrate.ateapi.v1.MintActorJwtResponse.expire_time:type_name -> google.protobuf.Timestamp - 35, // 112: substrate.ateapi.v1.MintActorCertificateRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef - 96, // 113: substrate.ateapi.v1.ListTagsRequest.options:type_name -> substrate.ateapi.v1.ListOptions - 33, // 114: substrate.ateapi.v1.ListTagsResponse.tags:type_name -> substrate.ateapi.v1.Tag - 33, // 115: substrate.ateapi.v1.CreateTagRequest.tag:type_name -> substrate.ateapi.v1.Tag - 33, // 116: substrate.ateapi.v1.UpdateTagRequest.tag:type_name -> substrate.ateapi.v1.Tag - 35, // 117: substrate.ateapi.v1.DeleteTagRequest.tag:type_name -> substrate.ateapi.v1.ObjectRef - 95, // 118: substrate.ateapi.v1.DeleteTagRequest.options:type_name -> substrate.ateapi.v1.DeleteOptions - 35, // 119: substrate.ateapi.v1.ListWorkerActorAssignmentsRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef - 96, // 120: substrate.ateapi.v1.ListWorkerActorAssignmentsRequest.options:type_name -> substrate.ateapi.v1.ListOptions - 121, // 121: substrate.ateapi.v1.ListWorkerActorAssignmentsResponse.actor_assignments:type_name -> substrate.ateapi.v1.ActorAssignment - 96, // 122: substrate.ateapi.v1.ListWorkersRequest.options:type_name -> substrate.ateapi.v1.ListOptions - 113, // 123: substrate.ateapi.v1.ListWorkersResponse.workers:type_name -> substrate.ateapi.v1.Worker - 35, // 124: substrate.ateapi.v1.GetWorkerRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef - 113, // 125: substrate.ateapi.v1.CreateWorkerRequest.worker:type_name -> substrate.ateapi.v1.Worker - 113, // 126: substrate.ateapi.v1.UpdateWorkerRequest.worker:type_name -> substrate.ateapi.v1.Worker - 35, // 127: substrate.ateapi.v1.DeleteWorkerRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef - 95, // 128: substrate.ateapi.v1.DeleteWorkerRequest.options:type_name -> substrate.ateapi.v1.DeleteOptions - 35, // 129: substrate.ateapi.v1.DrainWorkerRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef - 113, // 130: substrate.ateapi.v1.DrainWorkerResponse.worker:type_name -> substrate.ateapi.v1.Worker - 35, // 131: substrate.ateapi.v1.MarkWorkerAvailableRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef - 113, // 132: substrate.ateapi.v1.MarkWorkerAvailableResponse.worker:type_name -> substrate.ateapi.v1.Worker - 35, // 133: substrate.ateapi.v1.MarkWorkerUnavailableRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef - 113, // 134: substrate.ateapi.v1.MarkWorkerUnavailableResponse.worker:type_name -> substrate.ateapi.v1.Worker - 96, // 135: substrate.ateapi.v1.ListActorsRequest.options:type_name -> substrate.ateapi.v1.ListOptions - 17, // 136: substrate.ateapi.v1.ListActorsResponse.actors:type_name -> substrate.ateapi.v1.Actor - 14, // 137: substrate.ateapi.v1.Worker.metadata:type_name -> substrate.ateapi.v1.ResourceMetadata - 153, // 138: substrate.ateapi.v1.Worker.labels:type_name -> substrate.ateapi.v1.Worker.LabelsEntry - 114, // 139: substrate.ateapi.v1.Worker.status:type_name -> substrate.ateapi.v1.WorkerStatus + 36, // 96: substrate.ateapi.v1.ResumeActorRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef + 18, // 97: substrate.ateapi.v1.ResumeActorResponse.actor:type_name -> substrate.ateapi.v1.Actor + 36, // 98: substrate.ateapi.v1.RevertActorRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef + 18, // 99: substrate.ateapi.v1.RevertActorResponse.actor:type_name -> substrate.ateapi.v1.Actor + 36, // 100: substrate.ateapi.v1.DeleteActorRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef + 96, // 101: substrate.ateapi.v1.DeleteActorRequest.options:type_name -> substrate.ateapi.v1.DeleteOptions + 36, // 102: substrate.ateapi.v1.GetActorEgressPolicyRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef + 36, // 103: substrate.ateapi.v1.CreateActorEgressPolicyRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef + 19, // 104: substrate.ateapi.v1.CreateActorEgressPolicyRequest.egress_policy:type_name -> substrate.ateapi.v1.EgressPolicy + 36, // 105: substrate.ateapi.v1.UpdateActorEgressPolicyRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef + 19, // 106: substrate.ateapi.v1.UpdateActorEgressPolicyRequest.egress_policy:type_name -> substrate.ateapi.v1.EgressPolicy + 36, // 107: substrate.ateapi.v1.DeleteActorEgressPolicyRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef + 96, // 108: substrate.ateapi.v1.DeleteActorEgressPolicyRequest.options:type_name -> substrate.ateapi.v1.DeleteOptions + 36, // 109: substrate.ateapi.v1.GetTagRequest.tag:type_name -> substrate.ateapi.v1.ObjectRef + 36, // 110: substrate.ateapi.v1.MintActorJwtRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef + 157, // 111: substrate.ateapi.v1.MintActorJwtResponse.expire_time:type_name -> google.protobuf.Timestamp + 36, // 112: substrate.ateapi.v1.MintActorCertificateRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef + 97, // 113: substrate.ateapi.v1.ListTagsRequest.options:type_name -> substrate.ateapi.v1.ListOptions + 34, // 114: substrate.ateapi.v1.ListTagsResponse.tags:type_name -> substrate.ateapi.v1.Tag + 34, // 115: substrate.ateapi.v1.CreateTagRequest.tag:type_name -> substrate.ateapi.v1.Tag + 34, // 116: substrate.ateapi.v1.UpdateTagRequest.tag:type_name -> substrate.ateapi.v1.Tag + 36, // 117: substrate.ateapi.v1.DeleteTagRequest.tag:type_name -> substrate.ateapi.v1.ObjectRef + 96, // 118: substrate.ateapi.v1.DeleteTagRequest.options:type_name -> substrate.ateapi.v1.DeleteOptions + 36, // 119: substrate.ateapi.v1.ListWorkerActorAssignmentsRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef + 97, // 120: substrate.ateapi.v1.ListWorkerActorAssignmentsRequest.options:type_name -> substrate.ateapi.v1.ListOptions + 122, // 121: substrate.ateapi.v1.ListWorkerActorAssignmentsResponse.actor_assignments:type_name -> substrate.ateapi.v1.ActorAssignment + 97, // 122: substrate.ateapi.v1.ListWorkersRequest.options:type_name -> substrate.ateapi.v1.ListOptions + 114, // 123: substrate.ateapi.v1.ListWorkersResponse.workers:type_name -> substrate.ateapi.v1.Worker + 36, // 124: substrate.ateapi.v1.GetWorkerRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef + 114, // 125: substrate.ateapi.v1.CreateWorkerRequest.worker:type_name -> substrate.ateapi.v1.Worker + 114, // 126: substrate.ateapi.v1.UpdateWorkerRequest.worker:type_name -> substrate.ateapi.v1.Worker + 36, // 127: substrate.ateapi.v1.DeleteWorkerRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef + 96, // 128: substrate.ateapi.v1.DeleteWorkerRequest.options:type_name -> substrate.ateapi.v1.DeleteOptions + 36, // 129: substrate.ateapi.v1.DrainWorkerRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef + 114, // 130: substrate.ateapi.v1.DrainWorkerResponse.worker:type_name -> substrate.ateapi.v1.Worker + 36, // 131: substrate.ateapi.v1.MarkWorkerAvailableRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef + 114, // 132: substrate.ateapi.v1.MarkWorkerAvailableResponse.worker:type_name -> substrate.ateapi.v1.Worker + 36, // 133: substrate.ateapi.v1.MarkWorkerUnavailableRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef + 114, // 134: substrate.ateapi.v1.MarkWorkerUnavailableResponse.worker:type_name -> substrate.ateapi.v1.Worker + 97, // 135: substrate.ateapi.v1.ListActorsRequest.options:type_name -> substrate.ateapi.v1.ListOptions + 18, // 136: substrate.ateapi.v1.ListActorsResponse.actors:type_name -> substrate.ateapi.v1.Actor + 15, // 137: substrate.ateapi.v1.Worker.metadata:type_name -> substrate.ateapi.v1.ResourceMetadata + 156, // 138: substrate.ateapi.v1.Worker.labels:type_name -> substrate.ateapi.v1.Worker.LabelsEntry + 115, // 139: substrate.ateapi.v1.Worker.status:type_name -> substrate.ateapi.v1.WorkerStatus 8, // 140: substrate.ateapi.v1.WorkerStatus.state:type_name -> substrate.ateapi.v1.WorkerState - 120, // 141: substrate.ateapi.v1.WorkerStatus.capacity:type_name -> substrate.ateapi.v1.WorkerResources - 120, // 142: substrate.ateapi.v1.WorkerStatus.allocated:type_name -> substrate.ateapi.v1.WorkerResources - 115, // 143: substrate.ateapi.v1.WorkerStatus.sandbox_classes:type_name -> substrate.ateapi.v1.WorkerSandboxClass - 116, // 144: substrate.ateapi.v1.WorkerSandboxClass.default_runtime:type_name -> substrate.ateapi.v1.WorkerSandboxRuntime - 116, // 145: substrate.ateapi.v1.WorkerSandboxClass.restorable_runtimes:type_name -> substrate.ateapi.v1.WorkerSandboxRuntime - 118, // 146: substrate.ateapi.v1.WorkerSandboxRuntime.version:type_name -> substrate.ateapi.v1.VersionedSandboxCompat - 118, // 147: substrate.ateapi.v1.ActorSandboxRuntime.version:type_name -> substrate.ateapi.v1.VersionedSandboxCompat - 119, // 148: substrate.ateapi.v1.VersionedSandboxCompat.attributes:type_name -> substrate.ateapi.v1.AttributeEntry - 14, // 149: substrate.ateapi.v1.ActorAssignment.metadata:type_name -> substrate.ateapi.v1.ResourceMetadata - 35, // 150: substrate.ateapi.v1.ActorAssignment.actor:type_name -> substrate.ateapi.v1.ObjectRef - 38, // 151: substrate.ateapi.v1.ActorAssignment.resources:type_name -> substrate.ateapi.v1.Resources - 35, // 152: substrate.ateapi.v1.AcquireWorkerEpochRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef - 35, // 153: substrate.ateapi.v1.RegisterWorkerRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef - 120, // 154: substrate.ateapi.v1.RegisterWorkerRequest.capacity:type_name -> substrate.ateapi.v1.WorkerResources - 113, // 155: substrate.ateapi.v1.RegisterWorkerResponse.worker:type_name -> substrate.ateapi.v1.Worker - 35, // 156: substrate.ateapi.v1.SetWorkerSandboxRuntimesRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef - 115, // 157: substrate.ateapi.v1.SetWorkerSandboxRuntimesRequest.sandbox_classes:type_name -> substrate.ateapi.v1.WorkerSandboxClass - 113, // 158: substrate.ateapi.v1.SetWorkerSandboxRuntimesResponse.worker:type_name -> substrate.ateapi.v1.Worker - 35, // 159: substrate.ateapi.v1.MintWorkerActorCertificateRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef - 35, // 160: substrate.ateapi.v1.MintWorkerActorCertificateRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef - 35, // 161: substrate.ateapi.v1.MintWorkerActorJwtRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef - 154, // 162: substrate.ateapi.v1.MintWorkerActorJwtResponse.expire_time:type_name -> google.protobuf.Timestamp - 35, // 163: substrate.ateapi.v1.RequestActorSuspendRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef - 35, // 164: substrate.ateapi.v1.RequestActorSuspendRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef - 17, // 165: substrate.ateapi.v1.RequestActorSuspendResponse.actor:type_name -> substrate.ateapi.v1.Actor - 35, // 166: substrate.ateapi.v1.ReportActorCrashedRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef - 35, // 167: substrate.ateapi.v1.ReportActorCrashedRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef - 17, // 168: substrate.ateapi.v1.ReportActorCrashedResponse.actor:type_name -> substrate.ateapi.v1.Actor - 35, // 169: substrate.ateapi.v1.ReportActorSnapshotStateRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef - 138, // 170: substrate.ateapi.v1.ReportActorSnapshotStateRequest.state:type_name -> substrate.ateapi.v1.ActorSnapshotState - 139, // 171: substrate.ateapi.v1.ActorSnapshotState.latest_local:type_name -> substrate.ateapi.v1.SnapshotReport - 139, // 172: substrate.ateapi.v1.ActorSnapshotState.uploading:type_name -> substrate.ateapi.v1.SnapshotReport - 139, // 173: substrate.ateapi.v1.ActorSnapshotState.latest_durable:type_name -> substrate.ateapi.v1.SnapshotReport - 154, // 174: substrate.ateapi.v1.ActorSnapshotState.upload_due_time:type_name -> google.protobuf.Timestamp - 2, // 175: substrate.ateapi.v1.SnapshotReport.fidelity:type_name -> substrate.ateapi.v1.SnapshotFidelity - 10, // 176: substrate.ateapi.v1.SnapshotReport.object:type_name -> substrate.ateapi.v1.ObjectSnapshot - 154, // 177: substrate.ateapi.v1.SnapshotReport.local_commit_time:type_name -> google.protobuf.Timestamp - 154, // 178: substrate.ateapi.v1.SnapshotReport.durable_complete_time:type_name -> google.protobuf.Timestamp - 14, // 179: substrate.ateapi.v1.AccessPolicy.metadata:type_name -> substrate.ateapi.v1.ResourceMetadata - 141, // 180: substrate.ateapi.v1.AccessPolicy.bindings:type_name -> substrate.ateapi.v1.Binding - 140, // 181: substrate.ateapi.v1.CreateGlobalAccessPolicyRequest.access_policy:type_name -> substrate.ateapi.v1.AccessPolicy - 140, // 182: substrate.ateapi.v1.UpdateGlobalAccessPolicyRequest.access_policy:type_name -> substrate.ateapi.v1.AccessPolicy - 35, // 183: substrate.ateapi.v1.GetAtespaceAccessPolicyRequest.atespace:type_name -> substrate.ateapi.v1.ObjectRef - 35, // 184: substrate.ateapi.v1.CreateAtespaceAccessPolicyRequest.atespace:type_name -> substrate.ateapi.v1.ObjectRef - 140, // 185: substrate.ateapi.v1.CreateAtespaceAccessPolicyRequest.access_policy:type_name -> substrate.ateapi.v1.AccessPolicy - 35, // 186: substrate.ateapi.v1.UpdateAtespaceAccessPolicyRequest.atespace:type_name -> substrate.ateapi.v1.ObjectRef - 140, // 187: substrate.ateapi.v1.UpdateAtespaceAccessPolicyRequest.access_policy:type_name -> substrate.ateapi.v1.AccessPolicy - 35, // 188: substrate.ateapi.v1.DeleteAtespaceAccessPolicyRequest.atespace:type_name -> substrate.ateapi.v1.ObjectRef - 95, // 189: substrate.ateapi.v1.DeleteAtespaceAccessPolicyRequest.options:type_name -> substrate.ateapi.v1.DeleteOptions - 9, // 190: substrate.ateapi.v1.ResourceAnnotation.scope:type_name -> substrate.ateapi.v1.Scope - 155, // 191: substrate.ateapi.v1.resource:extendee -> google.protobuf.MessageOptions - 156, // 192: substrate.ateapi.v1.method:extendee -> google.protobuf.MethodOptions - 149, // 193: substrate.ateapi.v1.resource:type_name -> substrate.ateapi.v1.ResourceAnnotation - 150, // 194: substrate.ateapi.v1.method:type_name -> substrate.ateapi.v1.MethodAnnotation - 70, // 195: substrate.ateapi.v1.Control.GetActor:input_type -> substrate.ateapi.v1.GetActorRequest - 71, // 196: substrate.ateapi.v1.Control.CreateActor:input_type -> substrate.ateapi.v1.CreateActorRequest - 72, // 197: substrate.ateapi.v1.Control.UpdateActor:input_type -> substrate.ateapi.v1.UpdateActorRequest - 73, // 198: substrate.ateapi.v1.Control.SuspendActor:input_type -> substrate.ateapi.v1.SuspendActorRequest - 76, // 199: substrate.ateapi.v1.Control.ResumeActor:input_type -> substrate.ateapi.v1.ResumeActorRequest - 78, // 200: substrate.ateapi.v1.Control.RevertActor:input_type -> substrate.ateapi.v1.RevertActorRequest - 80, // 201: substrate.ateapi.v1.Control.DeleteActor:input_type -> substrate.ateapi.v1.DeleteActorRequest - 81, // 202: substrate.ateapi.v1.Control.GetActorEgressPolicy:input_type -> substrate.ateapi.v1.GetActorEgressPolicyRequest - 82, // 203: substrate.ateapi.v1.Control.CreateActorEgressPolicy:input_type -> substrate.ateapi.v1.CreateActorEgressPolicyRequest - 83, // 204: substrate.ateapi.v1.Control.UpdateActorEgressPolicy:input_type -> substrate.ateapi.v1.UpdateActorEgressPolicyRequest - 84, // 205: substrate.ateapi.v1.Control.DeleteActorEgressPolicy:input_type -> substrate.ateapi.v1.DeleteActorEgressPolicyRequest - 86, // 206: substrate.ateapi.v1.Control.MintActorJwt:input_type -> substrate.ateapi.v1.MintActorJwtRequest - 88, // 207: substrate.ateapi.v1.Control.MintActorCertificate:input_type -> substrate.ateapi.v1.MintActorCertificateRequest - 92, // 208: substrate.ateapi.v1.Control.CreateTag:input_type -> substrate.ateapi.v1.CreateTagRequest - 85, // 209: substrate.ateapi.v1.Control.GetTag:input_type -> substrate.ateapi.v1.GetTagRequest - 90, // 210: substrate.ateapi.v1.Control.ListTags:input_type -> substrate.ateapi.v1.ListTagsRequest - 93, // 211: substrate.ateapi.v1.Control.UpdateTag:input_type -> substrate.ateapi.v1.UpdateTagRequest - 94, // 212: substrate.ateapi.v1.Control.DeleteTag:input_type -> substrate.ateapi.v1.DeleteTagRequest - 99, // 213: substrate.ateapi.v1.Control.ListWorkers:input_type -> substrate.ateapi.v1.ListWorkersRequest - 101, // 214: substrate.ateapi.v1.Control.GetWorker:input_type -> substrate.ateapi.v1.GetWorkerRequest - 102, // 215: substrate.ateapi.v1.Control.CreateWorker:input_type -> substrate.ateapi.v1.CreateWorkerRequest - 103, // 216: substrate.ateapi.v1.Control.UpdateWorker:input_type -> substrate.ateapi.v1.UpdateWorkerRequest - 104, // 217: substrate.ateapi.v1.Control.DeleteWorker:input_type -> substrate.ateapi.v1.DeleteWorkerRequest - 105, // 218: substrate.ateapi.v1.Control.DrainWorker:input_type -> substrate.ateapi.v1.DrainWorkerRequest - 107, // 219: substrate.ateapi.v1.Control.MarkWorkerAvailable:input_type -> substrate.ateapi.v1.MarkWorkerAvailableRequest - 109, // 220: substrate.ateapi.v1.Control.MarkWorkerUnavailable:input_type -> substrate.ateapi.v1.MarkWorkerUnavailableRequest - 97, // 221: substrate.ateapi.v1.Control.ListWorkerActorAssignments:input_type -> substrate.ateapi.v1.ListWorkerActorAssignmentsRequest - 111, // 222: substrate.ateapi.v1.Control.ListActors:input_type -> substrate.ateapi.v1.ListActorsRequest - 60, // 223: substrate.ateapi.v1.Control.CreateAtespace:input_type -> substrate.ateapi.v1.CreateAtespaceRequest - 61, // 224: substrate.ateapi.v1.Control.GetAtespace:input_type -> substrate.ateapi.v1.GetAtespaceRequest - 62, // 225: substrate.ateapi.v1.Control.ListAtespaces:input_type -> substrate.ateapi.v1.ListAtespacesRequest - 64, // 226: substrate.ateapi.v1.Control.DeleteAtespace:input_type -> substrate.ateapi.v1.DeleteAtespaceRequest - 65, // 227: substrate.ateapi.v1.Control.CreateActorTemplate:input_type -> substrate.ateapi.v1.CreateActorTemplateRequest - 66, // 228: substrate.ateapi.v1.Control.GetActorTemplate:input_type -> substrate.ateapi.v1.GetActorTemplateRequest - 67, // 229: substrate.ateapi.v1.Control.ListActorTemplates:input_type -> substrate.ateapi.v1.ListActorTemplatesRequest - 69, // 230: substrate.ateapi.v1.Control.DeleteActorTemplate:input_type -> substrate.ateapi.v1.DeleteActorTemplateRequest - 142, // 231: substrate.ateapi.v1.Control.GetGlobalAccessPolicy:input_type -> substrate.ateapi.v1.GetGlobalAccessPolicyRequest - 143, // 232: substrate.ateapi.v1.Control.CreateGlobalAccessPolicy:input_type -> substrate.ateapi.v1.CreateGlobalAccessPolicyRequest - 144, // 233: substrate.ateapi.v1.Control.UpdateGlobalAccessPolicy:input_type -> substrate.ateapi.v1.UpdateGlobalAccessPolicyRequest - 145, // 234: substrate.ateapi.v1.Control.GetAtespaceAccessPolicy:input_type -> substrate.ateapi.v1.GetAtespaceAccessPolicyRequest - 146, // 235: substrate.ateapi.v1.Control.CreateAtespaceAccessPolicy:input_type -> substrate.ateapi.v1.CreateAtespaceAccessPolicyRequest - 147, // 236: substrate.ateapi.v1.Control.UpdateAtespaceAccessPolicy:input_type -> substrate.ateapi.v1.UpdateAtespaceAccessPolicyRequest - 148, // 237: substrate.ateapi.v1.Control.DeleteAtespaceAccessPolicy:input_type -> substrate.ateapi.v1.DeleteAtespaceAccessPolicyRequest - 122, // 238: substrate.ateapi.v1.WorkerService.AcquireWorkerEpoch:input_type -> substrate.ateapi.v1.AcquireWorkerEpochRequest - 124, // 239: substrate.ateapi.v1.WorkerService.RegisterWorker:input_type -> substrate.ateapi.v1.RegisterWorkerRequest - 126, // 240: substrate.ateapi.v1.WorkerService.SetWorkerSandboxRuntimes:input_type -> substrate.ateapi.v1.SetWorkerSandboxRuntimesRequest - 128, // 241: substrate.ateapi.v1.WorkerService.MintWorkerActorCertificate:input_type -> substrate.ateapi.v1.MintWorkerActorCertificateRequest - 130, // 242: substrate.ateapi.v1.WorkerService.MintWorkerActorJwt:input_type -> substrate.ateapi.v1.MintWorkerActorJwtRequest - 132, // 243: substrate.ateapi.v1.WorkerService.RequestActorSuspend:input_type -> substrate.ateapi.v1.RequestActorSuspendRequest - 136, // 244: substrate.ateapi.v1.WorkerService.ReportActorSnapshotState:input_type -> substrate.ateapi.v1.ReportActorSnapshotStateRequest - 134, // 245: substrate.ateapi.v1.WorkerService.ReportActorCrashed:input_type -> substrate.ateapi.v1.ReportActorCrashedRequest - 17, // 246: substrate.ateapi.v1.Control.GetActor:output_type -> substrate.ateapi.v1.Actor - 17, // 247: substrate.ateapi.v1.Control.CreateActor:output_type -> substrate.ateapi.v1.Actor - 17, // 248: substrate.ateapi.v1.Control.UpdateActor:output_type -> substrate.ateapi.v1.Actor - 74, // 249: substrate.ateapi.v1.Control.SuspendActor:output_type -> substrate.ateapi.v1.SuspendActorResponse - 77, // 250: substrate.ateapi.v1.Control.ResumeActor:output_type -> substrate.ateapi.v1.ResumeActorResponse - 79, // 251: substrate.ateapi.v1.Control.RevertActor:output_type -> substrate.ateapi.v1.RevertActorResponse - 17, // 252: substrate.ateapi.v1.Control.DeleteActor:output_type -> substrate.ateapi.v1.Actor - 18, // 253: substrate.ateapi.v1.Control.GetActorEgressPolicy:output_type -> substrate.ateapi.v1.EgressPolicy - 18, // 254: substrate.ateapi.v1.Control.CreateActorEgressPolicy:output_type -> substrate.ateapi.v1.EgressPolicy - 18, // 255: substrate.ateapi.v1.Control.UpdateActorEgressPolicy:output_type -> substrate.ateapi.v1.EgressPolicy - 18, // 256: substrate.ateapi.v1.Control.DeleteActorEgressPolicy:output_type -> substrate.ateapi.v1.EgressPolicy - 87, // 257: substrate.ateapi.v1.Control.MintActorJwt:output_type -> substrate.ateapi.v1.MintActorJwtResponse - 89, // 258: substrate.ateapi.v1.Control.MintActorCertificate:output_type -> substrate.ateapi.v1.MintActorCertificateResponse - 33, // 259: substrate.ateapi.v1.Control.CreateTag:output_type -> substrate.ateapi.v1.Tag - 33, // 260: substrate.ateapi.v1.Control.GetTag:output_type -> substrate.ateapi.v1.Tag - 91, // 261: substrate.ateapi.v1.Control.ListTags:output_type -> substrate.ateapi.v1.ListTagsResponse - 33, // 262: substrate.ateapi.v1.Control.UpdateTag:output_type -> substrate.ateapi.v1.Tag - 33, // 263: substrate.ateapi.v1.Control.DeleteTag:output_type -> substrate.ateapi.v1.Tag - 100, // 264: substrate.ateapi.v1.Control.ListWorkers:output_type -> substrate.ateapi.v1.ListWorkersResponse - 113, // 265: substrate.ateapi.v1.Control.GetWorker:output_type -> substrate.ateapi.v1.Worker - 113, // 266: substrate.ateapi.v1.Control.CreateWorker:output_type -> substrate.ateapi.v1.Worker - 113, // 267: substrate.ateapi.v1.Control.UpdateWorker:output_type -> substrate.ateapi.v1.Worker - 113, // 268: substrate.ateapi.v1.Control.DeleteWorker:output_type -> substrate.ateapi.v1.Worker - 106, // 269: substrate.ateapi.v1.Control.DrainWorker:output_type -> substrate.ateapi.v1.DrainWorkerResponse - 108, // 270: substrate.ateapi.v1.Control.MarkWorkerAvailable:output_type -> substrate.ateapi.v1.MarkWorkerAvailableResponse - 110, // 271: substrate.ateapi.v1.Control.MarkWorkerUnavailable:output_type -> substrate.ateapi.v1.MarkWorkerUnavailableResponse - 98, // 272: substrate.ateapi.v1.Control.ListWorkerActorAssignments:output_type -> substrate.ateapi.v1.ListWorkerActorAssignmentsResponse - 112, // 273: substrate.ateapi.v1.Control.ListActors:output_type -> substrate.ateapi.v1.ListActorsResponse - 34, // 274: substrate.ateapi.v1.Control.CreateAtespace:output_type -> substrate.ateapi.v1.Atespace - 34, // 275: substrate.ateapi.v1.Control.GetAtespace:output_type -> substrate.ateapi.v1.Atespace - 63, // 276: substrate.ateapi.v1.Control.ListAtespaces:output_type -> substrate.ateapi.v1.ListAtespacesResponse - 34, // 277: substrate.ateapi.v1.Control.DeleteAtespace:output_type -> substrate.ateapi.v1.Atespace - 36, // 278: substrate.ateapi.v1.Control.CreateActorTemplate:output_type -> substrate.ateapi.v1.ActorTemplate - 36, // 279: substrate.ateapi.v1.Control.GetActorTemplate:output_type -> substrate.ateapi.v1.ActorTemplate - 68, // 280: substrate.ateapi.v1.Control.ListActorTemplates:output_type -> substrate.ateapi.v1.ListActorTemplatesResponse - 36, // 281: substrate.ateapi.v1.Control.DeleteActorTemplate:output_type -> substrate.ateapi.v1.ActorTemplate - 140, // 282: substrate.ateapi.v1.Control.GetGlobalAccessPolicy:output_type -> substrate.ateapi.v1.AccessPolicy - 140, // 283: substrate.ateapi.v1.Control.CreateGlobalAccessPolicy:output_type -> substrate.ateapi.v1.AccessPolicy - 140, // 284: substrate.ateapi.v1.Control.UpdateGlobalAccessPolicy:output_type -> substrate.ateapi.v1.AccessPolicy - 140, // 285: substrate.ateapi.v1.Control.GetAtespaceAccessPolicy:output_type -> substrate.ateapi.v1.AccessPolicy - 140, // 286: substrate.ateapi.v1.Control.CreateAtespaceAccessPolicy:output_type -> substrate.ateapi.v1.AccessPolicy - 140, // 287: substrate.ateapi.v1.Control.UpdateAtespaceAccessPolicy:output_type -> substrate.ateapi.v1.AccessPolicy - 140, // 288: substrate.ateapi.v1.Control.DeleteAtespaceAccessPolicy:output_type -> substrate.ateapi.v1.AccessPolicy - 123, // 289: substrate.ateapi.v1.WorkerService.AcquireWorkerEpoch:output_type -> substrate.ateapi.v1.AcquireWorkerEpochResponse - 125, // 290: substrate.ateapi.v1.WorkerService.RegisterWorker:output_type -> substrate.ateapi.v1.RegisterWorkerResponse - 127, // 291: substrate.ateapi.v1.WorkerService.SetWorkerSandboxRuntimes:output_type -> substrate.ateapi.v1.SetWorkerSandboxRuntimesResponse - 129, // 292: substrate.ateapi.v1.WorkerService.MintWorkerActorCertificate:output_type -> substrate.ateapi.v1.MintWorkerActorCertificateResponse - 131, // 293: substrate.ateapi.v1.WorkerService.MintWorkerActorJwt:output_type -> substrate.ateapi.v1.MintWorkerActorJwtResponse - 133, // 294: substrate.ateapi.v1.WorkerService.RequestActorSuspend:output_type -> substrate.ateapi.v1.RequestActorSuspendResponse - 137, // 295: substrate.ateapi.v1.WorkerService.ReportActorSnapshotState:output_type -> substrate.ateapi.v1.ReportActorSnapshotStateResponse - 135, // 296: substrate.ateapi.v1.WorkerService.ReportActorCrashed:output_type -> substrate.ateapi.v1.ReportActorCrashedResponse - 246, // [246:297] is the sub-list for method output_type - 195, // [195:246] is the sub-list for method input_type - 193, // [193:195] is the sub-list for extension type_name - 191, // [191:193] is the sub-list for extension extendee - 0, // [0:191] is the sub-list for field type_name + 121, // 141: substrate.ateapi.v1.WorkerStatus.capacity:type_name -> substrate.ateapi.v1.WorkerResources + 121, // 142: substrate.ateapi.v1.WorkerStatus.allocated:type_name -> substrate.ateapi.v1.WorkerResources + 116, // 143: substrate.ateapi.v1.WorkerStatus.sandbox_classes:type_name -> substrate.ateapi.v1.WorkerSandboxClass + 117, // 144: substrate.ateapi.v1.WorkerSandboxClass.default_runtime:type_name -> substrate.ateapi.v1.WorkerSandboxRuntime + 117, // 145: substrate.ateapi.v1.WorkerSandboxClass.restorable_runtimes:type_name -> substrate.ateapi.v1.WorkerSandboxRuntime + 119, // 146: substrate.ateapi.v1.WorkerSandboxRuntime.version:type_name -> substrate.ateapi.v1.VersionedSandboxCompat + 119, // 147: substrate.ateapi.v1.ActorSandboxRuntime.version:type_name -> substrate.ateapi.v1.VersionedSandboxCompat + 120, // 148: substrate.ateapi.v1.VersionedSandboxCompat.attributes:type_name -> substrate.ateapi.v1.AttributeEntry + 15, // 149: substrate.ateapi.v1.ActorAssignment.metadata:type_name -> substrate.ateapi.v1.ResourceMetadata + 36, // 150: substrate.ateapi.v1.ActorAssignment.actor:type_name -> substrate.ateapi.v1.ObjectRef + 39, // 151: substrate.ateapi.v1.ActorAssignment.resources:type_name -> substrate.ateapi.v1.Resources + 9, // 152: substrate.ateapi.v1.WhoAmIResponse.kind:type_name -> substrate.ateapi.v1.PrincipalKind + 36, // 153: substrate.ateapi.v1.AcquireWorkerEpochRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef + 36, // 154: substrate.ateapi.v1.RegisterWorkerRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef + 121, // 155: substrate.ateapi.v1.RegisterWorkerRequest.capacity:type_name -> substrate.ateapi.v1.WorkerResources + 114, // 156: substrate.ateapi.v1.RegisterWorkerResponse.worker:type_name -> substrate.ateapi.v1.Worker + 36, // 157: substrate.ateapi.v1.SetWorkerSandboxRuntimesRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef + 116, // 158: substrate.ateapi.v1.SetWorkerSandboxRuntimesRequest.sandbox_classes:type_name -> substrate.ateapi.v1.WorkerSandboxClass + 114, // 159: substrate.ateapi.v1.SetWorkerSandboxRuntimesResponse.worker:type_name -> substrate.ateapi.v1.Worker + 36, // 160: substrate.ateapi.v1.MintWorkerActorCertificateRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef + 36, // 161: substrate.ateapi.v1.MintWorkerActorCertificateRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef + 36, // 162: substrate.ateapi.v1.MintWorkerActorJwtRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef + 157, // 163: substrate.ateapi.v1.MintWorkerActorJwtResponse.expire_time:type_name -> google.protobuf.Timestamp + 36, // 164: substrate.ateapi.v1.RequestActorSuspendRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef + 36, // 165: substrate.ateapi.v1.RequestActorSuspendRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef + 18, // 166: substrate.ateapi.v1.RequestActorSuspendResponse.actor:type_name -> substrate.ateapi.v1.Actor + 36, // 167: substrate.ateapi.v1.ReportActorCrashedRequest.worker:type_name -> substrate.ateapi.v1.ObjectRef + 36, // 168: substrate.ateapi.v1.ReportActorCrashedRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef + 18, // 169: substrate.ateapi.v1.ReportActorCrashedResponse.actor:type_name -> substrate.ateapi.v1.Actor + 36, // 170: substrate.ateapi.v1.ReportActorSnapshotStateRequest.actor:type_name -> substrate.ateapi.v1.ObjectRef + 141, // 171: substrate.ateapi.v1.ReportActorSnapshotStateRequest.state:type_name -> substrate.ateapi.v1.ActorSnapshotState + 142, // 172: substrate.ateapi.v1.ActorSnapshotState.latest_local:type_name -> substrate.ateapi.v1.SnapshotReport + 142, // 173: substrate.ateapi.v1.ActorSnapshotState.uploading:type_name -> substrate.ateapi.v1.SnapshotReport + 142, // 174: substrate.ateapi.v1.ActorSnapshotState.latest_durable:type_name -> substrate.ateapi.v1.SnapshotReport + 157, // 175: substrate.ateapi.v1.ActorSnapshotState.upload_due_time:type_name -> google.protobuf.Timestamp + 2, // 176: substrate.ateapi.v1.SnapshotReport.fidelity:type_name -> substrate.ateapi.v1.SnapshotFidelity + 11, // 177: substrate.ateapi.v1.SnapshotReport.object:type_name -> substrate.ateapi.v1.ObjectSnapshot + 157, // 178: substrate.ateapi.v1.SnapshotReport.local_commit_time:type_name -> google.protobuf.Timestamp + 157, // 179: substrate.ateapi.v1.SnapshotReport.durable_complete_time:type_name -> google.protobuf.Timestamp + 15, // 180: substrate.ateapi.v1.AccessPolicy.metadata:type_name -> substrate.ateapi.v1.ResourceMetadata + 144, // 181: substrate.ateapi.v1.AccessPolicy.bindings:type_name -> substrate.ateapi.v1.Binding + 143, // 182: substrate.ateapi.v1.CreateGlobalAccessPolicyRequest.access_policy:type_name -> substrate.ateapi.v1.AccessPolicy + 143, // 183: substrate.ateapi.v1.UpdateGlobalAccessPolicyRequest.access_policy:type_name -> substrate.ateapi.v1.AccessPolicy + 36, // 184: substrate.ateapi.v1.GetAtespaceAccessPolicyRequest.atespace:type_name -> substrate.ateapi.v1.ObjectRef + 36, // 185: substrate.ateapi.v1.CreateAtespaceAccessPolicyRequest.atespace:type_name -> substrate.ateapi.v1.ObjectRef + 143, // 186: substrate.ateapi.v1.CreateAtespaceAccessPolicyRequest.access_policy:type_name -> substrate.ateapi.v1.AccessPolicy + 36, // 187: substrate.ateapi.v1.UpdateAtespaceAccessPolicyRequest.atespace:type_name -> substrate.ateapi.v1.ObjectRef + 143, // 188: substrate.ateapi.v1.UpdateAtespaceAccessPolicyRequest.access_policy:type_name -> substrate.ateapi.v1.AccessPolicy + 36, // 189: substrate.ateapi.v1.DeleteAtespaceAccessPolicyRequest.atespace:type_name -> substrate.ateapi.v1.ObjectRef + 96, // 190: substrate.ateapi.v1.DeleteAtespaceAccessPolicyRequest.options:type_name -> substrate.ateapi.v1.DeleteOptions + 10, // 191: substrate.ateapi.v1.ResourceAnnotation.scope:type_name -> substrate.ateapi.v1.Scope + 158, // 192: substrate.ateapi.v1.resource:extendee -> google.protobuf.MessageOptions + 159, // 193: substrate.ateapi.v1.method:extendee -> google.protobuf.MethodOptions + 152, // 194: substrate.ateapi.v1.resource:type_name -> substrate.ateapi.v1.ResourceAnnotation + 153, // 195: substrate.ateapi.v1.method:type_name -> substrate.ateapi.v1.MethodAnnotation + 71, // 196: substrate.ateapi.v1.Control.GetActor:input_type -> substrate.ateapi.v1.GetActorRequest + 72, // 197: substrate.ateapi.v1.Control.CreateActor:input_type -> substrate.ateapi.v1.CreateActorRequest + 73, // 198: substrate.ateapi.v1.Control.UpdateActor:input_type -> substrate.ateapi.v1.UpdateActorRequest + 74, // 199: substrate.ateapi.v1.Control.SuspendActor:input_type -> substrate.ateapi.v1.SuspendActorRequest + 77, // 200: substrate.ateapi.v1.Control.ResumeActor:input_type -> substrate.ateapi.v1.ResumeActorRequest + 79, // 201: substrate.ateapi.v1.Control.RevertActor:input_type -> substrate.ateapi.v1.RevertActorRequest + 81, // 202: substrate.ateapi.v1.Control.DeleteActor:input_type -> substrate.ateapi.v1.DeleteActorRequest + 82, // 203: substrate.ateapi.v1.Control.GetActorEgressPolicy:input_type -> substrate.ateapi.v1.GetActorEgressPolicyRequest + 83, // 204: substrate.ateapi.v1.Control.CreateActorEgressPolicy:input_type -> substrate.ateapi.v1.CreateActorEgressPolicyRequest + 84, // 205: substrate.ateapi.v1.Control.UpdateActorEgressPolicy:input_type -> substrate.ateapi.v1.UpdateActorEgressPolicyRequest + 85, // 206: substrate.ateapi.v1.Control.DeleteActorEgressPolicy:input_type -> substrate.ateapi.v1.DeleteActorEgressPolicyRequest + 87, // 207: substrate.ateapi.v1.Control.MintActorJwt:input_type -> substrate.ateapi.v1.MintActorJwtRequest + 89, // 208: substrate.ateapi.v1.Control.MintActorCertificate:input_type -> substrate.ateapi.v1.MintActorCertificateRequest + 93, // 209: substrate.ateapi.v1.Control.CreateTag:input_type -> substrate.ateapi.v1.CreateTagRequest + 86, // 210: substrate.ateapi.v1.Control.GetTag:input_type -> substrate.ateapi.v1.GetTagRequest + 91, // 211: substrate.ateapi.v1.Control.ListTags:input_type -> substrate.ateapi.v1.ListTagsRequest + 94, // 212: substrate.ateapi.v1.Control.UpdateTag:input_type -> substrate.ateapi.v1.UpdateTagRequest + 95, // 213: substrate.ateapi.v1.Control.DeleteTag:input_type -> substrate.ateapi.v1.DeleteTagRequest + 100, // 214: substrate.ateapi.v1.Control.ListWorkers:input_type -> substrate.ateapi.v1.ListWorkersRequest + 102, // 215: substrate.ateapi.v1.Control.GetWorker:input_type -> substrate.ateapi.v1.GetWorkerRequest + 103, // 216: substrate.ateapi.v1.Control.CreateWorker:input_type -> substrate.ateapi.v1.CreateWorkerRequest + 104, // 217: substrate.ateapi.v1.Control.UpdateWorker:input_type -> substrate.ateapi.v1.UpdateWorkerRequest + 105, // 218: substrate.ateapi.v1.Control.DeleteWorker:input_type -> substrate.ateapi.v1.DeleteWorkerRequest + 106, // 219: substrate.ateapi.v1.Control.DrainWorker:input_type -> substrate.ateapi.v1.DrainWorkerRequest + 108, // 220: substrate.ateapi.v1.Control.MarkWorkerAvailable:input_type -> substrate.ateapi.v1.MarkWorkerAvailableRequest + 110, // 221: substrate.ateapi.v1.Control.MarkWorkerUnavailable:input_type -> substrate.ateapi.v1.MarkWorkerUnavailableRequest + 98, // 222: substrate.ateapi.v1.Control.ListWorkerActorAssignments:input_type -> substrate.ateapi.v1.ListWorkerActorAssignmentsRequest + 112, // 223: substrate.ateapi.v1.Control.ListActors:input_type -> substrate.ateapi.v1.ListActorsRequest + 61, // 224: substrate.ateapi.v1.Control.CreateAtespace:input_type -> substrate.ateapi.v1.CreateAtespaceRequest + 62, // 225: substrate.ateapi.v1.Control.GetAtespace:input_type -> substrate.ateapi.v1.GetAtespaceRequest + 63, // 226: substrate.ateapi.v1.Control.ListAtespaces:input_type -> substrate.ateapi.v1.ListAtespacesRequest + 65, // 227: substrate.ateapi.v1.Control.DeleteAtespace:input_type -> substrate.ateapi.v1.DeleteAtespaceRequest + 66, // 228: substrate.ateapi.v1.Control.CreateActorTemplate:input_type -> substrate.ateapi.v1.CreateActorTemplateRequest + 67, // 229: substrate.ateapi.v1.Control.GetActorTemplate:input_type -> substrate.ateapi.v1.GetActorTemplateRequest + 68, // 230: substrate.ateapi.v1.Control.ListActorTemplates:input_type -> substrate.ateapi.v1.ListActorTemplatesRequest + 70, // 231: substrate.ateapi.v1.Control.DeleteActorTemplate:input_type -> substrate.ateapi.v1.DeleteActorTemplateRequest + 145, // 232: substrate.ateapi.v1.Control.GetGlobalAccessPolicy:input_type -> substrate.ateapi.v1.GetGlobalAccessPolicyRequest + 146, // 233: substrate.ateapi.v1.Control.CreateGlobalAccessPolicy:input_type -> substrate.ateapi.v1.CreateGlobalAccessPolicyRequest + 147, // 234: substrate.ateapi.v1.Control.UpdateGlobalAccessPolicy:input_type -> substrate.ateapi.v1.UpdateGlobalAccessPolicyRequest + 148, // 235: substrate.ateapi.v1.Control.GetAtespaceAccessPolicy:input_type -> substrate.ateapi.v1.GetAtespaceAccessPolicyRequest + 149, // 236: substrate.ateapi.v1.Control.CreateAtespaceAccessPolicy:input_type -> substrate.ateapi.v1.CreateAtespaceAccessPolicyRequest + 150, // 237: substrate.ateapi.v1.Control.UpdateAtespaceAccessPolicy:input_type -> substrate.ateapi.v1.UpdateAtespaceAccessPolicyRequest + 151, // 238: substrate.ateapi.v1.Control.DeleteAtespaceAccessPolicy:input_type -> substrate.ateapi.v1.DeleteAtespaceAccessPolicyRequest + 123, // 239: substrate.ateapi.v1.Authentication.WhoAmI:input_type -> substrate.ateapi.v1.WhoAmIRequest + 125, // 240: substrate.ateapi.v1.WorkerService.AcquireWorkerEpoch:input_type -> substrate.ateapi.v1.AcquireWorkerEpochRequest + 127, // 241: substrate.ateapi.v1.WorkerService.RegisterWorker:input_type -> substrate.ateapi.v1.RegisterWorkerRequest + 129, // 242: substrate.ateapi.v1.WorkerService.SetWorkerSandboxRuntimes:input_type -> substrate.ateapi.v1.SetWorkerSandboxRuntimesRequest + 131, // 243: substrate.ateapi.v1.WorkerService.MintWorkerActorCertificate:input_type -> substrate.ateapi.v1.MintWorkerActorCertificateRequest + 133, // 244: substrate.ateapi.v1.WorkerService.MintWorkerActorJwt:input_type -> substrate.ateapi.v1.MintWorkerActorJwtRequest + 135, // 245: substrate.ateapi.v1.WorkerService.RequestActorSuspend:input_type -> substrate.ateapi.v1.RequestActorSuspendRequest + 139, // 246: substrate.ateapi.v1.WorkerService.ReportActorSnapshotState:input_type -> substrate.ateapi.v1.ReportActorSnapshotStateRequest + 137, // 247: substrate.ateapi.v1.WorkerService.ReportActorCrashed:input_type -> substrate.ateapi.v1.ReportActorCrashedRequest + 18, // 248: substrate.ateapi.v1.Control.GetActor:output_type -> substrate.ateapi.v1.Actor + 18, // 249: substrate.ateapi.v1.Control.CreateActor:output_type -> substrate.ateapi.v1.Actor + 18, // 250: substrate.ateapi.v1.Control.UpdateActor:output_type -> substrate.ateapi.v1.Actor + 75, // 251: substrate.ateapi.v1.Control.SuspendActor:output_type -> substrate.ateapi.v1.SuspendActorResponse + 78, // 252: substrate.ateapi.v1.Control.ResumeActor:output_type -> substrate.ateapi.v1.ResumeActorResponse + 80, // 253: substrate.ateapi.v1.Control.RevertActor:output_type -> substrate.ateapi.v1.RevertActorResponse + 18, // 254: substrate.ateapi.v1.Control.DeleteActor:output_type -> substrate.ateapi.v1.Actor + 19, // 255: substrate.ateapi.v1.Control.GetActorEgressPolicy:output_type -> substrate.ateapi.v1.EgressPolicy + 19, // 256: substrate.ateapi.v1.Control.CreateActorEgressPolicy:output_type -> substrate.ateapi.v1.EgressPolicy + 19, // 257: substrate.ateapi.v1.Control.UpdateActorEgressPolicy:output_type -> substrate.ateapi.v1.EgressPolicy + 19, // 258: substrate.ateapi.v1.Control.DeleteActorEgressPolicy:output_type -> substrate.ateapi.v1.EgressPolicy + 88, // 259: substrate.ateapi.v1.Control.MintActorJwt:output_type -> substrate.ateapi.v1.MintActorJwtResponse + 90, // 260: substrate.ateapi.v1.Control.MintActorCertificate:output_type -> substrate.ateapi.v1.MintActorCertificateResponse + 34, // 261: substrate.ateapi.v1.Control.CreateTag:output_type -> substrate.ateapi.v1.Tag + 34, // 262: substrate.ateapi.v1.Control.GetTag:output_type -> substrate.ateapi.v1.Tag + 92, // 263: substrate.ateapi.v1.Control.ListTags:output_type -> substrate.ateapi.v1.ListTagsResponse + 34, // 264: substrate.ateapi.v1.Control.UpdateTag:output_type -> substrate.ateapi.v1.Tag + 34, // 265: substrate.ateapi.v1.Control.DeleteTag:output_type -> substrate.ateapi.v1.Tag + 101, // 266: substrate.ateapi.v1.Control.ListWorkers:output_type -> substrate.ateapi.v1.ListWorkersResponse + 114, // 267: substrate.ateapi.v1.Control.GetWorker:output_type -> substrate.ateapi.v1.Worker + 114, // 268: substrate.ateapi.v1.Control.CreateWorker:output_type -> substrate.ateapi.v1.Worker + 114, // 269: substrate.ateapi.v1.Control.UpdateWorker:output_type -> substrate.ateapi.v1.Worker + 114, // 270: substrate.ateapi.v1.Control.DeleteWorker:output_type -> substrate.ateapi.v1.Worker + 107, // 271: substrate.ateapi.v1.Control.DrainWorker:output_type -> substrate.ateapi.v1.DrainWorkerResponse + 109, // 272: substrate.ateapi.v1.Control.MarkWorkerAvailable:output_type -> substrate.ateapi.v1.MarkWorkerAvailableResponse + 111, // 273: substrate.ateapi.v1.Control.MarkWorkerUnavailable:output_type -> substrate.ateapi.v1.MarkWorkerUnavailableResponse + 99, // 274: substrate.ateapi.v1.Control.ListWorkerActorAssignments:output_type -> substrate.ateapi.v1.ListWorkerActorAssignmentsResponse + 113, // 275: substrate.ateapi.v1.Control.ListActors:output_type -> substrate.ateapi.v1.ListActorsResponse + 35, // 276: substrate.ateapi.v1.Control.CreateAtespace:output_type -> substrate.ateapi.v1.Atespace + 35, // 277: substrate.ateapi.v1.Control.GetAtespace:output_type -> substrate.ateapi.v1.Atespace + 64, // 278: substrate.ateapi.v1.Control.ListAtespaces:output_type -> substrate.ateapi.v1.ListAtespacesResponse + 35, // 279: substrate.ateapi.v1.Control.DeleteAtespace:output_type -> substrate.ateapi.v1.Atespace + 37, // 280: substrate.ateapi.v1.Control.CreateActorTemplate:output_type -> substrate.ateapi.v1.ActorTemplate + 37, // 281: substrate.ateapi.v1.Control.GetActorTemplate:output_type -> substrate.ateapi.v1.ActorTemplate + 69, // 282: substrate.ateapi.v1.Control.ListActorTemplates:output_type -> substrate.ateapi.v1.ListActorTemplatesResponse + 37, // 283: substrate.ateapi.v1.Control.DeleteActorTemplate:output_type -> substrate.ateapi.v1.ActorTemplate + 143, // 284: substrate.ateapi.v1.Control.GetGlobalAccessPolicy:output_type -> substrate.ateapi.v1.AccessPolicy + 143, // 285: substrate.ateapi.v1.Control.CreateGlobalAccessPolicy:output_type -> substrate.ateapi.v1.AccessPolicy + 143, // 286: substrate.ateapi.v1.Control.UpdateGlobalAccessPolicy:output_type -> substrate.ateapi.v1.AccessPolicy + 143, // 287: substrate.ateapi.v1.Control.GetAtespaceAccessPolicy:output_type -> substrate.ateapi.v1.AccessPolicy + 143, // 288: substrate.ateapi.v1.Control.CreateAtespaceAccessPolicy:output_type -> substrate.ateapi.v1.AccessPolicy + 143, // 289: substrate.ateapi.v1.Control.UpdateAtespaceAccessPolicy:output_type -> substrate.ateapi.v1.AccessPolicy + 143, // 290: substrate.ateapi.v1.Control.DeleteAtespaceAccessPolicy:output_type -> substrate.ateapi.v1.AccessPolicy + 124, // 291: substrate.ateapi.v1.Authentication.WhoAmI:output_type -> substrate.ateapi.v1.WhoAmIResponse + 126, // 292: substrate.ateapi.v1.WorkerService.AcquireWorkerEpoch:output_type -> substrate.ateapi.v1.AcquireWorkerEpochResponse + 128, // 293: substrate.ateapi.v1.WorkerService.RegisterWorker:output_type -> substrate.ateapi.v1.RegisterWorkerResponse + 130, // 294: substrate.ateapi.v1.WorkerService.SetWorkerSandboxRuntimes:output_type -> substrate.ateapi.v1.SetWorkerSandboxRuntimesResponse + 132, // 295: substrate.ateapi.v1.WorkerService.MintWorkerActorCertificate:output_type -> substrate.ateapi.v1.MintWorkerActorCertificateResponse + 134, // 296: substrate.ateapi.v1.WorkerService.MintWorkerActorJwt:output_type -> substrate.ateapi.v1.MintWorkerActorJwtResponse + 136, // 297: substrate.ateapi.v1.WorkerService.RequestActorSuspend:output_type -> substrate.ateapi.v1.RequestActorSuspendResponse + 140, // 298: substrate.ateapi.v1.WorkerService.ReportActorSnapshotState:output_type -> substrate.ateapi.v1.ReportActorSnapshotStateResponse + 138, // 299: substrate.ateapi.v1.WorkerService.ReportActorCrashed:output_type -> substrate.ateapi.v1.ReportActorCrashedResponse + 248, // [248:300] is the sub-list for method output_type + 196, // [196:248] is the sub-list for method input_type + 194, // [194:196] is the sub-list for extension type_name + 192, // [192:194] is the sub-list for extension extendee + 0, // [0:192] is the sub-list for field type_name } func init() { file_api_ateapi_v1_ateapi_proto_init() } @@ -11171,10 +11340,10 @@ func file_api_ateapi_v1_ateapi_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_ateapi_v1_ateapi_proto_rawDesc), len(file_api_ateapi_v1_ateapi_proto_rawDesc)), - NumEnums: 10, - NumMessages: 144, + NumEnums: 11, + NumMessages: 146, NumExtensions: 2, - NumServices: 2, + NumServices: 3, }, GoTypes: file_api_ateapi_v1_ateapi_proto_goTypes, DependencyIndexes: file_api_ateapi_v1_ateapi_proto_depIdxs, diff --git a/api/ateapi/v1/ateapi.proto b/api/ateapi/v1/ateapi.proto index a5e777a4a6..e1f48cab4d 100644 --- a/api/ateapi/v1/ateapi.proto +++ b/api/ateapi/v1/ateapi.proto @@ -2636,6 +2636,37 @@ message ActorAssignment { Resources resources = 4; } +// Authentication reports how ateapi authenticated the caller. It is separate +// from Control because it operates on no resource. +service Authentication { + // WhoAmI returns the principal ateapi authenticated the caller as. Any + // authenticated caller may call it. + rpc WhoAmI(WhoAmIRequest) returns (WhoAmIResponse); +} + +message WhoAmIRequest {} + +message WhoAmIResponse { + // The principal ID that authorization policies name: the subject of a + // bearer JWT, or the first URI SAN of an mTLS client certificate. + string id = 1; + + // How the caller authenticated. + PrincipalKind kind = 2; + + // The issuer of the caller's bearer JWT. Empty for mTLS callers. + string issuer = 3; +} + +// PrincipalKind is the authentication method that established a principal. +enum PrincipalKind { + PRINCIPAL_KIND_UNSPECIFIED = 0; + // A bearer JWT verified against a configured JWT provider. + PRINCIPAL_KIND_JWT = 1; + // An mTLS client certificate. + PRINCIPAL_KIND_MTLS = 2; +} + // WorkerService is how a Worker tells the control plane what it observes. It is // separate from Control because the two have different callers and different // authorization: Control is the client-facing API, while these RPCs are served diff --git a/api/ateapi/v1/ateapi_grpc.pb.go b/api/ateapi/v1/ateapi_grpc.pb.go index 4c2cadb5d9..3519d3ef32 100644 --- a/api/ateapi/v1/ateapi_grpc.pb.go +++ b/api/ateapi/v1/ateapi_grpc.pb.go @@ -1886,6 +1886,118 @@ var Control_ServiceDesc = grpc.ServiceDesc{ Metadata: "api/ateapi/v1/ateapi.proto", } +const ( + Authentication_WhoAmI_FullMethodName = "/substrate.ateapi.v1.Authentication/WhoAmI" +) + +// AuthenticationClient is the client API for Authentication service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// Authentication reports how ateapi authenticated the caller. It is separate +// from Control because it operates on no resource. +type AuthenticationClient interface { + // WhoAmI returns the principal ateapi authenticated the caller as. Any + // authenticated caller may call it. + WhoAmI(ctx context.Context, in *WhoAmIRequest, opts ...grpc.CallOption) (*WhoAmIResponse, error) +} + +type authenticationClient struct { + cc grpc.ClientConnInterface +} + +func NewAuthenticationClient(cc grpc.ClientConnInterface) AuthenticationClient { + return &authenticationClient{cc} +} + +func (c *authenticationClient) WhoAmI(ctx context.Context, in *WhoAmIRequest, opts ...grpc.CallOption) (*WhoAmIResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(WhoAmIResponse) + err := c.cc.Invoke(ctx, Authentication_WhoAmI_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AuthenticationServer is the server API for Authentication service. +// All implementations must embed UnimplementedAuthenticationServer +// for forward compatibility. +// +// Authentication reports how ateapi authenticated the caller. It is separate +// from Control because it operates on no resource. +type AuthenticationServer interface { + // WhoAmI returns the principal ateapi authenticated the caller as. Any + // authenticated caller may call it. + WhoAmI(context.Context, *WhoAmIRequest) (*WhoAmIResponse, error) + mustEmbedUnimplementedAuthenticationServer() +} + +// UnimplementedAuthenticationServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedAuthenticationServer struct{} + +func (UnimplementedAuthenticationServer) WhoAmI(context.Context, *WhoAmIRequest) (*WhoAmIResponse, error) { + return nil, status.Error(codes.Unimplemented, "method WhoAmI not implemented") +} +func (UnimplementedAuthenticationServer) mustEmbedUnimplementedAuthenticationServer() {} +func (UnimplementedAuthenticationServer) testEmbeddedByValue() {} + +// UnsafeAuthenticationServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AuthenticationServer will +// result in compilation errors. +type UnsafeAuthenticationServer interface { + mustEmbedUnimplementedAuthenticationServer() +} + +func RegisterAuthenticationServer(s grpc.ServiceRegistrar, srv AuthenticationServer) { + // If the following call panics, it indicates UnimplementedAuthenticationServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Authentication_ServiceDesc, srv) +} + +func _Authentication_WhoAmI_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(WhoAmIRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthenticationServer).WhoAmI(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Authentication_WhoAmI_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthenticationServer).WhoAmI(ctx, req.(*WhoAmIRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Authentication_ServiceDesc is the grpc.ServiceDesc for Authentication service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Authentication_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "substrate.ateapi.v1.Authentication", + HandlerType: (*AuthenticationServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "WhoAmI", + Handler: _Authentication_WhoAmI_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api/ateapi/v1/ateapi.proto", +} + const ( WorkerService_AcquireWorkerEpoch_FullMethodName = "/substrate.ateapi.v1.WorkerService/AcquireWorkerEpoch" WorkerService_RegisterWorker_FullMethodName = "/substrate.ateapi.v1.WorkerService/RegisterWorker" diff --git a/cmd/ateapi/internal/apiauthn/whoami.go b/cmd/ateapi/internal/apiauthn/whoami.go new file mode 100644 index 0000000000..30a1d6963f --- /dev/null +++ b/cmd/ateapi/internal/apiauthn/whoami.go @@ -0,0 +1,44 @@ +// 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 apiauthn + +import ( + "context" + + "go.ate.dev/substrate/api/ateapi/v1" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/principal" +) + +// AuthenticationService serves the Authentication gRPC service. It reads the +// principal that the interceptor attached, so it must be served behind it. +type AuthenticationService struct { + ateapi.UnimplementedAuthenticationServer +} + +func (AuthenticationService) WhoAmI(ctx context.Context, _ *ateapi.WhoAmIRequest) (*ateapi.WhoAmIResponse, error) { + p, ok := principal.FromContext(ctx) + if !ok || p.ID == "" { + return nil, apierror.Unauthenticated("unauthenticated: missing principal in context") + } + kind := ateapi.PrincipalKind_PRINCIPAL_KIND_UNSPECIFIED + switch p.Kind { + case principal.KindJWT: + kind = ateapi.PrincipalKind_PRINCIPAL_KIND_JWT + case principal.KindMTLS: + kind = ateapi.PrincipalKind_PRINCIPAL_KIND_MTLS + } + return &ateapi.WhoAmIResponse{Id: p.ID, Kind: kind, Issuer: p.Issuer}, nil +} diff --git a/cmd/ateapi/internal/apiauthn/whoami_test.go b/cmd/ateapi/internal/apiauthn/whoami_test.go new file mode 100644 index 0000000000..e3ec8010e8 --- /dev/null +++ b/cmd/ateapi/internal/apiauthn/whoami_test.go @@ -0,0 +1,64 @@ +// 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 apiauthn + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/api/ateapi/v1" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/principal" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestWhoAmI(t *testing.T) { + tests := []struct { + name string + p principal.PrincipalInfo + want *ateapi.WhoAmIResponse + }{ + { + name: "jwt", + p: principal.PrincipalInfo{ID: "system:serviceaccount:ate-users:alice", Kind: principal.KindJWT, Issuer: "https://kubernetes.default.svc"}, + want: &ateapi.WhoAmIResponse{Id: "system:serviceaccount:ate-users:alice", Kind: ateapi.PrincipalKind_PRINCIPAL_KIND_JWT, Issuer: "https://kubernetes.default.svc"}, + }, + { + name: "mtls", + p: principal.PrincipalInfo{ID: "spiffe://cluster.local/ns/ate-system/sa/atelet", Kind: principal.KindMTLS}, + want: &ateapi.WhoAmIResponse{Id: "spiffe://cluster.local/ns/ate-system/sa/atelet", Kind: ateapi.PrincipalKind_PRINCIPAL_KIND_MTLS}, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := AuthenticationService{}.WhoAmI(principal.InjectContext(context.Background(), tc.p), &ateapi.WhoAmIRequest{}) + if err != nil { + t.Fatalf("WhoAmI: %v", err) + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("WhoAmI (-want +got):\n%s", diff) + } + }) + } +} + +func TestWhoAmIWithoutPrincipal(t *testing.T) { + _, err := AuthenticationService{}.WhoAmI(context.Background(), &ateapi.WhoAmIRequest{}) + if got := apierror.Code(err); got != codes.Unauthenticated { + t.Errorf("WhoAmI without principal: code %v, want %v", got, codes.Unauthenticated) + } +} diff --git a/cmd/ateapi/internal/authz/registry.go b/cmd/ateapi/internal/authz/registry.go index 741e716bbd..7168e99693 100644 --- a/cmd/ateapi/internal/authz/registry.go +++ b/cmd/ateapi/internal/authz/registry.go @@ -324,6 +324,9 @@ var defaultRPCPermissions = map[string]rpcRule{ return onTag(RelationCanDelete, r.GetTag(), field.NewPath("tag")) })), + // WhoAmI reveals only the caller's own principal. + ateapi.Authentication_WhoAmI_FullMethodName: inHandler("any authenticated caller"), + // WorkerService is served only to atelet, which each handler // authenticates by its mTLS SPIFFE ID. ateapi.WorkerService_AcquireWorkerEpoch_FullMethodName: inHandler("ateletauth: caller is atelet on the Worker's node"), diff --git a/cmd/ateapi/main.go b/cmd/ateapi/main.go index da71b00d13..8b8292cfb1 100644 --- a/cmd/ateapi/main.go +++ b/cmd/ateapi/main.go @@ -404,6 +404,7 @@ func main() { ) reflection.Register(mux) ateapi.RegisterControlServer(mux, controlSrv) + ateapi.RegisterAuthenticationServer(mux, apiauthn.AuthenticationService{}) ateapi.RegisterWorkerServiceServer(mux, workerservice.New(persistence, controlSrv, ateletSPIFFEID, actorIDCAPool, *actorJWTIssuer, actorIDJWTAuthorityPool)) // Every RPC needs the store, so a replica that cannot reach it leaves the diff --git a/cmd/kubectl-ate/README.md b/cmd/kubectl-ate/README.md index 21c27a77e4..3d3a79053d 100644 --- a/cmd/kubectl-ate/README.md +++ b/cmd/kubectl-ate/README.md @@ -26,6 +26,26 @@ By default, `kubectl-ate` will automatically read your `~/.kube/config`, discove If you prefer to route traffic directly (e.g., through a LoadBalancer or when running natively inside a cluster pod), simply provide the `--endpoint` flag to bypass the tunnel. +## Authentication +`kubectl-ate` authenticates to ate-api with the first of these it is given: + +1. `--token-file`: a bearer token file, re-read on every RPC, or `-` for stdin. +2. `--credential-plugin`, else `$ATE_CREDENTIAL_PLUGIN`: a [kubectl credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins). A path to an existing file runs with no arguments; any other value is split into a command and its arguments, honoring shell quoting. The plugin receives `--kubeconfig` and `--context` as `$ATE_KUBECONFIG` and `$ATE_KUBE_CONTEXT`. `kubectl-ate` caches its credential until the `expirationTimestamp`, and runs it again when ate-api rejects the credential. +3. A token minted for the shared `ate-client` ServiceAccount. + +Passing both `--token-file` and `--credential-plugin` is an error. + +```bash +# Authenticate as your own ServiceAccount (see docs/authentication.md) +export ATE_CREDENTIAL_PLUGIN="kubectl-ate credential --service-account=ate-users/alice" + +# Show the principal ate-api authenticated you as +kubectl ate whoami + +# Print a raw bearer token, e.g. for grpcurl +kubectl ate credential -o token +``` + ## Tracing The CLI supports on-demand tracing using the `--trace` flag. When enabled, the CLI will generate a trace ID and signal to the server that it wants the request to be traced. @@ -67,6 +87,7 @@ These flags can be appended to any command: | `--context` | | Name of the kubeconfig context to use | current context | | `--endpoint` | | Manual gRPC endpoint override (e.g., `localhost:8080`) | | | `--token-file` | | Path to a bearer token for ate-api authentication, or `-` for stdin | Kubernetes ServiceAccount token | +| `--credential-plugin` | | kubectl credential plugin that provides the ate-api credential: an executable's path, or a command line | `$ATE_CREDENTIAL_PLUGIN` | | `--output` | `-o` | Output format (`table`, `json`, `yaml`) | `table` | | `--trace` | | Enable on-demand tracing for the request | `false` | | `--verbose` | `-v` | Increase output verbosity; may be repeated or stacked (e.g., `-v`, `-vv`, `-vvvvv`) | `0` | diff --git a/cmd/kubectl-ate/internal/cmd/credential.go b/cmd/kubectl-ate/internal/cmd/credential.go new file mode 100644 index 0000000000..6768008264 --- /dev/null +++ b/cmd/kubectl-ate/internal/cmd/credential.go @@ -0,0 +1,213 @@ +// 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 cmd + +import ( + "fmt" + "io" + "os" + "os/exec" + "strings" + "time" + + "github.com/spf13/cobra" + "go.ate.dev/substrate/cmd/kubectl-ate/internal/credplugin" + "go.ate.dev/substrate/internal/ateclient" + "k8s.io/client-go/kubernetes" +) + +const ( + credentialLifetime = time.Hour + // credentialMinRemaining is how long a cached token must still be valid + // to be handed out again. + credentialMinRemaining = 5 * time.Minute +) + +var ( + credentialServiceAccount string + credentialAudience string + credentialOutput string + credentialRefresh bool + + // mintServiceAccountToken and newCredentialCache are replaced in tests. + mintServiceAccountToken = ateclient.MintServiceAccountToken + newCredentialCache = credplugin.DefaultCache +) + +var credentialCmd = &cobra.Command{ + Use: "credential", + Short: "Print a credential for ate-api, as a kubeconfig exec credential plugin", + Long: `credential mints a Kubernetes ServiceAccount token for ate-api and prints it +as an ExecCredential, so it can be passed to --credential-plugin (or named in a +kubeconfig exec stanza). Tokens are cached under ~/.ate/cache until shortly +before they expire. + +Without --kubeconfig or --context, it uses $` + kubeconfigEnv + ` and $` + kubeContextEnv + `, +which kubectl ate sets from its own flags when it runs the plugin. The audience +is --audience, else spec.cluster.config.audience from KUBERNETES_EXEC_INFO, else +ate-api's in-cluster Service name.`, + Example: ` # Act as a per-user ServiceAccount you are allowed to create tokens for + export ` + credentialPluginEnv + `="kubectl-ate credential --service-account=ate-users/alice" + kubectl ate whoami + + # Print the shared ate-client ServiceAccount's credential + kubectl ate credential --context=my-cluster + + # A raw token for grpcurl + grpcurl -H "authorization: Bearer $(kubectl ate credential -o token)" ...`, + Args: noArgs(""), + RunE: func(cmd *cobra.Command, _ []string) error { + info, err := credentialInfo() + if err != nil { + return err + } + token, expiry, err := serviceAccountCredential(cmd, info.Audience) + if err != nil { + return err + } + return writeCredential(cmd.OutOrStdout(), info, token, expiry) + }, +} + +var credentialWrapCmd = &cobra.Command{ + Use: "wrap -- COMMAND [ARG...]", + Short: "Run a command that prints a JWT and print it as an ExecCredential", + Long: `wrap runs COMMAND, reads a JWT from its standard output, and prints it as an +ExecCredential that expires at the JWT's exp claim. The token is not cached: +COMMAND is run every time the client needs a credential. + +Only wrap commands whose issuer limits who can obtain a token for ate-api's +configured audience; see docs/authentication.md.`, + Example: ` kubectl ate credential wrap -- my-corp-sso print-token --audience=ate`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + info, err := credentialInfo() + if err != nil { + return err + } + c := exec.CommandContext(cmd.Context(), args[0], args[1:]...) + c.Stderr = cmd.ErrOrStderr() + if info.Interactive { + c.Stdin = os.Stdin + } + out, err := c.Output() + if err != nil { + return fmt.Errorf("run %s: %w", args[0], err) + } + token := strings.TrimSpace(string(out)) + if token == "" { + return fmt.Errorf("%s printed no token", args[0]) + } + expiry, err := credplugin.JWTExpiry(token) + if err != nil { + return fmt.Errorf("%s did not print a JWT: %w", args[0], err) + } + return writeCredential(cmd.OutOrStdout(), info, token, expiry) + }, +} + +func credentialInfo() (credplugin.Info, error) { + if credentialOutput != "json" && credentialOutput != "token" { + return credplugin.Info{}, usageErrorf("invalid output format %q. Must be one of: json, token", credentialOutput) + } + return credplugin.ParseInfo(os.Getenv(credplugin.InfoEnv)) +} + +func writeCredential(out io.Writer, info credplugin.Info, token string, expiry time.Time) error { + if credentialOutput == "token" { + _, err := fmt.Fprintln(out, token) + return err + } + return credplugin.WriteCredential(out, info.APIVersion, token, expiry) +} + +// serviceAccountCredential returns a cached or newly minted token for the +// --service-account ServiceAccount. +func serviceAccountCredential(cmd *cobra.Command, execAudience string) (string, time.Time, error) { + errOut := cmd.ErrOrStderr() + namespace, name := ateclient.DefaultServiceAccount() + if credentialServiceAccount != "" { + var ok bool + namespace, name, ok = strings.Cut(credentialServiceAccount, "/") + if !ok || namespace == "" || name == "" { + return "", time.Time{}, usageErrorf("--service-account must be NAMESPACE/NAME, got %q", credentialServiceAccount) + } + } else { + warnSharedIdentityCredential(errOut) + } + audience := credentialAudience + if audience == "" { + audience = execAudience + } + if audience == "" { + audience = ateclient.DefaultAudience() + } + + kubeconfigPath, kubeContext := kubeconfig, k8sContext + if kubeconfigPath == "" { + kubeconfigPath = os.Getenv(kubeconfigEnv) + } + if kubeContext == "" { + kubeContext = os.Getenv(kubeContextEnv) + } + config, err := loadKubeConfig(errOut, kubeconfigPath, kubeContext) + if err != nil { + return "", time.Time{}, fmt.Errorf("while reading kubeconfig: %w", err) + } + // Keyed by the kubeconfig too: whoever can mint here is not necessarily + // whoever can mint through another kubeconfig context on the same host. + key := []string{config.Host, kubeconfigPath, kubeContext, namespace, name, audience} + cache, err := newCredentialCache() + if err != nil { + vlogf(errOut, vBasic, "Not caching the credential: %v", err) + } else if !credentialRefresh && os.Getenv(ateclient.RefreshCredentialEnv) == "" { + if token, expiry, ok := cache.Get(key, credentialMinRemaining); ok { + vlogf(errOut, vFlow, "Using the cached token for %s/%s, valid until %s", namespace, name, expiry.Format(time.RFC3339)) + return token, expiry, nil + } + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return "", time.Time{}, fmt.Errorf("while creating Kubernetes client: %w", err) + } + token, expiry, err := mintServiceAccountToken(cmd.Context(), clientset, namespace, name, audience, credentialLifetime) + if err != nil { + return "", time.Time{}, err + } + if cache != nil { + if err := cache.Put(key, token, expiry); err != nil { + vlogf(errOut, vBasic, "Failed to cache the credential: %v", err) + } + } + return token, expiry, nil +} + +// warnSharedIdentityCredential warns that the token is for the shared +// fallback ServiceAccount; see warnSharedIdentity. +func warnSharedIdentityCredential(errOut io.Writer) { + fmt.Fprintf(errOut, "Warning: using a token for the shared identity %s rather than one for you. Pass --service-account to use a ServiceAccount of your own.\n", + ateclient.FallbackPrincipal()) +} + +func init() { + credentialCmd.Flags().StringVar(&credentialServiceAccount, "service-account", "", "NAMESPACE/NAME of the ServiceAccount to mint a token for (defaults to the shared ate-client ServiceAccount)") + credentialCmd.Flags().StringVar(&credentialAudience, "audience", "", "audience of the minted token (overrides KUBERNETES_EXEC_INFO)") + credentialCmd.Flags().BoolVar(&credentialRefresh, "refresh", false, "mint a new token and replace the cached one") + // Shadows the root --output, whose formats do not apply here. + credentialCmd.PersistentFlags().StringVarP(&credentialOutput, "output", "o", "json", "output format: json (an ExecCredential) or token (the raw bearer token)") + credentialCmd.AddCommand(credentialWrapCmd) + rootCmd.AddCommand(credentialCmd) +} diff --git a/cmd/kubectl-ate/internal/cmd/credential_test.go b/cmd/kubectl-ate/internal/cmd/credential_test.go new file mode 100644 index 0000000000..e207e05708 --- /dev/null +++ b/cmd/kubectl-ate/internal/cmd/credential_test.go @@ -0,0 +1,208 @@ +// 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 cmd + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "go.ate.dev/substrate/cmd/kubectl-ate/internal/credplugin" + "go.ate.dev/substrate/internal/ateclient" + "k8s.io/client-go/kubernetes" + clientauthv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1" + clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" +) + +const testKubeconfig = `apiVersion: v1 +kind: Config +current-context: kind-a +clusters: +- name: a + cluster: {server: "https://a.invalid"} +- name: b + cluster: {server: "https://b.invalid"} +users: +- name: u + user: {token: kube-token} +contexts: +- name: kind-a + context: {cluster: a, user: u} +- name: kind-b + context: {cluster: b, user: u} +` + +// setupKubeconfigEnv points KUBECONFIG and HOME at fresh files, clears the +// credential plugin variables, and returns the kubeconfig path. +func setupKubeconfigEnv(t *testing.T) string { + t.Helper() + dir := t.TempDir() + kubeconfigPath := filepath.Join(dir, "kubeconfig") + if err := os.WriteFile(kubeconfigPath, []byte(testKubeconfig), 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("KUBECONFIG", kubeconfigPath) + t.Setenv("HOME", filepath.Join(dir, "home")) + for _, env := range []string{credentialPluginEnv, kubeconfigEnv, kubeContextEnv} { + t.Setenv(env, "") + } + return kubeconfigPath +} + +type mintCall struct{ host, namespace, name, audience string } + +// fakeMinter replaces the ServiceAccount token minter and the credential +// cache for the test, and records each mint. +func fakeMinter(t *testing.T) *[]mintCall { + t.Helper() + var calls []mintCall + expiry := time.Now().Add(time.Hour).Truncate(time.Second) + origMint, origCache := mintServiceAccountToken, newCredentialCache + mintServiceAccountToken = func(_ context.Context, clientset kubernetes.Interface, namespace, name, audience string, _ time.Duration) (string, time.Time, error) { + calls = append(calls, mintCall{clientset.CoreV1().RESTClient().Get().URL().Host, namespace, name, audience}) + return fmt.Sprintf("minted-%d", len(calls)), expiry, nil + } + cacheDir := filepath.Join(t.TempDir(), "cache") + newCredentialCache = func() (*credplugin.Cache, error) { + return &credplugin.Cache{Dir: cacheDir, Now: time.Now}, nil + } + t.Cleanup(func() { mintServiceAccountToken, newCredentialCache = origMint, origCache }) + return &calls +} + +func TestCredential(t *testing.T) { + setupKubeconfigEnv(t) + calls := fakeMinter(t) + t.Setenv(credplugin.InfoEnv, `{"apiVersion":"client.authentication.k8s.io/v1","kind":"ExecCredential","spec":{"interactive":false,"cluster":{"server":"https://api.ate-system.svc","config":{"audience":"exec-audience"}}}}`) + + out, errOut, err := runRoot(t, "credential") + if err != nil { + t.Fatalf("credential: %v", err) + } + var cred clientauthv1.ExecCredential + if err := json.Unmarshal([]byte(out), &cred); err != nil { + t.Fatalf("output is not an ExecCredential: %v\n%s", err, out) + } + if cred.APIVersion != credplugin.APIVersionV1 || cred.Status.Token != "minted-1" || cred.Status.ExpirationTimestamp == nil { + t.Errorf("ExecCredential = %+v %+v", cred.TypeMeta, cred.Status) + } + if !strings.Contains(errOut, "shared identity") { + t.Errorf("stderr = %q, want the shared identity warning", errOut) + } + + if _, _, err := runRoot(t, "credential"); err != nil { + t.Fatalf("second credential: %v", err) + } + if out, _, err = runRoot(t, "credential", "--service-account=ate-users/alice", "--audience=flag-audience", "-o", "token"); err != nil { + t.Fatalf("credential --service-account: %v", err) + } + if out != "minted-2\n" { + t.Errorf("-o token output = %q, want minted-2", out) + } + + want := []mintCall{ + {"a.invalid", "ate-system", "ate-client", "exec-audience"}, + {"a.invalid", "ate-users", "alice", "flag-audience"}, + } + if fmt.Sprint(*calls) != fmt.Sprint(want) { + t.Errorf("mints = %v, want %v (the second default run should hit the cache)", *calls, want) + } + + if _, _, err := runRoot(t, "credential", "--refresh"); err != nil { + t.Fatal(err) + } + if out, _, err = runRoot(t, "credential", "-o", "token"); err != nil || out != "minted-3\n" || len(*calls) != 3 { + t.Errorf("after --refresh: token %q (err %v) after %d mints, want the cached minted-3 after 3", out, err, len(*calls)) + } + + t.Setenv(ateclient.RefreshCredentialEnv, "1") + if out, _, err = runRoot(t, "credential", "-o", "token"); err != nil || out != "minted-4\n" { + t.Errorf("with %s set: token %q (err %v), want a new minted-4", ateclient.RefreshCredentialEnv, out, err) + } + t.Setenv(ateclient.RefreshCredentialEnv, "") + if out, _, err = runRoot(t, "credential", "-o", "token"); err != nil || out != "minted-4\n" || len(*calls) != 4 { + t.Errorf("after %s: token %q (err %v) after %d mints, want the cached minted-4 after 4", ateclient.RefreshCredentialEnv, out, err, len(*calls)) + } +} + +func TestCredentialKubeEnv(t *testing.T) { + kubeconfigPath := setupKubeconfigEnv(t) + calls := fakeMinter(t) + t.Setenv("KUBECONFIG", filepath.Join(t.TempDir(), "missing")) + t.Setenv(kubeconfigEnv, kubeconfigPath) + + if _, _, err := runRoot(t, "credential"); err != nil { + t.Fatalf("credential with %s: %v", kubeconfigEnv, err) + } + t.Setenv(kubeContextEnv, "kind-b") + if _, _, err := runRoot(t, "credential"); err != nil { + t.Fatalf("credential with %s: %v", kubeContextEnv, err) + } + if _, _, err := runRoot(t, "credential", "--context=kind-a", "--refresh"); err != nil { + t.Fatalf("credential --context: %v", err) + } + var hosts []string + for _, c := range *calls { + hosts = append(hosts, c.host) + } + if want := []string{"a.invalid", "b.invalid", "a.invalid"}; fmt.Sprint(hosts) != fmt.Sprint(want) { + t.Errorf("minted through %v, want %v", hosts, want) + } +} + +func TestCredentialErrors(t *testing.T) { + setupKubeconfigEnv(t) + fakeMinter(t) + for _, args := range [][]string{ + {"credential", "--service-account=alice"}, + {"credential", "-o", "yaml"}, + {"credential", "extra"}, + } { + if _, _, err := runRoot(t, args...); err == nil { + t.Errorf("%q succeeded, want error", args) + } + } +} + +func TestCredentialWrap(t *testing.T) { + setupKubeconfigEnv(t) + t.Setenv(credplugin.InfoEnv, `{"apiVersion":"client.authentication.k8s.io/v1beta1","kind":"ExecCredential","spec":{}}`) + jwt := "e30." + base64.RawURLEncoding.EncodeToString([]byte(`{"iss":"https://idp.example","exp":1760616000}`)) + ".sig" + + out, _, err := runRoot(t, "credential", "wrap", "--", "sh", "-c", "echo "+jwt) + if err != nil { + t.Fatalf("credential wrap: %v", err) + } + var cred clientauthv1beta1.ExecCredential + if err := json.Unmarshal([]byte(out), &cred); err != nil { + t.Fatalf("output is not an ExecCredential: %v\n%s", err, out) + } + if cred.APIVersion != credplugin.APIVersionV1beta1 || cred.Status.Token != jwt || cred.Status.ExpirationTimestamp.Unix() != 1760616000 { + t.Errorf("ExecCredential = %+v %+v", cred.TypeMeta, cred.Status) + } + + if _, _, err := runRoot(t, "credential", "wrap", "--", "sh", "-c", "echo opaque"); err == nil { + t.Error("wrapping a command that prints a non-JWT succeeded, want error") + } + if _, _, err := runRoot(t, "credential", "wrap", "--", "sh", "-c", "exit 3"); err == nil { + t.Error("wrapping a failing command succeeded, want error") + } +} diff --git a/cmd/kubectl-ate/internal/cmd/credentialplugin.go b/cmd/kubectl-ate/internal/cmd/credentialplugin.go new file mode 100644 index 0000000000..315834e238 --- /dev/null +++ b/cmd/kubectl-ate/internal/cmd/credentialplugin.go @@ -0,0 +1,140 @@ +// 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 cmd + +import ( + "errors" + "fmt" + "os" + "strings" + "unicode" + + "go.ate.dev/substrate/cmd/kubectl-ate/internal/credplugin" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" +) + +const ( + // credentialPluginEnv names the credential plugin when --credential-plugin + // is not given. + credentialPluginEnv = "ATE_CREDENTIAL_PLUGIN" + // kubeconfigEnv and kubeContextEnv pass --kubeconfig and --context to a + // credential plugin; `kubectl ate credential` uses them when it is given + // neither flag itself. + kubeconfigEnv = "ATE_KUBECONFIG" + kubeContextEnv = "ATE_KUBE_CONTEXT" +) + +// credentialPluginExec returns the exec stanza for --credential-plugin, else +// for $ATE_CREDENTIAL_PLUGIN. It returns nil when --token-file is given or +// neither names a plugin. +func credentialPluginExec() (*clientcmdapi.ExecConfig, error) { + if tokenFile != "" { + if credentialPlugin != "" { + return nil, usageErrorf("--token-file cannot be combined with --credential-plugin") + } + return nil, nil + } + value := credentialPlugin + if value == "" { + value = os.Getenv(credentialPluginEnv) + } + if value == "" { + return nil, nil + } + + // An existing file runs with no arguments, so its path may hold spaces. + command, args := value, []string(nil) + if _, err := os.Stat(value); err != nil { + words, err := splitCommandLine(value) + if err != nil { + return nil, usageErrorf("credential plugin: %w", err) + } + if len(words) == 0 { + return nil, usageErrorf("credential plugin is blank") + } + command, args = words[0], words[1:] + } + var env []clientcmdapi.ExecEnvVar + if kubeconfig != "" { + env = append(env, clientcmdapi.ExecEnvVar{Name: kubeconfigEnv, Value: kubeconfig}) + } + if k8sContext != "" { + env = append(env, clientcmdapi.ExecEnvVar{Name: kubeContextEnv, Value: k8sContext}) + } + return &clientcmdapi.ExecConfig{ + APIVersion: credplugin.APIVersionV1, + Command: command, + Args: args, + Env: env, + InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode, + ProvideClusterInfo: true, + }, nil +} + +// splitCommandLine splits s into words with shell quoting and backslash +// escapes, expanding nothing. +func splitCommandLine(s string) ([]string, error) { + var words []string + var word strings.Builder + inWord := false + var quote rune + rs := []rune(s) + for i := 0; i < len(rs); i++ { + r := rs[i] + switch { + case quote == '\'': + if r == '\'' { + quote = 0 + } else { + word.WriteRune(r) + } + case quote == '"': + switch { + case r == '"': + quote = 0 + case r == '\\' && i+1 < len(rs) && strings.ContainsRune("$`\"\\\n", rs[i+1]): + i++ + word.WriteRune(rs[i]) + default: + word.WriteRune(r) + } + case r == '\\': + if i+1 == len(rs) { + return nil, errors.New("trailing backslash") + } + i++ + word.WriteRune(rs[i]) + inWord = true + case r == '\'' || r == '"': + quote, inWord = r, true + case unicode.IsSpace(r): + if inWord { + words = append(words, word.String()) + word.Reset() + inWord = false + } + default: + word.WriteRune(r) + inWord = true + } + } + if quote != 0 { + return nil, fmt.Errorf("unterminated %c quote", quote) + } + if inWord { + words = append(words, word.String()) + } + return words, nil +} diff --git a/cmd/kubectl-ate/internal/cmd/credentialplugin_test.go b/cmd/kubectl-ate/internal/cmd/credentialplugin_test.go new file mode 100644 index 0000000000..36bed858ad --- /dev/null +++ b/cmd/kubectl-ate/internal/cmd/credentialplugin_test.go @@ -0,0 +1,121 @@ +// 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 cmd + +import ( + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" +) + +func setString(t *testing.T, p *string, v string) { + t.Helper() + old := *p + *p = v + t.Cleanup(func() { *p = old }) +} + +func TestCredentialPluginExec(t *testing.T) { + spaced := filepath.Join(t.TempDir(), "my plugin") + if err := os.WriteFile(spaced, nil, 0o700); err != nil { + t.Fatal(err) + } + execConfig := func(command string, args []string, env ...clientcmdapi.ExecEnvVar) *clientcmdapi.ExecConfig { + return &clientcmdapi.ExecConfig{ + APIVersion: "client.authentication.k8s.io/v1", + Command: command, + Args: args, + Env: env, + InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode, + ProvideClusterInfo: true, + } + } + for _, tc := range []struct { + name string + flag, env, tokenFile, kubeconfig, kubeContext string + want *clientcmdapi.ExecConfig + wantErr bool + }{ + {name: "unset"}, + {name: "token file over env", env: "plugin", tokenFile: "/token"}, + {name: "token file and flag", flag: "plugin", tokenFile: "/token", wantErr: true}, + { + name: "command line", + flag: "kubectl-ate credential --service-account='ate-users/alice'", + want: execConfig("kubectl-ate", []string{"credential", "--service-account=ate-users/alice"}), + }, + {name: "env", env: "plugin --x", want: execConfig("plugin", []string{"--x"})}, + {name: "flag over env", flag: "a", env: "b", want: execConfig("a", nil)}, + {name: "existing file", flag: spaced, want: execConfig(spaced, nil)}, + { + name: "kube flags", flag: spaced, kubeconfig: "/kubeconfig", kubeContext: "kind-a", + want: execConfig(spaced, nil, + clientcmdapi.ExecEnvVar{Name: kubeconfigEnv, Value: "/kubeconfig"}, + clientcmdapi.ExecEnvVar{Name: kubeContextEnv, Value: "kind-a"}), + }, + {name: "unterminated quote", flag: `plugin "x`, wantErr: true}, + {name: "blank", flag: " ", wantErr: true}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Setenv(credentialPluginEnv, tc.env) + setString(t, &credentialPlugin, tc.flag) + setString(t, &tokenFile, tc.tokenFile) + setString(t, &kubeconfig, tc.kubeconfig) + setString(t, &k8sContext, tc.kubeContext) + + got, err := credentialPluginExec() + if (err != nil) != tc.wantErr { + t.Fatalf("credentialPluginExec() error = %v, want error %t", err, tc.wantErr) + } + if diff := cmp.Diff(tc.want, got, cmpopts.EquateEmpty()); diff != "" { + t.Errorf("credentialPluginExec() (-want +got):\n%s", diff) + } + }) + } +} + +func TestSplitCommandLine(t *testing.T) { + for _, tc := range []struct { + in string + want []string + }{ + {"", nil}, + {" a b\tc ", []string{"a", "b", "c"}}, + {`'a b' "c d"`, []string{"a b", "c d"}}, + {`--flag="a b"`, []string{"--flag=a b"}}, + {`a\ b`, []string{"a b"}}, + {`"a\"b" "C:\path"`, []string{`a"b`, `C:\path`}}, + {`'a\b' 'it''s' ''`, []string{`a\b`, "its", ""}}, + {"x#y $HOME", []string{"x#y", "$HOME"}}, + } { + got, err := splitCommandLine(tc.in) + if err != nil { + t.Errorf("splitCommandLine(%q): %v", tc.in, err) + continue + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("splitCommandLine(%q) (-want +got):\n%s", tc.in, diff) + } + } + for _, in := range []string{`"a`, `'a`, `a\`} { + if got, err := splitCommandLine(in); err == nil { + t.Errorf("splitCommandLine(%q) = %q, want an error", in, got) + } + } +} diff --git a/cmd/kubectl-ate/internal/cmd/root.go b/cmd/kubectl-ate/internal/cmd/root.go index 14e8db801c..831002aaa0 100644 --- a/cmd/kubectl-ate/internal/cmd/root.go +++ b/cmd/kubectl-ate/internal/cmd/root.go @@ -28,14 +28,15 @@ import ( ) var ( - kubeconfig string - k8sContext string - endpoint string - tokenFile string - outputFmt string - traceEnabled bool - verbosity int - usePreview bool + kubeconfig string + k8sContext string + endpoint string + tokenFile string + credentialPlugin string + outputFmt string + traceEnabled bool + verbosity int + usePreview bool ) var rootCmd = &cobra.Command{ @@ -107,6 +108,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&k8sContext, "context", "", "kubeconfig context to use") rootCmd.PersistentFlags().StringVar(&endpoint, "endpoint", "", "manual override for the gRPC target (e.g., localhost:8080); if omitted, automatically port-forwards") rootCmd.PersistentFlags().StringVar(&tokenFile, "token-file", "", "path to a bearer token for ate-api authentication, or - for stdin (defaults to a Kubernetes ServiceAccount token)") + rootCmd.PersistentFlags().StringVar(&credentialPlugin, "credential-plugin", "", "kubectl credential plugin that provides the ate-api credential: an executable's path, or a command line (defaults to $"+credentialPluginEnv+")") rootCmd.PersistentFlags().StringVarP(&outputFmt, "output", "o", "table", "output format: table, json, or yaml") rootCmd.PersistentFlags().BoolVar(&traceEnabled, "trace", false, "enable tracing for the request") rootCmd.PersistentFlags().CountVarP(&verbosity, "verbose", "v", "output verbosity; may be repeated (e.g. -v, -vv, -vvvvv)") diff --git a/cmd/kubectl-ate/internal/cmd/root_test.go b/cmd/kubectl-ate/internal/cmd/root_test.go index ae9c65a27b..af925278cc 100644 --- a/cmd/kubectl-ate/internal/cmd/root_test.go +++ b/cmd/kubectl-ate/internal/cmd/root_test.go @@ -22,6 +22,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/spf13/cobra" + "github.com/spf13/pflag" "go.ate.dev/substrate/internal/preview" "google.golang.org/grpc/metadata" ) @@ -42,6 +43,40 @@ func resetRootFlags(t *testing.T) { verbosity = 0 } +// runRoot executes rootCmd with args and resets every flag afterwards. +func runRoot(t *testing.T, args ...string) (stdout, stderr string, err error) { + t.Helper() + var out, errOut bytes.Buffer + rootCmd.SetOut(&out) + rootCmd.SetErr(&errOut) + rootCmd.SetArgs(args) + defer func() { + resetFlags(rootCmd) + resetRootFlags(t) + }() + err = rootCmd.ExecuteContext(context.Background()) + return out.String(), errOut.String(), err +} + +func resetFlags(cmd *cobra.Command) { + reset := func(f *pflag.Flag) { + if !f.Changed { + return + } + if sv, ok := f.Value.(pflag.SliceValue); ok { + _ = sv.Replace(nil) + } else { + _ = f.Value.Set(f.DefValue) + } + f.Changed = false + } + cmd.Flags().VisitAll(reset) + cmd.PersistentFlags().VisitAll(reset) + for _, c := range cmd.Commands() { + resetFlags(c) + } +} + func TestVersionFlag(t *testing.T) { t.Cleanup(func() { resetRootFlags(t) }) diff --git a/cmd/kubectl-ate/internal/cmd/verbose.go b/cmd/kubectl-ate/internal/cmd/verbose.go index c6ef24a6e9..57741d60cc 100644 --- a/cmd/kubectl-ate/internal/cmd/verbose.go +++ b/cmd/kubectl-ate/internal/cmd/verbose.go @@ -29,6 +29,7 @@ import ( "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "k8s.io/client-go/rest" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) // Verbosity levels for -v / --verbose. @@ -75,11 +76,19 @@ func loadKubeConfig(errOut io.Writer, kubeconfigPath, contextName string) (*rest func newAPIClient(cmd *cobra.Command) (*ateclient.Client, error) { ctx := cmd.Context() errOut := cmd.ErrOrStderr() - warnSharedIdentity(errOut, tokenFile) + plugin, err := credentialPluginExec() + if err != nil { + return nil, err + } + warnSharedIdentity(errOut, tokenFile, plugin) if verbosity >= vFlow { - vlogf(errOut, vFlow, "Connecting to ate-api-server (endpoint=%q, kubeconfig=%q, context=%q, token-file=%q, trace=%t)", - endpoint, kubeconfig, k8sContext, tokenFile, traceEnabled) + pluginCommand := "" + if plugin != nil { + pluginCommand = plugin.Command + } + vlogf(errOut, vFlow, "Connecting to ate-api-server (endpoint=%q, kubeconfig=%q, context=%q, token-file=%q, credential-plugin=%q, trace=%t)", + endpoint, kubeconfig, k8sContext, tokenFile, pluginCommand, traceEnabled) } else if endpoint != "" { vlogf(errOut, vBasic, "Connecting to ate-api-server at %s", endpoint) } else { @@ -87,10 +96,15 @@ func newAPIClient(cmd *cobra.Command) (*ateclient.Client, error) { } start := time.Now() - cli, err := ateclient.NewClient( - ctx, kubeconfig, k8sContext, endpoint, tokenFile, traceEnabled, - grpc.WithChainUnaryInterceptor(newLoggingUnaryInterceptor(errOut)), - ) + cli, err := ateclient.Dial(ctx, ateclient.DialOptions{ + Kubeconfig: kubeconfig, + KubeContext: k8sContext, + Endpoint: endpoint, + TokenFile: tokenFile, + CredentialPlugin: plugin, + Trace: traceEnabled, + GRPCOptions: []grpc.DialOption{grpc.WithChainUnaryInterceptor(newLoggingUnaryInterceptor(errOut))}, + }) elapsed := time.Since(start).Round(time.Millisecond) if err != nil { vlogf(errOut, vBasic, "Connection to ate-api-server failed after %s: %v", elapsed, err) @@ -109,12 +123,13 @@ func newAPIClient(cmd *cobra.Command) (*ateclient.Client, error) { // warnSharedIdentity warns on errOut when the command will act as the shared // fallback ServiceAccount rather than as the user. Everyone relying on the // fallback is the same principal, holding whatever that principal is granted: -// global owner wherever the install names it a bootstrap owner. -func warnSharedIdentity(errOut io.Writer, tokenFile string) { - if tokenFile != "" { +// global owner wherever the install names it a bootstrap owner. A plugin's +// identity is unknown here; `kubectl ate credential` warns itself. +func warnSharedIdentity(errOut io.Writer, tokenFile string, plugin *clientcmdapi.ExecConfig) { + if tokenFile != "" || plugin != nil { return } - fmt.Fprintf(errOut, "Warning: no --token-file given; acting as the shared identity %s rather than as yourself. Pass --token-file to act as yourself.\n", + fmt.Fprintf(errOut, "Warning: no --credential-plugin or --token-file given; acting as the shared identity %s rather than as yourself. Pass either to act as yourself.\n", ateclient.FallbackPrincipal()) } diff --git a/cmd/kubectl-ate/internal/cmd/verbose_test.go b/cmd/kubectl-ate/internal/cmd/verbose_test.go index 66dc39863b..6fbf9e1ad7 100644 --- a/cmd/kubectl-ate/internal/cmd/verbose_test.go +++ b/cmd/kubectl-ate/internal/cmd/verbose_test.go @@ -31,6 +31,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" metricsv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) @@ -289,11 +290,11 @@ func TestTopWorkersRunner_VerboseSurfacesMetricsIssues(t *testing.T) { } func TestWarnSharedIdentity(t *testing.T) { - t.Run("warns without a token file", func(t *testing.T) { + t.Run("warns without a credential", func(t *testing.T) { var buf bytes.Buffer - warnSharedIdentity(&buf, "") + warnSharedIdentity(&buf, "", nil) got := buf.String() - for _, want := range []string{"Warning:", "ate-client", "shared identity", "--token-file"} { + for _, want := range []string{"Warning:", "ate-client", "shared identity", "--credential-plugin", "--token-file"} { if !strings.Contains(got, want) { t.Errorf("warning %q does not mention %q", got, want) } @@ -301,7 +302,14 @@ func TestWarnSharedIdentity(t *testing.T) { }) t.Run("silent with a token file", func(t *testing.T) { var buf bytes.Buffer - warnSharedIdentity(&buf, "/path/to/token") + warnSharedIdentity(&buf, "/path/to/token", nil) + if got := buf.String(); got != "" { + t.Errorf("warning = %q, want none", got) + } + }) + t.Run("silent with a credential plugin", func(t *testing.T) { + var buf bytes.Buffer + warnSharedIdentity(&buf, "", &clientcmdapi.ExecConfig{Command: "plugin"}) if got := buf.String(); got != "" { t.Errorf("warning = %q, want none", got) } diff --git a/cmd/kubectl-ate/internal/cmd/whoami.go b/cmd/kubectl-ate/internal/cmd/whoami.go new file mode 100644 index 0000000000..426e8c5eb4 --- /dev/null +++ b/cmd/kubectl-ate/internal/cmd/whoami.go @@ -0,0 +1,43 @@ +// 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 cmd + +import ( + "github.com/spf13/cobra" + "go.ate.dev/substrate/api/ateapi/v1" + "go.ate.dev/substrate/cmd/kubectl-ate/internal/printer" +) + +var whoamiCmd = &cobra.Command{ + Use: "whoami", + Short: "Show the principal ate-api authenticates you as", + Args: noArgs(""), + RunE: func(cmd *cobra.Command, _ []string) error { + cli, err := newAPIClient(cmd) + if err != nil { + return err + } + defer cli.Close() + resp, err := cli.Authentication().WhoAmI(cmd.Context(), &ateapi.WhoAmIRequest{}) + if err != nil { + return err + } + return printer.PrintWhoAmITo(cmd.OutOrStdout(), resp, outputFmt) + }, +} + +func init() { + rootCmd.AddCommand(whoamiCmd) +} diff --git a/cmd/kubectl-ate/internal/credplugin/cache.go b/cmd/kubectl-ate/internal/credplugin/cache.go new file mode 100644 index 0000000000..c86726feec --- /dev/null +++ b/cmd/kubectl-ate/internal/credplugin/cache.go @@ -0,0 +1,90 @@ +// 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 credplugin + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "os" + "path/filepath" + "strings" + "time" +) + +// Cache keeps tokens on disk so that consecutive CLI invocations, each a new +// plugin process, reuse one token instead of minting one per command. +type Cache struct { + Dir string + Now func() time.Time +} + +// DefaultCache is the cache under ~/.ate/cache. +func DefaultCache() (*Cache, error) { + home, err := os.UserHomeDir() + if err != nil { + return nil, err + } + return &Cache{Dir: filepath.Join(home, ".ate", "cache", "credentials"), Now: time.Now}, nil +} + +type cacheEntry struct { + Token string `json:"token"` + Expiry time.Time `json:"expiry"` +} + +// path names the file for the token identified by keyParts. +func (c *Cache) path(keyParts []string) string { + sum := sha256.Sum256([]byte(strings.Join(keyParts, "\x00"))) + return filepath.Join(c.Dir, hex.EncodeToString(sum[:])+".json") +} + +// Get returns the cached token for keyParts if it is valid for at least +// minRemaining. +func (c *Cache) Get(keyParts []string, minRemaining time.Duration) (string, time.Time, bool) { + b, err := os.ReadFile(c.path(keyParts)) + if err != nil { + return "", time.Time{}, false + } + var e cacheEntry + if err := json.Unmarshal(b, &e); err != nil || e.Token == "" || c.Now().Add(minRemaining).After(e.Expiry) { + return "", time.Time{}, false + } + return e.Token, e.Expiry, true +} + +// Put caches token for keyParts. The file is mode 0600 in a 0700 directory. +func (c *Cache) Put(keyParts []string, token string, expiry time.Time) error { + b, err := json.Marshal(cacheEntry{Token: token, Expiry: expiry}) + if err != nil { + return err + } + if err := os.MkdirAll(c.Dir, 0o700); err != nil { + return err + } + tmp, err := os.CreateTemp(c.Dir, ".token-*") + if err != nil { + return err + } + defer os.Remove(tmp.Name()) + if _, err := tmp.Write(b); err != nil { + tmp.Close() + return err + } + if err := tmp.Close(); err != nil { + return err + } + return os.Rename(tmp.Name(), c.path(keyParts)) +} diff --git a/cmd/kubectl-ate/internal/credplugin/credplugin_test.go b/cmd/kubectl-ate/internal/credplugin/credplugin_test.go new file mode 100644 index 0000000000..cc0e85340e --- /dev/null +++ b/cmd/kubectl-ate/internal/credplugin/credplugin_test.go @@ -0,0 +1,139 @@ +// 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 credplugin + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "os" + "path/filepath" + "testing" + "time" + + clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" +) + +func TestParseInfo(t *testing.T) { + for _, tc := range []struct { + name string + env string + want Info + wantErr bool + }{ + {name: "unset", want: Info{APIVersion: APIVersionV1}}, + { + name: "v1 with audience", + env: `{"apiVersion":"client.authentication.k8s.io/v1","kind":"ExecCredential","spec":{"interactive":true,"cluster":{"server":"https://x","config":{"audience":"api.ate-system.svc"}}}}`, + want: Info{APIVersion: APIVersionV1, Interactive: true, Audience: "api.ate-system.svc"}, + }, + { + name: "v1beta1 without cluster", + env: `{"apiVersion":"client.authentication.k8s.io/v1beta1","kind":"ExecCredential","spec":{}}`, + want: Info{APIVersion: APIVersionV1beta1}, + }, + {name: "v1alpha1", env: `{"apiVersion":"client.authentication.k8s.io/v1alpha1","kind":"ExecCredential"}`, wantErr: true}, + {name: "malformed", env: `{`, wantErr: true}, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := ParseInfo(tc.env) + if (err != nil) != tc.wantErr { + t.Fatalf("ParseInfo err = %v, wantErr %t", err, tc.wantErr) + } + if got != tc.want { + t.Errorf("ParseInfo = %+v, want %+v", got, tc.want) + } + }) + } +} + +func TestWriteCredential(t *testing.T) { + expiry := time.Date(2026, 10, 16, 12, 0, 0, 0, time.UTC) + var buf bytes.Buffer + if err := WriteCredential(&buf, APIVersionV1beta1, "tok", expiry); err != nil { + t.Fatal(err) + } + var got clientauthv1beta1.ExecCredential + if err := json.Unmarshal(buf.Bytes(), &got); err != nil { + t.Fatal(err) + } + if got.APIVersion != APIVersionV1beta1 || got.Kind != "ExecCredential" { + t.Errorf("apiVersion/kind = %s/%s", got.APIVersion, got.Kind) + } + if got.Status.Token != "tok" || !got.Status.ExpirationTimestamp.Time.Equal(expiry) { + t.Errorf("status = %+v", got.Status) + } + + buf.Reset() + if err := WriteCredential(&buf, APIVersionV1, "tok", time.Time{}); err != nil { + t.Fatal(err) + } + if bytes.Contains(buf.Bytes(), []byte("expirationTimestamp")) { + t.Errorf("credential without expiry = %s, want no expirationTimestamp", buf.String()) + } +} + +func jwt(payload string) string { + return "e30." + base64.RawURLEncoding.EncodeToString([]byte(payload)) + ".sig" +} + +func TestJWTExpiry(t *testing.T) { + if got, err := JWTExpiry(jwt(`{"exp":1760616000}`)); err != nil || got.Unix() != 1760616000 { + t.Errorf("JWTExpiry = (%v, %v), want 1760616000", got, err) + } + if got, err := JWTExpiry(jwt(`{"sub":"a"}`)); err != nil || !got.IsZero() { + t.Errorf("JWTExpiry without exp = (%v, %v), want zero", got, err) + } + if _, err := JWTExpiry("opaque-access-token"); err == nil { + t.Error("JWTExpiry(opaque token) succeeded, want error") + } +} + +func TestCache(t *testing.T) { + now := time.Date(2026, 10, 16, 12, 0, 0, 0, time.UTC) + c := &Cache{Dir: filepath.Join(t.TempDir(), "cache"), Now: func() time.Time { return now }} + key := []string{"https://kube", "ctx", "ate-system/ate-client", "api.ate-system.svc"} + + if _, _, ok := c.Get(key, time.Minute); ok { + t.Fatal("Get on an empty cache hit") + } + if err := c.Put(key, "tok", now.Add(time.Hour)); err != nil { + t.Fatal(err) + } + if tok, _, ok := c.Get(key, time.Minute); !ok || tok != "tok" { + t.Errorf("Get = (%q, %t), want tok", tok, ok) + } + if _, _, ok := c.Get([]string{"https://kube", "ctx", "ate-system/ate-client", "other"}, time.Minute); ok { + t.Error("Get for another audience hit") + } + if _, _, ok := c.Get(key, 2*time.Hour); ok { + t.Error("Get hit for a token expiring within minRemaining") + } + + dirInfo, err := os.Stat(c.Dir) + if err != nil { + t.Fatal(err) + } + if mode := dirInfo.Mode().Perm(); mode != 0o700 { + t.Errorf("cache dir mode = %o, want 700", mode) + } + fileInfo, err := os.Stat(c.path(key)) + if err != nil { + t.Fatal(err) + } + if mode := fileInfo.Mode().Perm(); mode != 0o600 { + t.Errorf("cache file mode = %o, want 600", mode) + } +} diff --git a/cmd/kubectl-ate/internal/credplugin/execinfo.go b/cmd/kubectl-ate/internal/credplugin/execinfo.go new file mode 100644 index 0000000000..3b4b06d6f9 --- /dev/null +++ b/cmd/kubectl-ate/internal/credplugin/execinfo.go @@ -0,0 +1,116 @@ +// 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 credplugin implements the plugin side of the kubeconfig exec +// credential protocol for `kubectl ate credential`. +package credplugin + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "strings" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientauthv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1" +) + +const ( + // InfoEnv carries the ExecCredential a client passes to its plugin. + InfoEnv = "KUBERNETES_EXEC_INFO" + + APIVersionV1 = "client.authentication.k8s.io/v1" + APIVersionV1beta1 = "client.authentication.k8s.io/v1beta1" +) + +// Info is what the client told the plugin through InfoEnv. +type Info struct { + // APIVersion is the version the response must use. + APIVersion string + Interactive bool + // Audience is spec.cluster.config.audience, if the client sent cluster + // info. + Audience string +} + +// ParseInfo parses the value of InfoEnv. An empty value, as when the plugin +// is run by hand, yields APIVersionV1 and no audience. +func ParseInfo(env string) (Info, error) { + if env == "" { + return Info{APIVersion: APIVersionV1}, nil + } + // v1 and v1beta1 share the fields read here. + var cred clientauthv1.ExecCredential + if err := json.Unmarshal([]byte(env), &cred); err != nil { + return Info{}, fmt.Errorf("parse %s: %w", InfoEnv, err) + } + if cred.APIVersion != APIVersionV1 && cred.APIVersion != APIVersionV1beta1 { + return Info{}, fmt.Errorf("%s has unsupported apiVersion %q", InfoEnv, cred.APIVersion) + } + info := Info{APIVersion: cred.APIVersion, Interactive: cred.Spec.Interactive} + if c := cred.Spec.Cluster; c != nil && len(c.Config.Raw) > 0 { + var config struct { + Audience string `json:"audience"` + } + if err := json.Unmarshal(c.Config.Raw, &config); err != nil { + return Info{}, fmt.Errorf("parse %s spec.cluster.config: %w", InfoEnv, err) + } + info.Audience = config.Audience + } + return info, nil +} + +// WriteCredential writes an ExecCredential carrying token. A zero expiry is +// left out, telling the client to use the token until it is rejected. +func WriteCredential(w io.Writer, apiVersion, token string, expiry time.Time) error { + cred := clientauthv1.ExecCredential{ + TypeMeta: metav1.TypeMeta{APIVersion: apiVersion, Kind: "ExecCredential"}, + Status: &clientauthv1.ExecCredentialStatus{Token: token}, + } + if !expiry.IsZero() { + exp := metav1.NewTime(expiry) + cred.Status.ExpirationTimestamp = &exp + } + return json.NewEncoder(w).Encode(cred) +} + +// JWTExpiry returns the exp claim of a JWT without verifying it, or the zero +// time if it has none. +func JWTExpiry(token string) (time.Time, error) { + parts := strings.Split(token, ".") + if len(parts) != 3 { + return time.Time{}, errors.New("not a JWT") + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return time.Time{}, fmt.Errorf("decode JWT payload: %w", err) + } + var claims struct { + Exp *json.Number `json:"exp"` + } + if err := json.Unmarshal(payload, &claims); err != nil { + return time.Time{}, fmt.Errorf("parse JWT payload: %w", err) + } + if claims.Exp == nil { + return time.Time{}, nil + } + exp, err := claims.Exp.Float64() + if err != nil { + return time.Time{}, fmt.Errorf("parse JWT exp: %w", err) + } + return time.Unix(int64(exp), 0), nil +} diff --git a/cmd/kubectl-ate/internal/printer/printer.go b/cmd/kubectl-ate/internal/printer/printer.go index 8d418c46c9..d61de40b4a 100644 --- a/cmd/kubectl-ate/internal/printer/printer.go +++ b/cmd/kubectl-ate/internal/printer/printer.go @@ -264,6 +264,18 @@ func PrintWorkerTopYAML(out io.Writer, items []*WorkerTopItem) error { return err } +// PrintWhoAmITo prints the caller's principal to the provided writer. +func PrintWhoAmITo(out io.Writer, resp *ateapi.WhoAmIResponse, format string) error { + if format == "json" || format == "yaml" { + return printProto(out, resp, format) + } + kind := strings.ToLower(strings.TrimPrefix(resp.GetKind().String(), "PRINCIPAL_KIND_")) + w := tabwriter.NewWriter(out, 0, 0, 3, ' ', 0) + fmt.Fprintln(w, "PRINCIPAL\tMETHOD\tISSUER") + fmt.Fprintf(w, "%s\t%s\t%s\n", resp.GetId(), kind, resp.GetIssuer()) + return w.Flush() +} + // PrintActorTo prints a single actor to the provided writer. func PrintActorTo(out io.Writer, actor *ateapi.Actor, format string) error { if format == "json" || format == "yaml" { diff --git a/cmd/kubectl-ate/internal/printer/printer_test.go b/cmd/kubectl-ate/internal/printer/printer_test.go index bcbf0a3bfd..051ada1390 100644 --- a/cmd/kubectl-ate/internal/printer/printer_test.go +++ b/cmd/kubectl-ate/internal/printer/printer_test.go @@ -941,3 +941,29 @@ func TestPrintWorkerTopTo_Invalid(t *testing.T) { t.Errorf("expected error for invalid format, got nil") } } + +func TestPrintWhoAmITo(t *testing.T) { + resp := &ateapi.WhoAmIResponse{ + Id: "system:serviceaccount:ate-users:alice", + Kind: ateapi.PrincipalKind_PRINCIPAL_KIND_JWT, + Issuer: "https://kubernetes.default.svc", + } + var buf bytes.Buffer + if err := PrintWhoAmITo(&buf, resp, "table"); err != nil { + t.Fatal(err) + } + want := "PRINCIPAL METHOD ISSUER\n" + + "system:serviceaccount:ate-users:alice jwt https://kubernetes.default.svc\n" + if diff := cmp.Diff(want, buf.String()); diff != "" { + t.Errorf("table (-want +got):\n%s", diff) + } + + buf.Reset() + if err := PrintWhoAmITo(&buf, resp, "yaml"); err != nil { + t.Fatal(err) + } + want = "id: system:serviceaccount:ate-users:alice\nissuer: https://kubernetes.default.svc\nkind: PRINCIPAL_KIND_JWT\n" + if diff := cmp.Diff(want, buf.String()); diff != "" { + t.Errorf("yaml (-want +got):\n%s", diff) + } +} diff --git a/docs/authentication.md b/docs/authentication.md index 4bccc4fa52..8d47540b74 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -27,24 +27,132 @@ What an authenticated caller may do is decided by authorization; see provider issued the token, so only configure providers you trust not to issue a token whose subject is another principal's ID. +## Client credentials + +`kubectl-ate` gets its credential from the kubectl credential plugin that +`--credential-plugin` or `$ATE_CREDENTIAL_PLUGIN` names; see the +[kubectl-ate README](../cmd/kubectl-ate/README.md#authentication). It caches +the plugin's token until its `expirationTimestamp`, and runs the plugin again +when `ate-api` returns `UNAUTHENTICATED`. If the plugin returns the +rejected token again, the client runs it once more, and on every later run, +with `ATE_REFRESH_CREDENTIAL=1`, which tells a plugin that caches tokens to +return a new one. + +`ate-api` checks only a JWT's issuer, audience, signature, and expiry. A +credential source is therefore safe only when its issuer limits who can get a +token for the configured audience. Kubernetes ServiceAccount tokens qualify, +because RBAC decides who may create them. Google ID tokens for the Cloud SDK's +shared client ID do not, since any Google account can get one. Neither do +Google service account ID tokens or GitHub Actions OIDC tokens: any service +account or workflow can request one for any audience. + +`kubectl ate credential` is the built-in plugin. It mints a ServiceAccount +token through your kubeconfig and caches it under `~/.ate/cache`. +`kubectl ate credential wrap -- COMMAND` adapts a command that prints a JWT. +`kubectl ate whoami` shows the principal `ate-api` authenticated you as. + +### Per-user ServiceAccounts + +Without a credential, every user acts as the shared `ate-client` +ServiceAccount. To give a user their own identity, let them create tokens for +a ServiceAccount of their own: + +```sh +kubectl create namespace ate-users +kubectl -n ate-users create serviceaccount alice +kubectl -n ate-users create role alice-token --verb=create --resource=serviceaccounts/token --resource-name=alice +kubectl -n ate-users create rolebinding alice-token --role=alice-token --user=alice@example.com +``` + +Alice then sets +`ATE_CREDENTIAL_PLUGIN="kubectl-ate credential --service-account=ate-users/alice"`, +and `kubectl ate whoami` reports `system:serviceaccount:ate-users:alice`. +Access policies grant to that ID. The token carries the ServiceAccount's +groups, not Alice's. + +A user who is not a cluster admin also needs to port-forward to `ate-api` and +read its trust bundle. Create these roles once: + +```sh +kubectl apply -f - <<'EOF' +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: ate-api-client + namespace: ate-system +rules: +- apiGroups: [""] + resources: [services] + resourceNames: [api] + verbs: [get] +- apiGroups: [""] + resources: [pods] + verbs: [list] +- apiGroups: [""] + resources: [pods/portforward] + verbs: [get, create] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: ate-api-client +rules: +- apiGroups: [certificates.k8s.io] + resources: [clustertrustbundles] + verbs: [list] +EOF +``` + +and bind them to each user: + +```sh +kubectl -n ate-system create rolebinding alice-ate-api-client --role=ate-api-client --user=alice@example.com +kubectl create clusterrolebinding alice-ate-api-client --clusterrole=ate-api-client --user=alice@example.com +``` + +`ate-api` verifies a token's signature and expiry without asking Kubernetes +whether its ServiceAccount still exists. Deleting Alice's ServiceAccount stops +her getting new tokens, but one she already holds keeps working until it +expires, plus `ate-api`'s five-minute allowance for clock skew. +`kubectl ate credential` requests one-hour tokens; Alice can request longer +ones herself, up to the cluster's maximum token lifetime. To cut off her access +at once, remove the access policy grants to her ID. A ServiceAccount recreated +with the same name gets the old one's grants. + +### In-cluster workloads + +A pod can act as its own ServiceAccount with +`--endpoint=api.ate-system.svc:443` and a `--token-file` pointing at a +projected ServiceAccount token with audience `api.ate-system.svc`. The client +rereads the file on every RPC, so it picks up the kubelet's rotations. The +pod's ServiceAccount must be able to list ClusterTrustBundles, which verify +`ate-api`. The install's NetworkPolicy admits only pods in namespaces labeled +`ate.dev/api-access=true`. + ## Google Cloud CLI tokens The Google Cloud CLI currently issues user identity tokens with issuer `https://accounts.google.com` and audience `32555940559.apps.googleusercontent.com`, the Cloud SDK's shared client ID. These values are examples rather than built-in defaults; verify the claims -issued by your identity provider and configure them explicitly. +issued by your identity provider and configure them explicitly. Any Google +account can obtain such a token, so this provider admits every Google account. + +With the provider configured, wrap the token as a credential plugin: + +```sh +export ATE_CREDENTIAL_PLUGIN="kubectl-ate credential wrap -- gcloud auth print-identity-token" +``` -With the provider configured, pipe the token to `kubectl-ate`: +or pass it for one command with `--token-file`, which accepts a file path or +`-` for stdin: ```sh gcloud auth print-identity-token | kubectl ate --token-file=- get actors ``` -`--token-file` accepts either a file path or `-` for stdin and only replaces the -credential sent to `ate-api`. `kubectl-ate` -still uses kubeconfig access to establish its port-forward and obtain the -server trust bundle. +Either way, `kubectl-ate` still uses kubeconfig access to establish its +port-forward and obtain the server trust bundle. For a manifest-based installation, replace the authentication ConfigMap and restart the deployment: diff --git a/docs/authorization.md b/docs/authorization.md index 271123faa9..73ae9493f4 100644 --- a/docs/authorization.md +++ b/docs/authorization.md @@ -93,8 +93,10 @@ kubectl ate --token-file me.token get atespaces ## `kubectl ate` and `ate-setup` identities -`kubectl ate` sends the token from `--token-file` (`-` for stdin). Without -one, it uses your kubeconfig to mint a token for `ate-client` (or +`kubectl ate` sends the token from `--token-file` (`-` for stdin), else the +credential from `--credential-plugin`; see +[authentication.md](authentication.md#client-credentials). Without either, it +uses your kubeconfig to mint a token for `ate-client` (or `ATE_CLIENT_SERVICE_ACCOUNT`) and warns that it is acting as a shared identity. It still needs Kubernetes rights to port-forward to `ate-api-server` unless you pass `--endpoint`. @@ -315,8 +317,9 @@ For a shared cluster, beyond replacing the default bootstrap owner: ### Not yet supported -- **Signing in.** `kubectl ate` has no login flow or credential plugin; people - obtain tokens themselves. +- **Signing in.** `kubectl ate` has no login flow of its own; it runs the + kubectl credential plugin `--credential-plugin` names, or people obtain + tokens themselves. - **JWT providers at install time.** `ate-setup` configures only the cluster's ServiceAccount issuer; add others to the ConfigMap afterwards. - **Groups.** Policy members are individual principals. diff --git a/internal/ateclient/builder.go b/internal/ateclient/builder.go index f267e2dedc..4833d63cbb 100644 --- a/internal/ateclient/builder.go +++ b/internal/ateclient/builder.go @@ -19,10 +19,9 @@ import ( "crypto/tls" "crypto/x509" "fmt" - "io" "os" - "strings" "sync" + "time" "go.ate.dev/substrate/api/ateapi/v1" "go.ate.dev/substrate/internal/clustertrustbundle" @@ -36,11 +35,11 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.40.0" "google.golang.org/grpc" "google.golang.org/grpc/credentials" - authv1 "k8s.io/api/authentication/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" metricsv1beta1 "k8s.io/metrics/pkg/client/clientset/versioned" ) @@ -81,6 +80,18 @@ func FallbackPrincipal() string { return fmt.Sprintf("system:serviceaccount:%s:%s", systemNamespace(), clientServiceAccount()) } +// DefaultServiceAccount is the ServiceAccount a client mints its token from +// when it has no other credential. +func DefaultServiceAccount() (namespace, name string) { + return systemNamespace(), clientServiceAccount() +} + +// DefaultAudience is the audience of tokens minted for ateapi: the in-cluster +// DNS name of its Service. +func DefaultAudience() string { + return apiServerName() +} + // systemNamespace is the namespace the client expects ateapi to be running in. func systemNamespace() string { if ns := os.Getenv(NamespaceEnv); ns != "" { @@ -140,35 +151,66 @@ func (c *Client) WorkerService() ateapi.WorkerServiceClient { return ateapi.NewWorkerServiceClient(c.conn) } +// Authentication returns an Authentication client on the same connection. +func (c *Client) Authentication() ateapi.AuthenticationClient { + return ateapi.NewAuthenticationClient(c.conn) +} + // NewClient creates a new Ate API client. If endpoint is empty, it automatically -// port-forwards to the ate-api-server pod in substrate's system namespace. -// extraOpts are appended to the options used to dial the server. +// port-forwards to the ate-api-server pod in substrate's system namespace. It +// authenticates with tokenFile if set, and otherwise with a token minted for +// the default ServiceAccount, refreshed before it expires. extraOpts are +// appended to the options used to dial the server. func NewClient(ctx context.Context, kubeconfigPath, k8sContext, endpoint, tokenFile string, traceEnabled bool, extraOpts ...grpc.DialOption) (*Client, error) { - tp, err := initTracing(ctx, traceEnabled) + return Dial(ctx, DialOptions{ + Kubeconfig: kubeconfigPath, + KubeContext: k8sContext, + Endpoint: endpoint, + TokenFile: tokenFile, + Trace: traceEnabled, + GRPCOptions: extraOpts, + }) +} + +// DialOptions configures Dial. +type DialOptions struct { + // Kubeconfig and KubeContext select the cluster substrate runs in. Empty + // values use the default loading rules and the current context. + Kubeconfig string + KubeContext string + // Endpoint is dialed directly instead of port-forwarding to the ateapi + // Service. + Endpoint string + // TokenFile is a bearer token file, re-read on every RPC, or "-" to read + // one token from stdin. + TokenFile string + // CredentialPlugin is a kubeconfig exec credential plugin, used when + // TokenFile is empty. Without either, the client mints a token for the + // default ServiceAccount. + CredentialPlugin *clientcmdapi.ExecConfig + Trace bool + GRPCOptions []grpc.DialOption +} + +// Dial creates a new Ate API client as opts describes. +func Dial(ctx context.Context, opts DialOptions) (*Client, error) { + tp, err := initTracing(ctx, opts.Trace) if err != nil { return nil, fmt.Errorf("failed to initialize tracing: %w", err) } - - var cli *Client - if endpoint != "" { - cli, err = dialDirect(ctx, kubeconfigPath, k8sContext, endpoint, tokenFile, traceEnabled, extraOpts...) - } else { - cli, err = dialPortForward(ctx, kubeconfigPath, k8sContext, tokenFile, traceEnabled, extraOpts...) - } - + cli, err := dial(ctx, opts) if err != nil { if tp != nil { _ = tp.Shutdown(ctx) } return nil, err } - cli.tracerProvider = tp return cli, nil } -func dialDirect(ctx context.Context, kubeconfigPath, k8sContext, endpoint, tokenFile string, traceEnabled bool, extraOpts ...grpc.DialOption) (*Client, error) { - config, err := LoadKubeConfig(kubeconfigPath, k8sContext) +func dial(ctx context.Context, opts DialOptions) (*Client, error) { + config, err := LoadKubeConfig(opts.Kubeconfig, opts.KubeContext) if err != nil { return nil, fmt.Errorf("failed to load kubeconfig: %w", err) } @@ -182,102 +224,74 @@ func dialDirect(ctx context.Context, kubeconfigPath, k8sContext, endpoint, token return nil, fmt.Errorf("failed to create k8s client: %w", err) } - // Verify the server before attaching the bearer token below: the token - // must never be sent over an unauthenticated channel. + // Verify the server before attaching the credential below: a token must + // never be sent over an unauthenticated channel. tlsCfg, err := serverTLSConfig(ctx, clientset) if err != nil { return nil, err } - - var opts []grpc.DialOption - opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg))) - opts = append(opts, grpc.WithStatsHandler(otelgrpc.NewClientHandler())) - opts = append(opts, grpc.WithDefaultServiceConfig(roundRobinServiceConfig)) - tokenOpt, err := bearerTokenDialOption(ctx, clientset, tokenFile) + creds, err := clientCredentials(opts, clientset) if err != nil { return nil, err } - opts = append(opts, tokenOpt) - if traceEnabled { - opts = append(opts, grpc.WithChainUnaryInterceptor(newTraceInterceptor())) + target, stopForward := opts.Endpoint, func() {} + if target == "" { + // TODO: Should we special-case a LoadBalancer "api" Service and dial its + // address directly instead of port-forwarding? + localPort, stop, err := portforward.ServicePortForward(ctx, config, clientset, systemNamespace(), apiServiceName(), 443) + if err != nil { + return nil, err + } + target, stopForward = fmt.Sprintf("127.0.0.1:%d", localPort), stop } - opts = append(opts, extraOpts...) - - conn, err := grpc.NewClient(endpoint, opts...) + conn, err := connect(target, tlsCfg, creds, opts) if err != nil { - return nil, fmt.Errorf("failed to dial manual endpoint: %w", err) + stopForward() + return nil, err } return &Client{ ControlClient: ateapi.NewControlClient(conn), conn: conn, - cancel: func() {}, + cancel: stopForward, }, nil } -// LoadKubeConfig loads a Kubernetes client configuration from the specified kubeconfig path and context. -func LoadKubeConfig(kubeconfigPath, k8sContext string) (*rest.Config, error) { - loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() - loadingRules.ExplicitPath = kubeconfigPath - configOverrides := &clientcmd.ConfigOverrides{CurrentContext: k8sContext} - return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides).ClientConfig() -} - -func dialPortForward(ctx context.Context, kubeconfigPath, k8sContext, tokenFile string, traceEnabled bool, extraOpts ...grpc.DialOption) (*Client, error) { - config, err := LoadKubeConfig(kubeconfigPath, k8sContext) - if err != nil { - return nil, fmt.Errorf("failed to load kubeconfig: %w", err) +// connect dials target, attaching creds to every RPC and retrying an RPC once +// when ateapi rejects them. +func connect(target string, tlsCfg *tls.Config, creds refreshableCredentials, opts DialOptions) (*grpc.ClientConn, error) { + if cc, ok := creds.(interface { + clientCertificate(*tls.CertificateRequestInfo) (*tls.Certificate, error) + }); ok { + tlsCfg.GetClientCertificate = cc.clientCertificate } - - // Older clusters use the deprecated beta ClusterTrustBundle API. - // Suppress its warnings when falling back from v1. - config.WarningHandlerWithContext = &rest.NoWarnings{} - - clientset, err := kubernetes.NewForConfig(config) - if err != nil { - return nil, fmt.Errorf("failed to create k8s client: %w", err) + dialOpts := []grpc.DialOption{ + grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)), + grpc.WithStatsHandler(otelgrpc.NewClientHandler()), + grpc.WithPerRPCCredentials(creds), + grpc.WithChainUnaryInterceptor(retryUnauthenticated(creds)), } - - // TODO: Should we special-case a LoadBalancer "api" Service and dial its - // address directly instead of port-forwarding? - localPort, stopForward, err := portforward.ServicePortForward(ctx, config, clientset, systemNamespace(), apiServiceName(), 443) - if err != nil { - return nil, err + if opts.Endpoint != "" { + dialOpts = append(dialOpts, grpc.WithDefaultServiceConfig(roundRobinServiceConfig)) } - localEndpoint := fmt.Sprintf("127.0.0.1:%d", localPort) - - tlsCfg, err := serverTLSConfig(ctx, clientset) - if err != nil { - stopForward() - return nil, err - } - - var opts []grpc.DialOption - opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg))) - opts = append(opts, grpc.WithStatsHandler(otelgrpc.NewClientHandler())) - tokenOpt, err := bearerTokenDialOption(ctx, clientset, tokenFile) - if err != nil { - stopForward() - return nil, err - } - opts = append(opts, tokenOpt) - - if traceEnabled { - opts = append(opts, grpc.WithChainUnaryInterceptor(newTraceInterceptor())) + if opts.Trace { + dialOpts = append(dialOpts, grpc.WithChainUnaryInterceptor(newTraceInterceptor())) } - opts = append(opts, extraOpts...) + dialOpts = append(dialOpts, opts.GRPCOptions...) - conn, err := grpc.NewClient(localEndpoint, opts...) + conn, err := grpc.NewClient(target, dialOpts...) if err != nil { - stopForward() - return nil, fmt.Errorf("failed to dial gRPC over tunnel: %w", err) + return nil, fmt.Errorf("failed to dial ateapi at %s: %w", target, err) } + return conn, nil +} - return &Client{ - ControlClient: ateapi.NewControlClient(conn), - conn: conn, - cancel: stopForward, - }, nil +// LoadKubeConfig loads a Kubernetes client configuration from the specified kubeconfig path and context. +func LoadKubeConfig(kubeconfigPath, k8sContext string) (*rest.Config, error) { + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + loadingRules.ExplicitPath = kubeconfigPath + configOverrides := &clientcmd.ConfigOverrides{CurrentContext: k8sContext} + return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides).ClientConfig() } func serverTLSConfig(ctx context.Context, clientset kubernetes.Interface) (*tls.Config, error) { @@ -314,79 +328,37 @@ func serverTLSConfig(ctx context.Context, clientset kubernetes.Interface) (*tls. }, nil } -// bearerTokenDialOption attaches the configured token, or mints an ate-client -// ServiceAccount token when tokenFile is empty. +// clientCredentials returns the credential source opts selects. // // The minted token authenticates as FallbackPrincipal, not as whoever runs // the client: anyone Kubernetes RBAC lets create a token for that // ServiceAccount gets the same identity, and whatever it is granted. -func bearerTokenDialOption(ctx context.Context, clientset *kubernetes.Clientset, tokenFile string) (grpc.DialOption, error) { - if tokenFile == "-" { +func clientCredentials(opts DialOptions, clientset kubernetes.Interface) (refreshableCredentials, error) { + switch { + case opts.TokenFile == "-": creds, err := readBearerToken(os.Stdin) if err != nil { return nil, fmt.Errorf("read bearer token from stdin: %w", err) } - return grpc.WithPerRPCCredentials(creds), nil - } - if tokenFile != "" { - return grpc.WithPerRPCCredentials(fileBearerTokenCreds(tokenFile)), nil - } - expirationSeconds := int64(3600) - tokenRequest := &authv1.TokenRequest{ - Spec: authv1.TokenRequestSpec{ - Audiences: []string{apiServerName()}, - ExpirationSeconds: &expirationSeconds, - }, - } - token, err := clientset.CoreV1().ServiceAccounts(systemNamespace()).CreateToken(ctx, clientServiceAccount(), tokenRequest, metav1.CreateOptions{}) - if err != nil { - return nil, fmt.Errorf("failed to request ateapi bearer token: %w", err) - } - if token.Status.Token == "" { - return nil, fmt.Errorf("failed to request ateapi bearer token: token response was empty") - } - return grpc.WithPerRPCCredentials(bearerTokenCreds(token.Status.Token)), nil -} - -func readBearerToken(r io.Reader) (bearerTokenCreds, error) { - b, err := io.ReadAll(r) - if err != nil { - return "", err - } - token := strings.TrimSpace(string(b)) - if token == "" { - return "", fmt.Errorf("bearer token is empty") - } - return bearerTokenCreds(token), nil -} - -type bearerTokenCreds string - -func (c bearerTokenCreds) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) { - if c == "" { - return nil, fmt.Errorf("bearer token is empty") - } - return map[string]string{"authorization": "Bearer " + string(c)}, nil -} - -func (c bearerTokenCreds) RequireTransportSecurity() bool { return true } - -type fileBearerTokenCreds string - -func (c fileBearerTokenCreds) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) { - b, err := os.ReadFile(string(c)) - if err != nil { - return nil, fmt.Errorf("read bearer token file %q: %w", c, err) - } - token := strings.TrimSpace(string(b)) - if token == "" { - return nil, fmt.Errorf("bearer token file %q is empty", c) + return creds, nil + case opts.TokenFile != "": + return fileBearerTokenCreds(opts.TokenFile), nil + case opts.CredentialPlugin != nil: + server := "https://" + apiServerName() + if opts.Endpoint != "" { + server = "https://" + opts.Endpoint + } + cluster, err := execCluster(server, apiServerName(), DefaultAudience()) + if err != nil { + return nil, err + } + return newExecCreds(opts.CredentialPlugin, cluster) } - return map[string]string{"authorization": "Bearer " + token}, nil + return newMintedTokenCreds(func(ctx context.Context) (string, time.Time, error) { + return MintServiceAccountToken(ctx, clientset, systemNamespace(), clientServiceAccount(), DefaultAudience(), time.Hour) + }), nil } -func (c fileBearerTokenCreds) RequireTransportSecurity() bool { return true } - // initTracing returns (nil, nil) when tracing is disabled: the OTel globals // stay noop so no traceparent is injected, the server roots the trace, and the // server side sampling ratio applies. A NeverSample provider here would diff --git a/internal/ateclient/credentials.go b/internal/ateclient/credentials.go new file mode 100644 index 0000000000..0a9c0af9d2 --- /dev/null +++ b/internal/ateclient/credentials.go @@ -0,0 +1,323 @@ +// 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 ateclient + +import ( + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "strings" + "sync" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/status" + authv1 "k8s.io/api/authentication/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/pkg/apis/clientauthentication" + "k8s.io/client-go/plugin/pkg/client/auth/exec" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/transport" +) + +// tokenExpiryMargin is how long before expiry a minted token is replaced. +const tokenExpiryMargin = time.Minute + +// RefreshCredentialEnv is set to "1" on every exec plugin run after the +// plugin returned a token ate-api had just rejected. A plugin that caches +// tokens should then return a new one. +const RefreshCredentialEnv = "ATE_REFRESH_CREDENTIAL" + +// refreshableCredentials attaches a credential to each RPC and can replace it +// when ateapi rejects it. +type refreshableCredentials interface { + credentials.PerRPCCredentials + // refresh discards the credential if it is still rejected, the + // authorization metadata ateapi refused, so the next RPC gets a fresh + // one. It reports whether the next RPC will send a different credential. + refresh(rejected string) bool +} + +// retryUnauthenticated retries an RPC once, with a fresh credential, when +// ateapi rejects the one it sent. This relies on ateapi returning +// Unauthenticated only before an RPC has changed anything. +func retryUnauthenticated(creds refreshableCredentials) grpc.UnaryClientInterceptor { + return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + // The credentials cache, so this returns what the RPC will send. + md, err := creds.GetRequestMetadata(ctx) + if err != nil { + return fmt.Errorf("get credentials for ate-api: %w", err) + } + err = invoker(ctx, method, req, reply, cc, opts...) + if status.Code(err) != codes.Unauthenticated || !creds.refresh(md["authorization"]) { + return err + } + return invoker(ctx, method, req, reply, cc, opts...) + } +} + +func readBearerToken(r io.Reader) (bearerTokenCreds, error) { + b, err := io.ReadAll(r) + if err != nil { + return "", err + } + token := strings.TrimSpace(string(b)) + if token == "" { + return "", fmt.Errorf("bearer token is empty") + } + return bearerTokenCreds(token), nil +} + +type bearerTokenCreds string + +func (c bearerTokenCreds) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) { + if c == "" { + return nil, fmt.Errorf("bearer token is empty") + } + return map[string]string{"authorization": "Bearer " + string(c)}, nil +} + +func (c bearerTokenCreds) RequireTransportSecurity() bool { return true } + +func (c bearerTokenCreds) refresh(string) bool { return false } + +type fileBearerTokenCreds string + +func (c fileBearerTokenCreds) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) { + b, err := os.ReadFile(string(c)) + if err != nil { + return nil, fmt.Errorf("read bearer token file %q: %w", c, err) + } + token := strings.TrimSpace(string(b)) + if token == "" { + return nil, fmt.Errorf("bearer token file %q is empty", c) + } + return map[string]string{"authorization": "Bearer " + token}, nil +} + +func (c fileBearerTokenCreds) RequireTransportSecurity() bool { return true } + +func (c fileBearerTokenCreds) refresh(rejected string) bool { + md, err := c.GetRequestMetadata(context.Background()) + return err == nil && md["authorization"] != rejected +} + +// MintServiceAccountToken requests a token for the ServiceAccount +// namespace/name, bound to audience, and returns it with its expiry. +func MintServiceAccountToken(ctx context.Context, clientset kubernetes.Interface, namespace, name, audience string, lifetime time.Duration) (string, time.Time, error) { + seconds := int64(lifetime / time.Second) + tr, err := clientset.CoreV1().ServiceAccounts(namespace).CreateToken(ctx, name, &authv1.TokenRequest{ + Spec: authv1.TokenRequestSpec{ + Audiences: []string{audience}, + ExpirationSeconds: &seconds, + }, + }, metav1.CreateOptions{}) + if err != nil { + return "", time.Time{}, fmt.Errorf("request a token for ServiceAccount %s/%s: %w", namespace, name, err) + } + if tr.Status.Token == "" { + return "", time.Time{}, fmt.Errorf("request a token for ServiceAccount %s/%s: token response was empty", namespace, name) + } + return tr.Status.Token, tr.Status.ExpirationTimestamp.Time, nil +} + +// mintedTokenCreds mints a token on first use and again once it nears expiry +// or ateapi rejects it. +type mintedTokenCreds struct { + mint func(context.Context) (string, time.Time, error) + now func() time.Time + + mu sync.Mutex + token string + expiry time.Time +} + +func newMintedTokenCreds(mint func(context.Context) (string, time.Time, error)) *mintedTokenCreds { + return &mintedTokenCreds{mint: mint, now: time.Now} +} + +func (c *mintedTokenCreds) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) { + c.mu.Lock() + defer c.mu.Unlock() + if c.token == "" || !c.now().Before(c.expiry.Add(-tokenExpiryMargin)) { + token, expiry, err := c.mint(ctx) + if err != nil { + return nil, err + } + c.token, c.expiry = token, expiry + } + return map[string]string{"authorization": "Bearer " + c.token}, nil +} + +func (c *mintedTokenCreds) RequireTransportSecurity() bool { return true } + +func (c *mintedTokenCreds) refresh(rejected string) bool { + c.mu.Lock() + defer c.mu.Unlock() + if "Bearer "+c.token == rejected { + c.token = "" + } + return true +} + +// execCreds runs a kubeconfig exec credential plugin through client-go's +// authenticator, which caches the credential until its expirationTimestamp. +// That authenticator is built for net/http, so execCreds drives it with +// in-memory requests to a stub transport. +type execCreds struct { + config *clientcmdapi.ExecConfig + cluster *clientauthentication.Cluster + + mu sync.Mutex + wrap transport.WrapperFunc + getCert func() (*tls.Certificate, error) +} + +// execCluster is passed to a plugin as spec.cluster in KUBERNETES_EXEC_INFO +// when it sets provideClusterInfo. +func execCluster(server, serverName, audience string) (*clientauthentication.Cluster, error) { + config, err := json.Marshal(map[string]string{"audience": audience}) + if err != nil { + return nil, err + } + return &clientauthentication.Cluster{ + Server: server, + TLSServerName: serverName, + Config: &runtime.Unknown{Raw: config, ContentType: runtime.ContentTypeJSON}, + }, nil +} + +func newExecCreds(config *clientcmdapi.ExecConfig, cluster *clientauthentication.Cluster) (*execCreds, error) { + c := &execCreds{config: config, cluster: cluster} + if err := c.use(config); err != nil { + return nil, err + } + return c, nil +} + +// use switches to client-go's authenticator for config. +func (c *execCreds) use(config *clientcmdapi.ExecConfig) error { + a, err := exec.GetAuthenticator(config, c.cluster) + if err != nil { + return fmt.Errorf("exec credential plugin %s: %w", config.Command, err) + } + var tc transport.Config + if err := a.UpdateTransportConfig(&tc); err != nil { + return fmt.Errorf("exec credential plugin %s: %w", config.Command, err) + } + if tc.WrapTransport == nil || tc.TLS.GetCertHolder == nil { + return errors.New("exec credential plugin: authenticator did not install its transport hooks") + } + c.mu.Lock() + defer c.mu.Unlock() + c.wrap, c.getCert = tc.WrapTransport, tc.TLS.GetCertHolder.GetCert + return nil +} + +// stubTransport answers every request with code and records the +// Authorization header it was sent. +type stubTransport struct { + code int + authorization string +} + +func (s *stubTransport) RoundTrip(req *http.Request) (*http.Response, error) { + s.authorization = req.Header.Get("Authorization") + return &http.Response{StatusCode: s.code, Body: http.NoBody, Request: req}, nil +} + +// authorization returns the Authorization header the authenticator attaches. +// A 401 response makes the authenticator rerun the plugin, unless another +// caller already replaced the credential it attached. +func (c *execCreds) authorization(ctx context.Context, code int) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://ateapi.invalid/", nil) + if err != nil { + return "", err + } + c.mu.Lock() + wrap := c.wrap + c.mu.Unlock() + stub := &stubTransport{code: code} + if _, err := wrap(stub).RoundTrip(req); err != nil { + return "", err + } + return stub.authorization, nil +} + +func (c *execCreds) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) { + auth, err := c.authorization(ctx, http.StatusOK) + if err != nil { + return nil, err + } + if auth == "" { + // The plugin returned only a client certificate. + return nil, nil + } + return map[string]string{"authorization": auth}, nil +} + +func (c *execCreds) RequireTransportSecurity() bool { return true } + +func (c *execCreds) refresh(rejected string) bool { + ctx := context.Background() + current, err := c.authorization(ctx, http.StatusOK) + if err != nil { + return false + } + if current == rejected { + if _, err := c.authorization(ctx, http.StatusUnauthorized); err != nil { + return false + } + if current, err = c.authorization(ctx, http.StatusOK); err != nil { + return false + } + } + if current == rejected { + // The plugin served the rejected token again, likely from its own + // cache. + config := c.config.DeepCopy() + config.Env = append(config.Env, clientcmdapi.ExecEnvVar{Name: RefreshCredentialEnv, Value: "1"}) + if err := c.use(config); err != nil { + return false + } + if current, err = c.authorization(ctx, http.StatusOK); err != nil { + return false + } + } + return current != rejected +} + +// clientCertificate presents the plugin's client certificate, if it returned +// one. A plugin failure sends no certificate rather than failing the +// handshake, so the error surfaces from GetRequestMetadata instead. +func (c *execCreds) clientCertificate(*tls.CertificateRequestInfo) (*tls.Certificate, error) { + c.mu.Lock() + getCert := c.getCert + c.mu.Unlock() + cert, err := getCert() + if err != nil || cert == nil { + return &tls.Certificate{}, nil + } + return cert, nil +} diff --git a/internal/ateclient/credentials_test.go b/internal/ateclient/credentials_test.go new file mode 100644 index 0000000000..343404d716 --- /dev/null +++ b/internal/ateclient/credentials_test.go @@ -0,0 +1,652 @@ +// 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 ateclient + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "math/big" + "net" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "testing" + "time" + + "go.ate.dev/substrate/api/ateapi/v1" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + clientauthv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" +) + +// The test binary doubles as an exec credential plugin when pluginEnv is set. +// Each run increments a counter in pluginStateEnv's directory and returns +// "token-". With pluginSameTokenEnv it always returns "token-1"; with +// pluginCachedTokenEnv it returns "token-1" unless RefreshCredentialEnv is set. +const ( + pluginEnv = "ATECLIENT_TEST_PLUGIN" + pluginStateEnv = "ATECLIENT_TEST_PLUGIN_STATE" + pluginTTLEnv = "ATECLIENT_TEST_PLUGIN_TTL" + pluginSameTokenEnv = "ATECLIENT_TEST_PLUGIN_SAME_TOKEN" + pluginCachedTokenEnv = "ATECLIENT_TEST_PLUGIN_CACHED_TOKEN" + pluginCertEnv = "ATECLIENT_TEST_PLUGIN_CERT" +) + +func TestMain(m *testing.M) { + if os.Getenv(pluginEnv) != "" { + if err := runTestPlugin(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + os.Exit(0) + } + os.Exit(m.Run()) +} + +func runTestPlugin() error { + dir := os.Getenv(pluginStateEnv) + n := 1 + if b, err := os.ReadFile(filepath.Join(dir, "count")); err == nil { + n, _ = strconv.Atoi(string(b)) + n++ + } + if err := os.WriteFile(filepath.Join(dir, "count"), []byte(strconv.Itoa(n)), 0o600); err != nil { + return err + } + info := os.Getenv("KUBERNETES_EXEC_INFO") + if err := os.WriteFile(filepath.Join(dir, fmt.Sprintf("info-%d.json", n)), []byte(info), 0o600); err != nil { + return err + } + var in clientauthv1.ExecCredential + if err := json.Unmarshal([]byte(info), &in); err != nil { + return err + } + + token := fmt.Sprintf("token-%d", n) + if os.Getenv(pluginSameTokenEnv) != "" || os.Getenv(pluginCachedTokenEnv) != "" && os.Getenv(RefreshCredentialEnv) == "" { + token = "token-1" + } + out := clientauthv1.ExecCredential{ + TypeMeta: in.TypeMeta, + Status: &clientauthv1.ExecCredentialStatus{Token: token}, + } + if ttl := os.Getenv(pluginTTLEnv); ttl != "" { + d, err := time.ParseDuration(ttl) + if err != nil { + return err + } + exp := metav1.NewTime(time.Now().Add(d)) + out.Status.ExpirationTimestamp = &exp + } + if certDir := os.Getenv(pluginCertEnv); certDir != "" { + cert, err := os.ReadFile(filepath.Join(certDir, "tls.crt")) + if err != nil { + return err + } + key, err := os.ReadFile(filepath.Join(certDir, "tls.key")) + if err != nil { + return err + } + out.Status.ClientCertificateData, out.Status.ClientKeyData = string(cert), string(key) + } + return json.NewEncoder(os.Stdout).Encode(out) +} + +// testPlugin is an exec stanza that runs the test binary as a plugin. +func testPlugin(t *testing.T, apiVersion string, env ...clientcmdapi.ExecEnvVar) (*clientcmdapi.ExecConfig, string) { + t.Helper() + self, err := os.Executable() + if err != nil { + t.Fatal(err) + } + dir := t.TempDir() + return &clientcmdapi.ExecConfig{ + APIVersion: apiVersion, + Command: self, + Env: append([]clientcmdapi.ExecEnvVar{{Name: pluginEnv, Value: "1"}, {Name: pluginStateEnv, Value: dir}}, env...), + InteractiveMode: clientcmdapi.NeverExecInteractiveMode, + ProvideClusterInfo: true, + }, dir +} + +func pluginRuns(t *testing.T, dir string) int { + t.Helper() + b, err := os.ReadFile(filepath.Join(dir, "count")) + if errors.Is(err, os.ErrNotExist) { + return 0 + } + if err != nil { + t.Fatal(err) + } + n, _ := strconv.Atoi(string(b)) + return n +} + +func authorization(t *testing.T, creds credentials.PerRPCCredentials) string { + t.Helper() + md, err := creds.GetRequestMetadata(context.Background()) + if err != nil { + t.Fatalf("GetRequestMetadata: %v", err) + } + return md["authorization"] +} + +func TestExecCreds(t *testing.T) { + for _, apiVersion := range []string{"client.authentication.k8s.io/v1", "client.authentication.k8s.io/v1beta1"} { + t.Run(apiVersion, func(t *testing.T) { + plugin, dir := testPlugin(t, apiVersion, clientcmdapi.ExecEnvVar{Name: pluginTTLEnv, Value: "1h"}) + cluster, err := execCluster("https://api.ate-system.svc", "api.ate-system.svc", "my-audience") + if err != nil { + t.Fatal(err) + } + creds, err := newExecCreds(plugin, cluster) + if err != nil { + t.Fatalf("newExecCreds: %v", err) + } + + if got := authorization(t, creds); got != "Bearer token-1" { + t.Errorf("authorization = %q, want Bearer token-1", got) + } + if got := authorization(t, creds); got != "Bearer token-1" { + t.Errorf("second authorization = %q, want the cached Bearer token-1", got) + } + if n := pluginRuns(t, dir); n != 1 { + t.Errorf("plugin ran %d times, want 1", n) + } + + b, err := os.ReadFile(filepath.Join(dir, "info-1.json")) + if err != nil { + t.Fatal(err) + } + var info clientauthv1.ExecCredential + if err := json.Unmarshal(b, &info); err != nil { + t.Fatal(err) + } + if info.APIVersion != apiVersion { + t.Errorf("KUBERNETES_EXEC_INFO apiVersion = %q, want %q", info.APIVersion, apiVersion) + } + var config struct{ Audience string } + if err := json.Unmarshal(info.Spec.Cluster.Config.Raw, &config); err != nil { + t.Fatalf("spec.cluster.config: %v", err) + } + if config.Audience != "my-audience" { + t.Errorf("spec.cluster.config.audience = %q, want my-audience", config.Audience) + } + + if !creds.refresh("Bearer token-1") { + t.Error("refresh = false, want true for a plugin that returns a new token") + } + if got := authorization(t, creds); got != "Bearer token-2" { + t.Errorf("authorization after refresh = %q, want Bearer token-2", got) + } + if n := pluginRuns(t, dir); n != 2 { + t.Errorf("plugin ran %d times, want 2", n) + } + }) + } +} + +func TestExecCredsExpiry(t *testing.T) { + plugin, dir := testPlugin(t, "client.authentication.k8s.io/v1", clientcmdapi.ExecEnvVar{Name: pluginTTLEnv, Value: "-1s"}) + creds, err := newExecCreds(plugin, nil) + if err != nil { + t.Fatal(err) + } + authorization(t, creds) + if got := authorization(t, creds); got != "Bearer token-2" { + t.Errorf("authorization after expiry = %q, want Bearer token-2", got) + } + if n := pluginRuns(t, dir); n != 2 { + t.Errorf("plugin ran %d times, want 2", n) + } +} + +func TestExecCredsRefreshSameToken(t *testing.T) { + plugin, _ := testPlugin(t, "client.authentication.k8s.io/v1", clientcmdapi.ExecEnvVar{Name: pluginSameTokenEnv, Value: "1"}) + creds, err := newExecCreds(plugin, nil) + if err != nil { + t.Fatal(err) + } + authorization(t, creds) + if creds.refresh("Bearer token-1") { + t.Error("refresh = true, want false when the plugin returns the rejected token again") + } +} + +func TestExecCredsRefreshCachedToken(t *testing.T) { + plugin, dir := testPlugin(t, "client.authentication.k8s.io/v1", clientcmdapi.ExecEnvVar{Name: pluginCachedTokenEnv, Value: "1"}) + creds, err := newExecCreds(plugin, nil) + if err != nil { + t.Fatal(err) + } + authorization(t, creds) + if !creds.refresh("Bearer token-1") { + t.Error("refresh = false, want true once the plugin skips the rejected token") + } + if got := authorization(t, creds); got != "Bearer token-3" { + t.Errorf("authorization after refresh = %q, want Bearer token-3", got) + } + if n := pluginRuns(t, dir); n != 3 { + t.Errorf("plugin ran %d times, want 3", n) + } + + if !creds.refresh("Bearer token-3") { + t.Error("second refresh = false, want true") + } + if got := authorization(t, creds); got != "Bearer token-4" { + t.Errorf("authorization after the second refresh = %q, want Bearer token-4", got) + } + if n := pluginRuns(t, dir); n != 4 { + t.Errorf("plugin ran %d times, want 4", n) + } +} + +func TestExecCredsConcurrentRefresh(t *testing.T) { + plugin, _ := testPlugin(t, "client.authentication.k8s.io/v1", clientcmdapi.ExecEnvVar{Name: pluginCachedTokenEnv, Value: "1"}) + creds, err := newExecCreds(plugin, nil) + if err != nil { + t.Fatal(err) + } + authorization(t, creds) + var wg sync.WaitGroup + for range 4 { + wg.Go(func() { + for range 20 { + if _, err := creds.GetRequestMetadata(context.Background()); err != nil { + t.Error(err) + } + if _, err := creds.clientCertificate(nil); err != nil { + t.Error(err) + } + } + }) + } + if !creds.refresh("Bearer token-1") { + t.Error("refresh = false, want true") + } + wg.Wait() +} + +func TestExecCredsRefreshAlreadyRotated(t *testing.T) { + plugin, dir := testPlugin(t, "client.authentication.k8s.io/v1") + creds, err := newExecCreds(plugin, nil) + if err != nil { + t.Fatal(err) + } + authorization(t, creds) + if !creds.refresh("Bearer token-0") { + t.Error("refresh = false, want true") + } + if n := pluginRuns(t, dir); n != 1 { + t.Errorf("plugin ran %d times, want 1: the rejected token was already replaced", n) + } +} + +func TestExecCredsClientCertificate(t *testing.T) { + ca := newTestCA(t) + certDir := t.TempDir() + certPEM, keyPEM := ca.issue(t, "client", nil) + if err := os.WriteFile(filepath.Join(certDir, "tls.crt"), certPEM, 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(certDir, "tls.key"), keyPEM, 0o600); err != nil { + t.Fatal(err) + } + plugin, _ := testPlugin(t, "client.authentication.k8s.io/v1", clientcmdapi.ExecEnvVar{Name: pluginCertEnv, Value: certDir}) + creds, err := newExecCreds(plugin, nil) + if err != nil { + t.Fatal(err) + } + cert, err := creds.clientCertificate(nil) + if err != nil { + t.Fatalf("clientCertificate: %v", err) + } + if cert.Leaf == nil || cert.Leaf.Subject.CommonName != "client" { + t.Errorf("clientCertificate leaf = %v, want CN=client", cert.Leaf) + } +} + +func TestExecCredsMissingPlugin(t *testing.T) { + creds, err := newExecCreds(&clientcmdapi.ExecConfig{ + APIVersion: "client.authentication.k8s.io/v1", + Command: "ateclient-test-missing-plugin", + InteractiveMode: clientcmdapi.NeverExecInteractiveMode, + InstallHint: "install the missing plugin", + }, nil) + if err != nil { + t.Fatal(err) + } + if cert, err := creds.clientCertificate(nil); err != nil || len(cert.Certificate) != 0 { + t.Errorf("clientCertificate = (%v, %v), want an empty certificate and no error", cert, err) + } + if _, err := creds.GetRequestMetadata(context.Background()); err == nil || !strings.Contains(err.Error(), "install the missing plugin") { + t.Errorf("GetRequestMetadata error = %v, want one carrying the install hint", err) + } +} + +func TestMintedTokenCreds(t *testing.T) { + now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + mints := 0 + creds := newMintedTokenCreds(func(context.Context) (string, time.Time, error) { + mints++ + return fmt.Sprintf("token-%d", mints), now.Add(time.Hour), nil + }) + creds.now = func() time.Time { return now } + + if got := authorization(t, creds); got != "Bearer token-1" { + t.Errorf("authorization = %q, want Bearer token-1", got) + } + now = now.Add(58 * time.Minute) + if got := authorization(t, creds); got != "Bearer token-1" { + t.Errorf("authorization before the expiry margin = %q, want Bearer token-1", got) + } + now = now.Add(time.Minute) + if got := authorization(t, creds); got != "Bearer token-2" { + t.Errorf("authorization within the expiry margin = %q, want Bearer token-2", got) + } + + creds.refresh("Bearer token-1") + if got := authorization(t, creds); got != "Bearer token-2" { + t.Errorf("authorization after refresh for an old token = %q, want Bearer token-2", got) + } + creds.refresh("Bearer token-2") + if got := authorization(t, creds); got != "Bearer token-3" { + t.Errorf("authorization after refresh = %q, want Bearer token-3", got) + } +} + +type failingCreds struct{ fakeCreds } + +func (failingCreds) GetRequestMetadata(context.Context, ...string) (map[string]string, error) { + return nil, errors.New("plugin failed") +} + +func TestRetryUnauthenticatedCredentialError(t *testing.T) { + calls := 0 + invoker := func(context.Context, string, any, any, *grpc.ClientConn, ...grpc.CallOption) error { + calls++ + return nil + } + err := retryUnauthenticated(&failingCreds{})(context.Background(), "/m", nil, nil, nil, invoker) + if _, isStatus := status.FromError(err); isStatus || !strings.Contains(err.Error(), "plugin failed") { + t.Errorf("err = %v, want a local error, not a gRPC status, carrying the credential error", err) + } + if calls != 0 { + t.Errorf("invoker called %d times, want 0", calls) + } +} + +type fakeCreds struct { + bearerTokenCreds + refreshes int + canRetry bool +} + +func (f *fakeCreds) refresh(string) bool { + f.refreshes++ + return f.canRetry +} + +func TestRetryUnauthenticated(t *testing.T) { + for _, tc := range []struct { + name string + errs []error + canRetry bool + wantCode codes.Code + wantCalls int + }{ + {"success", []error{nil}, true, codes.OK, 1}, + {"retried", []error{status.Error(codes.Unauthenticated, ""), nil}, true, codes.OK, 2}, + {"retried once", []error{status.Error(codes.Unauthenticated, ""), status.Error(codes.Unauthenticated, "")}, true, codes.Unauthenticated, 2}, + {"cannot retry", []error{status.Error(codes.Unauthenticated, "")}, false, codes.Unauthenticated, 1}, + {"other error", []error{status.Error(codes.PermissionDenied, "")}, true, codes.PermissionDenied, 1}, + } { + t.Run(tc.name, func(t *testing.T) { + creds := &fakeCreds{bearerTokenCreds: "t", canRetry: tc.canRetry} + calls := 0 + invoker := func(context.Context, string, any, any, *grpc.ClientConn, ...grpc.CallOption) error { + err := tc.errs[calls] + calls++ + return err + } + err := retryUnauthenticated(creds)(context.Background(), "/m", nil, nil, nil, invoker) + if got := status.Code(err); got != tc.wantCode { + t.Errorf("code = %v, want %v", got, tc.wantCode) + } + if calls != tc.wantCalls { + t.Errorf("calls = %d, want %d", calls, tc.wantCalls) + } + }) + } +} + +// fakeAuthentication rejects the tokens in rejected and reports the bearer +// token as the caller's ID. +type fakeAuthentication struct { + ateapi.UnimplementedAuthenticationServer + mu sync.Mutex + rejected map[string]bool +} + +func (f *fakeAuthentication) WhoAmI(ctx context.Context, _ *ateapi.WhoAmIRequest) (*ateapi.WhoAmIResponse, error) { + md, _ := metadata.FromIncomingContext(ctx) + token := strings.TrimPrefix(strings.Join(md.Get("authorization"), ""), "Bearer ") + f.mu.Lock() + defer f.mu.Unlock() + if token == "" || f.rejected[token] { + return nil, status.Error(codes.Unauthenticated, "rejected") + } + return &ateapi.WhoAmIResponse{Id: token, Kind: ateapi.PrincipalKind_PRINCIPAL_KIND_JWT}, nil +} + +func TestConnectExecCredential(t *testing.T) { + for _, tc := range []struct { + name string + env clientcmdapi.ExecEnvVar + wantID string + wantRuns int + }{ + {"new token", clientcmdapi.ExecEnvVar{Name: pluginTTLEnv, Value: "1h"}, "token-2", 2}, + {"cached token", clientcmdapi.ExecEnvVar{Name: pluginCachedTokenEnv, Value: "1"}, "token-3", 3}, + } { + t.Run(tc.name, func(t *testing.T) { + testConnectExecCredential(t, tc.env, tc.wantID, tc.wantRuns) + }) + } +} + +// testConnectExecCredential calls WhoAmI through a server that rejects token-1. +func testConnectExecCredential(t *testing.T, env clientcmdapi.ExecEnvVar, wantID string, wantRuns int) { + ca := newTestCA(t) + certPEM, keyPEM := ca.issue(t, "ateapi.test", []string{"ateapi.test"}) + serverCert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + t.Fatal(err) + } + lis, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + srv := grpc.NewServer(grpc.Creds(credentials.NewTLS(&tls.Config{Certificates: []tls.Certificate{serverCert}, MinVersion: tls.VersionTLS13}))) + authn := &fakeAuthentication{rejected: map[string]bool{"token-1": true}} + ateapi.RegisterAuthenticationServer(srv, authn) + go srv.Serve(lis) + t.Cleanup(srv.Stop) + + plugin, dir := testPlugin(t, "client.authentication.k8s.io/v1", env) + creds, err := newExecCreds(plugin, nil) + if err != nil { + t.Fatal(err) + } + roots := x509.NewCertPool() + roots.AppendCertsFromPEM(ca.pem) + tlsCfg := &tls.Config{MinVersion: tls.VersionTLS13, RootCAs: roots, ServerName: "ateapi.test"} + conn, err := connect(lis.Addr().String(), tlsCfg, creds, DialOptions{Endpoint: lis.Addr().String()}) + if err != nil { + t.Fatalf("connect: %v", err) + } + defer conn.Close() + + resp, err := ateapi.NewAuthenticationClient(conn).WhoAmI(context.Background(), &ateapi.WhoAmIRequest{}) + if err != nil { + t.Fatalf("WhoAmI: %v", err) + } + if resp.GetId() != wantID { + t.Errorf("WhoAmI id = %q, want %s after the rejected token-1 was replaced", resp.GetId(), wantID) + } + if n := pluginRuns(t, dir); n != wantRuns { + t.Errorf("plugin ran %d times, want %d", n, wantRuns) + } +} + +type testCA struct { + cert *x509.Certificate + key *ecdsa.PrivateKey + pem []byte +} + +func newTestCA(t *testing.T) *testCA { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "test-ca"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) + if err != nil { + t.Fatal(err) + } + cert, err := x509.ParseCertificate(der) + if err != nil { + t.Fatal(err) + } + return &testCA{cert: cert, key: key, pem: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})} +} + +func (ca *testCA) issue(t *testing.T, cn string, dnsNames []string) (certPEM, keyPEM []byte) { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{CommonName: cn}, + DNSNames: dnsNames, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, ca.cert, &key.PublicKey, ca.key) + if err != nil { + t.Fatal(err) + } + keyDER, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + t.Fatal(err) + } + return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}) +} + +func TestClientCredentials(t *testing.T) { + plugin := &clientcmdapi.ExecConfig{APIVersion: "client.authentication.k8s.io/v1", Command: "plugin", InteractiveMode: clientcmdapi.NeverExecInteractiveMode} + clientset := fake.NewClientset() + + creds, err := clientCredentials(DialOptions{TokenFile: "/token", CredentialPlugin: plugin}, clientset) + if _, ok := creds.(fileBearerTokenCreds); err != nil || !ok { + t.Errorf("with a token file and a plugin: %T, %v; want the token file", creds, err) + } + creds, err = clientCredentials(DialOptions{Endpoint: "ateapi.example:443", CredentialPlugin: plugin}, clientset) + if c, ok := creds.(*execCreds); err != nil || !ok || c.cluster.Server != "https://ateapi.example:443" { + t.Errorf("with a plugin: %T, %v; want the plugin, told the endpoint", creds, err) + } + creds, err = clientCredentials(DialOptions{}, clientset) + if _, ok := creds.(*mintedTokenCreds); err != nil || !ok { + t.Errorf("with neither: %T, %v; want a minted token", creds, err) + } +} + +func TestConnectClientCertificate(t *testing.T) { + ca := newTestCA(t) + serverCertPEM, serverKeyPEM := ca.issue(t, "ateapi.test", []string{"ateapi.test"}) + serverCert, err := tls.X509KeyPair(serverCertPEM, serverKeyPEM) + if err != nil { + t.Fatal(err) + } + roots := x509.NewCertPool() + roots.AppendCertsFromPEM(ca.pem) + lis, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + srv := grpc.NewServer(grpc.Creds(credentials.NewTLS(&tls.Config{ + Certificates: []tls.Certificate{serverCert}, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: roots, + MinVersion: tls.VersionTLS13, + }))) + ateapi.RegisterAuthenticationServer(srv, &fakeAuthentication{}) + go srv.Serve(lis) + t.Cleanup(srv.Stop) + + certDir := t.TempDir() + clientCertPEM, clientKeyPEM := ca.issue(t, "client", nil) + if err := os.WriteFile(filepath.Join(certDir, "tls.crt"), clientCertPEM, 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(certDir, "tls.key"), clientKeyPEM, 0o600); err != nil { + t.Fatal(err) + } + plugin, _ := testPlugin(t, "client.authentication.k8s.io/v1", clientcmdapi.ExecEnvVar{Name: pluginCertEnv, Value: certDir}) + creds, err := newExecCreds(plugin, nil) + if err != nil { + t.Fatal(err) + } + tlsCfg := &tls.Config{MinVersion: tls.VersionTLS13, RootCAs: roots, ServerName: "ateapi.test"} + conn, err := connect(lis.Addr().String(), tlsCfg, creds, DialOptions{Endpoint: lis.Addr().String()}) + if err != nil { + t.Fatalf("connect: %v", err) + } + defer conn.Close() + if _, err := ateapi.NewAuthenticationClient(conn).WhoAmI(context.Background(), &ateapi.WhoAmIRequest{}); err != nil { + t.Errorf("WhoAmI through a server that requires the plugin's client certificate: %v", err) + } +} From a2892f6e951e9e7eb40afc728330c06f7465e1be Mon Sep 17 00:00:00 2001 From: Max Thompson Date: Thu, 22 Oct 2026 09:58:49 -0700 Subject: [PATCH 2/2] ateclient: retry only plugin credentials; fix plugin paths and env The Unauthenticated retry also covered the minted ate-client token. A minted token is replaced before it expires, so when ateapi rejected it, for example because it does not trust the issuer, every failing RPC was sent twice and minted a new token. Only exec plugin credentials are now retried, and the token-file, stdin, and minted credentials lose their refresh methods. All of them are still fetched before each RPC, so a credential that cannot be fetched is still reported as a local error. RPCs rejected together each forced a plugin rerun, because one refresh could charge its 401 against the token another had just fetched. Refresh is now serialized, so they share one refresh. client-go cleans a plugin's command, which turned ./plugin into a bare name looked up on PATH, so kubectl ate now makes a command containing a separator absolute. It also always sets ATE_KUBECONFIG and ATE_KUBE_CONTEXT, empty when the flag is not given, so the plugin no longer inherits values from the user's environment that kubectl ate itself ignores. The e2e authz suite now checks the principal WhoAmI reports for a ServiceAccount token sent from a token file and from a credential plugin, minting it with ateclient.MintServiceAccountToken. The test plugin claims its run number with an exclusive create so that concurrent runs are counted, the concurrent refresh test runs readers alongside the refreshers, the missing-plugin test no longer depends on client-go's rate-limited install hint, and the credential command tests use the package's existing runRootCLI helper. --- cmd/kubectl-ate/internal/cmd/credential.go | 8 +- .../internal/cmd/credential_test.go | 28 +-- .../internal/cmd/credentialplugin.go | 32 +-- .../internal/cmd/credentialplugin_test.go | 32 ++- cmd/kubectl-ate/internal/cmd/root_test.go | 35 ---- internal/ateclient/builder.go | 6 +- internal/ateclient/credentials.go | 39 ++-- internal/ateclient/credentials_test.go | 194 ++++++++++++------ internal/e2e/suites/authz/authz_test.go | 6 +- internal/e2e/suites/authz/helpers_test.go | 36 ++-- internal/e2e/suites/authz/whoami_test.go | 80 ++++++++ 11 files changed, 318 insertions(+), 178 deletions(-) create mode 100644 internal/e2e/suites/authz/whoami_test.go diff --git a/cmd/kubectl-ate/internal/cmd/credential.go b/cmd/kubectl-ate/internal/cmd/credential.go index 6768008264..769e87ee30 100644 --- a/cmd/kubectl-ate/internal/cmd/credential.go +++ b/cmd/kubectl-ate/internal/cmd/credential.go @@ -54,10 +54,10 @@ as an ExecCredential, so it can be passed to --credential-plugin (or named in a kubeconfig exec stanza). Tokens are cached under ~/.ate/cache until shortly before they expire. -Without --kubeconfig or --context, it uses $` + kubeconfigEnv + ` and $` + kubeContextEnv + `, -which kubectl ate sets from its own flags when it runs the plugin. The audience -is --audience, else spec.cluster.config.audience from KUBERNETES_EXEC_INFO, else -ate-api's in-cluster Service name.`, +Without --kubeconfig it uses $` + kubeconfigEnv + `, and without --context +$` + kubeContextEnv + `; kubectl ate sets both from its own flags when it runs the +plugin. The audience is --audience, else spec.cluster.config.audience from +KUBERNETES_EXEC_INFO, else ate-api's in-cluster Service name.`, Example: ` # Act as a per-user ServiceAccount you are allowed to create tokens for export ` + credentialPluginEnv + `="kubectl-ate credential --service-account=ate-users/alice" kubectl ate whoami diff --git a/cmd/kubectl-ate/internal/cmd/credential_test.go b/cmd/kubectl-ate/internal/cmd/credential_test.go index e207e05708..735bf8ecf6 100644 --- a/cmd/kubectl-ate/internal/cmd/credential_test.go +++ b/cmd/kubectl-ate/internal/cmd/credential_test.go @@ -93,7 +93,7 @@ func TestCredential(t *testing.T) { calls := fakeMinter(t) t.Setenv(credplugin.InfoEnv, `{"apiVersion":"client.authentication.k8s.io/v1","kind":"ExecCredential","spec":{"interactive":false,"cluster":{"server":"https://api.ate-system.svc","config":{"audience":"exec-audience"}}}}`) - out, errOut, err := runRoot(t, "credential") + out, errOut, err := runRootCLI(t, "credential") if err != nil { t.Fatalf("credential: %v", err) } @@ -108,10 +108,10 @@ func TestCredential(t *testing.T) { t.Errorf("stderr = %q, want the shared identity warning", errOut) } - if _, _, err := runRoot(t, "credential"); err != nil { + if _, _, err := runRootCLI(t, "credential"); err != nil { t.Fatalf("second credential: %v", err) } - if out, _, err = runRoot(t, "credential", "--service-account=ate-users/alice", "--audience=flag-audience", "-o", "token"); err != nil { + if out, _, err = runRootCLI(t, "credential", "--service-account=ate-users/alice", "--audience=flag-audience", "-o", "token"); err != nil { t.Fatalf("credential --service-account: %v", err) } if out != "minted-2\n" { @@ -126,19 +126,19 @@ func TestCredential(t *testing.T) { t.Errorf("mints = %v, want %v (the second default run should hit the cache)", *calls, want) } - if _, _, err := runRoot(t, "credential", "--refresh"); err != nil { + if _, _, err := runRootCLI(t, "credential", "--refresh"); err != nil { t.Fatal(err) } - if out, _, err = runRoot(t, "credential", "-o", "token"); err != nil || out != "minted-3\n" || len(*calls) != 3 { + if out, _, err = runRootCLI(t, "credential", "-o", "token"); err != nil || out != "minted-3\n" || len(*calls) != 3 { t.Errorf("after --refresh: token %q (err %v) after %d mints, want the cached minted-3 after 3", out, err, len(*calls)) } t.Setenv(ateclient.RefreshCredentialEnv, "1") - if out, _, err = runRoot(t, "credential", "-o", "token"); err != nil || out != "minted-4\n" { + if out, _, err = runRootCLI(t, "credential", "-o", "token"); err != nil || out != "minted-4\n" { t.Errorf("with %s set: token %q (err %v), want a new minted-4", ateclient.RefreshCredentialEnv, out, err) } t.Setenv(ateclient.RefreshCredentialEnv, "") - if out, _, err = runRoot(t, "credential", "-o", "token"); err != nil || out != "minted-4\n" || len(*calls) != 4 { + if out, _, err = runRootCLI(t, "credential", "-o", "token"); err != nil || out != "minted-4\n" || len(*calls) != 4 { t.Errorf("after %s: token %q (err %v) after %d mints, want the cached minted-4 after 4", ateclient.RefreshCredentialEnv, out, err, len(*calls)) } } @@ -149,14 +149,14 @@ func TestCredentialKubeEnv(t *testing.T) { t.Setenv("KUBECONFIG", filepath.Join(t.TempDir(), "missing")) t.Setenv(kubeconfigEnv, kubeconfigPath) - if _, _, err := runRoot(t, "credential"); err != nil { + if _, _, err := runRootCLI(t, "credential"); err != nil { t.Fatalf("credential with %s: %v", kubeconfigEnv, err) } t.Setenv(kubeContextEnv, "kind-b") - if _, _, err := runRoot(t, "credential"); err != nil { + if _, _, err := runRootCLI(t, "credential"); err != nil { t.Fatalf("credential with %s: %v", kubeContextEnv, err) } - if _, _, err := runRoot(t, "credential", "--context=kind-a", "--refresh"); err != nil { + if _, _, err := runRootCLI(t, "credential", "--context=kind-a", "--refresh"); err != nil { t.Fatalf("credential --context: %v", err) } var hosts []string @@ -176,7 +176,7 @@ func TestCredentialErrors(t *testing.T) { {"credential", "-o", "yaml"}, {"credential", "extra"}, } { - if _, _, err := runRoot(t, args...); err == nil { + if _, _, err := runRootCLI(t, args...); err == nil { t.Errorf("%q succeeded, want error", args) } } @@ -187,7 +187,7 @@ func TestCredentialWrap(t *testing.T) { t.Setenv(credplugin.InfoEnv, `{"apiVersion":"client.authentication.k8s.io/v1beta1","kind":"ExecCredential","spec":{}}`) jwt := "e30." + base64.RawURLEncoding.EncodeToString([]byte(`{"iss":"https://idp.example","exp":1760616000}`)) + ".sig" - out, _, err := runRoot(t, "credential", "wrap", "--", "sh", "-c", "echo "+jwt) + out, _, err := runRootCLI(t, "credential", "wrap", "--", "sh", "-c", "echo "+jwt) if err != nil { t.Fatalf("credential wrap: %v", err) } @@ -199,10 +199,10 @@ func TestCredentialWrap(t *testing.T) { t.Errorf("ExecCredential = %+v %+v", cred.TypeMeta, cred.Status) } - if _, _, err := runRoot(t, "credential", "wrap", "--", "sh", "-c", "echo opaque"); err == nil { + if _, _, err := runRootCLI(t, "credential", "wrap", "--", "sh", "-c", "echo opaque"); err == nil { t.Error("wrapping a command that prints a non-JWT succeeded, want error") } - if _, _, err := runRoot(t, "credential", "wrap", "--", "sh", "-c", "exit 3"); err == nil { + if _, _, err := runRootCLI(t, "credential", "wrap", "--", "sh", "-c", "exit 3"); err == nil { t.Error("wrapping a failing command succeeded, want error") } } diff --git a/cmd/kubectl-ate/internal/cmd/credentialplugin.go b/cmd/kubectl-ate/internal/cmd/credentialplugin.go index 315834e238..7b3a9f1ceb 100644 --- a/cmd/kubectl-ate/internal/cmd/credentialplugin.go +++ b/cmd/kubectl-ate/internal/cmd/credentialplugin.go @@ -18,6 +18,7 @@ import ( "errors" "fmt" "os" + "path/filepath" "strings" "unicode" @@ -30,8 +31,8 @@ const ( // is not given. credentialPluginEnv = "ATE_CREDENTIAL_PLUGIN" // kubeconfigEnv and kubeContextEnv pass --kubeconfig and --context to a - // credential plugin; `kubectl ate credential` uses them when it is given - // neither flag itself. + // credential plugin; `kubectl ate credential` uses each when it is not + // given that flag itself. kubeconfigEnv = "ATE_KUBECONFIG" kubeContextEnv = "ATE_KUBE_CONTEXT" ) @@ -66,18 +67,25 @@ func credentialPluginExec() (*clientcmdapi.ExecConfig, error) { } command, args = words[0], words[1:] } - var env []clientcmdapi.ExecEnvVar - if kubeconfig != "" { - env = append(env, clientcmdapi.ExecEnvVar{Name: kubeconfigEnv, Value: kubeconfig}) - } - if k8sContext != "" { - env = append(env, clientcmdapi.ExecEnvVar{Name: kubeContextEnv, Value: k8sContext}) + // client-go cleans the command, which would turn ./plugin into a name + // looked up on PATH. + if strings.ContainsRune(command, filepath.Separator) { + abs, err := filepath.Abs(command) + if err != nil { + return nil, fmt.Errorf("credential plugin %s: %w", command, err) + } + command = abs } return &clientcmdapi.ExecConfig{ - APIVersion: credplugin.APIVersionV1, - Command: command, - Args: args, - Env: env, + APIVersion: credplugin.APIVersionV1, + Command: command, + Args: args, + // Set even when empty, so that the plugin does not inherit values + // from the user's environment that kubectl ate itself ignores. + Env: []clientcmdapi.ExecEnvVar{ + {Name: kubeconfigEnv, Value: kubeconfig}, + {Name: kubeContextEnv, Value: k8sContext}, + }, InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode, ProvideClusterInfo: true, }, nil diff --git a/cmd/kubectl-ate/internal/cmd/credentialplugin_test.go b/cmd/kubectl-ate/internal/cmd/credentialplugin_test.go index 36bed858ad..ee02a568ee 100644 --- a/cmd/kubectl-ate/internal/cmd/credentialplugin_test.go +++ b/cmd/kubectl-ate/internal/cmd/credentialplugin_test.go @@ -36,16 +36,31 @@ func TestCredentialPluginExec(t *testing.T) { if err := os.WriteFile(spaced, nil, 0o700); err != nil { t.Fatal(err) } - execConfig := func(command string, args []string, env ...clientcmdapi.ExecEnvVar) *clientcmdapi.ExecConfig { + t.Chdir(t.TempDir()) + if err := os.WriteFile("rel-plugin", nil, 0o700); err != nil { + t.Fatal(err) + } + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + relPlugin := filepath.Join(wd, "rel-plugin") + execConfigEnv := func(command string, args []string, kubeconfig, kubeContext string) *clientcmdapi.ExecConfig { return &clientcmdapi.ExecConfig{ - APIVersion: "client.authentication.k8s.io/v1", - Command: command, - Args: args, - Env: env, + APIVersion: "client.authentication.k8s.io/v1", + Command: command, + Args: args, + Env: []clientcmdapi.ExecEnvVar{ + {Name: kubeconfigEnv, Value: kubeconfig}, + {Name: kubeContextEnv, Value: kubeContext}, + }, InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode, ProvideClusterInfo: true, } } + execConfig := func(command string, args []string) *clientcmdapi.ExecConfig { + return execConfigEnv(command, args, "", "") + } for _, tc := range []struct { name string flag, env, tokenFile, kubeconfig, kubeContext string @@ -63,11 +78,12 @@ func TestCredentialPluginExec(t *testing.T) { {name: "env", env: "plugin --x", want: execConfig("plugin", []string{"--x"})}, {name: "flag over env", flag: "a", env: "b", want: execConfig("a", nil)}, {name: "existing file", flag: spaced, want: execConfig(spaced, nil)}, + {name: "relative file", flag: "./rel-plugin", want: execConfig(relPlugin, nil)}, + {name: "relative command line", flag: "./rel-plugin --x", want: execConfig(relPlugin, []string{"--x"})}, + {name: "relative missing file", flag: "bin/plugin", want: execConfig(filepath.Join(wd, "bin", "plugin"), nil)}, { name: "kube flags", flag: spaced, kubeconfig: "/kubeconfig", kubeContext: "kind-a", - want: execConfig(spaced, nil, - clientcmdapi.ExecEnvVar{Name: kubeconfigEnv, Value: "/kubeconfig"}, - clientcmdapi.ExecEnvVar{Name: kubeContextEnv, Value: "kind-a"}), + want: execConfigEnv(spaced, nil, "/kubeconfig", "kind-a"), }, {name: "unterminated quote", flag: `plugin "x`, wantErr: true}, {name: "blank", flag: " ", wantErr: true}, diff --git a/cmd/kubectl-ate/internal/cmd/root_test.go b/cmd/kubectl-ate/internal/cmd/root_test.go index af925278cc..ae9c65a27b 100644 --- a/cmd/kubectl-ate/internal/cmd/root_test.go +++ b/cmd/kubectl-ate/internal/cmd/root_test.go @@ -22,7 +22,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/spf13/cobra" - "github.com/spf13/pflag" "go.ate.dev/substrate/internal/preview" "google.golang.org/grpc/metadata" ) @@ -43,40 +42,6 @@ func resetRootFlags(t *testing.T) { verbosity = 0 } -// runRoot executes rootCmd with args and resets every flag afterwards. -func runRoot(t *testing.T, args ...string) (stdout, stderr string, err error) { - t.Helper() - var out, errOut bytes.Buffer - rootCmd.SetOut(&out) - rootCmd.SetErr(&errOut) - rootCmd.SetArgs(args) - defer func() { - resetFlags(rootCmd) - resetRootFlags(t) - }() - err = rootCmd.ExecuteContext(context.Background()) - return out.String(), errOut.String(), err -} - -func resetFlags(cmd *cobra.Command) { - reset := func(f *pflag.Flag) { - if !f.Changed { - return - } - if sv, ok := f.Value.(pflag.SliceValue); ok { - _ = sv.Replace(nil) - } else { - _ = f.Value.Set(f.DefValue) - } - f.Changed = false - } - cmd.Flags().VisitAll(reset) - cmd.PersistentFlags().VisitAll(reset) - for _, c := range cmd.Commands() { - resetFlags(c) - } -} - func TestVersionFlag(t *testing.T) { t.Cleanup(func() { resetRootFlags(t) }) diff --git a/internal/ateclient/builder.go b/internal/ateclient/builder.go index 4833d63cbb..f3aca786f2 100644 --- a/internal/ateclient/builder.go +++ b/internal/ateclient/builder.go @@ -258,8 +258,8 @@ func dial(ctx context.Context, opts DialOptions) (*Client, error) { } // connect dials target, attaching creds to every RPC and retrying an RPC once -// when ateapi rejects them. -func connect(target string, tlsCfg *tls.Config, creds refreshableCredentials, opts DialOptions) (*grpc.ClientConn, error) { +// when ateapi rejects a refreshable credential. +func connect(target string, tlsCfg *tls.Config, creds credentials.PerRPCCredentials, opts DialOptions) (*grpc.ClientConn, error) { if cc, ok := creds.(interface { clientCertificate(*tls.CertificateRequestInfo) (*tls.Certificate, error) }); ok { @@ -333,7 +333,7 @@ func serverTLSConfig(ctx context.Context, clientset kubernetes.Interface) (*tls. // The minted token authenticates as FallbackPrincipal, not as whoever runs // the client: anyone Kubernetes RBAC lets create a token for that // ServiceAccount gets the same identity, and whatever it is granted. -func clientCredentials(opts DialOptions, clientset kubernetes.Interface) (refreshableCredentials, error) { +func clientCredentials(opts DialOptions, clientset kubernetes.Interface) (credentials.PerRPCCredentials, error) { switch { case opts.TokenFile == "-": creds, err := readBearerToken(os.Stdin) diff --git a/internal/ateclient/credentials.go b/internal/ateclient/credentials.go index 0a9c0af9d2..cc50a90f4a 100644 --- a/internal/ateclient/credentials.go +++ b/internal/ateclient/credentials.go @@ -59,18 +59,20 @@ type refreshableCredentials interface { refresh(rejected string) bool } -// retryUnauthenticated retries an RPC once, with a fresh credential, when -// ateapi rejects the one it sent. This relies on ateapi returning -// Unauthenticated only before an RPC has changed anything. -func retryUnauthenticated(creds refreshableCredentials) grpc.UnaryClientInterceptor { +// retryUnauthenticated reports a credential it cannot fetch as a local error, +// and retries an RPC once when ateapi rejects a refreshable credential. The +// retry relies on ateapi rejecting a credential before the RPC changes anything. +func retryUnauthenticated(creds credentials.PerRPCCredentials) grpc.UnaryClientInterceptor { return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { - // The credentials cache, so this returns what the RPC will send. + // Refreshable credentials cache, so for them this is what the RPC + // will send. md, err := creds.GetRequestMetadata(ctx) if err != nil { return fmt.Errorf("get credentials for ate-api: %w", err) } err = invoker(ctx, method, req, reply, cc, opts...) - if status.Code(err) != codes.Unauthenticated || !creds.refresh(md["authorization"]) { + rc, ok := creds.(refreshableCredentials) + if status.Code(err) != codes.Unauthenticated || !ok || !rc.refresh(md["authorization"]) { return err } return invoker(ctx, method, req, reply, cc, opts...) @@ -100,8 +102,6 @@ func (c bearerTokenCreds) GetRequestMetadata(_ context.Context, _ ...string) (ma func (c bearerTokenCreds) RequireTransportSecurity() bool { return true } -func (c bearerTokenCreds) refresh(string) bool { return false } - type fileBearerTokenCreds string func (c fileBearerTokenCreds) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) { @@ -118,11 +118,6 @@ func (c fileBearerTokenCreds) GetRequestMetadata(_ context.Context, _ ...string) func (c fileBearerTokenCreds) RequireTransportSecurity() bool { return true } -func (c fileBearerTokenCreds) refresh(rejected string) bool { - md, err := c.GetRequestMetadata(context.Background()) - return err == nil && md["authorization"] != rejected -} - // MintServiceAccountToken requests a token for the ServiceAccount // namespace/name, bound to audience, and returns it with its expiry. func MintServiceAccountToken(ctx context.Context, clientset kubernetes.Interface, namespace, name, audience string, lifetime time.Duration) (string, time.Time, error) { @@ -142,8 +137,7 @@ func MintServiceAccountToken(ctx context.Context, clientset kubernetes.Interface return tr.Status.Token, tr.Status.ExpirationTimestamp.Time, nil } -// mintedTokenCreds mints a token on first use and again once it nears expiry -// or ateapi rejects it. +// mintedTokenCreds mints a token on first use and again once it nears expiry. type mintedTokenCreds struct { mint func(context.Context) (string, time.Time, error) now func() time.Time @@ -172,15 +166,6 @@ func (c *mintedTokenCreds) GetRequestMetadata(ctx context.Context, _ ...string) func (c *mintedTokenCreds) RequireTransportSecurity() bool { return true } -func (c *mintedTokenCreds) refresh(rejected string) bool { - c.mu.Lock() - defer c.mu.Unlock() - if "Bearer "+c.token == rejected { - c.token = "" - } - return true -} - // execCreds runs a kubeconfig exec credential plugin through client-go's // authenticator, which caches the credential until its expirationTimestamp. // That authenticator is built for net/http, so execCreds drives it with @@ -189,6 +174,10 @@ type execCreds struct { config *clientcmdapi.ExecConfig cluster *clientauthentication.Cluster + // refreshMu serializes refresh, so that RPCs rejected together share one + // refresh. + refreshMu sync.Mutex + mu sync.Mutex wrap transport.WrapperFunc getCert func() (*tls.Certificate, error) @@ -280,6 +269,8 @@ func (c *execCreds) GetRequestMetadata(ctx context.Context, _ ...string) (map[st func (c *execCreds) RequireTransportSecurity() bool { return true } func (c *execCreds) refresh(rejected string) bool { + c.refreshMu.Lock() + defer c.refreshMu.Unlock() ctx := context.Background() current, err := c.authorization(ctx, http.StatusOK) if err != nil { diff --git a/internal/ateclient/credentials_test.go b/internal/ateclient/credentials_test.go index 343404d716..8bdb92702d 100644 --- a/internal/ateclient/credentials_test.go +++ b/internal/ateclient/credentials_test.go @@ -30,7 +30,6 @@ import ( "net" "os" "path/filepath" - "strconv" "strings" "sync" "testing" @@ -49,9 +48,10 @@ import ( ) // The test binary doubles as an exec credential plugin when pluginEnv is set. -// Each run increments a counter in pluginStateEnv's directory and returns -// "token-". With pluginSameTokenEnv it always returns "token-1"; with -// pluginCachedTokenEnv it returns "token-1" unless RefreshCredentialEnv is set. +// Run n writes its KUBERNETES_EXEC_INFO to info-.json in pluginStateEnv's +// directory and returns "token-". With pluginSameTokenEnv it always +// returns "token-1"; with pluginCachedTokenEnv it returns "token-1" unless +// RefreshCredentialEnv is set. const ( pluginEnv = "ATECLIENT_TEST_PLUGIN" pluginStateEnv = "ATECLIENT_TEST_PLUGIN_STATE" @@ -74,17 +74,24 @@ func TestMain(m *testing.M) { func runTestPlugin() error { dir := os.Getenv(pluginStateEnv) - n := 1 - if b, err := os.ReadFile(filepath.Join(dir, "count")); err == nil { - n, _ = strconv.Atoi(string(b)) - n++ - } - if err := os.WriteFile(filepath.Join(dir, "count"), []byte(strconv.Itoa(n)), 0o600); err != nil { - return err - } info := os.Getenv("KUBERNETES_EXEC_INFO") - if err := os.WriteFile(filepath.Join(dir, fmt.Sprintf("info-%d.json", n)), []byte(info), 0o600); err != nil { - return err + n := 1 + for ; ; n++ { + f, err := os.OpenFile(filepath.Join(dir, fmt.Sprintf("info-%d.json", n)), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) + if errors.Is(err, os.ErrExist) { + continue + } + if err != nil { + return err + } + _, err = f.WriteString(info) + if cerr := f.Close(); err == nil { + err = cerr + } + if err != nil { + return err + } + break } var in clientauthv1.ExecCredential if err := json.Unmarshal([]byte(info), &in); err != nil { @@ -140,15 +147,11 @@ func testPlugin(t *testing.T, apiVersion string, env ...clientcmdapi.ExecEnvVar) func pluginRuns(t *testing.T, dir string) int { t.Helper() - b, err := os.ReadFile(filepath.Join(dir, "count")) - if errors.Is(err, os.ErrNotExist) { - return 0 - } + runs, err := filepath.Glob(filepath.Join(dir, "info-*.json")) if err != nil { t.Fatal(err) } - n, _ := strconv.Atoi(string(b)) - return n + return len(runs) } func authorization(t *testing.T, creds credentials.PerRPCCredentials) string { @@ -161,6 +164,7 @@ func authorization(t *testing.T, creds credentials.PerRPCCredentials) string { } func TestExecCreds(t *testing.T) { + t.Parallel() for _, apiVersion := range []string{"client.authentication.k8s.io/v1", "client.authentication.k8s.io/v1beta1"} { t.Run(apiVersion, func(t *testing.T) { plugin, dir := testPlugin(t, apiVersion, clientcmdapi.ExecEnvVar{Name: pluginTTLEnv, Value: "1h"}) @@ -216,6 +220,7 @@ func TestExecCreds(t *testing.T) { } func TestExecCredsExpiry(t *testing.T) { + t.Parallel() plugin, dir := testPlugin(t, "client.authentication.k8s.io/v1", clientcmdapi.ExecEnvVar{Name: pluginTTLEnv, Value: "-1s"}) creds, err := newExecCreds(plugin, nil) if err != nil { @@ -231,6 +236,7 @@ func TestExecCredsExpiry(t *testing.T) { } func TestExecCredsRefreshSameToken(t *testing.T) { + t.Parallel() plugin, _ := testPlugin(t, "client.authentication.k8s.io/v1", clientcmdapi.ExecEnvVar{Name: pluginSameTokenEnv, Value: "1"}) creds, err := newExecCreds(plugin, nil) if err != nil { @@ -243,6 +249,7 @@ func TestExecCredsRefreshSameToken(t *testing.T) { } func TestExecCredsRefreshCachedToken(t *testing.T) { + t.Parallel() plugin, dir := testPlugin(t, "client.authentication.k8s.io/v1", clientcmdapi.ExecEnvVar{Name: pluginCachedTokenEnv, Value: "1"}) creds, err := newExecCreds(plugin, nil) if err != nil { @@ -271,32 +278,61 @@ func TestExecCredsRefreshCachedToken(t *testing.T) { } func TestExecCredsConcurrentRefresh(t *testing.T) { - plugin, _ := testPlugin(t, "client.authentication.k8s.io/v1", clientcmdapi.ExecEnvVar{Name: pluginCachedTokenEnv, Value: "1"}) - creds, err := newExecCreds(plugin, nil) - if err != nil { - t.Fatal(err) - } - authorization(t, creds) - var wg sync.WaitGroup - for range 4 { - wg.Go(func() { - for range 20 { - if _, err := creds.GetRequestMetadata(context.Background()); err != nil { - t.Error(err) - } - if _, err := creds.clientCertificate(nil); err != nil { - t.Error(err) - } + t.Parallel() + for _, tc := range []struct { + name string + env clientcmdapi.ExecEnvVar + wantRuns int + }{ + {"new token", clientcmdapi.ExecEnvVar{Name: pluginTTLEnv, Value: "1h"}, 2}, + {"cached token", clientcmdapi.ExecEnvVar{Name: pluginCachedTokenEnv, Value: "1"}, 3}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + plugin, dir := testPlugin(t, "client.authentication.k8s.io/v1", tc.env) + creds, err := newExecCreds(plugin, nil) + if err != nil { + t.Fatal(err) + } + authorization(t, creds) + var readers, refreshers sync.WaitGroup + done := make(chan struct{}) + for range 4 { + readers.Go(func() { + for { + select { + case <-done: + return + default: + } + if _, err := creds.GetRequestMetadata(context.Background()); err != nil { + t.Error(err) + } + if _, err := creds.clientCertificate(nil); err != nil { + t.Error(err) + } + } + }) + } + for range 8 { + refreshers.Go(func() { + if !creds.refresh("Bearer token-1") { + t.Error("refresh = false, want true") + } + }) + } + refreshers.Wait() + close(done) + readers.Wait() + if n := pluginRuns(t, dir); n != tc.wantRuns { + t.Errorf("plugin ran %d times, want %d", n, tc.wantRuns) } }) } - if !creds.refresh("Bearer token-1") { - t.Error("refresh = false, want true") - } - wg.Wait() } func TestExecCredsRefreshAlreadyRotated(t *testing.T) { + t.Parallel() plugin, dir := testPlugin(t, "client.authentication.k8s.io/v1") creds, err := newExecCreds(plugin, nil) if err != nil { @@ -312,6 +348,7 @@ func TestExecCredsRefreshAlreadyRotated(t *testing.T) { } func TestExecCredsClientCertificate(t *testing.T) { + t.Parallel() ca := newTestCA(t) certDir := t.TempDir() certPEM, keyPEM := ca.issue(t, "client", nil) @@ -336,11 +373,11 @@ func TestExecCredsClientCertificate(t *testing.T) { } func TestExecCredsMissingPlugin(t *testing.T) { + t.Parallel() creds, err := newExecCreds(&clientcmdapi.ExecConfig{ APIVersion: "client.authentication.k8s.io/v1", Command: "ateclient-test-missing-plugin", InteractiveMode: clientcmdapi.NeverExecInteractiveMode, - InstallHint: "install the missing plugin", }, nil) if err != nil { t.Fatal(err) @@ -348,8 +385,8 @@ func TestExecCredsMissingPlugin(t *testing.T) { if cert, err := creds.clientCertificate(nil); err != nil || len(cert.Certificate) != 0 { t.Errorf("clientCertificate = (%v, %v), want an empty certificate and no error", cert, err) } - if _, err := creds.GetRequestMetadata(context.Background()); err == nil || !strings.Contains(err.Error(), "install the missing plugin") { - t.Errorf("GetRequestMetadata error = %v, want one carrying the install hint", err) + if _, err := creds.GetRequestMetadata(context.Background()); err == nil || !strings.Contains(err.Error(), "ateclient-test-missing-plugin") { + t.Errorf("GetRequestMetadata error = %v, want one naming the missing plugin", err) } } @@ -373,15 +410,6 @@ func TestMintedTokenCreds(t *testing.T) { if got := authorization(t, creds); got != "Bearer token-2" { t.Errorf("authorization within the expiry margin = %q, want Bearer token-2", got) } - - creds.refresh("Bearer token-1") - if got := authorization(t, creds); got != "Bearer token-2" { - t.Errorf("authorization after refresh for an old token = %q, want Bearer token-2", got) - } - creds.refresh("Bearer token-2") - if got := authorization(t, creds); got != "Bearer token-3" { - t.Errorf("authorization after refresh = %q, want Bearer token-3", got) - } } type failingCreds struct{ fakeCreds } @@ -455,6 +483,7 @@ type fakeAuthentication struct { ateapi.UnimplementedAuthenticationServer mu sync.Mutex rejected map[string]bool + calls int } func (f *fakeAuthentication) WhoAmI(ctx context.Context, _ *ateapi.WhoAmIRequest) (*ateapi.WhoAmIResponse, error) { @@ -462,6 +491,7 @@ func (f *fakeAuthentication) WhoAmI(ctx context.Context, _ *ateapi.WhoAmIRequest token := strings.TrimPrefix(strings.Join(md.Get("authorization"), ""), "Bearer ") f.mu.Lock() defer f.mu.Unlock() + f.calls++ if token == "" || f.rejected[token] { return nil, status.Error(codes.Unauthenticated, "rejected") } @@ -469,6 +499,7 @@ func (f *fakeAuthentication) WhoAmI(ctx context.Context, _ *ateapi.WhoAmIRequest } func TestConnectExecCredential(t *testing.T) { + t.Parallel() for _, tc := range []struct { name string env clientcmdapi.ExecEnvVar @@ -484,8 +515,10 @@ func TestConnectExecCredential(t *testing.T) { } } -// testConnectExecCredential calls WhoAmI through a server that rejects token-1. -func testConnectExecCredential(t *testing.T, env clientcmdapi.ExecEnvVar, wantID string, wantRuns int) { +// connectFakeAuthentication connects with creds to a fakeAuthentication +// server that rejects token-1. +func connectFakeAuthentication(t *testing.T, creds credentials.PerRPCCredentials) (*fakeAuthentication, ateapi.AuthenticationClient) { + t.Helper() ca := newTestCA(t) certPEM, keyPEM := ca.issue(t, "ateapi.test", []string{"ateapi.test"}) serverCert, err := tls.X509KeyPair(certPEM, keyPEM) @@ -502,11 +535,6 @@ func testConnectExecCredential(t *testing.T, env clientcmdapi.ExecEnvVar, wantID go srv.Serve(lis) t.Cleanup(srv.Stop) - plugin, dir := testPlugin(t, "client.authentication.k8s.io/v1", env) - creds, err := newExecCreds(plugin, nil) - if err != nil { - t.Fatal(err) - } roots := x509.NewCertPool() roots.AppendCertsFromPEM(ca.pem) tlsCfg := &tls.Config{MinVersion: tls.VersionTLS13, RootCAs: roots, ServerName: "ateapi.test"} @@ -514,9 +542,19 @@ func testConnectExecCredential(t *testing.T, env clientcmdapi.ExecEnvVar, wantID if err != nil { t.Fatalf("connect: %v", err) } - defer conn.Close() + t.Cleanup(func() { conn.Close() }) + return authn, ateapi.NewAuthenticationClient(conn) +} - resp, err := ateapi.NewAuthenticationClient(conn).WhoAmI(context.Background(), &ateapi.WhoAmIRequest{}) +func testConnectExecCredential(t *testing.T, env clientcmdapi.ExecEnvVar, wantID string, wantRuns int) { + plugin, dir := testPlugin(t, "client.authentication.k8s.io/v1", env) + creds, err := newExecCreds(plugin, nil) + if err != nil { + t.Fatal(err) + } + _, client := connectFakeAuthentication(t, creds) + + resp, err := client.WhoAmI(context.Background(), &ateapi.WhoAmIRequest{}) if err != nil { t.Fatalf("WhoAmI: %v", err) } @@ -528,6 +566,39 @@ func testConnectExecCredential(t *testing.T, env clientcmdapi.ExecEnvVar, wantID } } +func TestConnectMintedTokenNotRetried(t *testing.T) { + t.Parallel() + mints := 0 + creds := newMintedTokenCreds(func(context.Context) (string, time.Time, error) { + mints++ + return fmt.Sprintf("token-%d", mints), time.Now().Add(time.Hour), nil + }) + authn, client := connectFakeAuthentication(t, creds) + + if _, err := client.WhoAmI(context.Background(), &ateapi.WhoAmIRequest{}); status.Code(err) != codes.Unauthenticated { + t.Fatalf("WhoAmI error = %v, want Unauthenticated for the rejected token-1", err) + } + authn.mu.Lock() + defer authn.mu.Unlock() + if authn.calls != 1 || mints != 1 { + t.Errorf("WhoAmI reached the server %d times after %d mints, want 1 and 1", authn.calls, mints) + } +} + +func TestConnectCredentialError(t *testing.T) { + t.Parallel() + authn, client := connectFakeAuthentication(t, fileBearerTokenCreds(filepath.Join(t.TempDir(), "missing"))) + _, err := client.WhoAmI(context.Background(), &ateapi.WhoAmIRequest{}) + if _, isStatus := status.FromError(err); isStatus || !strings.Contains(err.Error(), "missing") { + t.Errorf("WhoAmI error = %v, want a local error, not a gRPC status, naming the token file", err) + } + authn.mu.Lock() + defer authn.mu.Unlock() + if authn.calls != 0 { + t.Errorf("WhoAmI reached the server %d times, want 0", authn.calls) + } +} + type testCA struct { cert *x509.Certificate key *ecdsa.PrivateKey @@ -605,6 +676,7 @@ func TestClientCredentials(t *testing.T) { } func TestConnectClientCertificate(t *testing.T) { + t.Parallel() ca := newTestCA(t) serverCertPEM, serverKeyPEM := ca.issue(t, "ateapi.test", []string{"ateapi.test"}) serverCert, err := tls.X509KeyPair(serverCertPEM, serverKeyPEM) diff --git a/internal/e2e/suites/authz/authz_test.go b/internal/e2e/suites/authz/authz_test.go index f6c9709833..70b771d5e3 100644 --- a/internal/e2e/suites/authz/authz_test.go +++ b/internal/e2e/suites/authz/authz_test.go @@ -31,8 +31,9 @@ import ( ) // TestAuthz deploys one fixture, the expensive part, and runs every check -// against it. The subtests run in order, not in parallel: each sets the -// fixture atespace's AccessPolicy for its own principals. +// against it. The subtests run in order, not in parallel: BasicFlow and +// SecurityRegression each set the fixture atespace's AccessPolicy for their +// own principals. func TestAuthz(t *testing.T) { env, err := e2e.CheckEnv("BUCKET_NAME", "KO_DOCKER_REPO") if err != nil { @@ -43,4 +44,5 @@ func TestAuthz(t *testing.T) { t.Run("BasicFlow", func(t *testing.T) { testBasicFlow(t, ctx, f) }) t.Run("SecurityRegression", func(t *testing.T) { testSecurityRegression(t, ctx, f) }) + t.Run("WhoAmI", func(t *testing.T) { testWhoAmI(t, ctx, f) }) } diff --git a/internal/e2e/suites/authz/helpers_test.go b/internal/e2e/suites/authz/helpers_test.go index f7d3137d28..0beac87cb7 100644 --- a/internal/e2e/suites/authz/helpers_test.go +++ b/internal/e2e/suites/authz/helpers_test.go @@ -24,7 +24,6 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - authv1 "k8s.io/api/authentication/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -32,7 +31,6 @@ import ( "go.ate.dev/substrate/api/ateapi/v1" "go.ate.dev/substrate/internal/ateclient" "go.ate.dev/substrate/internal/e2e" - "go.ate.dev/substrate/internal/installdefaults" "go.ate.dev/substrate/internal/resources" ) @@ -150,21 +148,21 @@ func newPrincipal(t *testing.T, ctx context.Context, clients *e2e.Clients, names // for the existing ServiceAccount namespace/name. func principalFor(t *testing.T, ctx context.Context, clients *e2e.Clients, namespace, name string) *principal { t.Helper() - expiration := int64(3600) - token, err := clients.K8s.CoreV1().ServiceAccounts(namespace).CreateToken(ctx, name, &authv1.TokenRequest{ - Spec: authv1.TokenRequestSpec{ - Audiences: []string{installdefaults.APIServiceName + "." + installdefaults.SystemNamespace + ".svc"}, - ExpirationSeconds: &expiration, - }, - }, metav1.CreateOptions{}) - if err != nil { - t.Fatalf("minting a token for %s/%s: %v", namespace, name, err) - } return &principal{ name: name, member: fmt.Sprintf("user:system:serviceaccount:%s:%s", namespace, name), - api: clientWithToken(t, ctx, token.Status.Token), + api: clientWithToken(t, ctx, mintToken(t, ctx, clients, namespace, name)), + } +} + +// mintToken returns an ateapi token for the ServiceAccount namespace/name. +func mintToken(t *testing.T, ctx context.Context, clients *e2e.Clients, namespace, name string) string { + t.Helper() + token, _, err := ateclient.MintServiceAccountToken(ctx, clients.K8s, namespace, name, ateclient.DefaultAudience(), time.Hour) + if err != nil { + t.Fatal(err) } + return token } // clientWithToken returns an ateapi client that sends token as its bearer @@ -175,11 +173,19 @@ func clientWithToken(t *testing.T, ctx context.Context, token string) *ateclient if err := os.WriteFile(tokenFile, []byte(token), 0o600); err != nil { t.Fatalf("writing token: %v", err) } + return dial(t, ctx, ateclient.DialOptions{TokenFile: tokenFile}) +} + +// dial returns an ateapi client for the cluster under test, authenticated as +// opts describes. +func dial(t *testing.T, ctx context.Context, opts ateclient.DialOptions) *ateclient.Client { + t.Helper() + opts.Kubeconfig, opts.KubeContext = e2e.KubeConfig, e2e.KubeContext connectCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - api, err := ateclient.NewClient(connectCtx, e2e.KubeConfig, e2e.KubeContext, "", tokenFile, false) + api, err := ateclient.Dial(connectCtx, opts) if err != nil { - t.Fatalf("ateclient.NewClient: %v", err) + t.Fatalf("ateclient.Dial: %v", err) } t.Cleanup(api.Close) return api diff --git a/internal/e2e/suites/authz/whoami_test.go b/internal/e2e/suites/authz/whoami_test.go new file mode 100644 index 0000000000..6ea7349e9f --- /dev/null +++ b/internal/e2e/suites/authz/whoami_test.go @@ -0,0 +1,80 @@ +// 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 authz + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientauthv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + + "go.ate.dev/substrate/api/ateapi/v1" + "go.ate.dev/substrate/internal/ateclient" +) + +// testWhoAmI checks the principal ateapi reports for one ServiceAccount's +// token, sent from a token file and from a credential plugin. +func testWhoAmI(t *testing.T, ctx context.Context, f *fixture) { + const name = "whoami" + p := newPrincipal(t, ctx, f.clients, f.atespace, name) + plugin := pluginClient(t, ctx, mintToken(t, ctx, f.clients, f.atespace, name)) + + want := strings.TrimPrefix(p.member, "user:") + for _, c := range []struct { + how string + api *ateclient.Client + }{ + {"token file", p.api}, + {"credential plugin", plugin}, + } { + resp, err := c.api.Authentication().WhoAmI(ctx, &ateapi.WhoAmIRequest{}) + if err != nil { + t.Errorf("%s: WhoAmI: %v", c.how, err) + continue + } + if resp.GetId() != want || resp.GetKind() != ateapi.PrincipalKind_PRINCIPAL_KIND_JWT || resp.GetIssuer() == "" { + t.Errorf("%s: WhoAmI = %v, want id %q, kind JWT, and an issuer", c.how, resp, want) + } + } +} + +// pluginClient returns an ateapi client whose credential plugin prints an +// ExecCredential carrying token. +func pluginClient(t *testing.T, ctx context.Context, token string) *ateclient.Client { + t.Helper() + cred, err := json.Marshal(clientauthv1.ExecCredential{ + TypeMeta: metav1.TypeMeta{APIVersion: "client.authentication.k8s.io/v1", Kind: "ExecCredential"}, + Status: &clientauthv1.ExecCredentialStatus{Token: token}, + }) + if err != nil { + t.Fatal(err) + } + credFile := filepath.Join(t.TempDir(), "credential.json") + if err := os.WriteFile(credFile, cred, 0o600); err != nil { + t.Fatalf("writing credential: %v", err) + } + return dial(t, ctx, ateclient.DialOptions{CredentialPlugin: &clientcmdapi.ExecConfig{ + APIVersion: "client.authentication.k8s.io/v1", + Command: "cat", + Args: []string{credFile}, + InteractiveMode: clientcmdapi.NeverExecInteractiveMode, + }}) +}