validation: resolve validation todo in ateapi.proto (#1600)

Fixes a few todos in the `ateapi.proto`

* Adds custom validation for `create_time` and `update_time` fields.
* Adds better validation method for the `Container.image` field.

- [ ] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
This commit is contained in:
shrutiyam-glitch
2026-09-14 11:34:48 -04:00
committed by GitHub
parent 03d7428deb
commit 704eeb4376
11 changed files with 152 additions and 41 deletions
@@ -21,6 +21,8 @@ import (
"regexp"
"strings"
"github.com/distribution/reference"
"github.com/agent-substrate/substrate/cmd/ateapi/internal/store"
"github.com/agent-substrate/substrate/internal/resources"
"github.com/agent-substrate/substrate/internal/volumepath"
@@ -269,11 +271,21 @@ func ValidateCustom_SystemInfoVolumeSource_DataSources(_ context.Context, _ oper
return errs
}
// validatePinnedImage requires an image reference to include a digest
// (e.g. "name@sha256:...").
// validatePinnedImage requires a well-formed OCI image reference pinned by
// digest (e.g. "name@sha256:..."): changing the image content under a fixed
// reference invalidates snapshots. It parses with the same grammar the
// container runtimes use, so a malformed digest is rejected rather than
// treated as pinned.
func validatePinnedImage(fldPath *field.Path, value string) field.ErrorList {
if !strings.Contains(value, "@") {
return field.ErrorList{field.Invalid(fldPath, value, "must include a digest")}
if value == "" {
return nil // required is enforced by tags
}
ref, err := reference.ParseNormalizedNamed(value)
if err != nil {
return field.ErrorList{field.Invalid(fldPath, value, fmt.Sprintf("must be a well-formed image reference: %v", err))}
}
if _, ok := ref.(reference.Digested); !ok {
return field.ErrorList{field.Invalid(fldPath, value, "must be pinned by digest (changing the image invalidates snapshots)")}
}
return nil
}
@@ -40,7 +40,7 @@ import (
func validActorTemplate(mutations ...func(*ateapipb.ActorTemplate)) *ateapipb.ActorTemplate {
template := &ateapipb.ActorTemplate{
Metadata: &ateapipb.ResourceMetadata{Atespace: "ns1", Name: "tmpl-a"},
Containers: []*ateapipb.Container{{Name: "main", Image: "example.com/app:v1@sha256:abc"}},
Containers: []*ateapipb.Container{{Name: "main", Image: "example.com/app:v1@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}},
SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots"},
SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR, ConfigName: "gvisor-default"},
}
@@ -308,7 +308,7 @@ func TestCreateActorTemplateIgnoresServerOwnedFields(t *testing.T) {
tmpl.Metadata.Uid = "11111111-1111-1111-1111-111111111111"
tmpl.Metadata.Version = 42
tmpl.WorkerSelector = &ateapipb.Selector{MatchLabels: map[string]string{"pool": "default"}}
tmpl.Containers = []*ateapipb.Container{{Name: "main", Image: "example.com/app:v1@sha256:abc"}}
tmpl.Containers = []*ateapipb.Container{{Name: "main", Image: "example.com/app:v1@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}}
tmpl.SnapshotsConfig = &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots"}
tmpl.Resources = &ateapipb.Resources{Limits: []*ateapipb.Limits{{Name: "memory", Quantity: "1Gi"}}}
// Server-owned status a client must not be able to set.
@@ -550,14 +550,14 @@ func TestValidateActorTemplate(t *testing.T) {
name: "too many containers",
mutate: func(tmpl *ateapipb.ActorTemplate) {
for i := 0; i < 10; i++ {
tmpl.Containers = append(tmpl.Containers, &ateapipb.Container{Name: fmt.Sprintf("c-%d", i), Image: "example.com/app:v1@sha256:abc"})
tmpl.Containers = append(tmpl.Containers, &ateapipb.Container{Name: fmt.Sprintf("c-%d", i), Image: "example.com/app:v1@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"})
}
},
want: field.ErrorList{field.TooMany(field.NewPath("containers"), 11, 10).WithOrigin("maxItems")},
}, {
name: "duplicate container name",
mutate: func(tmpl *ateapipb.ActorTemplate) {
tmpl.Containers = append(tmpl.Containers, &ateapipb.Container{Name: "main", Image: "example.com/other:v1@sha256:abc"})
tmpl.Containers = append(tmpl.Containers, &ateapipb.Container{Name: "main", Image: "example.com/other:v1@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"})
},
want: field.ErrorList{field.Duplicate(field.NewPath("containers").Index(1), nil)},
}, {
@@ -631,7 +631,7 @@ func TestValidateActorTemplate(t *testing.T) {
}, {
name: "the same path in different containers is allowed",
mutate: func(tmpl *ateapipb.ActorTemplate) {
tmpl.Containers = append(tmpl.Containers, &ateapipb.Container{Name: "sidecar", Image: "example.com/side:v1@sha256:abc"})
tmpl.Containers = append(tmpl.Containers, &ateapipb.Container{Name: "sidecar", Image: "example.com/side:v1@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"})
tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{{Name: "data", MountPath: "/var/data"}}
tmpl.Containers[1].VolumeMounts = []*ateapipb.VolumeMount{{Name: "data", MountPath: "/var/data"}}
},
@@ -712,9 +712,41 @@ func TestValidateActorTemplate(t *testing.T) {
}, {
name: "image too long",
mutate: func(tmpl *ateapipb.ActorTemplate) {
tmpl.Containers[0].Image = strings.Repeat("x", 513) + "@sha256:abc"
tmpl.Containers[0].Image = strings.Repeat("x", 513) + "@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
want: field.ErrorList{field.TooLong(field.NewPath("containers").Index(0).Child("image"), nil, 512).WithOrigin("maxLength")},
want: field.ErrorList{
field.Invalid(field.NewPath("containers").Index(0).Child("image"), nil, ""),
field.TooLong(field.NewPath("containers").Index(0).Child("image"), nil, 512).WithOrigin("maxLength"),
},
}, {
name: "invalid image: bare repository without digest",
mutate: func(tmpl *ateapipb.ActorTemplate) {
tmpl.Containers[0].Image = "ubuntu"
},
want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("image"), nil, "")},
}, {
name: "valid image: pinned by digest",
mutate: func(tmpl *ateapipb.ActorTemplate) {
tmpl.Containers[0].Image = "example.com/app@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
}, {
name: "invalid image: uppercase repository",
mutate: func(tmpl *ateapipb.ActorTemplate) {
tmpl.Containers[0].Image = "example.com/App:v1"
},
want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("image"), nil, "")},
}, {
name: "invalid image: malformed digest",
mutate: func(tmpl *ateapipb.ActorTemplate) {
tmpl.Containers[0].Image = "example.com/app@sha256:abc"
},
want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("image"), nil, "")},
}, {
name: "invalid image: empty tag",
mutate: func(tmpl *ateapipb.ActorTemplate) {
tmpl.Containers[0].Image = "example.com/app:"
},
want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("image"), nil, "")},
}, {
name: "container image missing digest",
mutate: func(tmpl *ateapipb.ActorTemplate) {
@@ -949,14 +981,14 @@ func TestValidateActorTemplate(t *testing.T) {
tmpl.Volumes = []*ateapipb.Volume{{
Name: "scratch",
DurableDir: &ateapipb.DurableDirVolumeSource{},
Image: &ateapipb.ImageVolumeSource{Reference: "example.com/app@sha256:abc"},
Image: &ateapipb.ImageVolumeSource{Reference: "example.com/app@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},
}}
},
want: field.ErrorList{field.Invalid(field.NewPath("volumes").Index(0), nil, "one of").WithOrigin("union")},
}, {
name: "valid image volume",
mutate: func(tmpl *ateapipb.ActorTemplate) {
tmpl.Volumes = []*ateapipb.Volume{{Name: "tools", Image: &ateapipb.ImageVolumeSource{Reference: "example.com/app@sha256:abc"}}}
tmpl.Volumes = []*ateapipb.Volume{{Name: "tools", Image: &ateapipb.ImageVolumeSource{Reference: "example.com/app@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}}}
},
}, {
name: "image volume missing reference",
@@ -970,6 +1002,18 @@ func TestValidateActorTemplate(t *testing.T) {
tmpl.Volumes = []*ateapipb.Volume{{Name: "tools", Image: &ateapipb.ImageVolumeSource{Reference: "example.com/app:v1"}}}
},
want: field.ErrorList{field.Invalid(field.NewPath("volumes").Index(0).Child("image", "reference"), nil, "")},
}, {
name: "image volume reference with malformed digest",
mutate: func(tmpl *ateapipb.ActorTemplate) {
tmpl.Volumes = []*ateapipb.Volume{{Name: "tools", Image: &ateapipb.ImageVolumeSource{Reference: "example.com/app@sha256:abc"}}}
},
want: field.ErrorList{field.Invalid(field.NewPath("volumes").Index(0).Child("image", "reference"), nil, "")},
}, {
name: "image volume reference not a reference at all",
mutate: func(tmpl *ateapipb.ActorTemplate) {
tmpl.Volumes = []*ateapipb.Volume{{Name: "tools", Image: &ateapipb.ImageVolumeSource{Reference: "@"}}}
},
want: field.ErrorList{field.Invalid(field.NewPath("volumes").Index(0).Child("image", "reference"), nil, "")},
}, {
name: "valid external volume template",
mutate: func(tmpl *ateapipb.ActorTemplate) {
@@ -34,7 +34,7 @@ func TestActorTemplateCRUD(t *testing.T) {
created, err := tc.client.CreateActorTemplate(ctx, &ateapipb.CreateActorTemplateRequest{
ActorTemplate: &ateapipb.ActorTemplate{
Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "tmpl-a"},
Containers: []*ateapipb.Container{{Name: "main", Image: "example.com/app:v1@sha256:abc"}},
Containers: []*ateapipb.Container{{Name: "main", Image: "example.com/app:v1@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}},
SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots"},
SandboxConfig: &ateapipb.SandboxConfig{
SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR,
@@ -54,7 +54,7 @@ func TestActorTemplateCRUD(t *testing.T) {
}
want := &ateapipb.ActorTemplate{
Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "tmpl-a", Version: 1},
Containers: []*ateapipb.Container{{Name: "main", Image: "example.com/app:v1@sha256:abc"}},
Containers: []*ateapipb.Container{{Name: "main", Image: "example.com/app:v1@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}},
SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots"},
SandboxConfig: &ateapipb.SandboxConfig{
SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR,
@@ -70,7 +70,7 @@ func TestActorTemplateCRUD(t *testing.T) {
_, err = tc.client.CreateActorTemplate(ctx, &ateapipb.CreateActorTemplateRequest{
ActorTemplate: &ateapipb.ActorTemplate{
Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "tmpl-a"},
Containers: []*ateapipb.Container{{Name: "main", Image: "example.com/app:v1@sha256:abc"}},
Containers: []*ateapipb.Container{{Name: "main", Image: "example.com/app:v1@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}},
SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots"},
SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR, ConfigName: "gvisor-default"},
},
@@ -126,7 +126,7 @@ func TestActorTemplateCRUD(t *testing.T) {
_, err = tc.client.CreateActorTemplate(ctx, &ateapipb.CreateActorTemplateRequest{
ActorTemplate: &ateapipb.ActorTemplate{
Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "tmpl-unnamed-config"},
Containers: []*ateapipb.Container{{Name: "main", Image: "example.com/app:v1@sha256:abc"}},
Containers: []*ateapipb.Container{{Name: "main", Image: "example.com/app:v1@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}},
SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots"},
SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR},
},
@@ -184,7 +184,7 @@ func TestCreateActor_SubstrateTemplateRef(t *testing.T) {
if _, err := tc.client.CreateActorTemplate(ctx, &ateapipb.CreateActorTemplateRequest{
ActorTemplate: &ateapipb.ActorTemplate{
Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "sub-tmpl"},
Containers: []*ateapipb.Container{{Name: "main", Image: "example.com/app:v1@sha256:abc"}},
Containers: []*ateapipb.Container{{Name: "main", Image: "example.com/app:v1@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}},
SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots"},
SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR, ConfigName: "gvisor-default"},
},
@@ -313,7 +313,7 @@ func TestCreateActor_RejectsSnapshotWithExternalVolumes(t *testing.T) {
ConfigName: "gvisor-default",
},
Containers: []*ateapipb.Container{{
Name: "main", Image: "main@sha256:abc", VolumeMounts: []*ateapipb.VolumeMount{{Name: "data", MountPath: "/data"}},
Name: "main", Image: "main@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", VolumeMounts: []*ateapipb.VolumeMount{{Name: "data", MountPath: "/data"}},
}},
Volumes: []*ateapipb.Volume{{
Name: "data",
@@ -753,7 +753,7 @@ func TestUpdateActor_RepointTemplate(t *testing.T) {
Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: name},
Containers: []*ateapipb.Container{{
Name: "main",
Image: "example.com/app:v1@sha256:abc",
Image: "example.com/app:v1@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
VolumeMounts: []*ateapipb.VolumeMount{{Name: "data", MountPath: tmpl.mountPath}},
}},
Volumes: tmpl.volumes,
@@ -1865,7 +1865,7 @@ func TestResumeActorPassesLiteralEnv(t *testing.T) {
createTemplateWithContainers(t, tc, ns, []*ateapipb.Container{
{
Name: "main",
Image: "main@sha256:abc",
Image: "main@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
Command: []string{"/main"},
Env: []*ateapipb.EnvVar{
{
@@ -385,7 +385,7 @@ func createTemplate(t *testing.T, tc *testContext, ns string) *ateapipb.ActorTem
return createTemplateWithContainers(t, tc, ns, []*ateapipb.Container{
{
Name: "main",
Image: "main@sha256:abc",
Image: "main@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
Command: []string{"/main"},
},
})
@@ -411,7 +411,7 @@ func createTemplateWithVolumes(t *testing.T, tc *testContext, ns string, volumes
return createTemplateWithContainersAndVolumes(t, tc, ns, []*ateapipb.Container{
{
Name: "main",
Image: "main@sha256:abc",
Image: "main@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
Command: []string{"/main"},
VolumeMounts: mounts,
},
@@ -476,7 +476,7 @@ func createTemplateWithContainersAndVolumes(t *testing.T, tc *testContext, ns st
// testPauseImage is the pause image the default test SandboxConfig carries;
// it is what a resolved WorkloadSpec's sandbox assets should name.
const testPauseImage = "pause@sha256:abc"
const testPauseImage = "pause@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
// ensureDefaultGvisorSandboxConfig creates the cluster-scoped "gvisor-default"
// SandboxConfig (idempotently) and waits for it to appear in the lister.
@@ -527,7 +527,7 @@ func createWorkerPool(t *testing.T, tc *testContext, ns string, name string, lab
},
Spec: atev1alpha1.WorkerPoolSpec{
Replicas: 1,
WorkerImage: "ateom@sha256:abc",
WorkerImage: "ateom@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
},
}
_, err := tc.substrateClient.ApiV1alpha1().WorkerPools(ns).Create(context.Background(), wp, metav1.CreateOptions{})
@@ -571,7 +571,7 @@ func createTemplateWithSelector(t *testing.T, tc *testContext, name string, sele
ConfigName: "gvisor-default",
},
Containers: []*ateapipb.Container{
{Name: "main", Image: "main@sha256:abc", Command: []string{"/main"}},
{Name: "main", Image: "main@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Command: []string{"/main"}},
},
WorkerSelector: selector,
},
@@ -77,6 +77,31 @@ func ateDeepEqual[T any](a, b T) bool {
return reflect.DeepEqual(a, b)
}
// ValidateCustom_ResourceMetadata checks the server-stamped timestamps: each,
// when set, must be a valid google.protobuf.Timestamp, and update_time must
// not precede create_time. Both fields are scrubbed from input, so a
// violation here is a server stamping bug surfaced by the final-object
// validation pass, not a client error.
func ValidateCustom_ResourceMetadata(_ context.Context, _ operation.Operation, fldPath *field.Path, obj, _ *ateapipb.ResourceMetadata) field.ErrorList {
var errs field.ErrorList
createTimeValid := false
if ct := obj.GetCreateTime(); ct != nil {
if err := ct.CheckValid(); err != nil {
errs = append(errs, field.Invalid(fldPath.Child("create_time"), ct.String(), err.Error()))
} else {
createTimeValid = true
}
}
if ut := obj.GetUpdateTime(); ut != nil {
if err := ut.CheckValid(); err != nil {
errs = append(errs, field.Invalid(fldPath.Child("update_time"), ut.String(), err.Error()))
} else if createTimeValid && ut.AsTime().Before(obj.GetCreateTime().AsTime()) {
errs = append(errs, field.Invalid(fldPath.Child("update_time"), ut.String(), "must not precede create_time"))
}
}
return errs
}
// This is needed because DV doesn't have a standard format for IP addresses yet.
func ValidateCustom_WorkerAssignment_WorkerPodIp(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList {
return validation.IsValidIP(fldPath, *value)
@@ -93,6 +93,26 @@ func TestValidateResourceMetadataCreate(t *testing.T) {
name: "unspecified updateTime",
obj: valid(func(rm *ateapipb.ResourceMetadata) { rm.UpdateTime = nil }),
want: nil,
}, {
name: "invalid createTime: seconds out of range",
obj: valid(func(rm *ateapipb.ResourceMetadata) { rm.CreateTime = &timestamppb.Timestamp{Seconds: 253402300800} }),
want: field.ErrorList{field.Invalid(field.NewPath("create_time"), nil, "")},
}, {
name: "invalid updateTime: negative nanos",
obj: valid(func(rm *ateapipb.ResourceMetadata) { rm.UpdateTime = &timestamppb.Timestamp{Seconds: 5309, Nanos: -1} }),
want: field.ErrorList{field.Invalid(field.NewPath("update_time"), nil, "")},
}, {
name: "invalid updateTime: precedes createTime",
obj: valid(func(rm *ateapipb.ResourceMetadata) { rm.UpdateTime = &timestamppb.Timestamp{Seconds: 866} }),
want: field.ErrorList{field.Invalid(field.NewPath("update_time"), nil, "")},
}, {
name: "valid updateTime: equals createTime",
obj: valid(func(rm *ateapipb.ResourceMetadata) { rm.UpdateTime = &timestamppb.Timestamp{Seconds: 867} }),
want: nil,
}, {
name: "valid updateTime: set without createTime",
obj: valid(func(rm *ateapipb.ResourceMetadata) { rm.CreateTime = nil }),
want: nil,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -201,9 +221,14 @@ func TestValidateResourceMetadataUpdate(t *testing.T) {
want: field.ErrorList{field.Invalid(field.NewPath("update_time"), nil, "").WithOrigin("update")},
}, {
name: "update_time: changed to valid",
oldObj: valid(func(rm *ateapipb.ResourceMetadata) { rm.UpdateTime.Seconds = 123 }),
newObj: valid(func(rm *ateapipb.ResourceMetadata) { rm.UpdateTime.Seconds = 456 }),
oldObj: valid(func(rm *ateapipb.ResourceMetadata) { rm.UpdateTime.Seconds = 1000 }),
newObj: valid(func(rm *ateapipb.ResourceMetadata) { rm.UpdateTime.Seconds = 6000 }),
want: nil,
}, {
name: "update_time: changed to precede create_time",
oldObj: valid(),
newObj: valid(func(rm *ateapipb.ResourceMetadata) { rm.UpdateTime.Seconds = 866 }),
want: field.ErrorList{field.Invalid(field.NewPath("update_time"), nil, "")},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -5033,6 +5033,11 @@ func Validate_ResourceMetadata(
ctx context.Context, op operation.Operation, fldPath *field.Path,
obj, oldObj *ateapipb.ResourceMetadata) (errs field.ErrorList) {
// custom validation
if e := ValidateCustom_ResourceMetadata(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
errs = append(errs, e...)
}
{ // field ateapipb.ResourceMetadata.Atespace
fn := func(
fldPath *field.Path,
+1 -1
View File
@@ -18,6 +18,7 @@ require (
github.com/aws/smithy-go v1.25.1
github.com/container-storage-interface/spec v1.12.0
github.com/containerd/ttrpc v1.2.8
github.com/distribution/reference v0.6.0
github.com/envoyproxy/go-control-plane v0.14.0
github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260812071801-353463cc7248
github.com/fsnotify/fsnotify v1.9.0
@@ -111,7 +112,6 @@ require (
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v29.5.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/docker/go-connections v0.7.0 // indirect
+6 -6
View File
@@ -722,6 +722,8 @@ func (x *Selector) GetMatchLabels() map[string]string {
}
// ResourceMetadata holds the common fields carried by every Substrate resource.
//
// +k8s:customValidation # timestamps must be valid, and update_time must not precede create_time
type ResourceMetadata struct {
state protoimpl.MessageState `protogen:"open.v1"`
// atespace is the namespace the resource belongs to. Empty for global-scoped
@@ -766,7 +768,6 @@ type ResourceMetadata struct {
//
// +k8s:optional
// +k8s:immutable
// TODO: validate that this is a valid timestamp
CreateTime *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"`
// update_time is the time the resource was last updated.
//
@@ -774,8 +775,6 @@ type ResourceMetadata struct {
//
// +k8s:optional
// +k8s:update=NoUnset
// TODO: validate that this is a valid timestamp
// TODO: validate that UpdateTime >= CreateTime
UpdateTime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -2598,12 +2597,13 @@ type Container struct {
// +k8s:required
// +k8s:format=k8s-short-name
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
// image is the container image name. Must include a digest
// image is the OCI image reference the container runs:
// [registry/]repository[:tag]@digest. Must be pinned by digest
// (e.g. "name@sha256:...").
//
// +k8s:required
// +k8s:maxLength=512 # matches ImageVolumeSource.reference's bound
// +k8s:customValidation
// +k8s:customValidation # must be a well-formed image reference, pinned by digest
Image string `protobuf:"bytes,2,opt,name=image,proto3" json:"image,omitempty"`
// Entrypoint array; when set, the image's ENTRYPOINT and CMD are both
// ignored and the process argv is command + args. Unlike Kubernetes,
@@ -3161,7 +3161,7 @@ type ImageVolumeSource struct {
//
// +k8s:required
// +k8s:maxLength=512
// +k8s:customValidation
// +k8s:customValidation # must be a well-formed image reference, pinned by digest
Reference string `protobuf:"bytes,1,opt,name=reference,proto3" json:"reference,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
+6 -6
View File
@@ -221,6 +221,8 @@ message Selector {
}
// ResourceMetadata holds the common fields carried by every Substrate resource.
//
// +k8s:customValidation # timestamps must be valid, and update_time must not precede create_time
message ResourceMetadata {
// atespace is the namespace the resource belongs to. Empty for global-scoped
// resources. Caller-specified at creation and immutable thereafter.
@@ -268,7 +270,6 @@ message ResourceMetadata {
//
// +k8s:optional
// +k8s:immutable
// TODO: validate that this is a valid timestamp
google.protobuf.Timestamp create_time = 5;
// update_time is the time the resource was last updated.
@@ -277,8 +278,6 @@ message ResourceMetadata {
//
// +k8s:optional
// +k8s:update=NoUnset
// TODO: validate that this is a valid timestamp
// TODO: validate that UpdateTime >= CreateTime
google.protobuf.Timestamp update_time = 6;
}
@@ -912,12 +911,13 @@ message Container {
// +k8s:format=k8s-short-name
string name = 1;
// image is the container image name. Must include a digest
// image is the OCI image reference the container runs:
// [registry/]repository[:tag]@digest. Must be pinned by digest
// (e.g. "name@sha256:...").
//
// +k8s:required
// +k8s:maxLength=512 # matches ImageVolumeSource.reference's bound
// +k8s:customValidation
// +k8s:customValidation # must be a well-formed image reference, pinned by digest
string image = 2;
// Entrypoint array; when set, the image's ENTRYPOINT and CMD are both
@@ -1098,7 +1098,7 @@ message ImageVolumeSource {
//
// +k8s:required
// +k8s:maxLength=512
// +k8s:customValidation
// +k8s:customValidation # must be a well-formed image reference, pinned by digest
string reference = 1;
}