mirror of
https://github.com/agent-substrate/substrate.git
synced 2026-10-20 06:29:56 +08:00
Fixes #2407, #2436. > It's a good idea to open an issue first for discussion. - [x] Tests pass - [x] Appropriate changes to documentation are included in the PR ## What changes One rename in the volume API, split out of #2567 so the breaking part lands before GA and the shared-volume feature can follow as a non-breaking addition. **`Volume.external_volume_template` becomes `Volume.external`**, typed `ExternalVolumeSource`, with the existing `ExternalVolumeTemplate` under its `template` member. The field name now matches its siblings (`durable_dir`, `system_info`, `image`) and the message follows the `*VolumeSource` convention (#2407, #2436). The `ExternalVolumes` preview gate moves onto the new member, so gating behaviour is unchanged. `ExternalVolumeSource` is a union with one member today; the shared-volume reference is added as a second member in #2567. ```yaml volumes: - name: data external: template: capacity: 10Gi storageClassName: standard ``` The per-actor record in ActorStatus gets wrapped in a new ExternalVolumes message. This will make changes for shared volume support easier post GA. Callers, tests, demos, benchmark manifests and docs are updated; generated code is regenerated. ## Testing Unit, store (Postgres), controlapi and functional suites pass; `hack/verify/gofmt.sh`, `hack/verify/boilerplate.sh`, `apitool validate` and its exemption test pass. #### Breaking change `ActorTemplate.volumes[].externalVolumeTemplate` is gone; templates must use `volumes[].external.template`. Field number 3 is retired, not reused, so templates stored under the old field decode with no volume source and must be recreated (they fail validation on their next update). Go consumers of `pkg/proto/ateapipb` see the new `ExternalVolumeSource` type and the moved getter. Requesting the `breaking-change` label. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
644 lines
18 KiB
Go
644 lines
18 KiB
Go
// Copyright 2026 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package controlapi
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"go.ate.dev/substrate/internal/proto/ateletpb"
|
|
"go.ate.dev/substrate/pkg/proto/ateapipb"
|
|
"google.golang.org/protobuf/testing/protocmp"
|
|
)
|
|
|
|
func TestWorkloadSpecFromActorTemplate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
template *ateapipb.ActorTemplate
|
|
want *ateletpb.WorkloadSpec
|
|
}{
|
|
{
|
|
name: "converts DurableDir volume and mounts",
|
|
template: &ateapipb.ActorTemplate{
|
|
Metadata: &ateapipb.ResourceMetadata{Atespace: "agent-ns", Name: "tmpl1"},
|
|
Volumes: []*ateapipb.Volume{
|
|
{Name: "home", DurableDir: &ateapipb.DurableDirVolumeSource{}},
|
|
},
|
|
Containers: []*ateapipb.Container{
|
|
{
|
|
Name: "main",
|
|
Image: "main",
|
|
VolumeMounts: []*ateapipb.VolumeMount{
|
|
{Name: "home", MountPath: "/home/user"},
|
|
{Name: "home", MountPath: "/workspace"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: &ateletpb.WorkloadSpec{
|
|
Volumes: []*ateletpb.Volume{
|
|
{
|
|
Name: "home",
|
|
DurableDir: &ateletpb.DurableDirVolumeSource{},
|
|
},
|
|
},
|
|
Containers: []*ateletpb.Container{
|
|
{
|
|
Name: "main",
|
|
Image: "main",
|
|
VolumeMounts: []*ateletpb.VolumeMount{
|
|
{Name: "home", MountPath: "/home/user"},
|
|
{Name: "home", MountPath: "/workspace"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "converts SystemInfo volume with actorMetadata items",
|
|
template: &ateapipb.ActorTemplate{
|
|
Metadata: &ateapipb.ResourceMetadata{Atespace: "agent-ns", Name: "tmpl1"},
|
|
Volumes: []*ateapipb.Volume{
|
|
{
|
|
Name: "system-info",
|
|
SystemInfo: &ateapipb.SystemInfoVolumeSource{
|
|
DataSources: []*ateapipb.SystemInfoDataSource{
|
|
{ActorMetadata: &ateapipb.ActorMetadataDataSource{
|
|
Items: []*ateapipb.ActorMetadataItem{
|
|
{Field: ateapipb.ActorMetadataField_ACTOR_METADATA_FIELD_NAME, Path: "actor-name"},
|
|
{Field: ateapipb.ActorMetadataField_ACTOR_METADATA_FIELD_ATESPACE, Path: "atespace"},
|
|
{Field: ateapipb.ActorMetadataField_ACTOR_METADATA_FIELD_UID, Path: "identity/actor-uid"},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Containers: []*ateapipb.Container{
|
|
{
|
|
Name: "main",
|
|
Image: "main",
|
|
VolumeMounts: []*ateapipb.VolumeMount{
|
|
{Name: "system-info", MountPath: "/run/ate"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: &ateletpb.WorkloadSpec{
|
|
Volumes: []*ateletpb.Volume{
|
|
{
|
|
Name: "system-info",
|
|
SystemInfo: &ateletpb.SystemInfoVolumeSource{
|
|
DataSources: []*ateletpb.SystemInfoDataSource{
|
|
{ActorMetadata: &ateletpb.ActorMetadataDataSource{
|
|
Items: []*ateletpb.ActorMetadataItem{
|
|
{Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_NAME, Path: "actor-name"},
|
|
{Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_ATESPACE, Path: "atespace"},
|
|
{Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_UID, Path: "identity/actor-uid"},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Containers: []*ateletpb.Container{
|
|
{
|
|
Name: "main",
|
|
Image: "main",
|
|
VolumeMounts: []*ateletpb.VolumeMount{
|
|
{Name: "system-info", MountPath: "/run/ate"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "converts Image volume and mounts",
|
|
template: &ateapipb.ActorTemplate{
|
|
Metadata: &ateapipb.ResourceMetadata{Atespace: "agent-ns", Name: "tmpl1"},
|
|
Volumes: []*ateapipb.Volume{
|
|
{Name: "home", DurableDir: &ateapipb.DurableDirVolumeSource{}},
|
|
{Name: "agent", Image: &ateapipb.ImageVolumeSource{Reference: "example.com/agent@sha256:abc"}},
|
|
},
|
|
Containers: []*ateapipb.Container{
|
|
{
|
|
Name: "main",
|
|
Image: "main",
|
|
VolumeMounts: []*ateapipb.VolumeMount{
|
|
{Name: "home", MountPath: "/home/user"},
|
|
{Name: "agent", MountPath: "/ate"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: &ateletpb.WorkloadSpec{
|
|
Volumes: []*ateletpb.Volume{
|
|
{
|
|
Name: "home",
|
|
DurableDir: &ateletpb.DurableDirVolumeSource{},
|
|
},
|
|
{
|
|
Name: "agent",
|
|
Image: &ateletpb.ImageVolumeSource{Reference: "example.com/agent@sha256:abc"},
|
|
},
|
|
},
|
|
Containers: []*ateletpb.Container{
|
|
{
|
|
Name: "main",
|
|
Image: "main",
|
|
VolumeMounts: []*ateletpb.VolumeMount{
|
|
{Name: "home", MountPath: "/home/user"},
|
|
{Name: "agent", MountPath: "/ate"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "converts trustBundle data sources carrying the name for node-side resolution",
|
|
template: &ateapipb.ActorTemplate{
|
|
Metadata: &ateapipb.ResourceMetadata{Atespace: "agent-ns", Name: "tmpl1"},
|
|
Volumes: []*ateapipb.Volume{
|
|
{
|
|
Name: "system-info",
|
|
SystemInfo: &ateapipb.SystemInfoVolumeSource{
|
|
DataSources: []*ateapipb.SystemInfoDataSource{
|
|
{TrustBundle: &ateapipb.TrustBundleDataSource{Names: []string{"egress-trust"}, Path: "trust/ca.pem"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Containers: []*ateapipb.Container{
|
|
{
|
|
Name: "main",
|
|
Image: "main",
|
|
VolumeMounts: []*ateapipb.VolumeMount{
|
|
{Name: "system-info", MountPath: "/run/substrate/certs"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
// The NAME crosses the wire; atelet resolves it against its
|
|
// allowlist and ClusterTrustBundle informer at write time.
|
|
want: &ateletpb.WorkloadSpec{
|
|
Volumes: []*ateletpb.Volume{
|
|
{
|
|
Name: "system-info",
|
|
SystemInfo: &ateletpb.SystemInfoVolumeSource{
|
|
DataSources: []*ateletpb.SystemInfoDataSource{
|
|
{TrustBundle: &ateletpb.TrustBundleDataSource{Names: []string{"egress-trust"}, Path: "trust/ca.pem"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Containers: []*ateletpb.Container{
|
|
{
|
|
Name: "main",
|
|
Image: "main",
|
|
VolumeMounts: []*ateletpb.VolumeMount{
|
|
{Name: "system-info", MountPath: "/run/substrate/certs"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "skips non-DurableDir volumes",
|
|
template: &ateapipb.ActorTemplate{
|
|
Metadata: &ateapipb.ResourceMetadata{Atespace: "agent-ns", Name: "tmpl1"},
|
|
Volumes: []*ateapipb.Volume{
|
|
{Name: "unsupported"},
|
|
{Name: "home", DurableDir: &ateapipb.DurableDirVolumeSource{}},
|
|
},
|
|
Containers: []*ateapipb.Container{
|
|
{
|
|
Name: "main",
|
|
Image: "main",
|
|
VolumeMounts: []*ateapipb.VolumeMount{
|
|
{Name: "home", MountPath: "/workspace"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: &ateletpb.WorkloadSpec{
|
|
Volumes: []*ateletpb.Volume{
|
|
{
|
|
Name: "home",
|
|
DurableDir: &ateletpb.DurableDirVolumeSource{},
|
|
},
|
|
},
|
|
Containers: []*ateletpb.Container{
|
|
{
|
|
Name: "main",
|
|
Image: "main",
|
|
VolumeMounts: []*ateletpb.VolumeMount{
|
|
{Name: "home", MountPath: "/workspace"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "container without volume mounts has none",
|
|
template: &ateapipb.ActorTemplate{
|
|
Metadata: &ateapipb.ResourceMetadata{Atespace: "agent-ns", Name: "tmpl1"},
|
|
Volumes: []*ateapipb.Volume{
|
|
{Name: "home", DurableDir: &ateapipb.DurableDirVolumeSource{}},
|
|
},
|
|
Containers: []*ateapipb.Container{
|
|
{Name: "main", Image: "main"},
|
|
},
|
|
},
|
|
want: &ateletpb.WorkloadSpec{
|
|
Volumes: []*ateletpb.Volume{
|
|
{
|
|
Name: "home",
|
|
DurableDir: &ateletpb.DurableDirVolumeSource{},
|
|
},
|
|
},
|
|
Containers: []*ateletpb.Container{{Name: "main", Image: "main"}},
|
|
},
|
|
},
|
|
{
|
|
name: "maps literal env",
|
|
template: &ateapipb.ActorTemplate{
|
|
Metadata: &ateapipb.ResourceMetadata{Atespace: "agent-ns", Name: "tmpl1"},
|
|
Containers: []*ateapipb.Container{
|
|
{
|
|
Name: "main",
|
|
Image: "main",
|
|
Env: []*ateapipb.EnvVar{
|
|
{Name: "LITERAL", Value: "plain"},
|
|
{Name: "EMPTY", Value: ""},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: &ateletpb.WorkloadSpec{
|
|
Containers: []*ateletpb.Container{{
|
|
Name: "main",
|
|
Image: "main",
|
|
Env: []*ateletpb.EnvVar{
|
|
{Name: "LITERAL", Value: "plain"},
|
|
{Name: "EMPTY", Value: ""},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
{
|
|
name: "maps command and args",
|
|
template: &ateapipb.ActorTemplate{
|
|
Metadata: &ateapipb.ResourceMetadata{Atespace: "agent-ns", Name: "tmpl1"},
|
|
Containers: []*ateapipb.Container{
|
|
{
|
|
Name: "main",
|
|
Image: "main",
|
|
Command: []string{"/entrypoint"},
|
|
Args: []string{"--foo", "--bar"},
|
|
},
|
|
},
|
|
},
|
|
want: &ateletpb.WorkloadSpec{
|
|
Containers: []*ateletpb.Container{{
|
|
Name: "main",
|
|
Image: "main",
|
|
Command: []string{"/entrypoint"},
|
|
Args: []string{"--foo", "--bar"},
|
|
}},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := workloadSpecFromActorTemplate(tt.template, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("workloadSpecFromActorTemplate failed: %v", err)
|
|
}
|
|
if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" {
|
|
t.Errorf("WorkloadSpec mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorkloadSpecFromActorTemplatePropagatesWakeupProbe(t *testing.T) {
|
|
got, err := workloadSpecFromActorTemplate(&ateapipb.ActorTemplate{
|
|
Metadata: &ateapipb.ResourceMetadata{Atespace: "agent-ns", Name: "tmpl-wakeup-probe"},
|
|
Containers: []*ateapipb.Container{
|
|
{
|
|
Name: "with-probe",
|
|
Image: "main",
|
|
WakeupProbe: &ateapipb.WakeupProbe{
|
|
HttpGet: &ateapipb.HttpGetAction{Path: "/health", Port: 8080},
|
|
TimeoutSeconds: 45,
|
|
},
|
|
},
|
|
{
|
|
Name: "without-probe",
|
|
Image: "side",
|
|
},
|
|
},
|
|
}, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("workloadSpecFromActorTemplate failed: %v", err)
|
|
}
|
|
|
|
want := &ateletpb.WorkloadSpec{
|
|
Containers: []*ateletpb.Container{
|
|
{
|
|
Name: "with-probe",
|
|
Image: "main",
|
|
WakeupProbe: &ateletpb.WakeupProbe{
|
|
HttpGet: &ateletpb.HttpGetAction{Path: "/health", Port: 8080},
|
|
TimeoutSeconds: 45,
|
|
},
|
|
},
|
|
{
|
|
Name: "without-probe",
|
|
Image: "side",
|
|
},
|
|
},
|
|
}
|
|
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
|
|
t.Errorf("WorkloadSpec mismatch (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
func TestAppendExternalVolumes(t *testing.T) {
|
|
template := &ateapipb.ActorTemplate{
|
|
Containers: []*ateapipb.Container{
|
|
{
|
|
Name: "main",
|
|
VolumeMounts: []*ateapipb.VolumeMount{
|
|
{Name: "vol-1", MountPath: "/mnt/vol1"},
|
|
},
|
|
},
|
|
},
|
|
Volumes: []*ateapipb.Volume{
|
|
{
|
|
Name: "vol-1",
|
|
External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{
|
|
StorageClassName: "pd-standard",
|
|
}},
|
|
},
|
|
{
|
|
Name: "vol-2",
|
|
DurableDir: &ateapipb.DurableDirVolumeSource{},
|
|
},
|
|
{
|
|
Name: "unmounted-vol",
|
|
External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{
|
|
StorageClassName: "pd-standard",
|
|
}},
|
|
},
|
|
},
|
|
}
|
|
|
|
actor := &ateapipb.Actor{
|
|
Metadata: &ateapipb.ResourceMetadata{
|
|
Atespace: "space-abc",
|
|
Name: "actor-123",
|
|
},
|
|
Status: &ateapipb.ActorStatus{
|
|
ExternalVolumes: &ateapipb.ExternalVolumes{Volumes: []*ateapipb.ExternalVolumeStatus{
|
|
{
|
|
Name: "vol-1",
|
|
StorageVolumeId: "vol-gce-pd-123",
|
|
VolumeType: "pd-standard",
|
|
VolumeContext: map[string]string{"foo": "bar"},
|
|
},
|
|
}},
|
|
},
|
|
}
|
|
|
|
for _, tt := range []struct {
|
|
name string
|
|
volumePublishContexts map[string]map[string]string
|
|
wantPublishCtx map[string]string
|
|
}{
|
|
{
|
|
name: "with attachment metadata",
|
|
volumePublishContexts: map[string]map[string]string{"vol-1": {"devicePath": "/dev/xvdba"}},
|
|
wantPublishCtx: map[string]string{"devicePath": "/dev/xvdba"},
|
|
},
|
|
{
|
|
name: "without attachment metadata",
|
|
},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
workloadSpec := &ateletpb.WorkloadSpec{}
|
|
if err := appendExternalVolumes(workloadSpec, template, actor, tt.volumePublishContexts); err != nil {
|
|
t.Fatalf("appendExternalVolumes unexpected error: %v", err)
|
|
}
|
|
|
|
want := &ateletpb.WorkloadSpec{
|
|
Volumes: []*ateletpb.Volume{
|
|
{
|
|
Name: "vol-1",
|
|
External: &ateletpb.ExternalVolumeSource{
|
|
StorageVolumeId: "vol-gce-pd-123",
|
|
VolumeType: "pd-standard",
|
|
VolumeContext: map[string]string{"foo": "bar"},
|
|
PublishContext: tt.wantPublishCtx,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
if diff := cmp.Diff(want, workloadSpec, protocmp.Transform()); diff != "" {
|
|
t.Errorf("appendExternalVolumes mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Test missing mounted volume returns an error
|
|
missingActor := &ateapipb.Actor{
|
|
Metadata: &ateapipb.ResourceMetadata{
|
|
Atespace: "space-abc",
|
|
Name: "actor-123",
|
|
},
|
|
Status: &ateapipb.ActorStatus{ExternalVolumes: &ateapipb.ExternalVolumes{Volumes: []*ateapipb.ExternalVolumeStatus{}}},
|
|
}
|
|
if err := appendExternalVolumes(&ateletpb.WorkloadSpec{}, template, missingActor, nil); err == nil {
|
|
t.Errorf("appendExternalVolumes expected error for missing volume, got nil")
|
|
}
|
|
}
|
|
|
|
func TestWorkloadSpecFromActorTemplatePropagatesSecurityContext(t *testing.T) {
|
|
got, err := workloadSpecFromActorTemplate(&ateapipb.ActorTemplate{
|
|
Metadata: &ateapipb.ResourceMetadata{Atespace: "agent-ns", Name: "tmpl-caps"},
|
|
Containers: []*ateapipb.Container{
|
|
{
|
|
Name: "adjusted",
|
|
Image: "main",
|
|
SecurityContext: &ateapipb.SecurityContext{
|
|
Capabilities: &ateapipb.Capabilities{
|
|
Add: []string{"NET_ADMIN"},
|
|
Drop: []string{"ALL"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "unset",
|
|
Image: "side",
|
|
},
|
|
{
|
|
// An empty capabilities block asks for no adjustment, so
|
|
// nothing is put on the wire for it.
|
|
Name: "empty",
|
|
Image: "third",
|
|
SecurityContext: &ateapipb.SecurityContext{Capabilities: &ateapipb.Capabilities{}},
|
|
},
|
|
},
|
|
}, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("workloadSpecFromActorTemplate failed: %v", err)
|
|
}
|
|
|
|
want := &ateletpb.WorkloadSpec{
|
|
Containers: []*ateletpb.Container{
|
|
{
|
|
Name: "adjusted",
|
|
Image: "main",
|
|
SecurityContext: &ateletpb.SecurityContext{
|
|
Capabilities: &ateletpb.Capabilities{
|
|
Add: []string{"NET_ADMIN"},
|
|
Drop: []string{"ALL"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "unset",
|
|
Image: "side",
|
|
},
|
|
{
|
|
Name: "empty",
|
|
Image: "third",
|
|
},
|
|
},
|
|
}
|
|
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
|
|
t.Errorf("WorkloadSpec mismatch (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
func TestToAteletResources(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in *ateapipb.Resources
|
|
want *ateletpb.ResourceLimits
|
|
wantErr bool
|
|
}{{
|
|
name: "nil resources",
|
|
in: nil,
|
|
want: nil,
|
|
}, {
|
|
name: "empty limits",
|
|
in: &ateapipb.Resources{},
|
|
want: nil,
|
|
}, {
|
|
name: "memory only",
|
|
in: &ateapipb.Resources{Limits: []*ateapipb.Limits{{Name: "memory", Quantity: "256Mi"}}},
|
|
want: &ateletpb.ResourceLimits{MemoryBytes: 268435456},
|
|
}, {
|
|
name: "cpu only",
|
|
in: &ateapipb.Resources{Limits: []*ateapipb.Limits{{Name: "cpu", Quantity: "200m"}}},
|
|
want: &ateletpb.ResourceLimits{CpuMillis: 200},
|
|
}, {
|
|
name: "both",
|
|
in: &ateapipb.Resources{Limits: []*ateapipb.Limits{
|
|
{Name: "cpu", Quantity: "1"},
|
|
{Name: "memory", Quantity: "1Gi"},
|
|
}},
|
|
want: &ateletpb.ResourceLimits{MemoryBytes: 1073741824, CpuMillis: 1000},
|
|
}, {
|
|
name: "invalid quantity",
|
|
in: &ateapipb.Resources{Limits: []*ateapipb.Limits{{Name: "cpu", Quantity: "banana"}}},
|
|
wantErr: true,
|
|
}}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := toAteletResources(tc.in)
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Fatal("toAteletResources() error = nil, want parse error")
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("toAteletResources() failed: %v", err)
|
|
}
|
|
if tc.want == nil {
|
|
if got != nil {
|
|
t.Fatalf("toAteletResources() = %v, want nil", got)
|
|
}
|
|
return
|
|
}
|
|
if got == nil {
|
|
t.Fatalf("toAteletResources() = nil, want %v", tc.want)
|
|
}
|
|
if got.GetMemoryBytes() != tc.want.GetMemoryBytes() {
|
|
t.Errorf("memory_bytes = %d, want %d", got.GetMemoryBytes(), tc.want.GetMemoryBytes())
|
|
}
|
|
if got.GetCpuMillis() != tc.want.GetCpuMillis() {
|
|
t.Errorf("cpu_millis = %d, want %d", got.GetCpuMillis(), tc.want.GetCpuMillis())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The limits a template declares must reach the workload spec atelet receives.
|
|
// Without this, removing the Resources field from the ateletpb.Container
|
|
// literal leaves every test in the repository green while limits never leave
|
|
// ate-api-server.
|
|
func TestWorkloadSpecFromActorTemplatePropagatesResources(t *testing.T) {
|
|
got, err := workloadSpecFromActorTemplate(&ateapipb.ActorTemplate{
|
|
Metadata: &ateapipb.ResourceMetadata{Atespace: "agent-ns", Name: "tmpl-limits"},
|
|
Containers: []*ateapipb.Container{
|
|
{
|
|
Name: "limited",
|
|
Image: "main",
|
|
Resources: &ateapipb.Resources{Limits: []*ateapipb.Limits{
|
|
{Name: "cpu", Quantity: "200m"},
|
|
{Name: "memory", Quantity: "256Mi"},
|
|
}},
|
|
},
|
|
{Name: "unlimited", Image: "main"},
|
|
},
|
|
}, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("workloadSpecFromActorTemplate failed: %v", err)
|
|
}
|
|
if len(got.GetContainers()) != 2 {
|
|
t.Fatalf("containers = %d, want 2", len(got.GetContainers()))
|
|
}
|
|
|
|
limited := got.GetContainers()[0].GetResources()
|
|
if limited == nil {
|
|
t.Fatal("limited container Resources = nil, want the declared limits")
|
|
}
|
|
if limited.GetMemoryBytes() != 268435456 {
|
|
t.Errorf("memory_bytes = %d, want 268435456", limited.GetMemoryBytes())
|
|
}
|
|
if limited.GetCpuMillis() != 200 {
|
|
t.Errorf("cpu_millis = %d, want 200", limited.GetCpuMillis())
|
|
}
|
|
if r := got.GetContainers()[1].GetResources(); r != nil {
|
|
t.Errorf("unlimited container Resources = %v, want nil", r)
|
|
}
|
|
}
|