diff --git a/cmd/ateapi/internal/apivalidation/actor.go b/cmd/ateapi/internal/apivalidation/actor.go index a57b113742..fc1b6895a7 100644 --- a/cmd/ateapi/internal/apivalidation/actor.go +++ b/cmd/ateapi/internal/apivalidation/actor.go @@ -16,12 +16,11 @@ package apivalidation import ( "context" - "strings" + "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/api/validate" - "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -134,34 +133,14 @@ func ValidateCustom_WorkerAssignment_WorkerPodIps(_ context.Context, _ operation return validateDualStackIPs(fldPath, value) } -// ValidateCustom_ExternalVolume_VolumeType checks that a volume type string is well-formed. -// It allows an optional "substrate.io/" prefix, followed by a valid DNS-1123 subdomain. func ValidateCustom_ExternalVolume_VolumeType(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { - if value == nil || *value == "" { - return nil - } - var errs field.ErrorList - valToValidate := strings.TrimPrefix(*value, "substrate.io/") - for _, msg := range validation.IsDNS1123Subdomain(valToValidate) { - errs = append(errs, field.Invalid(fldPath, *value, msg)) - } - return errs + return resources.ValidateVolumeType(fldPath, *value) } -// ValidateCustom_ExternalVolume_StorageVolumeId checks that an external volume's storage ID does not -// contain control characters (U+0000-U+0008, U+000B, U+000C, U+000E-U+001F, U+007F-U+009F). func ValidateCustom_ExternalVolume_StorageVolumeId(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { - if value == nil || *value == "" { - return nil - } - for _, r := range *value { - if (r >= 0x0000 && r <= 0x0008) || - r == 0x000B || - r == 0x000C || - (r >= 0x000E && r <= 0x001F) || - (r >= 0x007F && r <= 0x009F) { - return field.ErrorList{field.Invalid(fldPath, *value, "must not contain control characters (U+0000-U+0008, U+000B, U+000C, U+000E-U+001F, U+007F-U+009F)")} - } - } - return nil + return resources.ValidateStorageVolumeID(fldPath, *value) +} + +func ValidateCustom_ExternalVolume_VolumeContext(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[string]string) field.ErrorList { + return resources.ValidateCSIMap(fldPath, value) } diff --git a/cmd/ateapi/internal/apivalidation/actor_template.go b/cmd/ateapi/internal/apivalidation/actor_template.go index 1418f0188f..483861fe91 100644 --- a/cmd/ateapi/internal/apivalidation/actor_template.go +++ b/cmd/ateapi/internal/apivalidation/actor_template.go @@ -17,13 +17,8 @@ package apivalidation import ( "context" "fmt" - "regexp" - "strings" - - "github.com/distribution/reference" "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/volumepath" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/api/resource" @@ -95,91 +90,40 @@ func ValidateCustom_CreateActorTemplateRequest_ActorTemplate(_ context.Context, return errs } -// httpGetPathRE constrains wakeup probe paths to RFC 3986 path-segment -// characters only, with well-formed percent-escapes, and no query string -// or fragment. -var httpGetPathRE = regexp.MustCompile(`^/([A-Za-z0-9\-._~!$&'()*+,;=:@/]|%[0-9A-Fa-f]{2})*$`) - func ValidateCustom_HttpGetAction_Path(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { - if !httpGetPathRE.MatchString(*value) { - return field.ErrorList{field.Invalid(fldPath, *value, "must be a URL path starting with '/', using only RFC 3986 path-segment characters, without query or fragment")} - } - return nil + return resources.ValidateHTTPGetPath(fldPath, *value) } -// mountPathBadSegmentRE matches '.' or '..' path segments. -var mountPathBadSegmentRE = regexp.MustCompile(`(^|/)[.][.]?(/|$)`) - -// ValidateCustom_VolumeMount_MountPath requires a clean absolute Unix path -// that starts with '/', is not '/', and contains no ':', '.' or '..' -// segments, '//', trailing '/', or control characters. func ValidateCustom_VolumeMount_MountPath(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { - p := *value - bad := !strings.HasPrefix(p, "/") || len(p) == 1 || - strings.HasSuffix(p, "/") || strings.Contains(p, "//") || - strings.Contains(p, ":") || mountPathBadSegmentRE.MatchString(p) - if !bad { - for _, r := range p { - if r < 0x20 || r == 0x7f { - bad = true - break - } - } - } - if bad { - return field.ErrorList{field.Invalid(fldPath, p, "must be a clean absolute Unix path: must start with '/', not be '/', and contain no ':', '..', '.', '//', trailing '/', or control characters")} - } - return nil + return resources.ValidateMountPath(fldPath, *value) } // ValidateCustom_Container_VolumeMounts rejects mounts that nest under one -// another. +// another. Mount-path uniqueness is enforced by the list key. func ValidateCustom_Container_VolumeMounts(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []*ateapipb.VolumeMount) field.ErrorList { - var errs field.ErrorList + paths := make([]string, len(value)) for i, m := range value { - path := m.GetMountPath() - if path == "" { - continue // required is enforced by tags - } - // Nested mounts are unsupported (volumes cannot mount onto - // other volumes). - for j := 0; j < i; j++ { - prior := value[j].GetMountPath() - if prior == "" || prior == path { - continue - } - if strings.HasPrefix(path, prior+"/") || strings.HasPrefix(prior, path+"/") { - errs = append(errs, field.Invalid(fldPath.Index(i).Child("mount_path"), path, - fmt.Sprintf("must not nest under or over another mount (%q)", prior))) - } - } - } - return errs -} - -// validateProjectedPath applies the projected-path rule shared with atelet, -// which re-checks it before writing to the host. -func validateProjectedPath(fldPath *field.Path, p string) field.ErrorList { - if err := volumepath.ValidateProjected(p); err != nil { - return field.ErrorList{field.Invalid(fldPath, p, err.Error())} + paths[i] = m.GetMountPath() } - return nil + return resources.ValidateNestedMountPaths(fldPath, paths) } func ValidateCustom_ActorMetadataItem_Path(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { - return validateProjectedPath(fldPath, *value) + return resources.ValidateProjectedPath(fldPath, *value) } func ValidateCustom_TrustBundleDataSource_Path(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { - return validateProjectedPath(fldPath, *value) + return resources.ValidateProjectedPath(fldPath, *value) } -// ValidateCustom_SystemInfoVolumeSource_DataSources requires every projected -// file path to be unique across all data sources: atelet writes them in -// order into one tree, so a repeated path silently clobbers the earlier file. +// ValidateCustom_SystemInfoVolumeSource_DataSources allows at most one +// actor_metadata entry and requires every projected file path to be unique +// across all data sources: atelet writes them in order into one tree, so a +// repeated path silently clobbers the earlier file. func ValidateCustom_SystemInfoVolumeSource_DataSources(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []*ateapipb.SystemInfoDataSource) field.ErrorList { var errs field.ErrorList seen := sets.New[string]() + sawMetadata := false for i, ds := range value { switch { case ds == nil: @@ -189,6 +133,10 @@ func ValidateCustom_SystemInfoVolumeSource_DataSources(_ context.Context, _ oper } seen.Insert(ds.TrustBundle.Path) case ds.ActorMetadata != nil: + if sawMetadata { + errs = append(errs, field.Forbidden(fldPath.Index(i).Child("actor_metadata"), "at most one actor_metadata entry may appear")) + } + sawMetadata = true for j, item := range ds.ActorMetadata.Items { if item == nil { continue @@ -203,31 +151,12 @@ func ValidateCustom_SystemInfoVolumeSource_DataSources(_ context.Context, _ oper return errs } -// 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 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 -} - func ValidateCustom_ImageVolumeSource_Reference(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { - return validatePinnedImage(fldPath, *value) + return resources.ValidatePinnedImage(fldPath, *value) } func ValidateCustom_Container_Image(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { - return validatePinnedImage(fldPath, *value) + return resources.ValidatePinnedImage(fldPath, *value) } func ValidateCustom_ExternalVolumeTemplate_Capacity(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { @@ -247,45 +176,14 @@ func ValidateCustom_SnapshotConfig_StorageLocation(_ context.Context, _ operatio return nil } -// envVarNameRE constrains env var names to any printable ASCII character -// except '='. -var envVarNameRE = regexp.MustCompile(`^[ -<>-~]+$`) - func ValidateCustom_EnvVar_Name(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { - if !envVarNameRE.MatchString(*value) { - return field.ErrorList{field.Invalid(fldPath, *value, "may contain any printable ASCII character except '='")} - } - return nil -} - -// capabilityRE constrains Linux capability names: uppercase, without the -// "CAP_" prefix (which is added when the OCI spec is written; the prefixed -// spelling would silently grant nothing). -var capabilityRE = regexp.MustCompile(`^[A-Z][A-Z0-9_]*$`) - -func validateCapabilities(fldPath *field.Path, caps []string, allowAll bool) field.ErrorList { - var errs field.ErrorList - for i, c := range caps { - p := fldPath.Index(i) - switch { - case c == "ALL" && !allowAll: - errs = append(errs, field.Invalid(p, c, "add does not accept 'ALL'; name the individual capabilities the container needs")) - case c == "ALL": - case len(c) > 63: - errs = append(errs, field.TooLong(p, nil, 63)) - case strings.HasPrefix(c, "CAP_"): - errs = append(errs, field.Invalid(p, c, "must be named without the 'CAP_' prefix (e.g. 'NET_BIND_SERVICE')")) - case !capabilityRE.MatchString(c): - errs = append(errs, field.Invalid(p, c, "must be an uppercase capability name like 'NET_BIND_SERVICE'")) - } - } - return errs + return resources.ValidateEnvVarName(fldPath, *value) } func ValidateCustom_Capabilities_Add(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []string) field.ErrorList { - return validateCapabilities(fldPath, value, false) + return resources.ValidateCapabilities(fldPath, value, false) } func ValidateCustom_Capabilities_Drop(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []string) field.ErrorList { - return validateCapabilities(fldPath, value, true) + return resources.ValidateCapabilities(fldPath, value, true) } diff --git a/cmd/ateapi/internal/apivalidation/actor_template_test.go b/cmd/ateapi/internal/apivalidation/actor_template_test.go index 7ed4788ee7..fe044102bf 100644 --- a/cmd/ateapi/internal/apivalidation/actor_template_test.go +++ b/cmd/ateapi/internal/apivalidation/actor_template_test.go @@ -447,40 +447,6 @@ func TestValidateActorTemplate(t *testing.T) { } }, want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("volume_mounts").Index(1).Child("mount_path"), nil, "")}, - }, { - name: "deeply nested mount path is rejected", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{ - {Name: "data", MountPath: "/data"}, - {Name: "deep", MountPath: "/data/a/b/c"}, - } - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("volume_mounts").Index(1).Child("mount_path"), nil, "")}, - }, { - name: "nesting is rejected regardless of listing order", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{ - {Name: "deep", MountPath: "/data/a/b/c"}, - {Name: "data", MountPath: "/data"}, - } - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("volume_mounts").Index(1).Child("mount_path"), nil, "")}, - }, { - name: "sibling subpaths under an unmounted ancestor are allowed", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{ - {Name: "data", MountPath: "/data/a"}, - {Name: "other", MountPath: "/data/b"}, - } - }, - }, { - name: "shared segment prefix below the root is allowed", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{ - {Name: "data", MountPath: "/data/a"}, - {Name: "other", MountPath: "/data/ab"}, - } - }, }, { name: "the same path in different containers is allowed", mutate: func(tmpl *ateapipb.ActorTemplate) { @@ -488,22 +454,6 @@ func TestValidateActorTemplate(t *testing.T) { tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{{Name: "data", MountPath: "/var/data"}} tmpl.Containers[1].VolumeMounts = []*ateapipb.VolumeMount{{Name: "data", MountPath: "/var/data"}} }, - }, { - name: "shared path prefix without nesting is allowed", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{ - {Name: "data", MountPath: "/data"}, - {Name: "other", MountPath: "/database"}, - } - }, - }, { - name: "two volumes at distinct paths are allowed", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{ - {Name: "data", MountPath: "/var/data"}, - {Name: "other", MountPath: "/mnt/other"}, - } - }, }, { name: "duplicate volume name", mutate: func(tmpl *ateapipb.ActorTemplate) { @@ -571,35 +521,11 @@ func TestValidateActorTemplate(t *testing.T) { 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) { @@ -623,23 +549,12 @@ func TestValidateActorTemplate(t *testing.T) { mutate: func(tmpl *ateapipb.ActorTemplate) { tmpl.Containers[0].Env = []*ateapipb.EnvVar{{Name: "PORT", Value: "8080"}, {Name: "DEBUG"}} }, - }, { - name: "env name with unusual printable characters is allowed", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].Env = []*ateapipb.EnvVar{{Name: "my.var-2 (test)!", Value: "v"}} - }, }, { name: "env name with an equals sign", mutate: func(tmpl *ateapipb.ActorTemplate) { tmpl.Containers[0].Env = []*ateapipb.EnvVar{{Name: "FOO=BAR"}} }, want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("env").Index(0).Child("name"), nil, "")}, - }, { - name: "env name with a control character", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].Env = []*ateapipb.EnvVar{{Name: "FOO BAR"}} - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("env").Index(0).Child("name"), nil, "")}, }, { name: "env missing name", mutate: func(tmpl *ateapipb.ActorTemplate) { @@ -660,12 +575,6 @@ func TestValidateActorTemplate(t *testing.T) { tmpl.Containers[0].SecurityContext = &ateapipb.SecurityContext{Capabilities: &ateapipb.Capabilities{Add: []string{"ALL"}}} }, want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("security_context", "capabilities", "add").Index(0), nil, "")}, - }, { - name: "capability with CAP_ prefix", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].SecurityContext = &ateapipb.SecurityContext{Capabilities: &ateapipb.Capabilities{Add: []string{"CAP_NET_BIND_SERVICE"}}} - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("security_context", "capabilities", "add").Index(0), nil, "")}, }, { name: "lowercase capability", mutate: func(tmpl *ateapipb.ActorTemplate) { @@ -806,24 +715,6 @@ func TestValidateActorTemplate(t *testing.T) { tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{{Name: "data", MountPath: "var/data"}} }, want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("volume_mounts").Index(0).Child("mount_path"), nil, "")}, - }, { - name: "root mount_path", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{{Name: "data", MountPath: "/"}} - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("volume_mounts").Index(0).Child("mount_path"), nil, "")}, - }, { - name: "mount_path with dot-dot segment", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{{Name: "data", MountPath: "/var/../etc"}} - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("volume_mounts").Index(0).Child("mount_path"), nil, "")}, - }, { - name: "mount_path with trailing slash", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{{Name: "data", MountPath: "/var/data/"}} - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("volume_mounts").Index(0).Child("mount_path"), nil, "")}, }, { name: "valid durable_dir volume", mutate: func(tmpl *ateapipb.ActorTemplate) { @@ -882,18 +773,6 @@ 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) { @@ -976,18 +855,6 @@ func TestValidateActorTemplate(t *testing.T) { tmpl.Containers[0].Resources = &ateapipb.Resources{Limits: []*ateapipb.Limits{{Name: "memory", Quantity: "two gigs"}}} }, want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("resources", "limits").Index(0).Child("quantity"), nil, "")}, - }, { - name: "zero limit quantity", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].Resources = &ateapipb.Resources{Limits: []*ateapipb.Limits{{Name: "memory", Quantity: "0"}}} - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("resources", "limits").Index(0).Child("quantity"), nil, "")}, - }, { - name: "cpu limit of 1000 cores", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].Resources = &ateapipb.Resources{Limits: []*ateapipb.Limits{{Name: "cpu", Quantity: "1k"}}} - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("resources", "limits").Index(0).Child("quantity"), nil, "")}, }, { name: "too many limits", mutate: func(tmpl *ateapipb.ActorTemplate) { diff --git a/cmd/ateapi/internal/apivalidation/validation_test.go b/cmd/ateapi/internal/apivalidation/validation_test.go index a8dd874f67..9a90cf92c7 100644 --- a/cmd/ateapi/internal/apivalidation/validation_test.go +++ b/cmd/ateapi/internal/apivalidation/validation_test.go @@ -760,58 +760,12 @@ func TestValidateSystemInfoVolumeSource(t *testing.T) { s.DataSources[0].ActorMetadata.Items[0].Path = strings.Repeat("p", 256) }), want: field.ErrorList{field.TooLong(itemsPath.Index(0).Child("path"), nil, 255).WithOrigin("maxLength")}, - }, { - name: "valid: nested item path", - obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { - s.DataSources[0].ActorMetadata.Items[0].Path = "meta/actor-name" - }), }, { name: "item path traversal", obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { s.DataSources[0].ActorMetadata.Items[0].Path = "../../traversal-escape" }), want: field.ErrorList{field.Invalid(itemsPath.Index(0).Child("path"), nil, "")}, - }, { - name: "absolute item path", - obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { - s.DataSources[0].ActorMetadata.Items[0].Path = "/escaped" - }), - want: field.ErrorList{field.Invalid(itemsPath.Index(0).Child("path"), nil, "")}, - }, { - name: "item path with dot segment", - obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { - s.DataSources[0].ActorMetadata.Items[0].Path = "./actor-name" - }), - want: field.ErrorList{field.Invalid(itemsPath.Index(0).Child("path"), nil, "")}, - }, { - name: "item path with trailing slash", - obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { - s.DataSources[0].ActorMetadata.Items[0].Path = "actor-name/" - }), - want: field.ErrorList{field.Invalid(itemsPath.Index(0).Child("path"), nil, "")}, - }, { - name: "item path with empty segment", - obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { - s.DataSources[0].ActorMetadata.Items[0].Path = "meta//actor-name" - }), - want: field.ErrorList{field.Invalid(itemsPath.Index(0).Child("path"), nil, "")}, - }, { - name: "item path with NUL byte", - obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { - s.DataSources[0].ActorMetadata.Items[0].Path = "actor\x00name" - }), - want: field.ErrorList{field.Invalid(itemsPath.Index(0).Child("path"), nil, "")}, - }, { - name: "valid: unicode item path", - obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { - s.DataSources[0].ActorMetadata.Items[0].Path = "méta/имя" - }), - }, { - name: "item path with too many segments", - obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { - s.DataSources[0].ActorMetadata.Items[0].Path = strings.Repeat("d/", 16) + "actor-name" - }), - want: field.ErrorList{field.Invalid(itemsPath.Index(0).Child("path"), nil, "")}, }, { name: "trust bundle path traversal", obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { @@ -830,6 +784,19 @@ func TestValidateSystemInfoVolumeSource(t *testing.T) { s.DataSources[1].TrustBundle.Path = "actor-uid" }), want: field.ErrorList{field.Duplicate(dsPath.Index(1).Child("trust_bundle", "path"), nil)}, + }, { + name: "second actor_metadata entry", + obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { + s.DataSources = append(s.DataSources, &ateapipb.SystemInfoDataSource{ + ActorMetadata: &ateapipb.ActorMetadataDataSource{ + Items: []*ateapipb.ActorMetadataItem{{ + Field: ateapipb.ActorMetadataField_ACTOR_METADATA_FIELD_NAME, + Path: "actor-name-2", + }}, + }, + }) + }), + want: field.ErrorList{field.Forbidden(dsPath.Index(2).Child("actor_metadata"), "")}, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -890,17 +857,10 @@ func TestValidateTrustBundleDataSource(t *testing.T) { name: "path too long", obj: valid(func(tb *ateapipb.TrustBundleDataSource) { tb.Path = strings.Repeat("p", 256) }), want: field.ErrorList{field.TooLong(field.NewPath("path"), nil, 255).WithOrigin("maxLength")}, - }, { - name: "valid: nested path", - obj: valid(func(tb *ateapipb.TrustBundleDataSource) { tb.Path = "certs/ca.pem" }), }, { name: "path traversal", obj: valid(func(tb *ateapipb.TrustBundleDataSource) { tb.Path = "../escape" }), want: field.ErrorList{field.Invalid(field.NewPath("path"), nil, "")}, - }, { - name: "absolute path", - obj: valid(func(tb *ateapipb.TrustBundleDataSource) { tb.Path = "/escaped" }), - want: field.ErrorList{field.Invalid(field.NewPath("path"), nil, "")}, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -949,29 +909,10 @@ func TestValidateExternalVolume(t *testing.T) { name: "invalid storage volume id with null U+0000", obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol\x00id" }), want: field.ErrorList{field.Invalid(field.NewPath("storage_volume_id"), nil, "")}, - }, { - name: "invalid storage volume id with unit separator U+001F", - obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol\x1fid" }), - want: field.ErrorList{field.Invalid(field.NewPath("storage_volume_id"), nil, "")}, - }, { - name: "invalid storage volume id with DEL U+007F", - obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol\x7fid" }), - want: field.ErrorList{field.Invalid(field.NewPath("storage_volume_id"), nil, "")}, - }, { - name: "invalid storage volume id with C1 control U+0080", - obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol\u0080id" }), - want: field.ErrorList{field.Invalid(field.NewPath("storage_volume_id"), nil, "")}, - }, { - name: "invalid storage volume id with C1 control U+009F", - obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol\u009fid" }), - want: field.ErrorList{field.Invalid(field.NewPath("storage_volume_id"), nil, "")}, }, { name: "storage volume id too long", obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = strings.Repeat("x", 257) }), want: field.ErrorList{field.TooLong(field.NewPath("storage_volume_id"), nil, 256).WithOrigin("maxLength")}, - }, { - name: "valid csi volume type", - obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "pd.csi.storage.gke.io" }), }, { name: "missing volume type", obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "" }), @@ -993,18 +934,6 @@ func TestValidateExternalVolume(t *testing.T) { }, { name: "valid volume with substrate.io prefixed volume type", obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "substrate.io/mock" }), - }, { - name: "invalid volume type with empty plugin after substrate.io prefix", - obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "substrate.io/" }), - want: field.ErrorList{field.Invalid(field.NewPath("volume_type"), nil, "")}, - }, { - name: "invalid volume type with invalid plugin name after substrate.io prefix", - obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "substrate.io/Mock_Plugin" }), - want: field.ErrorList{field.Invalid(field.NewPath("volume_type"), nil, "")}, - }, { - name: "invalid volume type with non-substrate prefix", - obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "other.io/mock" }), - want: field.ErrorList{field.Invalid(field.NewPath("volume_type"), nil, "")}, }, { name: "negative status", obj: valid(func(v *ateapipb.ExternalVolume) { v.Status = ateapipb.ExternalVolume_Status(-1) }), @@ -1017,25 +946,25 @@ func TestValidateExternalVolume(t *testing.T) { name: "storage volume id at the bound", obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = strings.Repeat("x", 256) }), }, { - name: "too many volume_context entries", + name: "volume_context with many entries, as a CSI driver may return", obj: valid(func(v *ateapipb.ExternalVolume) { - ctxMap := make(map[string]string, 33) - for i := 0; i < 33; i++ { - ctxMap[fmt.Sprintf("key-%d", i)] = "v" + ctxMap := make(map[string]string, 100) + for i := range 100 { + ctxMap[fmt.Sprintf("key-%03d", i)] = "v" } v.VolumeContext = ctxMap }), - want: field.ErrorList{field.TooMany(field.NewPath("volume_context"), 33, 32).WithOrigin("maxProperties")}, }, { - name: "volume_context key too long", - obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeContext = map[string]string{strings.Repeat("k", 129): "v"} }), - want: field.ErrorList{field.TooLong(field.NewPath("volume_context"), nil, 128).WithOrigin("maxLength")}, + name: "volume_context with one long value, as a CSI driver may return", + obj: valid(func(v *ateapipb.ExternalVolume) { + v.VolumeContext = map[string]string{"attachment": strings.Repeat("v", 4000)} + }), }, { - name: "volume_context value too long", + name: "volume_context over the size bound", obj: valid(func(v *ateapipb.ExternalVolume) { - v.VolumeContext = map[string]string{"attachment": strings.Repeat("v", 257)} + v.VolumeContext = map[string]string{"attachment": strings.Repeat("v", 40*1024)} }), - want: field.ErrorList{field.TooLong(field.NewPath("volume_context").Key("attachment"), nil, 256).WithOrigin("maxLength")}, + want: field.ErrorList{field.TooLong(field.NewPath("volume_context"), nil, 40*1024)}, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/cmd/ateapi/internal/apivalidation/zz_generated.validation.go b/cmd/ateapi/internal/apivalidation/zz_generated.validation.go index f27f64d8c1..6cace304e7 100644 --- a/cmd/ateapi/internal/apivalidation/zz_generated.validation.go +++ b/cmd/ateapi/internal/apivalidation/zz_generated.validation.go @@ -3866,26 +3866,14 @@ func Validate_ExternalVolume( } // call field-attached validations earlyReturn := false - if e := validate.MaxProperties(ctx, op, fldPath, obj, oldObj, 32).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) - earlyReturn = true - } if e := validate.OptionalMap(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { earlyReturn = true } if earlyReturn { return // do not proceed } - if e := validate.EachMapKey(ctx, op, fldPath, obj, oldObj, - func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { - return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 128) - }); len(e) != 0 { - errs = append(errs, e...) - } - if e := validate.EachMapVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, - func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { - return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256) - }); len(e) != 0 { + // custom validation + if e := ValidateCustom_ExternalVolume_VolumeContext(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } return diff --git a/cmd/ateapi/internal/controlapi/workflow_delete.go b/cmd/ateapi/internal/controlapi/workflow_delete.go index 90ce7d507b..e97af85ba5 100644 --- a/cmd/ateapi/internal/controlapi/workflow_delete.go +++ b/cmd/ateapi/internal/controlapi/workflow_delete.go @@ -179,12 +179,10 @@ func (w *ActorWorkflow) ensureAteletTerminated(ctx context.Context, actorRef res if vol.GetStorageVolumeId() != "" { workloadSpec.Volumes = append(workloadSpec.Volumes, &ateletpb.Volume{ Name: vol.GetName(), - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: vol.GetStorageVolumeId(), - VolumeType: vol.GetVolumeType(), - VolumeContext: vol.GetVolumeContext(), - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: vol.GetStorageVolumeId(), + VolumeType: vol.GetVolumeType(), + VolumeContext: vol.GetVolumeContext(), }, }) } diff --git a/cmd/ateapi/internal/controlapi/workflow_pause.go b/cmd/ateapi/internal/controlapi/workflow_pause.go index f48e68106b..d2893eb272 100644 --- a/cmd/ateapi/internal/controlapi/workflow_pause.go +++ b/cmd/ateapi/internal/controlapi/workflow_pause.go @@ -195,10 +195,8 @@ func (w *ActorWorkflow) ensureAteletPaused(ctx context.Context, actorRef resourc ActorTemplateName: actor.GetActorTemplate().GetName(), Spec: workloadSpec, Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL, - Config: &ateletpb.CheckpointRequest_LocalConfig{ - LocalConfig: &ateletpb.LocalCheckpointConfiguration{ - SnapshotName: inProgressSnap.GetUuid(), - }, + LocalConfig: &ateletpb.LocalCheckpointConfiguration{ + SnapshotName: inProgressSnap.GetUuid(), }, Fidelity: fidelityToAtelet(actorTemplate.GetSnapshotConfig().GetPreferredFidelity()), ActorUid: actor.GetMetadata().Uid, diff --git a/cmd/ateapi/internal/controlapi/workflow_resume.go b/cmd/ateapi/internal/controlapi/workflow_resume.go index 3ee70ac6c3..3e472702f9 100644 --- a/cmd/ateapi/internal/controlapi/workflow_resume.go +++ b/cmd/ateapi/internal/controlapi/workflow_resume.go @@ -713,9 +713,7 @@ func (w *ActorWorkflow) ensureAteletRestored(ctx context.Context, actorRef resou MemoryBytes: memBytes, } req.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL - req.Config = &ateletpb.RestoreRequest_LocalConfig{ - LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: localSnap.GetUuid()}, - } + req.LocalConfig = &ateletpb.LocalCheckpointConfiguration{SnapshotName: localSnap.GetUuid()} req.Fidelity = fidelityToAtelet(localSnap.GetFidelity()) tele.WireFidelity = ateattr.SnapshotFidelityValue(req.Fidelity) @@ -739,10 +737,8 @@ func (w *ActorWorkflow) ensureAteletRestored(ctx context.Context, actorRef resou ActorTemplateName: actor.GetActorTemplate().GetName(), Spec: workloadSpec, Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL, - Config: &ateletpb.RestoreRequest_ExternalConfig{ - ExternalConfig: &ateletpb.ExternalRestoreConfiguration{ - SnapshotUri: src.SnapshotURI.String(), - }, + ExternalConfig: &ateletpb.ExternalRestoreConfiguration{ + SnapshotUri: src.SnapshotURI.String(), }, Fidelity: scope, SandboxAssets: sandboxAssets, diff --git a/cmd/ateapi/internal/controlapi/workflow_suspend.go b/cmd/ateapi/internal/controlapi/workflow_suspend.go index 99c89fea5c..ec2612202b 100644 --- a/cmd/ateapi/internal/controlapi/workflow_suspend.go +++ b/cmd/ateapi/internal/controlapi/workflow_suspend.go @@ -261,10 +261,8 @@ func (w *ActorWorkflow) ensureAteletSuspended(ctx context.Context, actorRef reso ActorTemplateName: actor.GetActorTemplate().GetName(), Spec: workloadSpec, Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL, - Config: &ateletpb.CheckpointRequest_ExternalConfig{ - ExternalConfig: &ateletpb.ExternalCheckpointConfiguration{ - SnapshotUri: inProgressSnap.GetDurableSnapshot().GetObject().GetSnapshotUri(), - }, + ExternalConfig: &ateletpb.ExternalCheckpointConfiguration{ + SnapshotUri: inProgressSnap.GetDurableSnapshot().GetObject().GetSnapshotUri(), }, Fidelity: fidelityToAtelet(preferredFidelity(actor.GetMetadata().GetAtespace(), actorTemplate)), ActorUid: actor.GetMetadata().Uid, diff --git a/cmd/ateapi/internal/controlapi/workload_spec.go b/cmd/ateapi/internal/controlapi/workload_spec.go index e59021e09e..bd1df172ce 100644 --- a/cmd/ateapi/internal/controlapi/workload_spec.go +++ b/cmd/ateapi/internal/controlapi/workload_spec.go @@ -57,10 +57,8 @@ func workloadSpecFromActorTemplate(actorTemplate *ateapipb.ActorTemplate, actor switch { case vol.GetDurableDir() != nil: workloadSpec.Volumes = append(workloadSpec.Volumes, &ateletpb.Volume{ - Name: vol.GetName(), - Source: &ateletpb.Volume_DurableDir{ - DurableDir: &ateletpb.DurableDirVolume{}, - }, + Name: vol.GetName(), + DurableDir: &ateletpb.DurableDirVolume{}, }) case vol.GetSystemInfo() != nil: @@ -76,19 +74,15 @@ func workloadSpecFromActorTemplate(actorTemplate *ateapipb.ActorTemplate, actor }) } ateletSystemInfo.DataSources = append(ateletSystemInfo.DataSources, &ateletpb.SystemInfoDataSource{ - DataSource: &ateletpb.SystemInfoDataSource_ActorMetadata{ - ActorMetadata: actorMetadata, - }, + ActorMetadata: actorMetadata, }) case dataSource.GetTrustBundle() != nil: // atelet resolves named trustBundles against its allowlist // and ClusterTrustBundle informer at write time ateletSystemInfo.DataSources = append(ateletSystemInfo.DataSources, &ateletpb.SystemInfoDataSource{ - DataSource: &ateletpb.SystemInfoDataSource_TrustBundle{ - TrustBundle: &ateletpb.TrustBundleDataSource{ - Names: dataSource.GetTrustBundle().GetNames(), - Path: dataSource.GetTrustBundle().GetPath(), - }, + TrustBundle: &ateletpb.TrustBundleDataSource{ + Names: dataSource.GetTrustBundle().GetNames(), + Path: dataSource.GetTrustBundle().GetPath(), }, }) default: @@ -96,19 +90,15 @@ func workloadSpecFromActorTemplate(actorTemplate *ateapipb.ActorTemplate, actor } } workloadSpec.Volumes = append(workloadSpec.Volumes, &ateletpb.Volume{ - Name: vol.GetName(), - Source: &ateletpb.Volume_SystemInfo{ - SystemInfo: ateletSystemInfo, - }, + Name: vol.GetName(), + SystemInfo: ateletSystemInfo, }) case vol.GetImage() != nil: workloadSpec.Volumes = append(workloadSpec.Volumes, &ateletpb.Volume{ Name: vol.GetName(), - Source: &ateletpb.Volume_Image{ - Image: &ateletpb.ImageVolumeSource{ - Reference: vol.GetImage().GetReference(), - }, + Image: &ateletpb.ImageVolumeSource{ + Reference: vol.GetImage().GetReference(), }, }) @@ -188,13 +178,11 @@ func appendExternalVolumes(workloadSpec *ateletpb.WorkloadSpec, template *ateapi } workloadSpec.Volumes = append(workloadSpec.Volumes, &ateletpb.Volume{ Name: vol.GetName(), - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: storageVolID, - VolumeType: volType, - VolumeContext: volCtx, - PublishContext: volumePublishContexts[vol.GetName()], - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: storageVolID, + VolumeType: volType, + VolumeContext: volCtx, + PublishContext: volumePublishContexts[vol.GetName()], }, }) } diff --git a/cmd/ateapi/internal/controlapi/workload_spec_test.go b/cmd/ateapi/internal/controlapi/workload_spec_test.go index 7a86971ebc..68d569b5b6 100644 --- a/cmd/ateapi/internal/controlapi/workload_spec_test.go +++ b/cmd/ateapi/internal/controlapi/workload_spec_test.go @@ -50,8 +50,8 @@ func TestWorkloadSpecFromActorTemplate(t *testing.T) { want: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ { - Name: "home", - Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}, + Name: "home", + DurableDir: &ateletpb.DurableDirVolume{}, }, }, Containers: []*ateletpb.Container{ @@ -100,19 +100,15 @@ func TestWorkloadSpecFromActorTemplate(t *testing.T) { Volumes: []*ateletpb.Volume{ { Name: "system-info", - Source: &ateletpb.Volume_SystemInfo{ - SystemInfo: &ateletpb.SystemInfoVolume{ - DataSources: []*ateletpb.SystemInfoDataSource{ - {DataSource: &ateletpb.SystemInfoDataSource_ActorMetadata{ - 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"}, - }, - }, - }}, - }, + SystemInfo: &ateletpb.SystemInfoVolume{ + 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"}, + }, + }}, }, }, }, @@ -150,12 +146,12 @@ func TestWorkloadSpecFromActorTemplate(t *testing.T) { want: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ { - Name: "home", - Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}, + Name: "home", + DurableDir: &ateletpb.DurableDirVolume{}, }, { - Name: "agent", - Source: &ateletpb.Volume_Image{Image: &ateletpb.ImageVolumeSource{Reference: "example.com/agent@sha256:abc"}}, + Name: "agent", + Image: &ateletpb.ImageVolumeSource{Reference: "example.com/agent@sha256:abc"}, }, }, Containers: []*ateletpb.Container{ @@ -200,13 +196,9 @@ func TestWorkloadSpecFromActorTemplate(t *testing.T) { Volumes: []*ateletpb.Volume{ { Name: "system-info", - Source: &ateletpb.Volume_SystemInfo{ - SystemInfo: &ateletpb.SystemInfoVolume{ - DataSources: []*ateletpb.SystemInfoDataSource{ - {DataSource: &ateletpb.SystemInfoDataSource_TrustBundle{ - TrustBundle: &ateletpb.TrustBundleDataSource{Names: []string{"egress-trust"}, Path: "trust/ca.pem"}, - }}, - }, + SystemInfo: &ateletpb.SystemInfoVolume{ + DataSources: []*ateletpb.SystemInfoDataSource{ + {TrustBundle: &ateletpb.TrustBundleDataSource{Names: []string{"egress-trust"}, Path: "trust/ca.pem"}}, }, }, }, @@ -243,8 +235,8 @@ func TestWorkloadSpecFromActorTemplate(t *testing.T) { want: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ { - Name: "home", - Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}, + Name: "home", + DurableDir: &ateletpb.DurableDirVolume{}, }, }, Containers: []*ateletpb.Container{ @@ -272,8 +264,8 @@ func TestWorkloadSpecFromActorTemplate(t *testing.T) { want: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ { - Name: "home", - Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}, + Name: "home", + DurableDir: &ateletpb.DurableDirVolume{}, }, }, Containers: []*ateletpb.Container{{Name: "main", Image: "main"}}, @@ -456,13 +448,11 @@ func TestAppendExternalVolumes(t *testing.T) { Volumes: []*ateletpb.Volume{ { Name: "vol-1", - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: "vol-gce-pd-123", - VolumeType: "pd-standard", - VolumeContext: map[string]string{"foo": "bar"}, - PublishContext: tt.wantPublishCtx, - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: "vol-gce-pd-123", + VolumeType: "pd-standard", + VolumeContext: map[string]string{"foo": "bar"}, + PublishContext: tt.wantPublishCtx, }, }, }, diff --git a/cmd/ateapi/main.go b/cmd/ateapi/main.go index d1896c2b3b..5872c81a34 100644 --- a/cmd/ateapi/main.go +++ b/cmd/ateapi/main.go @@ -41,6 +41,7 @@ import ( "github.com/agent-substrate/substrate/internal/oidcdiscovery" "github.com/agent-substrate/substrate/internal/oidcjwt" "github.com/agent-substrate/substrate/internal/pluginsocket" + "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/internal/serverboot" "github.com/agent-substrate/substrate/internal/version" "github.com/agent-substrate/substrate/internal/volume" @@ -56,6 +57,7 @@ import ( "google.golang.org/grpc/credentials" "google.golang.org/grpc/keepalive" "google.golang.org/grpc/reflection" + "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" @@ -128,6 +130,11 @@ func main() { if err := validateActorJWTIssuer(*actorJWTIssuer); err != nil { serverboot.Fatal(ctx, "Invalid --actor-jwt-issuer", err) } + // atelet applies the same rule to EgressGateway.address on every Run and + // Restore; a value it would refuse must fail the rollout, not the actors. + if errs := resources.ValidateHostPort(field.NewPath("default-egress-gateway-address"), *defaultEgressGatewayAddress); len(errs) > 0 { + serverboot.Fatal(ctx, "Invalid --default-egress-gateway-address", errs.ToAggregate()) + } // Kept separate from ctx so that in-progress work (clients, informers) is // not cancelled the moment SIGTERM arrives. The drainOnShutdown diff --git a/cmd/atelet/imagevolume_test.go b/cmd/atelet/imagevolume_test.go index a4df056f00..e22c60cb86 100644 --- a/cmd/atelet/imagevolume_test.go +++ b/cmd/atelet/imagevolume_test.go @@ -72,7 +72,9 @@ func singleFileLayer(t *testing.T, path, body string) v1.Layer { return l } -func pushTestImage(t *testing.T, ref string, layers ...v1.Layer) { +// pushTestImage pushes the image and returns its digest-pinned reference, +// which is what validated workload specs carry. +func pushTestImage(t *testing.T, ref string, layers ...v1.Layer) string { t.Helper() img, err := mutate.AppendLayers(empty.Image, layers...) if err != nil { @@ -85,6 +87,11 @@ func pushTestImage(t *testing.T, ref string, layers ...v1.Layer) { if err := remote.Write(tag, img); err != nil { t.Fatalf("remote.Write(%q): %v", ref, err) } + digest, err := img.Digest() + if err != nil { + t.Fatalf("computing digest of %q: %v", ref, err) + } + return tag.Context().Name() + "@" + digest.String() } func newImageVolumeStore(t *testing.T) *imagecache.Store { @@ -104,8 +111,8 @@ func TestResolveImageVolumes_RecordsLayersAndDigest(t *testing.T) { pushTestImage(t, ref, singleFileLayer(t, "payload-binary", "binary")) volumes := []*ateletpb.Volume{{ - Name: "agent", - Source: &ateletpb.Volume_Image{Image: &ateletpb.ImageVolumeSource{Reference: ref}}, + Name: "agent", + Image: &ateletpb.ImageVolumeSource{Reference: ref}, }} mounts := []*ateletpb.VolumeMount{{Name: "agent", MountPath: "/ate"}} @@ -138,8 +145,8 @@ func TestResolveImageVolumes_MultiLayer(t *testing.T) { ) volumes := []*ateletpb.Volume{{ - Name: "agent", - Source: &ateletpb.Volume_Image{Image: &ateletpb.ImageVolumeSource{Reference: ref}}, + Name: "agent", + Image: &ateletpb.ImageVolumeSource{Reference: ref}, }} mounts := []*ateletpb.VolumeMount{{Name: "agent", MountPath: "/ate"}} @@ -161,8 +168,8 @@ func TestResolveImageVolumes_MultiLayer(t *testing.T) { // unused volume cannot fail the actor. func TestResolveImageVolumes_UnmountedVolumeNotPulled(t *testing.T) { volumes := []*ateletpb.Volume{{ - Name: "agent", - Source: &ateletpb.Volume_Image{Image: &ateletpb.ImageVolumeSource{Reference: "127.0.0.1:1/nope@sha256:abc"}}, + Name: "agent", + Image: &ateletpb.ImageVolumeSource{Reference: "127.0.0.1:1/nope@sha256:abc"}, }} got, err := resolveImageVolumes(t.Context(), newImageVolumeStore(t), volumes, nil) diff --git a/cmd/atelet/internal/apivalidation/validate.go b/cmd/atelet/internal/apivalidation/validate.go index cb69930019..651efe361c 100644 --- a/cmd/atelet/internal/apivalidation/validate.go +++ b/cmd/atelet/internal/apivalidation/validate.go @@ -16,9 +16,11 @@ package apivalidation import ( "context" + "fmt" "github.com/agent-substrate/substrate/internal/proto/ateletpb" "github.com/agent-substrate/substrate/internal/resources" + atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -33,6 +35,31 @@ func ValidateMintActorCertificateRequest(ctx context.Context, req *ateletpb.Mint return Validate_MintActorCertificateRequest(ctx, operation.Operation{Type: operation.Create}, nil, req, nil) } +// ValidateRunRequest runs the generated validation for req. +func ValidateRunRequest(ctx context.Context, req *ateletpb.RunRequest) field.ErrorList { + return Validate_RunRequest(ctx, operation.Operation{Type: operation.Create}, nil, req, nil) +} + +// ValidateTerminateRequest runs the generated validation for req. +func ValidateTerminateRequest(ctx context.Context, req *ateletpb.TerminateRequest) field.ErrorList { + return Validate_TerminateRequest(ctx, operation.Operation{Type: operation.Create}, nil, req, nil) +} + +// ValidateCheckpointRequest runs the generated validation for req. +func ValidateCheckpointRequest(ctx context.Context, req *ateletpb.CheckpointRequest) field.ErrorList { + return Validate_CheckpointRequest(ctx, operation.Operation{Type: operation.Create}, nil, req, nil) +} + +// ValidateRestoreRequest runs the generated validation for req. +func ValidateRestoreRequest(ctx context.Context, req *ateletpb.RestoreRequest) field.ErrorList { + return Validate_RestoreRequest(ctx, operation.Operation{Type: operation.Create}, nil, req, nil) +} + +// ValidateUploadPausedCheckpointRequest runs the generated validation for req. +func ValidateUploadPausedCheckpointRequest(ctx context.Context, req *ateletpb.UploadPausedCheckpointRequest) field.ErrorList { + return Validate_UploadPausedCheckpointRequest(ctx, operation.Operation{Type: operation.Create}, nil, req, nil) +} + // ValidateRegisterWorkerRequest runs the generated validation for req. func ValidateRegisterWorkerRequest(ctx context.Context, req *ateletpb.RegisterWorkerRequest) field.ErrorList { return Validate_RegisterWorkerRequest(ctx, operation.Operation{Type: operation.Create}, nil, req, nil) @@ -45,6 +72,86 @@ func ValidateCustom_Limits(_ context.Context, _ operation.Operation, fldPath *fi return resources.ValidateLimit(fldPath, value.GetName(), value.GetQuantity()) } +func ValidateCustom_EgressGateway_Address(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidateHostPort(fldPath, *value) +} + +// ValidateCustom_SandboxAssets_SandboxClass allows the sandbox classes of the +// SandboxConfig CRD. +func ValidateCustom_SandboxAssets_SandboxClass(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + switch atev1alpha1.SandboxClass(*value) { + case atev1alpha1.SandboxClassGvisor, atev1alpha1.SandboxClassMicroVM: + return nil + } + return field.ErrorList{field.NotSupported(fldPath, *value, []string{ + string(atev1alpha1.SandboxClassGvisor), string(atev1alpha1.SandboxClassMicroVM), + })} +} + +func ValidateCustom_SandboxAssets_PauseImage(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidatePinnedImage(fldPath, *value) +} + +func ValidateCustom_AssetFile_Sha256(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + if err := resources.ValidateRunscHash(*value); err != nil { + return field.ErrorList{field.Invalid(fldPath, *value, err.Error())} + } + return nil +} + +// ValidateCustom_UploadPausedCheckpointRequest_Atespace rejects the golden +// atespace: golden actors are never paused, so a paused checkpoint is never +// promoted to a golden snapshot. +func ValidateCustom_UploadPausedCheckpointRequest_Atespace(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + if *value == resources.GoldenActorAtespace { + return field.ErrorList{field.Forbidden(fldPath, fmt.Sprintf("atespace %q holds golden actors, which are never paused", *value))} + } + return nil +} + +// ValidateCustom_CheckpointRequest requires the config that matches type. +func ValidateCustom_CheckpointRequest(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *ateletpb.CheckpointRequest) field.ErrorList { + return validateConfigMatchesType(fldPath, value.GetType(), value.GetLocalConfig() != nil, value.GetExternalConfig() != nil) +} + +// ValidateCustom_RestoreRequest requires the config that matches type. +func ValidateCustom_RestoreRequest(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *ateletpb.RestoreRequest) field.ErrorList { + return validateConfigMatchesType(fldPath, value.GetType(), value.GetLocalConfig() != nil, value.GetExternalConfig() != nil) +} + +// validateConfigMatchesType requires the config that matches a checkpoint +// type. The union tags already require exactly one config. +func validateConfigMatchesType(fldPath *field.Path, typ ateletpb.CheckpointType, hasLocal, hasExternal bool) field.ErrorList { + switch { + case typ == ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL && !hasLocal: + return field.ErrorList{field.Required(fldPath.Child("local_config"), "required when type is CHECKPOINT_TYPE_LOCAL")} + case typ == ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL && !hasExternal: + return field.ErrorList{field.Required(fldPath.Child("external_config"), "required when type is CHECKPOINT_TYPE_EXTERNAL")} + } + return nil +} + +func ValidateCustom_ExternalCheckpointConfiguration_SnapshotUri(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return validateSnapshotURI(fldPath, *value) +} + +func ValidateCustom_ExternalRestoreConfiguration_SnapshotUri(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return validateSnapshotURI(fldPath, *value) +} + +func ValidateCustom_UploadPausedCheckpointRequest_DestinationSnapshotUri(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return validateSnapshotURI(fldPath, *value) +} + +// validateSnapshotURI requires a snapshot URI that resources.ParseSnapshotURI +// accepts. +func validateSnapshotURI(fldPath *field.Path, uri string) field.ErrorList { + if _, err := resources.ParseSnapshotURI(uri); err != nil { + return field.ErrorList{field.Invalid(fldPath, uri, err.Error())} + } + return nil +} + // ateDeepEqual is the deep-equal function declarative validation's generated // code calls by name; it delegates to resources.DeepEqual. func ateDeepEqual[T any](a, b T) bool { diff --git a/cmd/atelet/internal/apivalidation/validation_test.go b/cmd/atelet/internal/apivalidation/validation_test.go index 938a5b3e28..50f50d3c84 100644 --- a/cmd/atelet/internal/apivalidation/validation_test.go +++ b/cmd/atelet/internal/apivalidation/validation_test.go @@ -21,9 +21,33 @@ import ( "testing" "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "github.com/agent-substrate/substrate/internal/resources" + "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/util/validation/field" ) +const testDigestImage = "example.com/app@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + +const testSHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + +func validSandboxAssets(mutate ...func(*ateletpb.SandboxAssets)) *ateletpb.SandboxAssets { + a := &ateletpb.SandboxAssets{ + SandboxClass: "gvisor", + PauseImage: testDigestImage, + Assets: map[string]*ateletpb.ArchAssets{ + "amd64": {Files: map[string]*ateletpb.AssetFile{"gvisor": {Url: "gs://bucket/gvisor.tar.zstd", Sha256: testSHA256}}}, + "arm64": {Files: map[string]*ateletpb.AssetFile{"gvisor": {Url: "gs://bucket/gvisor.tar.zstd", Sha256: testSHA256}}}, + }, + } + for _, m := range mutate { + m(a) + } + return a +} + +// createOp is for tests of nested messages, which have no request wrapper. +var createOp = operation.Operation{Type: operation.Create} + func assertValidateErr(t *testing.T, got field.ErrorList, want field.ErrorList) { t.Helper() field.ErrorMatcher{}.ByType().ByField().ByOrigin().Test(t, want, got) @@ -171,6 +195,814 @@ func TestValidateMintActorCertificateRequest(t *testing.T) { } } +func TestValidateRunRequest(t *testing.T) { + valid := func(mutate ...func(*ateletpb.RunRequest)) *ateletpb.RunRequest { + r := &ateletpb.RunRequest{ + WorkerPodUid: "0f9a3b1c-2d4e-5f60-7182-93a4b5c6d7e8", + Atespace: "team-a", + ActorName: "actor-1", + ActorUid: "01234567-89ab-cdef-0123-456789abcdef", + ActorTemplateAtespace: "team-a", + ActorTemplateName: "tmpl-1", + Spec: &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Name: "worker", Image: testDigestImage}}}, + SandboxAssets: validSandboxAssets(), + CpuMilli: 500, + MemoryBytes: 1 << 30, + } + for _, m := range mutate { + m(r) + } + return r + } + containerName := field.NewPath("spec", "containers").Index(0).Child("name") + + tests := []struct { + name string + obj *ateletpb.RunRequest + want field.ErrorList + }{{ + name: "valid", + obj: valid(), + }, { + name: "missing worker_pod_uid", + obj: valid(func(r *ateletpb.RunRequest) { r.WorkerPodUid = "" }), + want: field.ErrorList{field.Required(field.NewPath("worker_pod_uid"), "")}, + }, { + name: "invalid worker_pod_uid: path escape", + obj: valid(func(r *ateletpb.RunRequest) { r.WorkerPodUid = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("worker_pod_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid worker_pod_uid: not a pod UID", + obj: valid(func(r *ateletpb.RunRequest) { r.WorkerPodUid = "ateom-1" }), + want: field.ErrorList{field.Invalid(field.NewPath("worker_pod_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "missing atespace", + obj: valid(func(r *ateletpb.RunRequest) { r.Atespace = "" }), + want: field.ErrorList{field.Required(field.NewPath("atespace"), "")}, + }, { + name: "invalid atespace: path escape", + obj: valid(func(r *ateletpb.RunRequest) { r.Atespace = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing actor_name", + obj: valid(func(r *ateletpb.RunRequest) { r.ActorName = "" }), + want: field.ErrorList{field.Required(field.NewPath("actor_name"), "")}, + }, { + name: "invalid actor_name: path escape", + obj: valid(func(r *ateletpb.RunRequest) { r.ActorName = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing actor_uid", + obj: valid(func(r *ateletpb.RunRequest) { r.ActorUid = "" }), + want: field.ErrorList{field.Required(field.NewPath("actor_uid"), "")}, + }, { + name: "invalid actor_uid: path escape", + obj: valid(func(r *ateletpb.RunRequest) { r.ActorUid = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid actor_template_atespace: uppercase", + obj: valid(func(r *ateletpb.RunRequest) { r.ActorTemplateAtespace = "Team-A" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_template_atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "invalid actor_template_name: trailing dash", + obj: valid(func(r *ateletpb.RunRequest) { r.ActorTemplateName = "tmpl-" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_template_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "unset template identity is allowed", + obj: valid(func(r *ateletpb.RunRequest) { + r.ActorTemplateAtespace = "" + r.ActorTemplateName = "" + }), + }, { + name: "missing spec", + obj: valid(func(r *ateletpb.RunRequest) { r.Spec = nil }), + want: field.ErrorList{field.Required(field.NewPath("spec"), "")}, + }, { + name: "missing sandbox_assets", + obj: valid(func(r *ateletpb.RunRequest) { r.SandboxAssets = nil }), + want: field.ErrorList{field.Required(field.NewPath("sandbox_assets"), "")}, + }, { + name: "sandbox asset errors surface at the sandbox_assets path", + obj: valid(func(r *ateletpb.RunRequest) { r.SandboxAssets.PauseImage = "registry.k8s.io/pause:3.10.2" }), + want: field.ErrorList{field.Invalid(field.NewPath("sandbox_assets", "pause_image"), nil, "")}, + }, { + name: "invalid container name: path escape", + obj: valid(func(r *ateletpb.RunRequest) { + r.Spec.Containers[0].Name = "../escape" + }), + want: field.ErrorList{field.Invalid(containerName, nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "duplicate container name", + obj: valid(func(r *ateletpb.RunRequest) { + r.Spec.Containers = append(r.Spec.Containers, &ateletpb.Container{Name: "worker", Image: testDigestImage}) + }), + want: field.ErrorList{field.Duplicate(field.NewPath("spec", "containers").Index(1), nil)}, + }, { + name: "egress gateway with a DNS name", + obj: valid(func(r *ateletpb.RunRequest) { + r.EgressGateway = &ateletpb.EgressGateway{Address: "atenet-egress.ate-system.svc:443"} + }), + }, { + name: "egress gateway with an IPv6 address", + obj: valid(func(r *ateletpb.RunRequest) { + r.EgressGateway = &ateletpb.EgressGateway{Address: "[fd00::1]:443"} + }), + }, { + name: "egress gateway missing address", + obj: valid(func(r *ateletpb.RunRequest) { + r.EgressGateway = &ateletpb.EgressGateway{} + }), + want: field.ErrorList{field.Required(field.NewPath("egress_gateway", "address"), "")}, + }, { + name: "egress gateway address without a port", + obj: valid(func(r *ateletpb.RunRequest) { + r.EgressGateway = &ateletpb.EgressGateway{Address: "atenet-egress.ate-system.svc"} + }), + want: field.ErrorList{field.Invalid(field.NewPath("egress_gateway", "address"), nil, "")}, + }, { + name: "egress gateway address too long", + obj: valid(func(r *ateletpb.RunRequest) { + r.EgressGateway = &ateletpb.EgressGateway{Address: strings.Repeat("a", 258) + ":443"} + }), + want: field.ErrorList{ + field.TooLong(field.NewPath("egress_gateway", "address"), nil, 261).WithOrigin("maxLength"), + field.Invalid(field.NewPath("egress_gateway", "address"), nil, ""), + }, + }, { + name: "negative cpu_milli", + obj: valid(func(r *ateletpb.RunRequest) { r.CpuMilli = -1 }), + want: field.ErrorList{field.Invalid(field.NewPath("cpu_milli"), nil, "").WithOrigin("minimum")}, + }, { + name: "cpu_milli at the bound", + obj: valid(func(r *ateletpb.RunRequest) { r.CpuMilli = 999999 }), + }, { + name: "cpu_milli over the bound", + obj: valid(func(r *ateletpb.RunRequest) { r.CpuMilli = 1000000 }), + want: field.ErrorList{field.Invalid(field.NewPath("cpu_milli"), nil, "").WithOrigin("maximum")}, + }, { + name: "negative memory_bytes", + obj: valid(func(r *ateletpb.RunRequest) { r.MemoryBytes = -1 }), + want: field.ErrorList{field.Invalid(field.NewPath("memory_bytes"), nil, "").WithOrigin("minimum")}, + }, { + name: "unset sizes are allowed", + obj: valid(func(r *ateletpb.RunRequest) { + r.CpuMilli = 0 + r.MemoryBytes = 0 + }), + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, ValidateRunRequest(context.Background(), tt.obj), tt.want) + }) + } +} + +// TestValidateSandboxAssets covers SandboxAssets and the asset files it holds. +func TestValidateSandboxAssets(t *testing.T) { + file := func(url, sha string) *ateletpb.ArchAssets { + return &ateletpb.ArchAssets{Files: map[string]*ateletpb.AssetFile{"gvisor": {Url: url, Sha256: sha}}} + } + amd64 := field.NewPath("assets").Key("amd64") + amd64File := amd64.Child("files").Key("gvisor") + + tests := []struct { + name string + obj *ateletpb.SandboxAssets + want field.ErrorList + }{{ + name: "valid gvisor", + obj: validSandboxAssets(), + }, { + name: "valid microvm", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.SandboxClass = "microvm" }), + }, { + name: "missing sandbox_class", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.SandboxClass = "" }), + want: field.ErrorList{field.Required(field.NewPath("sandbox_class"), "")}, + }, { + name: "unknown sandbox_class", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.SandboxClass = "kvm" }), + want: field.ErrorList{field.NotSupported[string](field.NewPath("sandbox_class"), nil, nil)}, + }, { + name: "sandbox_class is case-sensitive", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.SandboxClass = "GVISOR" }), + want: field.ErrorList{field.NotSupported[string](field.NewPath("sandbox_class"), nil, nil)}, + }, { + name: "sandbox_class too long", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.SandboxClass = strings.Repeat("g", 64) }), + want: field.ErrorList{ + field.TooLong(field.NewPath("sandbox_class"), nil, 63).WithOrigin("maxLength"), + field.NotSupported[string](field.NewPath("sandbox_class"), nil, nil), + }, + }, { + name: "valid: no pause_image (microvm has no pause container)", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.PauseImage = "" }), + }, { + name: "pause_image not pinned by digest", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.PauseImage = "registry.k8s.io/pause:3.10.2" }), + want: field.ErrorList{field.Invalid(field.NewPath("pause_image"), nil, "")}, + }, { + name: "missing assets", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.Assets = nil }), + want: field.ErrorList{field.Required(field.NewPath("assets"), "")}, + }, { + name: "too many architectures", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { + for _, arch := range []string{"386", "arm", "loong64", "mips64", "ppc64le", "riscv64", "s390x"} { + a.Assets[arch] = file("gs://bucket/gvisor.tar.zstd", testSHA256) + } + }), + want: field.ErrorList{field.TooMany(field.NewPath("assets"), 9, 8).WithOrigin("maxProperties")}, + }, { + name: "architecture with no files", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.Assets["amd64"] = &ateletpb.ArchAssets{} }), + want: field.ErrorList{field.Required(amd64.Child("files"), "")}, + }, { + name: "missing url", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.Assets["amd64"] = file("", testSHA256) }), + want: field.ErrorList{field.Required(amd64File.Child("url"), "")}, + }, { + name: "missing sha256", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.Assets["amd64"] = file("gs://bucket/gvisor.tar.zstd", "") }), + want: field.ErrorList{field.Required(amd64File.Child("sha256"), "")}, + }, { + name: "sha256 too short", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.Assets["amd64"] = file("gs://bucket/gvisor.tar.zstd", "deadbeef") }), + want: field.ErrorList{field.Invalid(amd64File.Child("sha256"), nil, "")}, + }, { + name: "sha256 too long", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { + a.Assets["amd64"] = file("gs://bucket/gvisor.tar.zstd", testSHA256+"0") + }), + want: field.ErrorList{ + field.TooLong(amd64File.Child("sha256"), nil, 64).WithOrigin("maxLength"), + field.Invalid(amd64File.Child("sha256"), nil, ""), + }, + }, { + name: "sha256 with a path escape", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { + a.Assets["amd64"] = file("gs://bucket/gvisor.tar.zstd", "../"+testSHA256[3:]) + }), + want: field.ErrorList{field.Invalid(amd64File.Child("sha256"), nil, "")}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, Validate_SandboxAssets(context.Background(), createOp, nil, tt.obj, nil), tt.want) + }) + } +} + +func TestValidateCheckpointRequest(t *testing.T) { + const snapshotURI = "gs://bucket/root/atespaces/team-a/actors/01234567-89ab-cdef-0123-456789abcdef/snapshots/snap-1" + valid := func(mutate ...func(*ateletpb.CheckpointRequest)) *ateletpb.CheckpointRequest { + r := &ateletpb.CheckpointRequest{ + WorkerPodUid: "0f9a3b1c-2d4e-5f60-7182-93a4b5c6d7e8", + Atespace: "team-a", + ActorName: "actor-1", + ActorUid: "01234567-89ab-cdef-0123-456789abcdef", + ActorTemplateAtespace: "team-a", + ActorTemplateName: "tmpl-1", + Spec: &ateletpb.WorkloadSpec{}, + Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL, + ExternalConfig: &ateletpb.ExternalCheckpointConfiguration{SnapshotUri: snapshotURI}, + Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, + } + for _, m := range mutate { + m(r) + } + return r + } + local := func(name string) func(*ateletpb.CheckpointRequest) { + return func(r *ateletpb.CheckpointRequest) { + r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL + r.ExternalConfig = nil + r.LocalConfig = &ateletpb.LocalCheckpointConfiguration{SnapshotName: name} + } + } + + tests := []struct { + name string + obj *ateletpb.CheckpointRequest + want field.ErrorList + }{{ + name: "valid external", + obj: valid(), + }, { + name: "valid local", + obj: valid(local("pause-snap-1")), + }, { + name: "valid volumes fidelity", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES }), + }, { + name: "missing worker_pod_uid", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.WorkerPodUid = "" }), + want: field.ErrorList{field.Required(field.NewPath("worker_pod_uid"), "")}, + }, { + name: "invalid worker_pod_uid: path escape", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.WorkerPodUid = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("worker_pod_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid worker_pod_uid: not a pod UID", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.WorkerPodUid = "ateom-1" }), + want: field.ErrorList{field.Invalid(field.NewPath("worker_pod_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid atespace: path escape", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.Atespace = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "invalid actor_name: uppercase", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.ActorName = "UPPER" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "invalid actor_uid: not a UUID", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.ActorUid = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid actor_template_name: trailing dash", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.ActorTemplateName = "tmpl-" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_template_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "unset template identity is allowed", + obj: valid(func(r *ateletpb.CheckpointRequest) { + r.ActorTemplateAtespace = "" + r.ActorTemplateName = "" + }), + }, { + name: "missing spec", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.Spec = nil }), + want: field.ErrorList{field.Required(field.NewPath("spec"), "")}, + }, { + name: "spec errors surface at the spec's path", + obj: valid(func(r *ateletpb.CheckpointRequest) { + r.Spec = &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Name: "_pause", Image: testDigestImage}}} + }), + want: field.ErrorList{field.Invalid(field.NewPath("spec", "containers").Index(0).Child("name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "unspecified type", + obj: valid(func(r *ateletpb.CheckpointRequest) { + r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_UNSPECIFIED + }), + want: field.ErrorList{field.Required(field.NewPath("type"), "")}, + }, { + name: "unknown type", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.Type = ateletpb.CheckpointType(3) }), + want: field.ErrorList{field.Invalid(field.NewPath("type"), nil, "").WithOrigin("maximum")}, + }, { + name: "no config", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.ExternalConfig = nil }), + want: field.ErrorList{ + field.Invalid(nil, nil, "").WithOrigin("union"), + field.Required(field.NewPath("external_config"), ""), + }, + }, { + name: "both configs", + obj: valid(func(r *ateletpb.CheckpointRequest) { + r.LocalConfig = &ateletpb.LocalCheckpointConfiguration{SnapshotName: "pause-snap-1"} + }), + want: field.ErrorList{field.Invalid(nil, nil, "").WithOrigin("union")}, + }, { + name: "local type with external config", + obj: valid(func(r *ateletpb.CheckpointRequest) { + r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL + }), + want: field.ErrorList{field.Required(field.NewPath("local_config"), "")}, + }, { + name: "external type with local config", + obj: valid(local("pause-snap-1"), func(r *ateletpb.CheckpointRequest) { + r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL + }), + want: field.ErrorList{field.Required(field.NewPath("external_config"), "")}, + }, { + name: "missing local snapshot_name", + obj: valid(local("")), + want: field.ErrorList{field.Required(field.NewPath("local_config", "snapshot_name"), "")}, + }, { + name: "invalid local snapshot_name: path escape", + obj: valid(local("../escape")), + want: field.ErrorList{field.Invalid(field.NewPath("local_config", "snapshot_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing external snapshot_uri", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.ExternalConfig.SnapshotUri = "" }), + want: field.ErrorList{field.Required(field.NewPath("external_config", "snapshot_uri"), "")}, + }, { + name: "invalid external snapshot_uri: no bucket", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.ExternalConfig.SnapshotUri = "relative/path" }), + want: field.ErrorList{field.Invalid(field.NewPath("external_config", "snapshot_uri"), nil, "")}, + }, { + name: "external snapshot_uri too long", + obj: valid(func(r *ateletpb.CheckpointRequest) { + r.ExternalConfig.SnapshotUri = "gs://bucket/" + strings.Repeat("p", 2048) + }), + want: field.ErrorList{ + field.TooLong(field.NewPath("external_config", "snapshot_uri"), nil, 2048).WithOrigin("maxLength"), + field.Invalid(field.NewPath("external_config", "snapshot_uri"), nil, ""), + }, + }, { + name: "unspecified fidelity", + obj: valid(func(r *ateletpb.CheckpointRequest) { + r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED + }), + want: field.ErrorList{field.Required(field.NewPath("fidelity"), "")}, + }, { + name: "unknown fidelity", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.Fidelity = 4 }), + want: field.ErrorList{field.Invalid(field.NewPath("fidelity"), nil, "").WithOrigin("maximum")}, + }, { + name: "rootfs fidelity is forwarded for the runtime to judge", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS }), + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, ValidateCheckpointRequest(context.Background(), tt.obj), tt.want) + }) + } +} + +func TestValidateRestoreRequest(t *testing.T) { + const snapshotURI = "gs://bucket/root/atespaces/team-a/actors/01234567-89ab-cdef-0123-456789abcdef/snapshots/snap-1" + valid := func(mutate ...func(*ateletpb.RestoreRequest)) *ateletpb.RestoreRequest { + r := &ateletpb.RestoreRequest{ + WorkerPodUid: "0f9a3b1c-2d4e-5f60-7182-93a4b5c6d7e8", + Atespace: "team-a", + ActorName: "actor-1", + ActorUid: "01234567-89ab-cdef-0123-456789abcdef", + ActorTemplateAtespace: "team-a", + ActorTemplateName: "tmpl-1", + Spec: &ateletpb.WorkloadSpec{}, + Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL, + ExternalConfig: &ateletpb.ExternalRestoreConfiguration{SnapshotUri: snapshotURI}, + Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, + SandboxAssets: validSandboxAssets(), + } + for _, m := range mutate { + m(r) + } + return r + } + local := func(name string) func(*ateletpb.RestoreRequest) { + return func(r *ateletpb.RestoreRequest) { + r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL + r.ExternalConfig = nil + r.LocalConfig = &ateletpb.LocalCheckpointConfiguration{SnapshotName: name} + } + } + + tests := []struct { + name string + obj *ateletpb.RestoreRequest + want field.ErrorList + }{{ + name: "valid external", + obj: valid(), + }, { + name: "valid local", + obj: valid(local("pause-snap-1")), + }, { + name: "valid volumes fidelity", + obj: valid(func(r *ateletpb.RestoreRequest) { r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES }), + }, { + name: "valid size and egress", + obj: valid(func(r *ateletpb.RestoreRequest) { + r.CpuMilli = 999999 + r.MemoryBytes = 1 << 30 + r.EgressGateway = &ateletpb.EgressGateway{Address: "10.0.0.1:15001"} + }), + }, { + name: "missing worker_pod_uid", + obj: valid(func(r *ateletpb.RestoreRequest) { r.WorkerPodUid = "" }), + want: field.ErrorList{field.Required(field.NewPath("worker_pod_uid"), "")}, + }, { + name: "invalid worker_pod_uid: path escape", + obj: valid(func(r *ateletpb.RestoreRequest) { r.WorkerPodUid = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("worker_pod_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid worker_pod_uid: not a pod UID", + obj: valid(func(r *ateletpb.RestoreRequest) { r.WorkerPodUid = "ateom-1" }), + want: field.ErrorList{field.Invalid(field.NewPath("worker_pod_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid atespace: path escape", + obj: valid(func(r *ateletpb.RestoreRequest) { r.Atespace = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "invalid actor_name: uppercase", + obj: valid(func(r *ateletpb.RestoreRequest) { r.ActorName = "UPPER" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "invalid actor_uid: not a UUID", + obj: valid(func(r *ateletpb.RestoreRequest) { r.ActorUid = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid actor_template_atespace: slash", + obj: valid(func(r *ateletpb.RestoreRequest) { r.ActorTemplateAtespace = "no/slashes" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_template_atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "unset template identity is allowed", + obj: valid(func(r *ateletpb.RestoreRequest) { + r.ActorTemplateAtespace = "" + r.ActorTemplateName = "" + }), + }, { + name: "missing spec", + obj: valid(func(r *ateletpb.RestoreRequest) { r.Spec = nil }), + want: field.ErrorList{field.Required(field.NewPath("spec"), "")}, + }, { + name: "spec errors surface at the spec's path", + obj: valid(func(r *ateletpb.RestoreRequest) { + r.Spec = &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Name: "_pause", Image: testDigestImage}}} + }), + want: field.ErrorList{field.Invalid(field.NewPath("spec", "containers").Index(0).Child("name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "unspecified type", + obj: valid(func(r *ateletpb.RestoreRequest) { r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_UNSPECIFIED }), + want: field.ErrorList{field.Required(field.NewPath("type"), "")}, + }, { + name: "unknown type", + obj: valid(func(r *ateletpb.RestoreRequest) { r.Type = ateletpb.CheckpointType(3) }), + want: field.ErrorList{field.Invalid(field.NewPath("type"), nil, "").WithOrigin("maximum")}, + }, { + name: "no config", + obj: valid(func(r *ateletpb.RestoreRequest) { r.ExternalConfig = nil }), + want: field.ErrorList{ + field.Invalid(nil, nil, "").WithOrigin("union"), + field.Required(field.NewPath("external_config"), ""), + }, + }, { + name: "both configs", + obj: valid(func(r *ateletpb.RestoreRequest) { + r.LocalConfig = &ateletpb.LocalCheckpointConfiguration{SnapshotName: "pause-snap-1"} + }), + want: field.ErrorList{field.Invalid(nil, nil, "").WithOrigin("union")}, + }, { + name: "local type with external config", + obj: valid(func(r *ateletpb.RestoreRequest) { r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL }), + want: field.ErrorList{field.Required(field.NewPath("local_config"), "")}, + }, { + name: "external type with local config", + obj: valid(local("pause-snap-1"), func(r *ateletpb.RestoreRequest) { + r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL + }), + want: field.ErrorList{field.Required(field.NewPath("external_config"), "")}, + }, { + name: "invalid local snapshot_name: path escape", + obj: valid(local("../escape")), + want: field.ErrorList{field.Invalid(field.NewPath("local_config", "snapshot_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing external snapshot_uri", + obj: valid(func(r *ateletpb.RestoreRequest) { r.ExternalConfig.SnapshotUri = "" }), + want: field.ErrorList{field.Required(field.NewPath("external_config", "snapshot_uri"), "")}, + }, { + name: "invalid external snapshot_uri: no bucket", + obj: valid(func(r *ateletpb.RestoreRequest) { r.ExternalConfig.SnapshotUri = "relative/path" }), + want: field.ErrorList{field.Invalid(field.NewPath("external_config", "snapshot_uri"), nil, "")}, + }, { + name: "unspecified fidelity", + obj: valid(func(r *ateletpb.RestoreRequest) { r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED }), + want: field.ErrorList{field.Required(field.NewPath("fidelity"), "")}, + }, { + name: "unknown fidelity", + obj: valid(func(r *ateletpb.RestoreRequest) { r.Fidelity = ateletpb.SnapshotFidelity(4) }), + want: field.ErrorList{field.Invalid(field.NewPath("fidelity"), nil, "").WithOrigin("maximum")}, + }, { + name: "rootfs fidelity is forwarded for the runtime to judge", + obj: valid(func(r *ateletpb.RestoreRequest) { r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS }), + }, { + name: "negative cpu_milli", + obj: valid(func(r *ateletpb.RestoreRequest) { r.CpuMilli = -1 }), + want: field.ErrorList{field.Invalid(field.NewPath("cpu_milli"), nil, "").WithOrigin("minimum")}, + }, { + name: "cpu_milli too large", + obj: valid(func(r *ateletpb.RestoreRequest) { r.CpuMilli = 1000000 }), + want: field.ErrorList{field.Invalid(field.NewPath("cpu_milli"), nil, "").WithOrigin("maximum")}, + }, { + name: "negative memory_bytes", + obj: valid(func(r *ateletpb.RestoreRequest) { r.MemoryBytes = -1 }), + want: field.ErrorList{field.Invalid(field.NewPath("memory_bytes"), nil, "").WithOrigin("minimum")}, + }, { + name: "empty egress gateway address", + obj: valid(func(r *ateletpb.RestoreRequest) { r.EgressGateway = &ateletpb.EgressGateway{} }), + want: field.ErrorList{field.Required(field.NewPath("egress_gateway", "address"), "")}, + }, { + name: "missing sandbox_assets", + obj: valid(func(r *ateletpb.RestoreRequest) { r.SandboxAssets = nil }), + want: field.ErrorList{field.Required(field.NewPath("sandbox_assets"), "")}, + }, { + name: "sandbox_assets errors surface at their path", + obj: valid(func(r *ateletpb.RestoreRequest) { + r.SandboxAssets = validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.SandboxClass = "kata" }) + }), + want: field.ErrorList{field.NotSupported[string](field.NewPath("sandbox_assets", "sandbox_class"), nil, nil)}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, ValidateRestoreRequest(context.Background(), tt.obj), tt.want) + }) + } +} + +func TestValidateUploadPausedCheckpointRequest(t *testing.T) { + valid := func(mutate ...func(*ateletpb.UploadPausedCheckpointRequest)) *ateletpb.UploadPausedCheckpointRequest { + r := &ateletpb.UploadPausedCheckpointRequest{ + Atespace: "team-a", + ActorName: "actor-1", + ActorUid: "01234567-89ab-cdef-0123-456789abcdef", + ActorTemplateAtespace: "team-a", + ActorTemplateName: "tmpl-1", + LocalSnapshotName: "pause-snap-1", + DestinationSnapshotUri: "gs://bucket/root/atespaces/team-a/actors/01234567-89ab-cdef-0123-456789abcdef/snapshots/snap-1", + DesiredFidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, + } + for _, m := range mutate { + m(r) + } + return r + } + + tests := []struct { + name string + obj *ateletpb.UploadPausedCheckpointRequest + want field.ErrorList + }{{ + name: "valid", + obj: valid(), + }, { + name: "valid volumes fidelity", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { + r.DesiredFidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES + }), + }, { + name: "missing atespace", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.Atespace = "" }), + want: field.ErrorList{field.Required(field.NewPath("atespace"), "")}, + }, { + name: "invalid atespace: path escape", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.Atespace = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "golden atespace", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.Atespace = resources.GoldenActorAtespace }), + want: field.ErrorList{field.Forbidden(field.NewPath("atespace"), "")}, + }, { + name: "invalid actor_name: uppercase", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.ActorName = "UPPER" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing actor_uid", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.ActorUid = "" }), + want: field.ErrorList{field.Required(field.NewPath("actor_uid"), "")}, + }, { + name: "invalid actor_uid: not a UUID", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.ActorUid = "actor-uid-1" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid actor_template_atespace: slash", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.ActorTemplateAtespace = "no/slashes" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_template_atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "unset template identity is allowed", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { + r.ActorTemplateAtespace = "" + r.ActorTemplateName = "" + }), + }, { + name: "missing local_snapshot_name", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.LocalSnapshotName = "" }), + want: field.ErrorList{field.Required(field.NewPath("local_snapshot_name"), "")}, + }, { + name: "invalid local_snapshot_name: path escape", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.LocalSnapshotName = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("local_snapshot_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing destination_snapshot_uri", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.DestinationSnapshotUri = "" }), + want: field.ErrorList{field.Required(field.NewPath("destination_snapshot_uri"), "")}, + }, { + name: "invalid destination_snapshot_uri", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.DestinationSnapshotUri = "not-a-uri" }), + want: field.ErrorList{field.Invalid(field.NewPath("destination_snapshot_uri"), nil, "")}, + }, { + name: "destination_snapshot_uri too long", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { + r.DestinationSnapshotUri = "gs://bucket/" + strings.Repeat("p", 2048) + }), + want: field.ErrorList{ + field.TooLong(field.NewPath("destination_snapshot_uri"), nil, 2048).WithOrigin("maxLength"), + field.Invalid(field.NewPath("destination_snapshot_uri"), nil, ""), + }, + }, { + name: "unspecified desired_fidelity", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { + r.DesiredFidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED + }), + want: field.ErrorList{field.Required(field.NewPath("desired_fidelity"), "")}, + }, { + name: "unknown desired_fidelity", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { + r.DesiredFidelity = 4 + }), + want: field.ErrorList{field.Invalid(field.NewPath("desired_fidelity"), nil, "").WithOrigin("maximum")}, + }, { + name: "rootfs desired_fidelity is forwarded for the runtime to judge", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { + r.DesiredFidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS + }), + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, ValidateUploadPausedCheckpointRequest(context.Background(), tt.obj), tt.want) + }) + } +} + +func TestValidateTerminateRequest(t *testing.T) { + valid := func(mutate ...func(*ateletpb.TerminateRequest)) *ateletpb.TerminateRequest { + r := &ateletpb.TerminateRequest{ + WorkerPodUid: "0f9a3b1c-2d4e-5f60-7182-93a4b5c6d7e8", + Atespace: "team-a", + ActorName: "actor-1", + ActorUid: "01234567-89ab-cdef-0123-456789abcdef", + ActorTemplateAtespace: "team-a", + ActorTemplateName: "tmpl-1", + Spec: &ateletpb.WorkloadSpec{}, + } + for _, m := range mutate { + m(r) + } + return r + } + + tests := []struct { + name string + obj *ateletpb.TerminateRequest + want field.ErrorList + }{{ + name: "valid", + obj: valid(), + }, { + name: "unset worker_pod_uid is allowed: node-only cleanup", + obj: valid(func(r *ateletpb.TerminateRequest) { r.WorkerPodUid = "" }), + }, { + name: "invalid worker_pod_uid: path escape", + obj: valid(func(r *ateletpb.TerminateRequest) { r.WorkerPodUid = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("worker_pod_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid worker_pod_uid: not a pod UID", + obj: valid(func(r *ateletpb.TerminateRequest) { r.WorkerPodUid = "ateom-1" }), + want: field.ErrorList{field.Invalid(field.NewPath("worker_pod_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "missing atespace", + obj: valid(func(r *ateletpb.TerminateRequest) { r.Atespace = "" }), + want: field.ErrorList{field.Required(field.NewPath("atespace"), "")}, + }, { + name: "invalid atespace: uppercase", + obj: valid(func(r *ateletpb.TerminateRequest) { r.Atespace = "Team-A" }), + want: field.ErrorList{field.Invalid(field.NewPath("atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing actor_name", + obj: valid(func(r *ateletpb.TerminateRequest) { r.ActorName = "" }), + want: field.ErrorList{field.Required(field.NewPath("actor_name"), "")}, + }, { + name: "invalid actor_name: path escape", + obj: valid(func(r *ateletpb.TerminateRequest) { r.ActorName = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing actor_uid", + obj: valid(func(r *ateletpb.TerminateRequest) { r.ActorUid = "" }), + want: field.ErrorList{field.Required(field.NewPath("actor_uid"), "")}, + }, { + name: "invalid actor_uid: not a uuid", + obj: valid(func(r *ateletpb.TerminateRequest) { r.ActorUid = "not-a-uuid" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid actor_template_atespace: uppercase", + obj: valid(func(r *ateletpb.TerminateRequest) { r.ActorTemplateAtespace = "Team-A" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_template_atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "invalid actor_template_name: trailing dash", + obj: valid(func(r *ateletpb.TerminateRequest) { r.ActorTemplateName = "tmpl-" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_template_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "spec errors surface at the spec's path", + obj: valid(func(r *ateletpb.TerminateRequest) { + r.Spec = &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Name: "_pause", Image: testDigestImage}}} + }), + want: field.ErrorList{field.Invalid(field.NewPath("spec", "containers").Index(0).Child("name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "unset template identity is allowed", + obj: valid(func(r *ateletpb.TerminateRequest) { + r.ActorTemplateAtespace = "" + r.ActorTemplateName = "" + }), + }, { + name: "unset spec is allowed", + obj: valid(func(r *ateletpb.TerminateRequest) { r.Spec = nil }), + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, ValidateTerminateRequest(context.Background(), tt.obj), tt.want) + }) + } +} + func TestValidateRegisterWorkerRequest(t *testing.T) { testRuntime := func() *ateletpb.SandboxRuntime { return &ateletpb.SandboxRuntime{ @@ -420,7 +1252,7 @@ func TestValidateRegisterWorkerRequest(t *testing.T) { }, }, { name: "quantity too long", - obj: withLimits(&ateletpb.Limits{Name: "memory", Quantity: strings.Repeat("1", 33)}), + obj: withLimits(&ateletpb.Limits{Name: "memory", Quantity: "1." + strings.Repeat("0", 31)}), want: field.ErrorList{field.TooLong(limitsPath.Index(0).Child("quantity"), nil, 32).WithOrigin("maxLength")}, }, { name: "duplicate resource name", @@ -434,21 +1266,6 @@ func TestValidateRegisterWorkerRequest(t *testing.T) { name: "malformed quantity", obj: withLimits(&ateletpb.Limits{Name: "cpu", Quantity: "not-a-quantity"}), want: field.ErrorList{field.Invalid(limitsPath.Index(0).Child("quantity"), nil, "")}, - }, { - name: "negative quantity", - obj: withLimits(&ateletpb.Limits{Name: "memory", Quantity: "-1Gi"}), - want: field.ErrorList{field.Invalid(limitsPath.Index(0).Child("quantity"), nil, "")}, - }, { - name: "zero quantity", - obj: withLimits(&ateletpb.Limits{Name: "memory", Quantity: "0"}), - want: field.ErrorList{field.Invalid(limitsPath.Index(0).Child("quantity"), nil, "")}, - }, { - name: "cpu below the bound", - obj: withLimits(&ateletpb.Limits{Name: "cpu", Quantity: "999"}), - }, { - name: "cpu at the bound", - obj: withLimits(&ateletpb.Limits{Name: "cpu", Quantity: "1000"}), - want: field.ErrorList{field.Invalid(limitsPath.Index(0).Child("quantity"), nil, "")}, }, { name: "too many limits", obj: withLimits( @@ -468,3 +1285,577 @@ func TestValidateRegisterWorkerRequest(t *testing.T) { }) } } + +// TestValidateWorkloadSpec covers the rules WorkloadSpec owns: the volumes and +// containers lists. One nested case per list proves the element validators +// run; their own rules are covered by TestValidateVolume and +// TestValidateContainer. +func TestValidateWorkloadSpec(t *testing.T) { + ctr := func(name string) *ateletpb.Container { + return &ateletpb.Container{Name: name, Image: testDigestImage} + } + valid := func(mutate ...func(*ateletpb.WorkloadSpec)) *ateletpb.WorkloadSpec { + s := &ateletpb.WorkloadSpec{ + Volumes: []*ateletpb.Volume{{Name: "data", DurableDir: &ateletpb.DurableDirVolume{}}}, + Containers: []*ateletpb.Container{ctr("main"), ctr("sidecar")}, + } + for _, m := range mutate { + m(s) + } + return s + } + + tests := []struct { + name string + obj *ateletpb.WorkloadSpec + want field.ErrorList + }{{ + name: "valid", + obj: valid(), + }, { + name: "empty spec", + obj: &ateletpb.WorkloadSpec{}, + }, { + name: "no volumes: the delete flow may send containers only", + obj: valid(func(s *ateletpb.WorkloadSpec) { s.Volumes = nil }), + }, { + name: "duplicate volume names", + obj: valid(func(s *ateletpb.WorkloadSpec) { + s.Volumes = append(s.Volumes, &ateletpb.Volume{Name: "data", Image: &ateletpb.ImageVolumeSource{Reference: testDigestImage}}) + }), + want: field.ErrorList{field.Duplicate(field.NewPath("volumes").Index(1), nil)}, + }, { + name: "too many volumes", + obj: valid(func(s *ateletpb.WorkloadSpec) { + s.Volumes = nil + for i := range 33 { + s.Volumes = append(s.Volumes, &ateletpb.Volume{Name: fmt.Sprintf("v%d", i), DurableDir: &ateletpb.DurableDirVolume{}}) + } + }), + want: field.ErrorList{field.TooMany(field.NewPath("volumes"), 33, 32).WithOrigin("maxItems")}, + }, { + name: "volume errors surface at the volume's path", + obj: valid(func(s *ateletpb.WorkloadSpec) { s.Volumes[0].DurableDir = nil }), + want: field.ErrorList{field.Invalid(field.NewPath("volumes").Index(0), nil, "").WithOrigin("union")}, + }, { + name: "duplicate container names", + obj: valid(func(s *ateletpb.WorkloadSpec) { + s.Containers = []*ateletpb.Container{ctr("main"), ctr("main")} + }), + want: field.ErrorList{field.Duplicate(field.NewPath("containers").Index(1), nil)}, + }, { + name: "too many containers", + obj: valid(func(s *ateletpb.WorkloadSpec) { + s.Containers = nil + for _, n := range []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"} { + s.Containers = append(s.Containers, ctr(n)) + } + }), + want: field.ErrorList{field.TooMany(field.NewPath("containers"), 11, 10).WithOrigin("maxItems")}, + }, { + name: "container errors surface at the container's path", + obj: valid(func(s *ateletpb.WorkloadSpec) { + s.Containers[0].Resources = &ateletpb.ResourceLimits{CpuMillis: -1} + }), + want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("resources", "cpu_millis"), nil, "").WithOrigin("minimum")}, + }, { + name: "mount references a declared volume", + obj: valid(func(s *ateletpb.WorkloadSpec) { + s.Containers[0].VolumeMounts = []*ateletpb.VolumeMount{{Name: "data", MountPath: "/data"}} + }), + }, { + name: "mount references an undeclared volume", + obj: valid(func(s *ateletpb.WorkloadSpec) { + s.Containers[1].VolumeMounts = []*ateletpb.VolumeMount{{Name: "data", MountPath: "/data"}, {Name: "ghost", MountPath: "/ghost"}} + }), + want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(1).Child("volume_mounts").Index(1).Child("name"), nil, "")}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, Validate_WorkloadSpec(context.Background(), createOp, nil, tt.obj, nil), tt.want) + }) + } +} + +// TestValidateVolume covers Volume and every source it can hold, with errors +// asserted at their paths under the volume. +func TestValidateVolume(t *testing.T) { + durable := func(mutate ...func(*ateletpb.Volume)) *ateletpb.Volume { + v := &ateletpb.Volume{Name: "data", DurableDir: &ateletpb.DurableDirVolume{}} + for _, m := range mutate { + m(v) + } + return v + } + external := func(mutate ...func(*ateletpb.ExternalVolumeSource)) *ateletpb.Volume { + e := &ateletpb.ExternalVolumeSource{ + StorageVolumeId: "projects/p/zones/z/disks/vol-1", + VolumeType: "substrate.io/mock", + VolumeContext: map[string]string{"fsType": "ext4"}, + } + for _, m := range mutate { + m(e) + } + return &ateletpb.Volume{Name: "data", External: e} + } + image := func(ref string) *ateletpb.Volume { + return &ateletpb.Volume{Name: "data", Image: &ateletpb.ImageVolumeSource{Reference: ref}} + } + systemInfo := func(ds ...*ateletpb.SystemInfoDataSource) *ateletpb.Volume { + return &ateletpb.Volume{Name: "data", SystemInfo: &ateletpb.SystemInfoVolume{DataSources: ds}} + } + bundle := func(name, path string) *ateletpb.SystemInfoDataSource { + return &ateletpb.SystemInfoDataSource{TrustBundle: &ateletpb.TrustBundleDataSource{Names: []string{name}, Path: path}} + } + item := func(f ateletpb.ActorMetadataField, path string) *ateletpb.ActorMetadataItem { + return &ateletpb.ActorMetadataItem{Field: f, Path: path} + } + metadata := func(items ...*ateletpb.ActorMetadataItem) *ateletpb.SystemInfoDataSource { + return &ateletpb.SystemInfoDataSource{ActorMetadata: &ateletpb.ActorMetadataDataSource{Items: items}} + } + fieldName := ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_NAME + + extPath := field.NewPath("external") + dsPath := field.NewPath("system_info", "data_sources") + bundlePath := dsPath.Index(0).Child("trust_bundle") + itemsPath := dsPath.Index(0).Child("actor_metadata", "items") + + tests := []struct { + name string + obj *ateletpb.Volume + want field.ErrorList + }{ + // Volume. + { + name: "valid durable dir", + obj: durable(), + }, { + name: "missing name", + obj: durable(func(v *ateletpb.Volume) { v.Name = "" }), + want: field.ErrorList{field.Required(field.NewPath("name"), "")}, + }, { + name: "invalid name: uppercase", + obj: durable(func(v *ateletpb.Volume) { v.Name = "Data" }), + want: field.ErrorList{field.Invalid(field.NewPath("name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "no source set", + obj: durable(func(v *ateletpb.Volume) { v.DurableDir = nil }), + want: field.ErrorList{field.Invalid(nil, nil, "").WithOrigin("union")}, + }, { + name: "two sources set", + obj: durable(func(v *ateletpb.Volume) { + v.External = &ateletpb.ExternalVolumeSource{StorageVolumeId: "vol-1"} + }), + want: field.ErrorList{field.Invalid(nil, nil, "").WithOrigin("union")}, + }, + + // ExternalVolumeSource. + { + name: "valid external", + obj: external(), + }, { + name: "external: missing storage_volume_id", + obj: external(func(e *ateletpb.ExternalVolumeSource) { e.StorageVolumeId = "" }), + want: field.ErrorList{field.Required(extPath.Child("storage_volume_id"), "")}, + }, { + name: "external: storage_volume_id with a control character", + obj: external(func(e *ateletpb.ExternalVolumeSource) { e.StorageVolumeId = "vol\x01" }), + want: field.ErrorList{field.Invalid(extPath.Child("storage_volume_id"), nil, "")}, + }, { + name: "external: storage_volume_id too long", + obj: external(func(e *ateletpb.ExternalVolumeSource) { e.StorageVolumeId = strings.Repeat("x", 257) }), + want: field.ErrorList{field.TooLong(extPath.Child("storage_volume_id"), nil, 256).WithOrigin("maxLength")}, + }, { + name: "external: unset volume_type is allowed", + obj: external(func(e *ateletpb.ExternalVolumeSource) { e.VolumeType = "" }), + }, { + name: "external: invalid volume_type: uppercase", + obj: external(func(e *ateletpb.ExternalVolumeSource) { e.VolumeType = "substrate.io/Mock" }), + want: field.ErrorList{field.Invalid(extPath.Child("volume_type"), nil, "")}, + }, { + name: "external: volume_context with one long value, as a CSI driver may return", + obj: external(func(e *ateletpb.ExternalVolumeSource) { + e.VolumeContext = map[string]string{"k": strings.Repeat("v", 4000)} + }), + }, { + name: "external: volume_context over the size bound", + obj: external(func(e *ateletpb.ExternalVolumeSource) { + e.VolumeContext = map[string]string{"k": strings.Repeat("v", 40*1024)} + }), + want: field.ErrorList{field.TooLong(extPath.Child("volume_context"), nil, 40*1024)}, + }, { + name: "external: publish_context with many entries, as a CSI driver may return", + obj: external(func(e *ateletpb.ExternalVolumeSource) { + e.PublishContext = make(map[string]string) + for i := range 100 { + e.PublishContext[fmt.Sprintf("key-%03d", i)] = "v" + } + }), + }, { + name: "external: publish_context over the size bound", + obj: external(func(e *ateletpb.ExternalVolumeSource) { + e.PublishContext = map[string]string{"a": strings.Repeat("v", 20*1024), "b": strings.Repeat("v", 20*1024)} + }), + want: field.ErrorList{field.TooLong(extPath.Child("publish_context"), nil, 40*1024)}, + }, + + // ImageVolumeSource. + { + name: "valid image", + obj: image(testDigestImage), + }, { + name: "image: missing reference", + obj: image(""), + want: field.ErrorList{field.Required(field.NewPath("image", "reference"), "")}, + }, { + name: "image: reference not pinned by digest", + obj: image("example.com/app:v1"), + want: field.ErrorList{field.Invalid(field.NewPath("image", "reference"), nil, "")}, + }, + + // SystemInfoVolume and SystemInfoDataSource. + { + name: "valid system info", + obj: systemInfo(bundle("podcert", "a.pem"), bundle("podcert", "b.pem"), metadata(item(fieldName, "name"))), + }, { + name: "system info: data source with neither set", + obj: systemInfo(&ateletpb.SystemInfoDataSource{}), + want: field.ErrorList{field.Invalid(dsPath.Index(0), nil, "").WithOrigin("union")}, + }, { + name: "system info: data source with both set", + obj: systemInfo(&ateletpb.SystemInfoDataSource{ + ActorMetadata: &ateletpb.ActorMetadataDataSource{Items: []*ateletpb.ActorMetadataItem{item(fieldName, "name")}}, + TrustBundle: &ateletpb.TrustBundleDataSource{Names: []string{"podcert"}, Path: "p"}, + }), + want: field.ErrorList{field.Invalid(dsPath.Index(0), nil, "").WithOrigin("union")}, + }, { + name: "system info: duplicate path across trust bundles", + obj: systemInfo(bundle("podcert", "a.pem"), bundle("podcert", "a.pem")), + want: field.ErrorList{field.Duplicate(dsPath.Index(1).Child("trust_bundle", "path"), nil)}, + }, { + name: "system info: duplicate path between bundle and metadata item", + obj: systemInfo(bundle("podcert", "name"), metadata(item(fieldName, "name"))), + want: field.ErrorList{field.Duplicate(dsPath.Index(1).Child("actor_metadata", "items").Index(0).Child("path"), nil)}, + }, { + name: "system info: second actor_metadata entry", + obj: systemInfo(metadata(item(fieldName, "name")), metadata(item(fieldName, "name-2"))), + want: field.ErrorList{field.Forbidden(dsPath.Index(1).Child("actor_metadata"), "")}, + }, { + name: "system info: too many data sources", + obj: systemInfo( + bundle("podcert", "a"), bundle("podcert", "b"), bundle("podcert", "c"), + bundle("podcert", "d"), bundle("podcert", "e"), bundle("podcert", "f"), + bundle("podcert", "g"), bundle("podcert", "h"), bundle("podcert", "i"), + ), + want: field.ErrorList{field.TooMany(dsPath, 9, 8).WithOrigin("maxItems")}, + }, + + // TrustBundleDataSource. + { + name: "trust bundle: missing path", + obj: systemInfo(bundle("podcert", "")), + want: field.ErrorList{field.Required(bundlePath.Child("path"), "")}, + }, { + name: "trust bundle: absolute path", + obj: systemInfo(bundle("podcert", "/trust/bundle.pem")), + want: field.ErrorList{field.Invalid(bundlePath.Child("path"), nil, "")}, + }, { + name: "trust bundle: path too long", + obj: systemInfo(bundle("podcert", strings.Repeat("p", 256))), + want: field.ErrorList{field.TooLong(bundlePath.Child("path"), nil, 255).WithOrigin("maxLength")}, + }, { + name: "trust bundle: no names", + obj: systemInfo(&ateletpb.SystemInfoDataSource{TrustBundle: &ateletpb.TrustBundleDataSource{Path: "trust/bundle.pem"}}), + want: field.ErrorList{field.Required(bundlePath.Child("names"), "")}, + }, { + name: "trust bundle: several names", + obj: systemInfo(&ateletpb.SystemInfoDataSource{TrustBundle: &ateletpb.TrustBundleDataSource{Names: []string{"egress-mitm.ate.dev", "system-roots.ate.dev"}, Path: "trust/bundle.pem"}}), + }, { + name: "trust bundle: too many names", + obj: systemInfo(&ateletpb.SystemInfoDataSource{TrustBundle: &ateletpb.TrustBundleDataSource{Names: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"}, Path: "trust/bundle.pem"}}), + want: field.ErrorList{field.TooMany(bundlePath.Child("names"), 9, 8).WithOrigin("maxItems")}, + }, { + name: "trust bundle: duplicate names", + obj: systemInfo(&ateletpb.SystemInfoDataSource{TrustBundle: &ateletpb.TrustBundleDataSource{Names: []string{"a", "a"}, Path: "trust/bundle.pem"}}), + want: field.ErrorList{field.Duplicate(bundlePath.Child("names").Index(1), nil)}, + }, { + name: "trust bundle: empty name", + obj: systemInfo(bundle("", "trust/bundle.pem")), + want: field.ErrorList{field.TooShort(bundlePath.Child("names").Index(0), "", 1).WithOrigin("minLength")}, + }, { + name: "trust bundle: name too long", + obj: systemInfo(bundle(strings.Repeat("n", 254), "trust/bundle.pem")), + want: field.ErrorList{field.TooLong(bundlePath.Child("names").Index(0), nil, 253).WithOrigin("maxLength")}, + }, + + // ActorMetadataDataSource. + { + name: "actor metadata: several fields", + obj: systemInfo(metadata( + item(fieldName, "name"), + item(ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_UID, "ids/uid"), + )), + }, { + name: "actor metadata: empty items", + obj: systemInfo(metadata()), + want: field.ErrorList{field.Required(itemsPath, "")}, + }, { + name: "actor metadata: same field projected twice", + obj: systemInfo(metadata(item(fieldName, "a"), item(fieldName, "b"))), + want: field.ErrorList{field.Duplicate(itemsPath.Index(1), nil)}, + }, { + name: "actor metadata: unspecified field", + obj: systemInfo(metadata(item(ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_UNSPECIFIED, "a"))), + want: field.ErrorList{field.Required(itemsPath.Index(0).Child("field"), "")}, + }, { + name: "actor metadata: field outside the enum", + obj: systemInfo(metadata(item(ateletpb.ActorMetadataField(4), "a"))), + want: field.ErrorList{field.Invalid(itemsPath.Index(0).Child("field"), nil, "").WithOrigin("maximum")}, + }, { + name: "actor metadata: escaping item path", + obj: systemInfo(metadata(item(fieldName, "../name"))), + want: field.ErrorList{field.Invalid(itemsPath.Index(0).Child("path"), nil, "")}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, Validate_Volume(context.Background(), createOp, nil, tt.obj, nil), tt.want) + }) + } +} + +// TestValidateContainer covers Container and every message it holds, with +// errors asserted at their paths under the container. +func TestValidateContainer(t *testing.T) { + valid := func(mutate ...func(*ateletpb.Container)) *ateletpb.Container { + c := &ateletpb.Container{ + Name: "main", + Image: testDigestImage, + Command: []string{"/bin/app"}, + Args: []string{"--serve"}, + Env: []*ateletpb.EnvEntry{{Name: "PORT", Value: "8080"}}, + VolumeMounts: []*ateletpb.VolumeMount{ + {Name: "data", MountPath: "/data"}, + {Name: "data", MountPath: "/mnt/data"}, + }, + } + for _, m := range mutate { + m(c) + } + return c + } + env := func(mutate func(*ateletpb.EnvEntry)) *ateletpb.Container { + return valid(func(c *ateletpb.Container) { mutate(c.Env[0]) }) + } + // mount replaces the mounts with a single one, so the nesting check + // between mounts stays out of the way. + mount := func(name, path string) *ateletpb.Container { + return valid(func(c *ateletpb.Container) { + c.VolumeMounts = []*ateletpb.VolumeMount{{Name: name, MountPath: path}} + }) + } + probe := func(mutate ...func(*ateletpb.WakeupProbe)) *ateletpb.Container { + p := &ateletpb.WakeupProbe{ + HttpGet: &ateletpb.HttpGetAction{Path: "/healthz", Port: 8080}, + TimeoutSeconds: 30, + } + for _, m := range mutate { + m(p) + } + return valid(func(c *ateletpb.Container) { c.WakeupProbe = p }) + } + caps := func(add, drop []string) *ateletpb.Container { + return valid(func(c *ateletpb.Container) { + c.SecurityContext = &ateletpb.SecurityContext{Capabilities: &ateletpb.Capabilities{Add: add, Drop: drop}} + }) + } + limits := func(r *ateletpb.ResourceLimits) *ateletpb.Container { + return valid(func(c *ateletpb.Container) { c.Resources = r }) + } + + envPath := field.NewPath("env").Index(0) + mountPath := field.NewPath("volume_mounts").Index(0) + httpGetPath := field.NewPath("wakeup_probe", "http_get") + capsPath := field.NewPath("security_context", "capabilities") + + tests := []struct { + name string + obj *ateletpb.Container + want field.ErrorList + }{ + // Container name, image, command, and args. + { + name: "valid: the same volume mounted at two paths", + obj: valid(), + }, { + name: "missing name", + obj: valid(func(c *ateletpb.Container) { c.Name = "" }), + want: field.ErrorList{field.Required(field.NewPath("name"), "")}, + }, { + name: "invalid name: uppercase", + obj: valid(func(c *ateletpb.Container) { c.Name = "Main" }), + want: field.ErrorList{field.Invalid(field.NewPath("name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing image", + obj: valid(func(c *ateletpb.Container) { c.Image = "" }), + want: field.ErrorList{field.Required(field.NewPath("image"), "")}, + }, { + name: "image not pinned by digest", + obj: valid(func(c *ateletpb.Container) { c.Image = "example.com/app:v1" }), + want: field.ErrorList{field.Invalid(field.NewPath("image"), nil, "")}, + }, { + name: "empty command and args are allowed", + obj: valid(func(c *ateletpb.Container) { + c.Command = nil + c.Args = nil + }), + }, { + name: "too many command items", + obj: valid(func(c *ateletpb.Container) { c.Command = make([]string, 65) }), + want: field.ErrorList{field.TooMany(field.NewPath("command"), 65, 64).WithOrigin("maxItems")}, + }, { + name: "arg over the length guardrail", + obj: valid(func(c *ateletpb.Container) { c.Args = []string{strings.Repeat("a", 4097)} }), + want: field.ErrorList{field.TooLong(field.NewPath("args").Index(0), nil, 4096).WithOrigin("maxLength")}, + }, + + // EnvEntry. + { + name: "duplicate env names", + obj: valid(func(c *ateletpb.Container) { + c.Env = append(c.Env, &ateletpb.EnvEntry{Name: "PORT", Value: "9"}) + }), + want: field.ErrorList{field.Duplicate(field.NewPath("env").Index(1), nil)}, + }, { + name: "env: missing name", + obj: env(func(e *ateletpb.EnvEntry) { e.Name = "" }), + want: field.ErrorList{field.Required(envPath.Child("name"), "")}, + }, { + name: "env: name with equals sign", + obj: env(func(e *ateletpb.EnvEntry) { e.Name = "A=B" }), + want: field.ErrorList{field.Invalid(envPath.Child("name"), nil, "")}, + }, { + name: "env: name too long", + obj: env(func(e *ateletpb.EnvEntry) { e.Name = strings.Repeat("N", 257) }), + want: field.ErrorList{field.TooLong(envPath.Child("name"), nil, 256).WithOrigin("maxLength")}, + }, { + name: "env: empty value is allowed", + obj: env(func(e *ateletpb.EnvEntry) { e.Value = "" }), + }, { + name: "env: value too long", + obj: env(func(e *ateletpb.EnvEntry) { e.Value = strings.Repeat("v", 32769) }), + want: field.ErrorList{field.TooLong(envPath.Child("value"), nil, 32768).WithOrigin("maxLength")}, + }, + + // VolumeMount. + { + name: "duplicate mount paths", + obj: valid(func(c *ateletpb.Container) { c.VolumeMounts[1].MountPath = "/data" }), + want: field.ErrorList{field.Duplicate(field.NewPath("volume_mounts").Index(1), nil)}, + }, { + name: "nested mount paths", + obj: valid(func(c *ateletpb.Container) { c.VolumeMounts[1].MountPath = "/data/nested" }), + want: field.ErrorList{field.Invalid(field.NewPath("volume_mounts").Index(1).Child("mount_path"), nil, "")}, + }, { + name: "mount: missing name", + obj: mount("", "/var/data"), + want: field.ErrorList{field.Required(mountPath.Child("name"), "")}, + }, { + name: "mount: invalid name: uppercase", + obj: mount("Data", "/var/data"), + want: field.ErrorList{field.Invalid(mountPath.Child("name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "mount: missing mount_path", + obj: mount("data", ""), + want: field.ErrorList{field.Required(mountPath.Child("mount_path"), "")}, + }, { + name: "mount: relative mount_path", + obj: mount("data", "var/data"), + want: field.ErrorList{field.Invalid(mountPath.Child("mount_path"), nil, "")}, + }, + + // WakeupProbe and HttpGetAction. + { + name: "valid probe", + obj: probe(), + }, { + name: "probe: missing http_get", + obj: probe(func(p *ateletpb.WakeupProbe) { p.HttpGet = nil }), + want: field.ErrorList{field.Required(httpGetPath, "")}, + }, { + name: "probe: missing timeout", + obj: probe(func(p *ateletpb.WakeupProbe) { p.TimeoutSeconds = 0 }), + want: field.ErrorList{field.Required(field.NewPath("wakeup_probe", "timeout_seconds"), "")}, + }, { + name: "probe: timeout above the bound", + obj: probe(func(p *ateletpb.WakeupProbe) { p.TimeoutSeconds = 3601 }), + want: field.ErrorList{field.Invalid(field.NewPath("wakeup_probe", "timeout_seconds"), nil, "").WithOrigin("maximum")}, + }, { + name: "probe: missing path", + obj: probe(func(p *ateletpb.WakeupProbe) { p.HttpGet.Path = "" }), + want: field.ErrorList{field.Required(httpGetPath.Child("path"), "")}, + }, { + name: "probe: path without a leading slash", + obj: probe(func(p *ateletpb.WakeupProbe) { p.HttpGet.Path = "healthz" }), + want: field.ErrorList{field.Invalid(httpGetPath.Child("path"), nil, "")}, + }, { + name: "probe: missing port", + obj: probe(func(p *ateletpb.WakeupProbe) { p.HttpGet.Port = 0 }), + want: field.ErrorList{field.Required(httpGetPath.Child("port"), "")}, + }, { + name: "probe: port above the range", + obj: probe(func(p *ateletpb.WakeupProbe) { p.HttpGet.Port = 65536 }), + want: field.ErrorList{field.Invalid(httpGetPath.Child("port"), nil, "").WithOrigin("maximum")}, + }, + + // SecurityContext. + { + name: "valid capabilities", + obj: caps([]string{"NET_BIND_SERVICE"}, []string{"ALL"}), + }, { + name: "empty security context", + obj: valid(func(c *ateletpb.Container) { c.SecurityContext = &ateletpb.SecurityContext{} }), + }, { + name: "capabilities: add does not accept ALL", + obj: caps([]string{"ALL"}, nil), + want: field.ErrorList{field.Invalid(capsPath.Child("add").Index(0), nil, "")}, + }, { + name: "capabilities: lowercase rejected", + obj: caps(nil, []string{"net_bind_service"}), + want: field.ErrorList{field.Invalid(capsPath.Child("drop").Index(0), nil, "")}, + }, { + name: "capabilities: duplicate rejected by the set", + obj: caps([]string{"SYS_TIME", "SYS_TIME"}, nil), + want: field.ErrorList{field.Duplicate(capsPath.Child("add").Index(1), nil)}, + }, + + // ResourceLimits. + { + name: "valid limits", + obj: limits(&ateletpb.ResourceLimits{MemoryBytes: 1 << 30, CpuMillis: 500}), + }, { + name: "limits: zero means unset", + obj: limits(&ateletpb.ResourceLimits{}), + }, { + name: "limits: negative memory_bytes", + obj: limits(&ateletpb.ResourceLimits{MemoryBytes: -1}), + want: field.ErrorList{field.Invalid(field.NewPath("resources", "memory_bytes"), nil, "").WithOrigin("minimum")}, + }, { + name: "limits: negative cpu_millis", + obj: limits(&ateletpb.ResourceLimits{CpuMillis: -1}), + want: field.ErrorList{field.Invalid(field.NewPath("resources", "cpu_millis"), nil, "").WithOrigin("minimum")}, + }, { + name: "limits: cpu_millis at the cap", + obj: limits(&ateletpb.ResourceLimits{CpuMillis: 999999}), + }, { + name: "limits: cpu_millis above the cap", + obj: limits(&ateletpb.ResourceLimits{CpuMillis: 1000000}), + want: field.ErrorList{field.Invalid(field.NewPath("resources", "cpu_millis"), nil, "").WithOrigin("maximum")}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, Validate_Container(context.Background(), createOp, nil, tt.obj, nil), tt.want) + }) + } +} diff --git a/cmd/atelet/internal/apivalidation/workloadspec.go b/cmd/atelet/internal/apivalidation/workloadspec.go new file mode 100644 index 0000000000..c0e3f94693 --- /dev/null +++ b/cmd/atelet/internal/apivalidation/workloadspec.go @@ -0,0 +1,153 @@ +// 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. + +// Custom validations for the WorkloadSpec tree. The field rules are shared +// with the control plane through internal/resources: values arriving here +// already passed them at template creation, so a failure is an internal +// inconsistency, not a user error. + +package apivalidation + +import ( + "context" + + "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "github.com/agent-substrate/substrate/internal/resources" + "k8s.io/apimachinery/pkg/api/operation" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +// ValidateCustom_WorkloadSpec rejects container volume mounts that name a +// volume the spec does not declare, mirroring the template-side rule, so a +// dangling mount is refused at the RPC edge rather than after atelet has +// already reset the actor's directories and mounted its external volumes. +func ValidateCustom_WorkloadSpec(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *ateletpb.WorkloadSpec) field.ErrorList { + declared := sets.New[string]() + for _, vol := range value.GetVolumes() { + declared.Insert(vol.GetName()) + } + var errs field.ErrorList + for i, ctr := range value.GetContainers() { + for j, mount := range ctr.GetVolumeMounts() { + name := mount.GetName() + if name == "" { + continue // required is enforced by tags + } + if !declared.Has(name) { + errs = append(errs, field.Invalid( + fldPath.Child("containers").Index(i).Child("volume_mounts").Index(j).Child("name"), + name, "must reference a volume declared in the spec")) + } + } + } + return errs +} + +func ValidateCustom_Container_Image(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidatePinnedImage(fldPath, *value) +} + +func ValidateCustom_EnvEntry_Name(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidateEnvVarName(fldPath, *value) +} + +func ValidateCustom_VolumeMount_MountPath(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidateMountPath(fldPath, *value) +} + +// ValidateCustom_Container_VolumeMounts rejects nested mounts. Mount-path +// uniqueness is enforced by the list key. +func ValidateCustom_Container_VolumeMounts(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []*ateletpb.VolumeMount) field.ErrorList { + paths := make([]string, len(value)) + for i, m := range value { + paths[i] = m.GetMountPath() + } + return resources.ValidateNestedMountPaths(fldPath, paths) +} + +func ValidateCustom_Capabilities_Add(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []string) field.ErrorList { + return resources.ValidateCapabilities(fldPath, value, false) +} + +func ValidateCustom_Capabilities_Drop(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []string) field.ErrorList { + return resources.ValidateCapabilities(fldPath, value, true) +} + +func ValidateCustom_HttpGetAction_Path(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidateHTTPGetPath(fldPath, *value) +} + +func ValidateCustom_ExternalVolumeSource_StorageVolumeId(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidateStorageVolumeID(fldPath, *value) +} + +func ValidateCustom_ExternalVolumeSource_VolumeType(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidateVolumeType(fldPath, *value) +} + +func ValidateCustom_ExternalVolumeSource_VolumeContext(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[string]string) field.ErrorList { + return resources.ValidateCSIMap(fldPath, value) +} + +func ValidateCustom_ExternalVolumeSource_PublishContext(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[string]string) field.ErrorList { + return resources.ValidateCSIMap(fldPath, value) +} + +func ValidateCustom_ImageVolumeSource_Reference(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidatePinnedImage(fldPath, *value) +} + +func ValidateCustom_ActorMetadataItem_Path(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidateProjectedPath(fldPath, *value) +} + +func ValidateCustom_TrustBundleDataSource_Path(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidateProjectedPath(fldPath, *value) +} + +// ValidateCustom_SystemInfoVolume_DataSources allows at most one +// actor_metadata entry and requires every projected file path to be unique +// across all data sources: atelet writes them in order into one tree, so a +// repeated path silently clobbers the earlier file. +func ValidateCustom_SystemInfoVolume_DataSources(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []*ateletpb.SystemInfoDataSource) field.ErrorList { + var errs field.ErrorList + seen := sets.New[string]() + sawMetadata := false + for i, ds := range value { + switch { + case ds == nil: + case ds.TrustBundle != nil: + if seen.Has(ds.TrustBundle.Path) { + errs = append(errs, field.Duplicate(fldPath.Index(i).Child("trust_bundle", "path"), ds.TrustBundle.Path)) + } + seen.Insert(ds.TrustBundle.Path) + case ds.ActorMetadata != nil: + if sawMetadata { + errs = append(errs, field.Forbidden(fldPath.Index(i).Child("actor_metadata"), "at most one actor_metadata entry may appear")) + } + sawMetadata = true + for j, item := range ds.ActorMetadata.Items { + if item == nil { + continue + } + if seen.Has(item.Path) { + errs = append(errs, field.Duplicate(fldPath.Index(i).Child("actor_metadata", "items").Index(j).Child("path"), item.Path)) + } + seen.Insert(item.Path) + } + } + } + return errs +} diff --git a/cmd/atelet/internal/apivalidation/zz_generated.validation.go b/cmd/atelet/internal/apivalidation/zz_generated.validation.go index c77dd28826..5974aab64d 100644 --- a/cmd/atelet/internal/apivalidation/zz_generated.validation.go +++ b/cmd/atelet/internal/apivalidation/zz_generated.validation.go @@ -29,48 +29,75 @@ import ( field "k8s.io/apimachinery/pkg/util/validation/field" ) -// Validate_AttributeEntry validates an instance of AttributeEntry according +// Validate_ActorMetadataDataSource validates an instance of ActorMetadataDataSource according // to declarative validation rules in the API schema. -func Validate_AttributeEntry( +func Validate_ActorMetadataDataSource( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateletpb.AttributeEntry) (errs field.ErrorList) { + obj, oldObj *ateletpb.ActorMetadataDataSource) (errs field.ErrorList) { - { // field ateletpb.AttributeEntry.Key + { // field ateletpb.ActorMetadataDataSource.Items fn := func( fldPath *field.Path, - obj, oldObj *string, + obj, oldObj []*ateletpb.ActorMetadataItem, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { - if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + if ateDeepEqual(obj, oldObj) { return nil } } // call field-attached validations earlyReturn := false - if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.PtrSliceNoNils[ateletpb.ActorMetadataItem](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 8).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } if earlyReturn { return // do not proceed } - if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 128); len(e) != 0 { + if e := validate.MinItems(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + // lists with map semantics require unique keys + if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.ActorMetadataItem, b *ateletpb.ActorMetadataItem) bool { return a.Field == b.Field }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.ActorMetadataItem, b *ateletpb.ActorMetadataItem) bool { return a.Field == b.Field }, ateDeepEqual, Validate_ActorMetadataItem); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.AttributeEntry) *string { - return &oldObj.Key + func(oldObj *ateletpb.ActorMetadataDataSource) []*ateletpb.ActorMetadataItem { + return oldObj.Items }) - errs = append(errs, fn(fldPath.Child("key"), &obj.Key, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("items"), obj.Items, oldVal, oldObj != nil)...) } - { // field ateletpb.AttributeEntry.Value + return errs +} + +// Validate_ActorMetadataItem validates an instance of ActorMetadataItem according +// to declarative validation rules in the API schema. +func Validate_ActorMetadataItem( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.ActorMetadataItem) (errs field.ErrorList) { + + { // field ateletpb.ActorMetadataItem.Field fn := func( fldPath *field.Path, - obj, oldObj *string, + obj, oldObj *ateletpb.ActorMetadataField, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -87,33 +114,22 @@ func Validate_AttributeEntry( if earlyReturn { return // do not proceed } - if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256); len(e) != 0 { + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 3); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.AttributeEntry) *string { - return &oldObj.Value + func(oldObj *ateletpb.ActorMetadataItem) *ateletpb.ActorMetadataField { + return &oldObj.Field }) - errs = append(errs, fn(fldPath.Child("value"), &obj.Value, oldVal, oldObj != nil)...) - } - - return errs -} - -// Validate_Limits validates an instance of Limits according -// to declarative validation rules in the API schema. -func Validate_Limits( - ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateletpb.Limits) (errs field.ErrorList) { - - // custom validation - if e := ValidateCustom_Limits(ctx, op, fldPath, obj, oldObj); len(e) != 0 { - errs = append(errs, e...) + errs = append(errs, fn(fldPath.Child("field"), &obj.Field, oldVal, oldObj != nil)...) } - { // field ateletpb.Limits.Name + { // field ateletpb.ActorMetadataItem.Path fn := func( fldPath *field.Path, obj, oldObj *string, @@ -133,60 +149,91 @@ func Validate_Limits( if earlyReturn { return // do not proceed } - if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 16); len(e) != 0 { + // custom validation + if e := ValidateCustom_ActorMetadataItem_Path(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 255); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MinLength(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.Limits) *string { - return &oldObj.Name + func(oldObj *ateletpb.ActorMetadataItem) *string { + return &oldObj.Path }) - errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("path"), &obj.Path, oldVal, oldObj != nil)...) } - { // field ateletpb.Limits.Quantity + return errs +} + +// Validate_ArchAssets validates an instance of ArchAssets according +// to declarative validation rules in the API schema. +func Validate_ArchAssets( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.ArchAssets) (errs field.ErrorList) { + + { // field ateletpb.ArchAssets.Files fn := func( fldPath *field.Path, - obj, oldObj *string, + obj, oldObj map[string]*ateletpb.AssetFile, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { - if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + if ateDeepEqual(obj, oldObj) { return nil } } // call field-attached validations earlyReturn := false - if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.PtrMapNoNils[string, ateletpb.AssetFile](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxProperties(ctx, op, fldPath, obj, oldObj, 16).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.RequiredMap(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } if earlyReturn { return // do not proceed } - if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 32); len(e) != 0 { + if e := validate.EachMapKey(ctx, op, fldPath, obj, oldObj, + func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 64) + }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the map and call the value type's validation function + if e := validate.EachPtrMapVal(ctx, op, fldPath, obj, oldObj, ateDeepEqual, Validate_AssetFile); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.Limits) *string { - return &oldObj.Quantity + func(oldObj *ateletpb.ArchAssets) map[string]*ateletpb.AssetFile { + return oldObj.Files }) - errs = append(errs, fn(fldPath.Child("quantity"), &obj.Quantity, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("files"), obj.Files, oldVal, oldObj != nil)...) } return errs } -// Validate_MintActorCertificateRequest validates an instance of MintActorCertificateRequest according +// Validate_AssetFile validates an instance of AssetFile according // to declarative validation rules in the API schema. -func Validate_MintActorCertificateRequest( +func Validate_AssetFile( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateletpb.MintActorCertificateRequest) (errs field.ErrorList) { + obj, oldObj *ateletpb.AssetFile) (errs field.ErrorList) { - { // field ateletpb.MintActorCertificateRequest.ActorAtespace + { // field ateletpb.AssetFile.Url fn := func( fldPath *field.Path, obj, oldObj *string, @@ -206,19 +253,19 @@ func Validate_MintActorCertificateRequest( if earlyReturn { return // do not proceed } - if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 2048); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.MintActorCertificateRequest) *string { - return &oldObj.ActorAtespace + func(oldObj *ateletpb.AssetFile) *string { + return &oldObj.Url }) - errs = append(errs, fn(fldPath.Child("actor_atespace"), &obj.ActorAtespace, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("url"), &obj.Url, oldVal, oldObj != nil)...) } - { // field ateletpb.MintActorCertificateRequest.ActorName + { // field ateletpb.AssetFile.Sha256 fn := func( fldPath *field.Path, obj, oldObj *string, @@ -238,19 +285,32 @@ func Validate_MintActorCertificateRequest( if earlyReturn { return // do not proceed } - if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + // custom validation + if e := ValidateCustom_AssetFile_Sha256(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 64); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.MintActorCertificateRequest) *string { - return &oldObj.ActorName + func(oldObj *ateletpb.AssetFile) *string { + return &oldObj.Sha256 }) - errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("sha256"), &obj.Sha256, oldVal, oldObj != nil)...) } - { // field ateletpb.MintActorCertificateRequest.ActorUid + return errs +} + +// Validate_AttributeEntry validates an instance of AttributeEntry according +// to declarative validation rules in the API schema. +func Validate_AttributeEntry( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.AttributeEntry) (errs field.ErrorList) { + + { // field ateletpb.AttributeEntry.Key fn := func( fldPath *field.Path, obj, oldObj *string, @@ -270,63 +330,63 @@ func Validate_MintActorCertificateRequest( if earlyReturn { return // do not proceed } - if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 128); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.MintActorCertificateRequest) *string { - return &oldObj.ActorUid + func(oldObj *ateletpb.AttributeEntry) *string { + return &oldObj.Key }) - errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("key"), &obj.Key, oldVal, oldObj != nil)...) } - { // field ateletpb.MintActorCertificateRequest.CertificateSigningRequest + { // field ateletpb.AttributeEntry.Value fn := func( fldPath *field.Path, - obj, oldObj []byte, + obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { - if ateDeepEqual(obj, oldObj) { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { return nil } } // call field-attached validations earlyReturn := false - if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } if earlyReturn { return // do not proceed } - if e := validate.MaxBytesSlice(ctx, op, fldPath, obj, oldObj, 16384); len(e) != 0 { + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.MintActorCertificateRequest) []byte { - return oldObj.CertificateSigningRequest + func(oldObj *ateletpb.AttributeEntry) *string { + return &oldObj.Value }) - errs = append(errs, fn(fldPath.Child("certificate_signing_request"), obj.CertificateSigningRequest, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("value"), &obj.Value, oldVal, oldObj != nil)...) } return errs } -// Validate_RegisterWorkerRequest validates an instance of RegisterWorkerRequest according +// Validate_Capabilities validates an instance of Capabilities according // to declarative validation rules in the API schema. -func Validate_RegisterWorkerRequest( +func Validate_Capabilities( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateletpb.RegisterWorkerRequest) (errs field.ErrorList) { + obj, oldObj *ateletpb.Capabilities) (errs field.ErrorList) { - { // field ateletpb.RegisterWorkerRequest.Capacity + { // field ateletpb.Capabilities.Add fn := func( fldPath *field.Path, - obj, oldObj *ateletpb.WorkerResources, + obj, oldObj []string, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -336,28 +396,37 @@ func Validate_RegisterWorkerRequest( } // call field-attached validations earlyReturn := false - if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 64).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } if earlyReturn { return // do not proceed } - // call the type's validation function - errs = append(errs, Validate_WorkerResources(ctx, op, fldPath, obj, oldObj)...) + // custom validation + if e := ValidateCustom_Capabilities_Add(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + // lists with set semantics require unique values + if e := validate.ValSliceUnique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual); len(e) != 0 { + errs = append(errs, e...) + } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.RegisterWorkerRequest) *ateletpb.WorkerResources { - return oldObj.Capacity + func(oldObj *ateletpb.Capabilities) []string { + return oldObj.Add }) - errs = append(errs, fn(fldPath.Child("capacity"), obj.Capacity, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("add"), obj.Add, oldVal, oldObj != nil)...) } - { // field ateletpb.RegisterWorkerRequest.DefaultRuntime + { // field ateletpb.Capabilities.Drop fn := func( fldPath *field.Path, - obj, oldObj *ateletpb.SandboxRuntime, + obj, oldObj []string, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -367,74 +436,97 @@ func Validate_RegisterWorkerRequest( } // call field-attached validations earlyReturn := false - if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 64).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } if earlyReturn { return // do not proceed } - // call the type's validation function - errs = append(errs, Validate_SandboxRuntime(ctx, op, fldPath, obj, oldObj)...) + // custom validation + if e := ValidateCustom_Capabilities_Drop(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + // lists with set semantics require unique values + if e := validate.ValSliceUnique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual); len(e) != 0 { + errs = append(errs, e...) + } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.RegisterWorkerRequest) *ateletpb.SandboxRuntime { - return oldObj.DefaultRuntime + func(oldObj *ateletpb.Capabilities) []string { + return oldObj.Drop }) - errs = append(errs, fn(fldPath.Child("default_runtime"), obj.DefaultRuntime, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("drop"), obj.Drop, oldVal, oldObj != nil)...) } - { // field ateletpb.RegisterWorkerRequest.RestorableRuntimes + return errs +} + +var unionMembershipFor_github_com_agent_substrate_substrate_internal_proto_ateletpb_CheckpointRequest_ = validate.NewUnionMembership(validate.NewUnionMember("local_config"), validate.NewUnionMember("external_config")) + +// Validate_CheckpointRequest validates an instance of CheckpointRequest according +// to declarative validation rules in the API schema. +func Validate_CheckpointRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.CheckpointRequest) (errs field.ErrorList) { + + if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_github_com_agent_substrate_substrate_internal_proto_ateletpb_CheckpointRequest_, + func(obj *ateletpb.CheckpointRequest) bool { + if obj == nil { + return false + } + return obj.LocalConfig != nil + }, + func(obj *ateletpb.CheckpointRequest) bool { + if obj == nil { + return false + } + return obj.ExternalConfig != nil + }); len(e) != 0 { + errs = append(errs, e...) + } + // custom validation + if e := ValidateCustom_CheckpointRequest(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + + { // field ateletpb.CheckpointRequest.WorkerPodUid fn := func( fldPath *field.Path, - obj, oldObj []*ateletpb.SandboxRuntime, + obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { - if ateDeepEqual(obj, oldObj) { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { return nil } } // call field-attached validations earlyReturn := false - if e := validate.PtrSliceNoNils[ateletpb.SandboxRuntime](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) - earlyReturn = true - } - if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 32).MarkShortCircuit(); len(e) != 0 { + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } - if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { - earlyReturn = true - } if earlyReturn { return // do not proceed } - // iterate the list and call the type's validation function - if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_SandboxRuntime); len(e) != 0 { + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.RegisterWorkerRequest) []*ateletpb.SandboxRuntime { - return oldObj.RestorableRuntimes + func(oldObj *ateletpb.CheckpointRequest) *string { + return &oldObj.WorkerPodUid }) - errs = append(errs, fn(fldPath.Child("restorable_runtimes"), obj.RestorableRuntimes, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("worker_pod_uid"), &obj.WorkerPodUid, oldVal, oldObj != nil)...) } - return errs -} - -// Validate_RequestActorSuspendRequest validates an instance of RequestActorSuspendRequest according -// to declarative validation rules in the API schema. -func Validate_RequestActorSuspendRequest( - ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateletpb.RequestActorSuspendRequest) (errs field.ErrorList) { - - { // field ateletpb.RequestActorSuspendRequest.ActorAtespace + { // field ateletpb.CheckpointRequest.Atespace fn := func( fldPath *field.Path, obj, oldObj *string, @@ -460,13 +552,13 @@ func Validate_RequestActorSuspendRequest( return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.RequestActorSuspendRequest) *string { - return &oldObj.ActorAtespace + func(oldObj *ateletpb.CheckpointRequest) *string { + return &oldObj.Atespace }) - errs = append(errs, fn(fldPath.Child("actor_atespace"), &obj.ActorAtespace, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("atespace"), &obj.Atespace, oldVal, oldObj != nil)...) } - { // field ateletpb.RequestActorSuspendRequest.ActorName + { // field ateletpb.CheckpointRequest.ActorName fn := func( fldPath *field.Path, obj, oldObj *string, @@ -492,13 +584,13 @@ func Validate_RequestActorSuspendRequest( return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.RequestActorSuspendRequest) *string { + func(oldObj *ateletpb.CheckpointRequest) *string { return &oldObj.ActorName }) errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) } - { // field ateletpb.RequestActorSuspendRequest.ActorUid + { // field ateletpb.CheckpointRequest.ActorUid fn := func( fldPath *field.Path, obj, oldObj *string, @@ -524,112 +616,3750 @@ func Validate_RequestActorSuspendRequest( return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.RequestActorSuspendRequest) *string { + func(oldObj *ateletpb.CheckpointRequest) *string { return &oldObj.ActorUid }) errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) } - return errs -} - -// Validate_Resources validates an instance of Resources according -// to declarative validation rules in the API schema. -func Validate_Resources( - ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateletpb.Resources) (errs field.ErrorList) { - - { // field ateletpb.Resources.Limits + { // field ateletpb.CheckpointRequest.ActorTemplateAtespace fn := func( fldPath *field.Path, - obj, oldObj []*ateletpb.Limits, + obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { - if ateDeepEqual(obj, oldObj) { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { return nil } } // call field-attached validations earlyReturn := false - if e := validate.PtrSliceNoNils[ateletpb.Limits](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) - earlyReturn = true - } - if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 2).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) - earlyReturn = true - } - if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { earlyReturn = true } if earlyReturn { return // do not proceed } - // lists with map semantics require unique keys - if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, - func(a *ateletpb.Limits, b *ateletpb.Limits) bool { return a.Name == b.Name }); len(e) != 0 { + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } - // iterate the list and call the type's validation function - if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, - func(a *ateletpb.Limits, b *ateletpb.Limits) bool { return a.Name == b.Name }, ateDeepEqual, Validate_Limits); len(e) != 0 { - errs = append(errs, e...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.CheckpointRequest) *string { + return &oldObj.ActorTemplateAtespace + }) + errs = append(errs, fn(fldPath.Child("actor_template_atespace"), &obj.ActorTemplateAtespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.CheckpointRequest.ActorTemplateName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.CheckpointRequest) *string { + return &oldObj.ActorTemplateName + }) + errs = append(errs, fn(fldPath.Child("actor_template_name"), &obj.ActorTemplateName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.CheckpointRequest.Spec + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.WorkloadSpec, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_WorkloadSpec(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.CheckpointRequest) *ateletpb.WorkloadSpec { + return oldObj.Spec + }) + errs = append(errs, fn(fldPath.Child("spec"), obj.Spec, oldVal, oldObj != nil)...) + } + + { // field ateletpb.CheckpointRequest.Type + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.CheckpointType, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 2); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.CheckpointRequest) *ateletpb.CheckpointType { + return &oldObj.Type + }) + errs = append(errs, fn(fldPath.Child("type"), &obj.Type, oldVal, oldObj != nil)...) + } + + { // field ateletpb.CheckpointRequest.LocalConfig + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.LocalCheckpointConfiguration, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_LocalCheckpointConfiguration(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.CheckpointRequest) *ateletpb.LocalCheckpointConfiguration { + return oldObj.LocalConfig + }) + errs = append(errs, fn(fldPath.Child("local_config"), obj.LocalConfig, oldVal, oldObj != nil)...) + } + + { // field ateletpb.CheckpointRequest.ExternalConfig + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.ExternalCheckpointConfiguration, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_ExternalCheckpointConfiguration(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.CheckpointRequest) *ateletpb.ExternalCheckpointConfiguration { + return oldObj.ExternalConfig + }) + errs = append(errs, fn(fldPath.Child("external_config"), obj.ExternalConfig, oldVal, oldObj != nil)...) + } + + { // field ateletpb.CheckpointRequest.Fidelity + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.SnapshotFidelity, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 3); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.CheckpointRequest) *ateletpb.SnapshotFidelity { + return &oldObj.Fidelity + }) + errs = append(errs, fn(fldPath.Child("fidelity"), &obj.Fidelity, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_Container validates an instance of Container according +// to declarative validation rules in the API schema. +func Validate_Container( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.Container) (errs field.ErrorList) { + + { // field ateletpb.Container.Name + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Container) *string { + return &oldObj.Name + }) + errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Container.Image + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_Container_Image(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 512); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Container) *string { + return &oldObj.Image + }) + errs = append(errs, fn(fldPath.Child("image"), &obj.Image, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Container.Command + fn := func( + fldPath *field.Path, + obj, oldObj []string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 64).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.EachValSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, + func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 4096) + }); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Container) []string { + return oldObj.Command + }) + errs = append(errs, fn(fldPath.Child("command"), obj.Command, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Container.Args + fn := func( + fldPath *field.Path, + obj, oldObj []string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 64).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.EachValSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, + func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 4096) + }); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Container) []string { + return oldObj.Args + }) + errs = append(errs, fn(fldPath.Child("args"), obj.Args, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Container.Env + fn := func( + fldPath *field.Path, + obj, oldObj []*ateletpb.EnvEntry, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.PtrSliceNoNils[ateletpb.EnvEntry](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 32).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // lists with map semantics require unique keys + if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.EnvEntry, b *ateletpb.EnvEntry) bool { return a.Name == b.Name }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.EnvEntry, b *ateletpb.EnvEntry) bool { return a.Name == b.Name }, ateDeepEqual, Validate_EnvEntry); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Container) []*ateletpb.EnvEntry { + return oldObj.Env + }) + errs = append(errs, fn(fldPath.Child("env"), obj.Env, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Container.WakeupProbe + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.WakeupProbe, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_WakeupProbe(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Container) *ateletpb.WakeupProbe { + return oldObj.WakeupProbe + }) + errs = append(errs, fn(fldPath.Child("wakeup_probe"), obj.WakeupProbe, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Container.VolumeMounts + fn := func( + fldPath *field.Path, + obj, oldObj []*ateletpb.VolumeMount, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.PtrSliceNoNils[ateletpb.VolumeMount](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 32).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_Container_VolumeMounts(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + // lists with map semantics require unique keys + if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.VolumeMount, b *ateletpb.VolumeMount) bool { return a.MountPath == b.MountPath }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.VolumeMount, b *ateletpb.VolumeMount) bool { return a.MountPath == b.MountPath }, ateDeepEqual, Validate_VolumeMount); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Container) []*ateletpb.VolumeMount { + return oldObj.VolumeMounts + }) + errs = append(errs, fn(fldPath.Child("volume_mounts"), obj.VolumeMounts, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Container.SecurityContext + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.SecurityContext, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_SecurityContext(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Container) *ateletpb.SecurityContext { + return oldObj.SecurityContext + }) + errs = append(errs, fn(fldPath.Child("security_context"), obj.SecurityContext, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Container.Resources + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.ResourceLimits, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_ResourceLimits(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Container) *ateletpb.ResourceLimits { + return oldObj.Resources + }) + errs = append(errs, fn(fldPath.Child("resources"), obj.Resources, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_EgressGateway validates an instance of EgressGateway according +// to declarative validation rules in the API schema. +func Validate_EgressGateway( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.EgressGateway) (errs field.ErrorList) { + + { // field ateletpb.EgressGateway.Address + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_EgressGateway_Address(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 261); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.EgressGateway) *string { + return &oldObj.Address + }) + errs = append(errs, fn(fldPath.Child("address"), &obj.Address, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_EnvEntry validates an instance of EnvEntry according +// to declarative validation rules in the API schema. +func Validate_EnvEntry( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.EnvEntry) (errs field.ErrorList) { + + { // field ateletpb.EnvEntry.Name + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_EnvEntry_Name(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.EnvEntry) *string { + return &oldObj.Name + }) + errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) + } + + { // field ateletpb.EnvEntry.Value + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 32768); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.EnvEntry) *string { + return &oldObj.Value + }) + errs = append(errs, fn(fldPath.Child("value"), &obj.Value, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_ExternalCheckpointConfiguration validates an instance of ExternalCheckpointConfiguration according +// to declarative validation rules in the API schema. +func Validate_ExternalCheckpointConfiguration( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.ExternalCheckpointConfiguration) (errs field.ErrorList) { + + { // field ateletpb.ExternalCheckpointConfiguration.SnapshotUri + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_ExternalCheckpointConfiguration_SnapshotUri(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 2048); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.ExternalCheckpointConfiguration) *string { + return &oldObj.SnapshotUri + }) + errs = append(errs, fn(fldPath.Child("snapshot_uri"), &obj.SnapshotUri, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_ExternalRestoreConfiguration validates an instance of ExternalRestoreConfiguration according +// to declarative validation rules in the API schema. +func Validate_ExternalRestoreConfiguration( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.ExternalRestoreConfiguration) (errs field.ErrorList) { + + { // field ateletpb.ExternalRestoreConfiguration.SnapshotUri + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_ExternalRestoreConfiguration_SnapshotUri(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 2048); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.ExternalRestoreConfiguration) *string { + return &oldObj.SnapshotUri + }) + errs = append(errs, fn(fldPath.Child("snapshot_uri"), &obj.SnapshotUri, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_ExternalVolumeSource validates an instance of ExternalVolumeSource according +// to declarative validation rules in the API schema. +func Validate_ExternalVolumeSource( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.ExternalVolumeSource) (errs field.ErrorList) { + + { // field ateletpb.ExternalVolumeSource.StorageVolumeId + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_ExternalVolumeSource_StorageVolumeId(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.ExternalVolumeSource) *string { + return &oldObj.StorageVolumeId + }) + errs = append(errs, fn(fldPath.Child("storage_volume_id"), &obj.StorageVolumeId, oldVal, oldObj != nil)...) + } + + { // field ateletpb.ExternalVolumeSource.VolumeType + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_ExternalVolumeSource_VolumeType(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 253); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.ExternalVolumeSource) *string { + return &oldObj.VolumeType + }) + errs = append(errs, fn(fldPath.Child("volume_type"), &obj.VolumeType, oldVal, oldObj != nil)...) + } + + { // field ateletpb.ExternalVolumeSource.VolumeContext + fn := func( + fldPath *field.Path, + obj, oldObj map[string]string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalMap(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_ExternalVolumeSource_VolumeContext(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.ExternalVolumeSource) map[string]string { + return oldObj.VolumeContext + }) + errs = append(errs, fn(fldPath.Child("volume_context"), obj.VolumeContext, oldVal, oldObj != nil)...) + } + + { // field ateletpb.ExternalVolumeSource.PublishContext + fn := func( + fldPath *field.Path, + obj, oldObj map[string]string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalMap(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_ExternalVolumeSource_PublishContext(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.ExternalVolumeSource) map[string]string { + return oldObj.PublishContext + }) + errs = append(errs, fn(fldPath.Child("publish_context"), obj.PublishContext, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_HttpGetAction validates an instance of HttpGetAction according +// to declarative validation rules in the API schema. +func Validate_HttpGetAction( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.HttpGetAction) (errs field.ErrorList) { + + { // field ateletpb.HttpGetAction.Path + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_HttpGetAction_Path(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 1024); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.HttpGetAction) *string { + return &oldObj.Path + }) + errs = append(errs, fn(fldPath.Child("path"), &obj.Path, oldVal, oldObj != nil)...) + } + + { // field ateletpb.HttpGetAction.Port + fn := func( + fldPath *field.Path, + obj, oldObj *int32, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 65535); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.HttpGetAction) *int32 { + return &oldObj.Port + }) + errs = append(errs, fn(fldPath.Child("port"), &obj.Port, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_ImageVolumeSource validates an instance of ImageVolumeSource according +// to declarative validation rules in the API schema. +func Validate_ImageVolumeSource( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.ImageVolumeSource) (errs field.ErrorList) { + + { // field ateletpb.ImageVolumeSource.Reference + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_ImageVolumeSource_Reference(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 512); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.ImageVolumeSource) *string { + return &oldObj.Reference + }) + errs = append(errs, fn(fldPath.Child("reference"), &obj.Reference, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_Limits validates an instance of Limits according +// to declarative validation rules in the API schema. +func Validate_Limits( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.Limits) (errs field.ErrorList) { + + // custom validation + if e := ValidateCustom_Limits(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + + { // field ateletpb.Limits.Name + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 16); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Limits) *string { + return &oldObj.Name + }) + errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Limits.Quantity + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 32); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Limits) *string { + return &oldObj.Quantity + }) + errs = append(errs, fn(fldPath.Child("quantity"), &obj.Quantity, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_LocalCheckpointConfiguration validates an instance of LocalCheckpointConfiguration according +// to declarative validation rules in the API schema. +func Validate_LocalCheckpointConfiguration( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.LocalCheckpointConfiguration) (errs field.ErrorList) { + + { // field ateletpb.LocalCheckpointConfiguration.SnapshotName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.LocalCheckpointConfiguration) *string { + return &oldObj.SnapshotName + }) + errs = append(errs, fn(fldPath.Child("snapshot_name"), &obj.SnapshotName, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_MintActorCertificateRequest validates an instance of MintActorCertificateRequest according +// to declarative validation rules in the API schema. +func Validate_MintActorCertificateRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.MintActorCertificateRequest) (errs field.ErrorList) { + + { // field ateletpb.MintActorCertificateRequest.ActorAtespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.MintActorCertificateRequest) *string { + return &oldObj.ActorAtespace + }) + errs = append(errs, fn(fldPath.Child("actor_atespace"), &obj.ActorAtespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.MintActorCertificateRequest.ActorName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.MintActorCertificateRequest) *string { + return &oldObj.ActorName + }) + errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.MintActorCertificateRequest.ActorUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.MintActorCertificateRequest) *string { + return &oldObj.ActorUid + }) + errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) + } + + { // field ateletpb.MintActorCertificateRequest.CertificateSigningRequest + fn := func( + fldPath *field.Path, + obj, oldObj []byte, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.MaxBytesSlice(ctx, op, fldPath, obj, oldObj, 16384); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.MintActorCertificateRequest) []byte { + return oldObj.CertificateSigningRequest + }) + errs = append(errs, fn(fldPath.Child("certificate_signing_request"), obj.CertificateSigningRequest, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_RegisterWorkerRequest validates an instance of RegisterWorkerRequest according +// to declarative validation rules in the API schema. +func Validate_RegisterWorkerRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.RegisterWorkerRequest) (errs field.ErrorList) { + + { // field ateletpb.RegisterWorkerRequest.Capacity + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.WorkerResources, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_WorkerResources(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RegisterWorkerRequest) *ateletpb.WorkerResources { + return oldObj.Capacity + }) + errs = append(errs, fn(fldPath.Child("capacity"), obj.Capacity, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RegisterWorkerRequest.DefaultRuntime + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.SandboxRuntime, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_SandboxRuntime(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RegisterWorkerRequest) *ateletpb.SandboxRuntime { + return oldObj.DefaultRuntime + }) + errs = append(errs, fn(fldPath.Child("default_runtime"), obj.DefaultRuntime, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RegisterWorkerRequest.RestorableRuntimes + fn := func( + fldPath *field.Path, + obj, oldObj []*ateletpb.SandboxRuntime, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.PtrSliceNoNils[ateletpb.SandboxRuntime](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 32).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_SandboxRuntime); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RegisterWorkerRequest) []*ateletpb.SandboxRuntime { + return oldObj.RestorableRuntimes + }) + errs = append(errs, fn(fldPath.Child("restorable_runtimes"), obj.RestorableRuntimes, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_RequestActorSuspendRequest validates an instance of RequestActorSuspendRequest according +// to declarative validation rules in the API schema. +func Validate_RequestActorSuspendRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.RequestActorSuspendRequest) (errs field.ErrorList) { + + { // field ateletpb.RequestActorSuspendRequest.ActorAtespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RequestActorSuspendRequest) *string { + return &oldObj.ActorAtespace + }) + errs = append(errs, fn(fldPath.Child("actor_atespace"), &obj.ActorAtespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RequestActorSuspendRequest.ActorName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RequestActorSuspendRequest) *string { + return &oldObj.ActorName + }) + errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RequestActorSuspendRequest.ActorUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RequestActorSuspendRequest) *string { + return &oldObj.ActorUid + }) + errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_ResourceLimits validates an instance of ResourceLimits according +// to declarative validation rules in the API schema. +func Validate_ResourceLimits( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.ResourceLimits) (errs field.ErrorList) { + + { // field ateletpb.ResourceLimits.MemoryBytes + fn := func( + fldPath *field.Path, + obj, oldObj *int64, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 0); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.ResourceLimits) *int64 { + return &oldObj.MemoryBytes + }) + errs = append(errs, fn(fldPath.Child("memory_bytes"), &obj.MemoryBytes, oldVal, oldObj != nil)...) + } + + { // field ateletpb.ResourceLimits.CpuMillis + fn := func( + fldPath *field.Path, + obj, oldObj *int64, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 999999); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 0); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.ResourceLimits) *int64 { + return &oldObj.CpuMillis + }) + errs = append(errs, fn(fldPath.Child("cpu_millis"), &obj.CpuMillis, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_Resources validates an instance of Resources according +// to declarative validation rules in the API schema. +func Validate_Resources( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.Resources) (errs field.ErrorList) { + + { // field ateletpb.Resources.Limits + fn := func( + fldPath *field.Path, + obj, oldObj []*ateletpb.Limits, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.PtrSliceNoNils[ateletpb.Limits](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 2).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // lists with map semantics require unique keys + if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.Limits, b *ateletpb.Limits) bool { return a.Name == b.Name }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.Limits, b *ateletpb.Limits) bool { return a.Name == b.Name }, ateDeepEqual, Validate_Limits); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Resources) []*ateletpb.Limits { + return oldObj.Limits + }) + errs = append(errs, fn(fldPath.Child("limits"), obj.Limits, oldVal, oldObj != nil)...) + } + + return errs +} + +var unionMembershipFor_github_com_agent_substrate_substrate_internal_proto_ateletpb_RestoreRequest_ = validate.NewUnionMembership(validate.NewUnionMember("local_config"), validate.NewUnionMember("external_config")) + +// Validate_RestoreRequest validates an instance of RestoreRequest according +// to declarative validation rules in the API schema. +func Validate_RestoreRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.RestoreRequest) (errs field.ErrorList) { + + if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_github_com_agent_substrate_substrate_internal_proto_ateletpb_RestoreRequest_, + func(obj *ateletpb.RestoreRequest) bool { + if obj == nil { + return false + } + return obj.LocalConfig != nil + }, + func(obj *ateletpb.RestoreRequest) bool { + if obj == nil { + return false + } + return obj.ExternalConfig != nil + }); len(e) != 0 { + errs = append(errs, e...) + } + // custom validation + if e := ValidateCustom_RestoreRequest(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + + { // field ateletpb.RestoreRequest.WorkerPodUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *string { + return &oldObj.WorkerPodUid + }) + errs = append(errs, fn(fldPath.Child("worker_pod_uid"), &obj.WorkerPodUid, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.Atespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *string { + return &oldObj.Atespace + }) + errs = append(errs, fn(fldPath.Child("atespace"), &obj.Atespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.ActorName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *string { + return &oldObj.ActorName + }) + errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.ActorUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *string { + return &oldObj.ActorUid + }) + errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.ActorTemplateAtespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *string { + return &oldObj.ActorTemplateAtespace + }) + errs = append(errs, fn(fldPath.Child("actor_template_atespace"), &obj.ActorTemplateAtespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.ActorTemplateName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *string { + return &oldObj.ActorTemplateName + }) + errs = append(errs, fn(fldPath.Child("actor_template_name"), &obj.ActorTemplateName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.Spec + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.WorkloadSpec, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_WorkloadSpec(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *ateletpb.WorkloadSpec { + return oldObj.Spec + }) + errs = append(errs, fn(fldPath.Child("spec"), obj.Spec, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.Type + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.CheckpointType, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 2); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *ateletpb.CheckpointType { + return &oldObj.Type + }) + errs = append(errs, fn(fldPath.Child("type"), &obj.Type, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.LocalConfig + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.LocalCheckpointConfiguration, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_LocalCheckpointConfiguration(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *ateletpb.LocalCheckpointConfiguration { + return oldObj.LocalConfig + }) + errs = append(errs, fn(fldPath.Child("local_config"), obj.LocalConfig, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.ExternalConfig + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.ExternalRestoreConfiguration, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_ExternalRestoreConfiguration(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *ateletpb.ExternalRestoreConfiguration { + return oldObj.ExternalConfig + }) + errs = append(errs, fn(fldPath.Child("external_config"), obj.ExternalConfig, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.Fidelity + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.SnapshotFidelity, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 3); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *ateletpb.SnapshotFidelity { + return &oldObj.Fidelity + }) + errs = append(errs, fn(fldPath.Child("fidelity"), &obj.Fidelity, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.EgressGateway + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.EgressGateway, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_EgressGateway(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *ateletpb.EgressGateway { + return oldObj.EgressGateway + }) + errs = append(errs, fn(fldPath.Child("egress_gateway"), obj.EgressGateway, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.CpuMilli + fn := func( + fldPath *field.Path, + obj, oldObj *int64, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 999999); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 0); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *int64 { + return &oldObj.CpuMilli + }) + errs = append(errs, fn(fldPath.Child("cpu_milli"), &obj.CpuMilli, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.MemoryBytes + fn := func( + fldPath *field.Path, + obj, oldObj *int64, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 0); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *int64 { + return &oldObj.MemoryBytes + }) + errs = append(errs, fn(fldPath.Child("memory_bytes"), &obj.MemoryBytes, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.SandboxAssets + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.SandboxAssets, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_SandboxAssets(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *ateletpb.SandboxAssets { + return oldObj.SandboxAssets + }) + errs = append(errs, fn(fldPath.Child("sandbox_assets"), obj.SandboxAssets, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_RunRequest validates an instance of RunRequest according +// to declarative validation rules in the API schema. +func Validate_RunRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.RunRequest) (errs field.ErrorList) { + + { // field ateletpb.RunRequest.WorkerPodUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *string { + return &oldObj.WorkerPodUid + }) + errs = append(errs, fn(fldPath.Child("worker_pod_uid"), &obj.WorkerPodUid, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.Atespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *string { + return &oldObj.Atespace + }) + errs = append(errs, fn(fldPath.Child("atespace"), &obj.Atespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.ActorName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *string { + return &oldObj.ActorName + }) + errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.ActorUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *string { + return &oldObj.ActorUid + }) + errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.ActorTemplateAtespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *string { + return &oldObj.ActorTemplateAtespace + }) + errs = append(errs, fn(fldPath.Child("actor_template_atespace"), &obj.ActorTemplateAtespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.ActorTemplateName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *string { + return &oldObj.ActorTemplateName + }) + errs = append(errs, fn(fldPath.Child("actor_template_name"), &obj.ActorTemplateName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.Spec + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.WorkloadSpec, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_WorkloadSpec(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *ateletpb.WorkloadSpec { + return oldObj.Spec + }) + errs = append(errs, fn(fldPath.Child("spec"), obj.Spec, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.SandboxAssets + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.SandboxAssets, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_SandboxAssets(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *ateletpb.SandboxAssets { + return oldObj.SandboxAssets + }) + errs = append(errs, fn(fldPath.Child("sandbox_assets"), obj.SandboxAssets, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.EgressGateway + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.EgressGateway, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_EgressGateway(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *ateletpb.EgressGateway { + return oldObj.EgressGateway + }) + errs = append(errs, fn(fldPath.Child("egress_gateway"), obj.EgressGateway, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.CpuMilli + fn := func( + fldPath *field.Path, + obj, oldObj *int64, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 999999); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 0); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *int64 { + return &oldObj.CpuMilli + }) + errs = append(errs, fn(fldPath.Child("cpu_milli"), &obj.CpuMilli, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.MemoryBytes + fn := func( + fldPath *field.Path, + obj, oldObj *int64, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 0); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *int64 { + return &oldObj.MemoryBytes + }) + errs = append(errs, fn(fldPath.Child("memory_bytes"), &obj.MemoryBytes, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_SandboxAssets validates an instance of SandboxAssets according +// to declarative validation rules in the API schema. +func Validate_SandboxAssets( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.SandboxAssets) (errs field.ErrorList) { + + { // field ateletpb.SandboxAssets.SandboxClass + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_SandboxAssets_SandboxClass(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 63); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.SandboxAssets) *string { + return &oldObj.SandboxClass + }) + errs = append(errs, fn(fldPath.Child("sandbox_class"), &obj.SandboxClass, oldVal, oldObj != nil)...) + } + + { // field ateletpb.SandboxAssets.Assets + fn := func( + fldPath *field.Path, + obj, oldObj map[string]*ateletpb.ArchAssets, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.PtrMapNoNils[string, ateletpb.ArchAssets](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxProperties(ctx, op, fldPath, obj, oldObj, 8).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.RequiredMap(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.EachMapKey(ctx, op, fldPath, obj, oldObj, + func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 16) + }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the map and call the value type's validation function + if e := validate.EachPtrMapVal(ctx, op, fldPath, obj, oldObj, ateDeepEqual, Validate_ArchAssets); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.SandboxAssets) map[string]*ateletpb.ArchAssets { + return oldObj.Assets + }) + errs = append(errs, fn(fldPath.Child("assets"), obj.Assets, oldVal, oldObj != nil)...) + } + + { // field ateletpb.SandboxAssets.PauseImage + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_SandboxAssets_PauseImage(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 512); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.SandboxAssets) *string { + return &oldObj.PauseImage + }) + errs = append(errs, fn(fldPath.Child("pause_image"), &obj.PauseImage, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_SandboxRuntime validates an instance of SandboxRuntime according +// to declarative validation rules in the API schema. +func Validate_SandboxRuntime( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.SandboxRuntime) (errs field.ErrorList) { + + { // field ateletpb.SandboxRuntime.SandboxClass + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 63); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.SandboxRuntime) *string { + return &oldObj.SandboxClass + }) + errs = append(errs, fn(fldPath.Child("sandbox_class"), &obj.SandboxClass, oldVal, oldObj != nil)...) + } + + { // field ateletpb.SandboxRuntime.Version + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.VersionedSandboxCompat, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_VersionedSandboxCompat(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.SandboxRuntime) *ateletpb.VersionedSandboxCompat { + return oldObj.Version + }) + errs = append(errs, fn(fldPath.Child("version"), obj.Version, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_SecurityContext validates an instance of SecurityContext according +// to declarative validation rules in the API schema. +func Validate_SecurityContext( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.SecurityContext) (errs field.ErrorList) { + + { // field ateletpb.SecurityContext.Capabilities + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.Capabilities, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_Capabilities(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.SecurityContext) *ateletpb.Capabilities { + return oldObj.Capabilities + }) + errs = append(errs, fn(fldPath.Child("capabilities"), obj.Capabilities, oldVal, oldObj != nil)...) + } + + return errs +} + +var unionMembershipFor_github_com_agent_substrate_substrate_internal_proto_ateletpb_SystemInfoDataSource_ = validate.NewUnionMembership(validate.NewUnionMember("actor_metadata"), validate.NewUnionMember("trust_bundle")) + +// Validate_SystemInfoDataSource validates an instance of SystemInfoDataSource according +// to declarative validation rules in the API schema. +func Validate_SystemInfoDataSource( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.SystemInfoDataSource) (errs field.ErrorList) { + + if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_github_com_agent_substrate_substrate_internal_proto_ateletpb_SystemInfoDataSource_, + func(obj *ateletpb.SystemInfoDataSource) bool { + if obj == nil { + return false + } + return obj.ActorMetadata != nil + }, + func(obj *ateletpb.SystemInfoDataSource) bool { + if obj == nil { + return false + } + return obj.TrustBundle != nil + }); len(e) != 0 { + errs = append(errs, e...) + } + + { // field ateletpb.SystemInfoDataSource.ActorMetadata + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.ActorMetadataDataSource, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_ActorMetadataDataSource(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.SystemInfoDataSource) *ateletpb.ActorMetadataDataSource { + return oldObj.ActorMetadata + }) + errs = append(errs, fn(fldPath.Child("actor_metadata"), obj.ActorMetadata, oldVal, oldObj != nil)...) + } + + { // field ateletpb.SystemInfoDataSource.TrustBundle + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.TrustBundleDataSource, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_TrustBundleDataSource(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.SystemInfoDataSource) *ateletpb.TrustBundleDataSource { + return oldObj.TrustBundle + }) + errs = append(errs, fn(fldPath.Child("trust_bundle"), obj.TrustBundle, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_SystemInfoVolume validates an instance of SystemInfoVolume according +// to declarative validation rules in the API schema. +func Validate_SystemInfoVolume( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.SystemInfoVolume) (errs field.ErrorList) { + + { // field ateletpb.SystemInfoVolume.DataSources + fn := func( + fldPath *field.Path, + obj, oldObj []*ateletpb.SystemInfoDataSource, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.PtrSliceNoNils[ateletpb.SystemInfoDataSource](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 8).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_SystemInfoVolume_DataSources(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_SystemInfoDataSource); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.SystemInfoVolume) []*ateletpb.SystemInfoDataSource { + return oldObj.DataSources + }) + errs = append(errs, fn(fldPath.Child("data_sources"), obj.DataSources, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_TerminateRequest validates an instance of TerminateRequest according +// to declarative validation rules in the API schema. +func Validate_TerminateRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.TerminateRequest) (errs field.ErrorList) { + + { // field ateletpb.TerminateRequest.WorkerPodUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.TerminateRequest) *string { + return &oldObj.WorkerPodUid + }) + errs = append(errs, fn(fldPath.Child("worker_pod_uid"), &obj.WorkerPodUid, oldVal, oldObj != nil)...) + } + + { // field ateletpb.TerminateRequest.Atespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.TerminateRequest) *string { + return &oldObj.Atespace + }) + errs = append(errs, fn(fldPath.Child("atespace"), &obj.Atespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.TerminateRequest.ActorName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.TerminateRequest) *string { + return &oldObj.ActorName + }) + errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.TerminateRequest.ActorUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.TerminateRequest) *string { + return &oldObj.ActorUid + }) + errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) + } + + { // field ateletpb.TerminateRequest.ActorTemplateAtespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.TerminateRequest) *string { + return &oldObj.ActorTemplateAtespace + }) + errs = append(errs, fn(fldPath.Child("actor_template_atespace"), &obj.ActorTemplateAtespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.TerminateRequest.ActorTemplateName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.TerminateRequest) *string { + return &oldObj.ActorTemplateName + }) + errs = append(errs, fn(fldPath.Child("actor_template_name"), &obj.ActorTemplateName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.TerminateRequest.Spec + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.WorkloadSpec, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_WorkloadSpec(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.TerminateRequest) *ateletpb.WorkloadSpec { + return oldObj.Spec + }) + errs = append(errs, fn(fldPath.Child("spec"), obj.Spec, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_TrustBundleDataSource validates an instance of TrustBundleDataSource according +// to declarative validation rules in the API schema. +func Validate_TrustBundleDataSource( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.TrustBundleDataSource) (errs field.ErrorList) { + + { // field ateletpb.TrustBundleDataSource.Path + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_TrustBundleDataSource_Path(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 255); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MinLength(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.TrustBundleDataSource) *string { + return &oldObj.Path + }) + errs = append(errs, fn(fldPath.Child("path"), &obj.Path, oldVal, oldObj != nil)...) + } + + { // field ateletpb.TrustBundleDataSource.Names + fn := func( + fldPath *field.Path, + obj, oldObj []string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 8).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.MinItems(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.EachValSliceVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, nil, + func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MinLength(ctx, op, fldPath, obj, oldObj, 1) + }); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.EachValSliceVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, nil, + func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 253) + }); len(e) != 0 { + errs = append(errs, e...) + } + // lists with set semantics require unique values + if e := validate.ValSliceUnique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.TrustBundleDataSource) []string { + return oldObj.Names + }) + errs = append(errs, fn(fldPath.Child("names"), obj.Names, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_UploadPausedCheckpointRequest validates an instance of UploadPausedCheckpointRequest according +// to declarative validation rules in the API schema. +func Validate_UploadPausedCheckpointRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.UploadPausedCheckpointRequest) (errs field.ErrorList) { + + { // field ateletpb.UploadPausedCheckpointRequest.Atespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_UploadPausedCheckpointRequest_Atespace(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.UploadPausedCheckpointRequest) *string { + return &oldObj.Atespace + }) + errs = append(errs, fn(fldPath.Child("atespace"), &obj.Atespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.UploadPausedCheckpointRequest.ActorName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.UploadPausedCheckpointRequest) *string { + return &oldObj.ActorName + }) + errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.UploadPausedCheckpointRequest.ActorUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.UploadPausedCheckpointRequest) *string { + return &oldObj.ActorUid + }) + errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) + } + + { // field ateletpb.UploadPausedCheckpointRequest.ActorTemplateAtespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.UploadPausedCheckpointRequest) *string { + return &oldObj.ActorTemplateAtespace + }) + errs = append(errs, fn(fldPath.Child("actor_template_atespace"), &obj.ActorTemplateAtespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.UploadPausedCheckpointRequest.ActorTemplateName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.UploadPausedCheckpointRequest) *string { + return &oldObj.ActorTemplateName + }) + errs = append(errs, fn(fldPath.Child("actor_template_name"), &obj.ActorTemplateName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.UploadPausedCheckpointRequest.LocalSnapshotName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.UploadPausedCheckpointRequest) *string { + return &oldObj.LocalSnapshotName + }) + errs = append(errs, fn(fldPath.Child("local_snapshot_name"), &obj.LocalSnapshotName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.UploadPausedCheckpointRequest.DestinationSnapshotUri + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_UploadPausedCheckpointRequest_DestinationSnapshotUri(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 2048); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.UploadPausedCheckpointRequest) *string { + return &oldObj.DestinationSnapshotUri + }) + errs = append(errs, fn(fldPath.Child("destination_snapshot_uri"), &obj.DestinationSnapshotUri, oldVal, oldObj != nil)...) + } + + { // field ateletpb.UploadPausedCheckpointRequest.DesiredFidelity + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.SnapshotFidelity, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 3); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.UploadPausedCheckpointRequest) *ateletpb.SnapshotFidelity { + return &oldObj.DesiredFidelity + }) + errs = append(errs, fn(fldPath.Child("desired_fidelity"), &obj.DesiredFidelity, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_VersionedSandboxCompat validates an instance of VersionedSandboxCompat according +// to declarative validation rules in the API schema. +func Validate_VersionedSandboxCompat( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.VersionedSandboxCompat) (errs field.ErrorList) { + + { // field ateletpb.VersionedSandboxCompat.SchemaVersion + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 64); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.VersionedSandboxCompat) *string { + return &oldObj.SchemaVersion + }) + errs = append(errs, fn(fldPath.Child("schema_version"), &obj.SchemaVersion, oldVal, oldObj != nil)...) + } + + { // field ateletpb.VersionedSandboxCompat.Attributes + fn := func( + fldPath *field.Path, + obj, oldObj []*ateletpb.AttributeEntry, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.PtrSliceNoNils[ateletpb.AttributeEntry](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 32).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // lists with map semantics require unique keys + if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.AttributeEntry, b *ateletpb.AttributeEntry) bool { return a.Key == b.Key }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.AttributeEntry, b *ateletpb.AttributeEntry) bool { return a.Key == b.Key }, ateDeepEqual, Validate_AttributeEntry); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.VersionedSandboxCompat) []*ateletpb.AttributeEntry { + return oldObj.Attributes + }) + errs = append(errs, fn(fldPath.Child("attributes"), obj.Attributes, oldVal, oldObj != nil)...) + } + + return errs +} + +var unionMembershipFor_github_com_agent_substrate_substrate_internal_proto_ateletpb_Volume_ = validate.NewUnionMembership(validate.NewUnionMember("durable_dir"), validate.NewUnionMember("external"), validate.NewUnionMember("system_info"), validate.NewUnionMember("image")) + +// Validate_Volume validates an instance of Volume according +// to declarative validation rules in the API schema. +func Validate_Volume( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.Volume) (errs field.ErrorList) { + + if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_github_com_agent_substrate_substrate_internal_proto_ateletpb_Volume_, + func(obj *ateletpb.Volume) bool { + if obj == nil { + return false + } + return obj.DurableDir != nil + }, + func(obj *ateletpb.Volume) bool { + if obj == nil { + return false + } + return obj.External != nil + }, + func(obj *ateletpb.Volume) bool { + if obj == nil { + return false + } + return obj.SystemInfo != nil + }, + func(obj *ateletpb.Volume) bool { + if obj == nil { + return false + } + return obj.Image != nil + }); len(e) != 0 { + errs = append(errs, e...) + } + + { // field ateletpb.Volume.Name + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.Resources) []*ateletpb.Limits { - return oldObj.Limits + func(oldObj *ateletpb.Volume) *string { + return &oldObj.Name }) - errs = append(errs, fn(fldPath.Child("limits"), obj.Limits, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) } - return errs -} - -// Validate_SandboxRuntime validates an instance of SandboxRuntime according -// to declarative validation rules in the API schema. -func Validate_SandboxRuntime( - ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateletpb.SandboxRuntime) (errs field.ErrorList) { + { // field ateletpb.Volume.DurableDir + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.DurableDirVolume, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Volume) *ateletpb.DurableDirVolume { + return oldObj.DurableDir + }) + errs = append(errs, fn(fldPath.Child("durable_dir"), obj.DurableDir, oldVal, oldObj != nil)...) + } - { // field ateletpb.SandboxRuntime.SandboxClass + { // field ateletpb.Volume.External fn := func( fldPath *field.Path, - obj, oldObj *string, + obj, oldObj *ateletpb.ExternalVolumeSource, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { - if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + if ateDeepEqual(obj, oldObj) { return nil } } // call field-attached validations earlyReturn := false - if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { earlyReturn = true } if earlyReturn { return // do not proceed } - if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 63); len(e) != 0 { - errs = append(errs, e...) + // call the type's validation function + errs = append(errs, Validate_ExternalVolumeSource(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Volume) *ateletpb.ExternalVolumeSource { + return oldObj.External + }) + errs = append(errs, fn(fldPath.Child("external"), obj.External, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Volume.SystemInfo + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.SystemInfoVolume, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed } + // call the type's validation function + errs = append(errs, Validate_SystemInfoVolume(ctx, op, fldPath, obj, oldObj)...) return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.SandboxRuntime) *string { - return &oldObj.SandboxClass + func(oldObj *ateletpb.Volume) *ateletpb.SystemInfoVolume { + return oldObj.SystemInfo }) - errs = append(errs, fn(fldPath.Child("sandbox_class"), &obj.SandboxClass, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("system_info"), obj.SystemInfo, oldVal, oldObj != nil)...) } - { // field ateletpb.SandboxRuntime.Version + { // field ateletpb.Volume.Image fn := func( fldPath *field.Path, - obj, oldObj *ateletpb.VersionedSandboxCompat, + obj, oldObj *ateletpb.ImageVolumeSource, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -639,34 +4369,33 @@ func Validate_SandboxRuntime( } // call field-attached validations earlyReturn := false - if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { earlyReturn = true } if earlyReturn { return // do not proceed } // call the type's validation function - errs = append(errs, Validate_VersionedSandboxCompat(ctx, op, fldPath, obj, oldObj)...) + errs = append(errs, Validate_ImageVolumeSource(ctx, op, fldPath, obj, oldObj)...) return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.SandboxRuntime) *ateletpb.VersionedSandboxCompat { - return oldObj.Version + func(oldObj *ateletpb.Volume) *ateletpb.ImageVolumeSource { + return oldObj.Image }) - errs = append(errs, fn(fldPath.Child("version"), obj.Version, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("image"), obj.Image, oldVal, oldObj != nil)...) } return errs } -// Validate_VersionedSandboxCompat validates an instance of VersionedSandboxCompat according +// Validate_VolumeMount validates an instance of VolumeMount according // to declarative validation rules in the API schema. -func Validate_VersionedSandboxCompat( +func Validate_VolumeMount( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateletpb.VersionedSandboxCompat) (errs field.ErrorList) { + obj, oldObj *ateletpb.VolumeMount) (errs field.ErrorList) { - { // field ateletpb.VersionedSandboxCompat.SchemaVersion + { // field ateletpb.VolumeMount.Name fn := func( fldPath *field.Path, obj, oldObj *string, @@ -686,63 +4415,127 @@ func Validate_VersionedSandboxCompat( if earlyReturn { return // do not proceed } - if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 64); len(e) != 0 { + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.VersionedSandboxCompat) *string { - return &oldObj.SchemaVersion + func(oldObj *ateletpb.VolumeMount) *string { + return &oldObj.Name }) - errs = append(errs, fn(fldPath.Child("schema_version"), &obj.SchemaVersion, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) } - { // field ateletpb.VersionedSandboxCompat.Attributes + { // field ateletpb.VolumeMount.MountPath fn := func( fldPath *field.Path, - obj, oldObj []*ateletpb.AttributeEntry, + obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { - if ateDeepEqual(obj, oldObj) { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { return nil } } // call field-attached validations earlyReturn := false - if e := validate.PtrSliceNoNils[ateletpb.AttributeEntry](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } - if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 32).MarkShortCircuit(); len(e) != 0 { + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_VolumeMount_MountPath(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 4096); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.VolumeMount) *string { + return &oldObj.MountPath + }) + errs = append(errs, fn(fldPath.Child("mount_path"), &obj.MountPath, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_WakeupProbe validates an instance of WakeupProbe according +// to declarative validation rules in the API schema. +func Validate_WakeupProbe( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.WakeupProbe) (errs field.ErrorList) { + + { // field ateletpb.WakeupProbe.HttpGet + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.HttpGetAction, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } - if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_HttpGetAction(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.WakeupProbe) *ateletpb.HttpGetAction { + return oldObj.HttpGet + }) + errs = append(errs, fn(fldPath.Child("http_get"), obj.HttpGet, oldVal, oldObj != nil)...) + } + + { // field ateletpb.WakeupProbe.TimeoutSeconds + fn := func( + fldPath *field.Path, + obj, oldObj *int32, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } if earlyReturn { return // do not proceed } - // lists with map semantics require unique keys - if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, - func(a *ateletpb.AttributeEntry, b *ateletpb.AttributeEntry) bool { return a.Key == b.Key }); len(e) != 0 { + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 3600); len(e) != 0 { errs = append(errs, e...) } - // iterate the list and call the type's validation function - if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, - func(a *ateletpb.AttributeEntry, b *ateletpb.AttributeEntry) bool { return a.Key == b.Key }, ateDeepEqual, Validate_AttributeEntry); len(e) != 0 { + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.VersionedSandboxCompat) []*ateletpb.AttributeEntry { - return oldObj.Attributes + func(oldObj *ateletpb.WakeupProbe) *int32 { + return &oldObj.TimeoutSeconds }) - errs = append(errs, fn(fldPath.Child("attributes"), obj.Attributes, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("timeout_seconds"), &obj.TimeoutSeconds, oldVal, oldObj != nil)...) } return errs @@ -817,3 +4610,109 @@ func Validate_WorkerResources( return errs } + +// Validate_WorkloadSpec validates an instance of WorkloadSpec according +// to declarative validation rules in the API schema. +func Validate_WorkloadSpec( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.WorkloadSpec) (errs field.ErrorList) { + + // custom validation + if e := ValidateCustom_WorkloadSpec(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + + { // field ateletpb.WorkloadSpec.Containers + fn := func( + fldPath *field.Path, + obj, oldObj []*ateletpb.Container, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.PtrSliceNoNils[ateletpb.Container](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 10).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // lists with map semantics require unique keys + if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.Container, b *ateletpb.Container) bool { return a.Name == b.Name }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.Container, b *ateletpb.Container) bool { return a.Name == b.Name }, ateDeepEqual, Validate_Container); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.WorkloadSpec) []*ateletpb.Container { + return oldObj.Containers + }) + errs = append(errs, fn(fldPath.Child("containers"), obj.Containers, oldVal, oldObj != nil)...) + } + + { // field ateletpb.WorkloadSpec.Volumes + fn := func( + fldPath *field.Path, + obj, oldObj []*ateletpb.Volume, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.PtrSliceNoNils[ateletpb.Volume](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 32).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // lists with map semantics require unique keys + if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.Volume, b *ateletpb.Volume) bool { return a.Name == b.Name }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.Volume, b *ateletpb.Volume) bool { return a.Name == b.Name }, ateDeepEqual, Validate_Volume); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.WorkloadSpec) []*ateletpb.Volume { + return oldObj.Volumes + }) + errs = append(errs, fn(fldPath.Child("volumes"), obj.Volumes, oldVal, oldObj != nil)...) + } + + return errs +} diff --git a/cmd/atelet/lifecycle_test.go b/cmd/atelet/lifecycle_test.go index c592bfb734..06fdb87a9a 100644 --- a/cmd/atelet/lifecycle_test.go +++ b/cmd/atelet/lifecycle_test.go @@ -172,8 +172,8 @@ func runLocalSnapshotGC(t *testing.T, fidelity ateletpb.SnapshotFidelity) { const ( atespace = "ate-demo" actorName = "counter" - actorUID = "actor-uid-1" - workerPodUID = "worker-pod-uid-1" + actorUID = "01234567-89ab-cdef-0123-456789abcdef" + workerPodUID = "5f1c0d2e-3a4b-4c5d-8e9f-0a1b2c3d4e5f" snapshotName = "pause-snap-1" ) @@ -182,7 +182,7 @@ func runLocalSnapshotGC(t *testing.T, fidelity ateletpb.SnapshotFidelity) { host := imageVolumeTestRegistry(t) image := host + "/actor:v1" - pushTestImage(t, image, singleFileLayer(t, "bin/app", "app")) + pinnedImage := pushTestImage(t, image, singleFileLayer(t, "bin/app", "app")) // A single "runsc" asset served from a fake bucket: enough to exercise the // content-addressed asset fetch without a gVisor release tarball. @@ -195,7 +195,7 @@ func runLocalSnapshotGC(t *testing.T, fidelity ateletpb.SnapshotFidelity) { } sandboxAssets := &ateletpb.SandboxAssets{ SandboxClass: "gvisor", - PauseImage: image, + PauseImage: pinnedImage, Assets: map[string]*ateletpb.ArchAssets{ runtime.GOARCH: {Files: map[string]*ateletpb.AssetFile{ runscAssetName: { @@ -206,7 +206,7 @@ func runLocalSnapshotGC(t *testing.T, fidelity ateletpb.SnapshotFidelity) { }, } spec := &ateletpb.WorkloadSpec{ - Containers: []*ateletpb.Container{{Name: "app", Image: image, Command: []string{"/bin/app"}}}, + Containers: []*ateletpb.Container{{Name: "app", Image: pinnedImage, Command: []string{"/bin/app"}}}, } if _, err := s.Run(ctx, &ateletpb.RunRequest{ @@ -233,9 +233,7 @@ func runLocalSnapshotGC(t *testing.T, fidelity ateletpb.SnapshotFidelity) { Spec: spec, Fidelity: fidelity, Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL, - Config: &ateletpb.CheckpointRequest_LocalConfig{ - LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, - }, + LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, }); err != nil { t.Fatalf("Checkpoint: %v", err) } @@ -256,9 +254,7 @@ func runLocalSnapshotGC(t *testing.T, fidelity ateletpb.SnapshotFidelity) { Spec: spec, Fidelity: fidelity, Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL, - Config: &ateletpb.RestoreRequest_LocalConfig{ - LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, - }, + LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, }); err != nil { t.Fatalf("Restore: %v", err) } @@ -333,8 +329,8 @@ func TestRestoreUsesRequestSandboxAssets(t *testing.T) { const ( atespace = "ate-demo" actorName = "counter" - actorUID = "actor-uid-1" - workerPodUID = "worker-pod-uid-1" + actorUID = "01234567-89ab-cdef-0123-456789abcdef" + workerPodUID = "5f1c0d2e-3a4b-4c5d-8e9f-0a1b2c3d4e5f" snapshotName = "pause-snap-1" ) @@ -343,11 +339,9 @@ func TestRestoreUsesRequestSandboxAssets(t *testing.T) { host := imageVolumeTestRegistry(t) image := host + "/actor:v1" - pushTestImage(t, image, singleFileLayer(t, "bin/app", "app")) - checkpointPause := host + "/pause:v1" - pushTestImage(t, checkpointPause, singleFileLayer(t, "pause", "pause-v1")) - restorePause := host + "/pause:v2" - pushTestImage(t, restorePause, singleFileLayer(t, "pause", "pause-v2")) + pinnedImage := pushTestImage(t, image, singleFileLayer(t, "bin/app", "app")) + checkpointPause := pushTestImage(t, host+"/pause:v1", singleFileLayer(t, "pause", "pause-v1")) + restorePause := pushTestImage(t, host+"/pause:v2", singleFileLayer(t, "pause", "pause-v2")) runsc := []byte("runsc binary") s := &AteomHerder{ @@ -371,7 +365,7 @@ func TestRestoreUsesRequestSandboxAssets(t *testing.T) { } } spec := &ateletpb.WorkloadSpec{ - Containers: []*ateletpb.Container{{Name: "app", Image: image, Command: []string{"/bin/app"}}}, + Containers: []*ateletpb.Container{{Name: "app", Image: pinnedImage, Command: []string{"/bin/app"}}}, } if _, err := s.Run(ctx, &ateletpb.RunRequest{ @@ -397,9 +391,7 @@ func TestRestoreUsesRequestSandboxAssets(t *testing.T) { Spec: spec, Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL, - Config: &ateletpb.CheckpointRequest_LocalConfig{ - LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, - }, + LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, }); err != nil { t.Fatalf("Checkpoint: %v", err) } @@ -427,9 +419,7 @@ func TestRestoreUsesRequestSandboxAssets(t *testing.T) { Spec: spec, Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL, - Config: &ateletpb.RestoreRequest_LocalConfig{ - LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, - }, + LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, }); err != nil { t.Fatalf("Restore: %v", err) } @@ -463,14 +453,14 @@ func TestActivationFailureBeforeRegistration(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { useTempNodeDirs(t) - const actorUID, snapshotName = "actor-uid-1", "pause-snap-1" + const actorUID, snapshotName = "0f6c2b0e-1b7e-4b3f-9a6a-1d2c3e4f5a61", "pause-snap-1" ctx := t.Context() store := newCTBStore(t) certA := string(testCertPEM(t)) store.set(t, certA) refresher := newSystemInfoVolumeRefresher(trustbundle.NewSource(store.lister.Get, nil), nil) spec := &ateletpb.WorkloadSpec{Volumes: []*ateletpb.Volume{{ - Name: "trust", Source: &ateletpb.Volume_SystemInfo{SystemInfo: trustVolumeSpec("ca.pem")}, + Name: "trust", SystemInfo: trustVolumeSpec("ca.pem"), }}} registered, err := refresher.Register(actorUID, resources.ActorRef{Atespace: "team-a", Name: "actor-1"}, systemInfoVolumesFor(actorUID, spec)) if err != nil { @@ -519,14 +509,14 @@ func TestActivationFailureBeforeRegistration(t *testing.T) { } if tc.restore { _, err = s.Restore(ctx, &ateletpb.RestoreRequest{ - Atespace: "team-a", ActorName: "actor-1", ActorUid: actorUID, WorkerPodUid: "worker-pod-uid-1", + Atespace: "team-a", ActorName: "actor-1", ActorUid: actorUID, WorkerPodUid: "5f1c0d2e-3a4b-4c5d-8e9f-0a1b2c3d4e5f", SandboxAssets: assets, Spec: spec, Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL, - Config: &ateletpb.RestoreRequest_LocalConfig{LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}}, + LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, }) } else { _, err = s.Run(ctx, &ateletpb.RunRequest{ - Atespace: "team-a", ActorName: "actor-1", ActorUid: actorUID, WorkerPodUid: "worker-pod-uid-1", + Atespace: "team-a", ActorName: "actor-1", ActorUid: actorUID, WorkerPodUid: "5f1c0d2e-3a4b-4c5d-8e9f-0a1b2c3d4e5f", SandboxAssets: assets, Spec: spec, }) } @@ -577,6 +567,7 @@ func TestRunFailureAfterRegistrationRemovesOwnRegistration(t *testing.T) { refresher := newSystemInfoVolumeRefresher(trustbundle.NewSource(store.lister.Get, nil), nil) content := []byte("runsc binary") assetHash := fmt.Sprintf("%x", sha256.Sum256(content)) + const actorUID = "0f6c2b0e-1b7e-4b3f-9a6a-1d2c3e4f5a62" s := &AteomHerder{ anonGCSClient: fakeObjectStorage{data: content}, imageCache: newImageVolumeStore(t), @@ -584,18 +575,18 @@ func TestRunFailureAfterRegistrationRemovesOwnRegistration(t *testing.T) { } spec := &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{{ - Name: "trust", - Source: &ateletpb.Volume_SystemInfo{SystemInfo: trustVolumeSpec("ca.pem")}, + Name: "trust", + SystemInfo: trustVolumeSpec("ca.pem"), }}, } _, err := s.Run(t.Context(), &ateletpb.RunRequest{ Atespace: "team-a", ActorName: "actor-run", - ActorUid: "actor-uid-run", - WorkerPodUid: "worker-pod-uid-1", + ActorUid: actorUID, + WorkerPodUid: "5f1c0d2e-3a4b-4c5d-8e9f-0a1b2c3d4e5f", SandboxAssets: &ateletpb.SandboxAssets{ SandboxClass: "gvisor", - PauseImage: "://invalid-image", + PauseImage: missingPauseImage(t), Assets: map[string]*ateletpb.ArchAssets{runtime.GOARCH: {Files: map[string]*ateletpb.AssetFile{ runscAssetName: {Url: "gs://test-bucket/runsc", Sha256: assetHash}, }}}, @@ -605,20 +596,29 @@ func TestRunFailureAfterRegistrationRemovesOwnRegistration(t *testing.T) { if err == nil || !strings.Contains(err.Error(), "while creating pause OCI bundle") { t.Fatalf("Run error = %v, want the post-registration OCI preparation failure", err) } - if _, err := os.ReadFile(filepath.Join(ateletpath.SystemInfoVolumeRoot("actor-uid-run", "trust"), "ca.pem")); err != nil { + if _, err := os.ReadFile(filepath.Join(ateletpath.SystemInfoVolumeRoot(actorUID, "trust"), "ca.pem")); err != nil { t.Fatalf("Register did not write its projection before OCI preparation failed: %v", err) } - if got := refresher.actors["actor-uid-run"]; got != nil { + if got := refresher.actors[actorUID]; got != nil { t.Fatalf("failed Run left its registration live: %p", got) } } +// missingPauseImage returns a digest-pinned reference that passes request +// validation but names a manifest the local test registry does not serve, so +// the pull fails only once OCI bundle preparation starts. +func missingPauseImage(t *testing.T) string { + t.Helper() + return imageVolumeTestRegistry(t) + "/pause@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +} + func TestRestoreFailureAfterRegistrationRemovesOwnRegistration(t *testing.T) { useTempNodeDirs(t) const ( - actorUID = "actor-uid-restore-after" + actorUID = "0f6c2b0e-1b7e-4b3f-9a6a-1d2c3e4f5a63" snapshotName = "pause-snap-after" ) + pauseImage := missingPauseImage(t) store := newCTBStore(t) store.set(t, string(testCertPEM(t))) refresher := newSystemInfoVolumeRefresher(trustbundle.NewSource(store.lister.Get, nil), nil) @@ -626,14 +626,14 @@ func TestRestoreFailureAfterRegistrationRemovesOwnRegistration(t *testing.T) { assetHash := fmt.Sprintf("%x", sha256.Sum256(content)) writeLocalSnapshot(t, ateletpath.LocalSnapshotDir(actorUID, snapshotName), sandboxAssetsRecord{ SandboxClass: "gvisor", - PauseImage: "://invalid-image", + PauseImage: pauseImage, Assets: map[string]assetEntry{"runsc": {URL: "gs://test-bucket/runsc", SHA256: assetHash}}, SnapshotFiles: []string{"checkpoint.img"}, }, map[string]string{"checkpoint.img": "guest-memory"}) spec := &ateletpb.WorkloadSpec{Volumes: []*ateletpb.Volume{{ - Name: "trust", - Source: &ateletpb.Volume_SystemInfo{SystemInfo: trustVolumeSpec("ca.pem")}, + Name: "trust", + SystemInfo: trustVolumeSpec("ca.pem"), }}} s := &AteomHerder{ anonGCSClient: fakeObjectStorage{data: content}, @@ -644,20 +644,18 @@ func TestRestoreFailureAfterRegistrationRemovesOwnRegistration(t *testing.T) { Atespace: "team-a", ActorName: "actor-restore-after", ActorUid: actorUID, - WorkerPodUid: "worker-pod-uid-1", + WorkerPodUid: "5f1c0d2e-3a4b-4c5d-8e9f-0a1b2c3d4e5f", SandboxAssets: &ateletpb.SandboxAssets{ SandboxClass: "gvisor", - PauseImage: "://invalid-image", + PauseImage: pauseImage, Assets: map[string]*ateletpb.ArchAssets{runtime.GOARCH: {Files: map[string]*ateletpb.AssetFile{ runscAssetName: {Url: "gs://test-bucket/runsc", Sha256: assetHash}, }}}, }, - Spec: spec, - Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, - Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL, - Config: &ateletpb.RestoreRequest_LocalConfig{ - LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, - }, + Spec: spec, + Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, + Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL, + LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, }) if err == nil || !strings.Contains(err.Error(), "while creating pause OCI bundle") { t.Fatalf("Restore error = %v, want the post-registration OCI preparation failure", err) @@ -681,7 +679,7 @@ func TestTerminateWithoutWorkerPodUID(t *testing.T) { const ( atespace = "ate-demo" actorName = "counter" - actorUID = "actor-uid-1" + actorUID = "0f6c2b0e-1b7e-4b3f-9a6a-1d2c3e4f5a64" snapshotName = "pause-snap-1" ) @@ -705,7 +703,7 @@ func TestTerminateWithoutWorkerPodUID(t *testing.T) { systemInfoVolumes: newSystemInfoVolumeRefresher(nil, nil), } spec := &ateletpb.WorkloadSpec{ - Containers: []*ateletpb.Container{{Name: "app", Image: "example.com/app:v1"}}, + Containers: []*ateletpb.Container{{Name: "app", Image: "example.com/app@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}}, } if _, err := s.Terminate(ctx, &ateletpb.TerminateRequest{ diff --git a/cmd/atelet/main.go b/cmd/atelet/main.go index c6e63d32b9..0cec439083 100644 --- a/cmd/atelet/main.go +++ b/cmd/atelet/main.go @@ -30,6 +30,7 @@ import ( "syscall" "time" + "github.com/agent-substrate/substrate/cmd/atelet/internal/apivalidation" "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" "github.com/agent-substrate/substrate/cmd/atelet/internal/credentialprovider" "github.com/agent-substrate/substrate/cmd/atelet/internal/sparsefile" @@ -77,7 +78,6 @@ import ( "google.golang.org/grpc/status" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" @@ -525,8 +525,8 @@ func NewService( } func (s *AteomHerder) Run(ctx context.Context, req *ateletpb.RunRequest) (resp *ateletpb.RunResponse, err error) { - if err := validateRunRequest(req); err != nil { - return nil, apierror.InvalidArgument("%v", err) + if errs := apivalidation.ValidateRunRequest(ctx, req); len(errs) > 0 { + return nil, resources.ToAPIError(errs) } actorUID := req.GetActorUid() @@ -646,8 +646,8 @@ func allocatedBytes(info os.FileInfo) int64 { } func (s *AteomHerder) Checkpoint(ctx context.Context, req *ateletpb.CheckpointRequest) (_ *ateletpb.CheckpointResponse, err error) { - if err := validateCheckpointRequest(req); err != nil { - return nil, apierror.InvalidArgument("%v", err) + if errs := apivalidation.ValidateCheckpointRequest(ctx, req); len(errs) > 0 { + return nil, resources.ToAPIError(errs) } actorUID := req.GetActorUid() @@ -893,7 +893,7 @@ func shouldHaveSnapshots(req *ateletpb.CheckpointRequest) bool { } for _, vol := range req.GetSpec().GetVolumes() { - if _, ok := vol.GetSource().(*ateletpb.Volume_DurableDir); ok { + if vol.GetDurableDir() != nil { return true } } @@ -952,8 +952,8 @@ func (s *AteomHerder) uploadSnapshot(ctx context.Context, uri resources.Snapshot // files and their self-describing manifest already sit under the actor's // local-checkpoints directory, written by an earlier local Checkpoint (pause). func (s *AteomHerder) UploadPausedCheckpoint(ctx context.Context, req *ateletpb.UploadPausedCheckpointRequest) (_ *ateletpb.UploadPausedCheckpointResponse, err error) { - if err := validateUploadPausedCheckpointRequest(req); err != nil { - return nil, apierror.InvalidArgument("%v", err) + if errs := apivalidation.ValidateUploadPausedCheckpointRequest(ctx, req); len(errs) > 0 { + return nil, resources.ToAPIError(errs) } tStart := time.Now() @@ -1054,8 +1054,8 @@ func readSnapshotManifest(dir string) ([]byte, error) { } func (s *AteomHerder) Restore(ctx context.Context, req *ateletpb.RestoreRequest) (resp *ateletpb.RestoreResponse, err error) { - if err := validateRestoreRequest(req); err != nil { - return nil, apierror.InvalidArgument("%v", err) + if errs := apivalidation.ValidateRestoreRequest(ctx, req); len(errs) > 0 { + return nil, resources.ToAPIError(errs) } actorUID := req.GetActorUid() @@ -1292,8 +1292,8 @@ func (s *AteomHerder) Restore(ctx context.Context, req *ateletpb.RestoreRequest) // Terminate terminates any running workload on ateom, unmounts external volumes, // and resets actor directories on the node. func (s *AteomHerder) Terminate(ctx context.Context, req *ateletpb.TerminateRequest) (*ateletpb.TerminateResponse, error) { - if err := validateTerminateRequest(req); err != nil { - return nil, apierror.InvalidArgument("%v", err) + if errs := apivalidation.ValidateTerminateRequest(ctx, req); len(errs) > 0 { + return nil, resources.ToAPIError(errs) } actorRef := resources.ActorRef{Atespace: req.GetAtespace(), Name: req.GetActorName()} @@ -1495,8 +1495,7 @@ func (s *AteomHerder) prepareOCIBundles( ) ([]*ateompb.ContainerSpec, error) { // Prepare host folders for volume types that need them. for _, vol := range spec.GetVolumes() { - switch vol.GetSource().(type) { - case *ateletpb.Volume_DurableDir: + if vol.GetDurableDir() != nil { volPath := ateletpath.DurableDirVolumeMountPoint(actorUID, vol.GetName()) if err := os.MkdirAll(volPath, 0o700); err != nil { return nil, fmt.Errorf("while creating %q: %w", volPath, err) @@ -1584,29 +1583,29 @@ func buildAteomWorkloadSpec(spec *ateletpb.WorkloadSpec, containerSpecs []*ateom return nil, fmt.Errorf("container %q mounts volume %q which is not defined in workload volumes", ctr.GetName(), volName) } - switch vol.GetSource().(type) { - case *ateletpb.Volume_DurableDir: + switch { + case vol.GetDurableDir() != nil: ddMounts = append(ddMounts, &ateompb.DurableDirVolumeMount{ VolumeName: volName, MountPath: vm.GetMountPath(), }) - case *ateletpb.Volume_External: + case vol.GetExternal() != nil: csiMounts = append(csiMounts, &ateompb.VolumeMount{ VolumeName: volName, MountPath: vm.GetMountPath(), }) - case *ateletpb.Volume_SystemInfo: + case vol.GetSystemInfo() != nil: siMounts = append(siMounts, &ateompb.SystemInfoVolumeMount{ VolumeName: volName, MountPath: vm.GetMountPath(), }) - case *ateletpb.Volume_Image: + case vol.GetImage() != nil: imgMounts = append(imgMounts, &ateompb.ImageVolumeMount{ VolumeName: volName, MountPath: vm.GetMountPath(), }) default: - return nil, fmt.Errorf("container %q mounts volume %q with unsupported source %T", ctr.GetName(), volName, vol.GetSource()) + return nil, fmt.Errorf("container %q mounts volume %q with no source set", ctr.GetName(), volName) } } container := &ateompb.Container{ @@ -1708,171 +1707,6 @@ func (d *AteomDialer) DialAteomPod(ctx context.Context, podUID string) (*grpc.Cl return conn, nil } -// validateRunRequest, validateCheckpointRequest, and validateRestoreRequest -// validate everything in their request that atelet turns into host filesystem -// paths, plus the request-specific fields. atelet listens on an insecure -// hostPort, so any reachable caller could otherwise smuggle a path separator -// or ".." through these fields and make atelet read/RemoveAll/write outside -// the intended directory tree, or collide bundles. Each RPC validates at its -// boundary, before any path is built. The field rules live in -// internal/resources so other components can apply them at their boundaries. -func validateRunRequest(req *ateletpb.RunRequest) error { - var errs field.ErrorList - errs = append(errs, resources.ValidateResourceName(req.GetAtespace(), field.NewPath("atespace"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorName(), field.NewPath("actor_name"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorUid(), field.NewPath("actor_uid"))...) - if len(errs) > 0 { - return errs.ToAggregate() - } - // TODO: Migrate all validations below to the validation framework. - if err := resources.ValidateWorkerPodUID(req.GetWorkerPodUid()); err != nil { - return err - } - names := make([]string, 0, len(req.GetSpec().GetContainers())) - for _, ctr := range req.GetSpec().GetContainers() { - names = append(names, ctr.GetName()) - } - return resources.ValidateContainerNames(names) -} - -func validateCheckpointRequest(req *ateletpb.CheckpointRequest) error { - var errs field.ErrorList - errs = append(errs, resources.ValidateResourceName(req.GetAtespace(), field.NewPath("atespace"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorName(), field.NewPath("actor_name"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorUid(), field.NewPath("actor_uid"))...) - if len(errs) > 0 { - return errs.ToAggregate() - } - // TODO: Migrate all validations below to the validation framework. - if err := resources.ValidateWorkerPodUID(req.GetWorkerPodUid()); err != nil { - return err - } - names := make([]string, 0, len(req.GetSpec().GetContainers())) - for _, ctr := range req.GetSpec().GetContainers() { - names = append(names, ctr.GetName()) - } - if err := resources.ValidateContainerNames(names); err != nil { - return err - } - - if err := validateFidelity(req.GetFidelity()); err != nil { - return err - } - - switch req.GetType() { - case ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL: - if _, err := resources.ParseSnapshotURI(req.GetExternalConfig().GetSnapshotUri()); err != nil { - return err - } - case ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL: - if !resources.IsValidResourceName(req.GetLocalConfig().GetSnapshotName()) { - return fmt.Errorf("invalid local snapshot name %q", req.GetLocalConfig().GetSnapshotName()) - } - default: - return fmt.Errorf("invalid checkpoint type: %v", req.GetType()) - } - return nil -} - -func validateRestoreRequest(req *ateletpb.RestoreRequest) error { - var errs field.ErrorList - errs = append(errs, resources.ValidateResourceName(req.GetAtespace(), field.NewPath("atespace"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorName(), field.NewPath("actor_name"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorUid(), field.NewPath("actor_uid"))...) - if len(errs) > 0 { - return errs.ToAggregate() - } - // TODO: Migrate all validations below to the validation framework. - if err := resources.ValidateWorkerPodUID(req.GetWorkerPodUid()); err != nil { - return err - } - names := make([]string, 0, len(req.GetSpec().GetContainers())) - for _, ctr := range req.GetSpec().GetContainers() { - names = append(names, ctr.GetName()) - } - if err := resources.ValidateContainerNames(names); err != nil { - return err - } - - if err := validateFidelity(req.GetFidelity()); err != nil { - return err - } - - if req.GetSandboxAssets() == nil { - return fmt.Errorf("missing sandbox_assets") - } - - switch req.GetType() { - case ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL: - if _, err := resources.ParseSnapshotURI(req.GetExternalConfig().GetSnapshotUri()); err != nil { - return err - } - case ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL: - if !resources.IsValidResourceName(req.GetLocalConfig().GetSnapshotName()) { - return fmt.Errorf("invalid local snapshot name %q", req.GetLocalConfig().GetSnapshotName()) - } - default: - return fmt.Errorf("invalid checkpoint type: %v", req.GetType()) - } - return nil -} - -func validateTerminateRequest(req *ateletpb.TerminateRequest) error { - var errs field.ErrorList - errs = append(errs, resources.ValidateResourceName(req.GetAtespace(), field.NewPath("atespace"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorName(), field.NewPath("actor_name"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorUid(), field.NewPath("actor_uid"))...) - if len(errs) > 0 { - return errs.ToAggregate() - } - if req.GetWorkerPodUid() != "" { - if err := resources.ValidateWorkerPodUID(req.GetWorkerPodUid()); err != nil { - return err - } - } - names := make([]string, 0, len(req.GetSpec().GetContainers())) - for _, ctr := range req.GetSpec().GetContainers() { - names = append(names, ctr.GetName()) - } - return resources.ValidateContainerNames(names) -} - -// validateFidelity accepts every level of the ladder. Which levels a sandbox -// runtime can actually serve is the runtime's call: atelet forwards the -// fidelity and ateom rejects what it cannot capture. -func validateFidelity(fidelity ateletpb.SnapshotFidelity) error { - switch fidelity { - case ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES, - ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS, - ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY: - return nil - case ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED: - return fmt.Errorf("snapshot fidelity must be non-zero") - default: - return fmt.Errorf("invalid snapshot fidelity: %v", fidelity) - } -} - -func validateUploadPausedCheckpointRequest(req *ateletpb.UploadPausedCheckpointRequest) error { - var errs field.ErrorList - errs = append(errs, resources.ValidateResourceName(req.GetAtespace(), field.NewPath("atespace"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorName(), field.NewPath("actor_name"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorUid(), field.NewPath("actor_uid"))...) - errs = append(errs, resources.ValidateResourceName(req.GetLocalSnapshotName(), field.NewPath("local_snapshot_name"))...) - // Golden actors are never paused (the golden flow commits a running - // actor), so never promote a paused checkpoint to a golden snapshot. - if req.GetAtespace() == resources.GoldenActorAtespace { - errs = append(errs, field.Forbidden(field.NewPath("atespace"), fmt.Sprintf("atespace %q holds golden actors, which are never paused", req.GetAtespace()))) - } - if _, err := resources.ParseSnapshotURI(req.GetDestinationSnapshotUri()); err != nil { - errs = append(errs, field.Invalid(field.NewPath("destination_snapshot_uri"), req.GetDestinationSnapshotUri(), err.Error())) - } - if err := validateFidelity(req.GetDesiredFidelity()); err != nil { - errs = append(errs, field.Invalid(field.NewPath("desired_fidelity"), req.GetDesiredFidelity().String(), err.Error())) - } - return errs.ToAggregate() -} - // writeFileAtomic writes data to path by writing a temp file in the same // directory and renaming it over the target so readers never observe a // truncated or partially written value. diff --git a/cmd/atelet/main_test.go b/cmd/atelet/main_test.go index 2fe768baf1..f17d6d08ea 100644 --- a/cmd/atelet/main_test.go +++ b/cmd/atelet/main_test.go @@ -538,216 +538,6 @@ func TestWriteFileAtomic(t *testing.T) { }) } -// validRunRequest, validCheckpointRequest, and validRestoreRequest build -// requests whose every field passes validation; the per-request tests below -// break one field per case. -func validRunRequest() *ateletpb.RunRequest { - return &ateletpb.RunRequest{ - Atespace: "ate-demo", - ActorName: "counter-1", - ActorTemplateAtespace: "ate-demo", - ActorTemplateName: "counter", - WorkerPodUid: "422938ba-8860-4983-a25d-d6bcb0a69d4e", - ActorUid: "123e4567-e89b-12d3-a456-426614174000", - Spec: &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Name: "worker"}}}, - } -} - -func validCheckpointRequest() *ateletpb.CheckpointRequest { - return &ateletpb.CheckpointRequest{ - Atespace: "ate-demo", - ActorName: "counter-1", - ActorTemplateAtespace: "ate-demo", - ActorTemplateName: "counter", - WorkerPodUid: "422938ba-8860-4983-a25d-d6bcb0a69d4e", - ActorUid: "123e4567-e89b-12d3-a456-426614174000", - Spec: &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Name: "worker"}}}, - Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL, - Config: &ateletpb.CheckpointRequest_ExternalConfig{ - ExternalConfig: &ateletpb.ExternalCheckpointConfiguration{ - SnapshotUri: testSnapshotURI, - }, - }, - Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, - } -} - -func validRestoreRequest() *ateletpb.RestoreRequest { - return &ateletpb.RestoreRequest{ - Atespace: "ate-demo", - ActorName: "counter-1", - ActorTemplateAtespace: "ate-demo", - ActorTemplateName: "counter", - WorkerPodUid: "422938ba-8860-4983-a25d-d6bcb0a69d4e", - ActorUid: "123e4567-e89b-12d3-a456-426614174000", - Spec: &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Name: "worker"}}}, - Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL, - Config: &ateletpb.RestoreRequest_ExternalConfig{ - ExternalConfig: &ateletpb.ExternalRestoreConfiguration{ - SnapshotUri: testSnapshotURI, - }, - }, - Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, - SandboxAssets: &ateletpb.SandboxAssets{ - SandboxClass: "gvisor", - PauseImage: testPauseImage, - }, - } -} - -func TestValidateRunRequest(t *testing.T) { - tests := []struct { - name string - mutate func(*ateletpb.RunRequest) - wantErr bool - }{ - {"valid", func(*ateletpb.RunRequest) {}, false}, - {"invalid worker pod uid", func(r *ateletpb.RunRequest) { r.WorkerPodUid = "../escape" }, true}, - {"invalid atespace", func(r *ateletpb.RunRequest) { r.Atespace = "../escape" }, true}, - {"invalid actor name", func(r *ateletpb.RunRequest) { r.ActorName = "../escape" }, true}, - {"invalid actor uid", func(r *ateletpb.RunRequest) { r.ActorUid = "../escape" }, true}, - {"any actor template identity accepted", func(r *ateletpb.RunRequest) { r.ActorTemplateAtespace, r.ActorTemplateName = "Not_Valid", "Not_Valid" }, false}, - {"empty actor template identity accepted", func(r *ateletpb.RunRequest) { r.ActorTemplateAtespace, r.ActorTemplateName = "", "" }, false}, - {"invalid container name", func(r *ateletpb.RunRequest) { - r.Spec.Containers = []*ateletpb.Container{{Name: "../escape"}} - }, true}, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - req := validRunRequest() - tc.mutate(req) - if err := validateRunRequest(req); (err != nil) != tc.wantErr { - t.Errorf("validateRunRequest err = %v, wantErr %v", err, tc.wantErr) - } - }) - } -} - -// Checkpoint and Restore must reject a bad snapshot URI even when -// every common field is valid. -func TestValidateCheckpointRequest(t *testing.T) { - makeReq := func(opts ...func(*ateletpb.CheckpointRequest)) *ateletpb.CheckpointRequest { - r := validCheckpointRequest() - for _, opt := range opts { - opt(r) - } - return r - } - - tests := []struct { - name string - req *ateletpb.CheckpointRequest - wantErr bool - }{ - {"valid", makeReq(), false}, - {"empty snapshot uri", makeReq(func(r *ateletpb.CheckpointRequest) { r.GetExternalConfig().SnapshotUri = "" }), true}, - {"bucketless snapshot uri", makeReq(func(r *ateletpb.CheckpointRequest) { r.GetExternalConfig().SnapshotUri = "relative/path" }), true}, - {"invalid worker pod uid", makeReq(func(r *ateletpb.CheckpointRequest) { r.WorkerPodUid = "../escape" }), true}, - {"invalid atespace", makeReq(func(r *ateletpb.CheckpointRequest) { r.Atespace = "../escape" }), true}, - {"invalid actor name", makeReq(func(r *ateletpb.CheckpointRequest) { r.ActorName = "../escape" }), true}, - {"invalid actor uid", makeReq(func(r *ateletpb.CheckpointRequest) { r.ActorUid = "../escape" }), true}, - {"any actor template identity accepted", makeReq(func(r *ateletpb.CheckpointRequest) { - r.ActorTemplateAtespace, r.ActorTemplateName = "Not_Valid", "Not_Valid" - }), false}, - {"empty actor template identity accepted", makeReq(func(r *ateletpb.CheckpointRequest) { r.ActorTemplateAtespace, r.ActorTemplateName = "", "" }), false}, - {"invalid container name", makeReq(func(r *ateletpb.CheckpointRequest) { - r.Spec.Containers = []*ateletpb.Container{{Name: "../escape"}} - }), true}, - {"invalid local snapshot prefix", makeReq(func(r *ateletpb.CheckpointRequest) { - r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL - r.Config = &ateletpb.CheckpointRequest_LocalConfig{LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: ""}} - }), true}, - {"local snapshot name escapes its directory", makeReq(func(r *ateletpb.CheckpointRequest) { - r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL - r.Config = &ateletpb.CheckpointRequest_LocalConfig{LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: "../escape"}} - }), true}, - {"nested local snapshot prefix", makeReq(func(r *ateletpb.CheckpointRequest) { - r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL - r.Config = &ateletpb.CheckpointRequest_LocalConfig{LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: "pause/2"}} - }), true}, - {"traversal local snapshot prefix", makeReq(func(r *ateletpb.CheckpointRequest) { - r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL - r.Config = &ateletpb.CheckpointRequest_LocalConfig{LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: ".."}} - }), true}, - {"unspecified snapshot type", makeReq(func(r *ateletpb.CheckpointRequest) { r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_UNSPECIFIED }), true}, - {"unspecified snapshot fidelity", makeReq(func(r *ateletpb.CheckpointRequest) { - r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED - }), true}, - {"invalid snapshot fidelity", makeReq(func(r *ateletpb.CheckpointRequest) { r.Fidelity = ateletpb.SnapshotFidelity(23) }), true}, - {"rootfs fidelity", makeReq(func(r *ateletpb.CheckpointRequest) { - r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS - }), false}, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if err := validateCheckpointRequest(tc.req); (err != nil) != tc.wantErr { - t.Errorf("validateCheckpointRequest err = %v, wantErr %v", err, tc.wantErr) - } - }) - } -} - -func TestValidateRestoreRequest(t *testing.T) { - makeReq := func(opts ...func(*ateletpb.RestoreRequest)) *ateletpb.RestoreRequest { - r := validRestoreRequest() - for _, opt := range opts { - opt(r) - } - return r - } - - tests := []struct { - name string - req *ateletpb.RestoreRequest - wantErr bool - }{ - {"valid", makeReq(), false}, - {"missing sandbox assets", makeReq(func(r *ateletpb.RestoreRequest) { r.SandboxAssets = nil }), true}, - {"empty snapshot uri", makeReq(func(r *ateletpb.RestoreRequest) { r.GetExternalConfig().SnapshotUri = "" }), true}, - {"bucketless snapshot uri", makeReq(func(r *ateletpb.RestoreRequest) { r.GetExternalConfig().SnapshotUri = "relative/path" }), true}, - {"invalid worker pod uid", makeReq(func(r *ateletpb.RestoreRequest) { r.WorkerPodUid = "../escape" }), true}, - {"invalid atespace", makeReq(func(r *ateletpb.RestoreRequest) { r.Atespace = "../escape" }), true}, - {"invalid actor name", makeReq(func(r *ateletpb.RestoreRequest) { r.ActorName = "../escape" }), true}, - {"invalid actor uid", makeReq(func(r *ateletpb.RestoreRequest) { r.ActorUid = "../escape" }), true}, - {"any actor template identity accepted", makeReq(func(r *ateletpb.RestoreRequest) { - r.ActorTemplateAtespace, r.ActorTemplateName = "Not_Valid", "Not_Valid" - }), false}, - {"empty actor template identity accepted", makeReq(func(r *ateletpb.RestoreRequest) { r.ActorTemplateAtespace, r.ActorTemplateName = "", "" }), false}, - {"invalid container name", makeReq(func(r *ateletpb.RestoreRequest) { - r.Spec.Containers = []*ateletpb.Container{{Name: "../escape"}} - }), true}, - {"invalid local snapshot prefix", makeReq(func(r *ateletpb.RestoreRequest) { - r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL - r.Config = &ateletpb.RestoreRequest_LocalConfig{LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: ""}} - }), true}, - {"local snapshot name escapes its directory", makeReq(func(r *ateletpb.RestoreRequest) { - r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL - r.Config = &ateletpb.RestoreRequest_LocalConfig{LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: "../escape"}} - }), true}, - {"nested local snapshot prefix", makeReq(func(r *ateletpb.RestoreRequest) { - r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL - r.Config = &ateletpb.RestoreRequest_LocalConfig{LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: "pause/2"}} - }), true}, - {"traversal local snapshot prefix", makeReq(func(r *ateletpb.RestoreRequest) { - r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL - r.Config = &ateletpb.RestoreRequest_LocalConfig{LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: ".."}} - }), true}, - {"unspecified snapshot type", makeReq(func(r *ateletpb.RestoreRequest) { r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_UNSPECIFIED }), true}, - {"unspecified snapshot fidelity", makeReq(func(r *ateletpb.RestoreRequest) { r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED }), true}, - {"invalid snapshot fidelity", makeReq(func(r *ateletpb.RestoreRequest) { r.Fidelity = ateletpb.SnapshotFidelity(23) }), true}, - {"rootfs fidelity", makeReq(func(r *ateletpb.RestoreRequest) { - r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS - }), false}, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if err := validateRestoreRequest(tc.req); (err != nil) != tc.wantErr { - t.Errorf("validateRestoreRequest err = %v, wantErr %v", err, tc.wantErr) - } - }) - } -} - // Every valid atelet scope must map to its ateom counterpart. func TestToAteomFidelity(t *testing.T) { tests := []struct { @@ -913,7 +703,10 @@ func TestRPCBoundariesReject(t *testing.T) { ctx := context.Background() badUID := "../escape" // valid actor ref, invalid worker pod UID const okAtespace, okID, okActorUID = "ate-demo", "counter-1", "123e4567-e89b-12d3-a456-426614174000" - okSpec := &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Name: "worker"}}} + okSpec := &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{ + Name: "worker", + Image: "example.com/app@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }}} wantInvalidArgument := func(t *testing.T, rpc string, err error) { t.Helper() @@ -1020,10 +813,10 @@ func TestBuildAteomWorkloadSpecForwardsContainerSpecs(t *testing.T) { func TestBuildAteomWorkloadSpecForwardsDurableDirMounts(t *testing.T) { in := &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "data", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, - {Name: "cache", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, - {Name: "scratch", Source: &ateletpb.Volume_External{External: &ateletpb.ExternalVolumeSource{}}}, - {Name: "system-info", Source: &ateletpb.Volume_SystemInfo{SystemInfo: &ateletpb.SystemInfoVolume{}}}, + {Name: "data", DurableDir: &ateletpb.DurableDirVolume{}}, + {Name: "cache", DurableDir: &ateletpb.DurableDirVolume{}}, + {Name: "scratch", External: &ateletpb.ExternalVolumeSource{}}, + {Name: "system-info", SystemInfo: &ateletpb.SystemInfoVolume{}}, }, Containers: []*ateletpb.Container{ { @@ -1089,7 +882,7 @@ func TestBuildAteomWorkloadSpecValidation(t *testing.T) { name: "missing volume definition", in: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "data", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, + {Name: "data", DurableDir: &ateletpb.DurableDirVolume{}}, }, Containers: []*ateletpb.Container{ { @@ -1117,14 +910,14 @@ func TestBuildAteomWorkloadSpecValidation(t *testing.T) { }, }, }, - wantErr: `container "ctr" mounts volume "data" with unsupported source `, + wantErr: `container "ctr" mounts volume "data" with no source set`, }, { name: "duplicate volume names", in: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "data", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, - {Name: "data", Source: &ateletpb.Volume_External{External: &ateletpb.ExternalVolumeSource{}}}, + {Name: "data", DurableDir: &ateletpb.DurableDirVolume{}}, + {Name: "data", External: &ateletpb.ExternalVolumeSource{}}, }, Containers: []*ateletpb.Container{ { @@ -1407,9 +1200,9 @@ func TestDrainOnShutdownForceStopsAfterTimeout(t *testing.T) { func TestBuildAteomWorkloadSpec_ImageVolumeMounts(t *testing.T) { spec := &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "agent", Source: &ateletpb.Volume_Image{Image: &ateletpb.ImageVolumeSource{}}}, - {Name: "data", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, - {Name: "ext", Source: &ateletpb.Volume_External{External: &ateletpb.ExternalVolumeSource{}}}, + {Name: "agent", Image: &ateletpb.ImageVolumeSource{}}, + {Name: "data", DurableDir: &ateletpb.DurableDirVolume{}}, + {Name: "ext", External: &ateletpb.ExternalVolumeSource{}}, }, Containers: []*ateletpb.Container{{ Name: "app", @@ -1508,6 +1301,21 @@ func writeLocalSnapshot(t *testing.T, dir string, rec sandboxAssetsRecord, conte } } +func validCheckpointRequest() *ateletpb.CheckpointRequest { + return &ateletpb.CheckpointRequest{ + Atespace: "ate-demo", + ActorName: "counter-1", + ActorTemplateAtespace: "ate-demo", + ActorTemplateName: "counter", + WorkerPodUid: "422938ba-8860-4983-a25d-d6bcb0a69d4e", + ActorUid: "123e4567-e89b-12d3-a456-426614174000", + Spec: &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Name: "worker"}}}, + Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL, + ExternalConfig: &ateletpb.ExternalCheckpointConfiguration{SnapshotUri: testSnapshotURI}, + Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, + } +} + func validUploadPausedCheckpointRequest() *ateletpb.UploadPausedCheckpointRequest { return &ateletpb.UploadPausedCheckpointRequest{ Atespace: "ate-demo", @@ -1697,47 +1505,6 @@ func TestUploadLocalCheckpointDir(t *testing.T) { }) } -func TestValidateUploadPausedCheckpointRequest(t *testing.T) { - tests := []struct { - name string - mutate func(*ateletpb.UploadPausedCheckpointRequest) - wantErr bool - }{ - {"valid", func(*ateletpb.UploadPausedCheckpointRequest) {}, false}, - {"valid volumes fidelity", func(r *ateletpb.UploadPausedCheckpointRequest) { - r.DesiredFidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES - }, false}, - {"valid rootfs fidelity", func(r *ateletpb.UploadPausedCheckpointRequest) { - r.DesiredFidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS - }, false}, - {"invalid atespace", func(r *ateletpb.UploadPausedCheckpointRequest) { r.Atespace = "../escape" }, true}, - {"golden atespace rejected", func(r *ateletpb.UploadPausedCheckpointRequest) { r.Atespace = resources.GoldenActorAtespace }, true}, - {"invalid actor name", func(r *ateletpb.UploadPausedCheckpointRequest) { r.ActorName = "UPPER" }, true}, - {"invalid actor uid", func(r *ateletpb.UploadPausedCheckpointRequest) { r.ActorUid = "" }, true}, - {"any actor template identity accepted", func(r *ateletpb.UploadPausedCheckpointRequest) { r.ActorTemplateAtespace = "no/slashes" }, false}, - {"empty actor template identity accepted", func(r *ateletpb.UploadPausedCheckpointRequest) { - r.ActorTemplateAtespace, r.ActorTemplateName = "", "" - }, false}, - {"invalid snapshot name", func(r *ateletpb.UploadPausedCheckpointRequest) { r.LocalSnapshotName = "../escape" }, true}, - {"invalid snapshot uri", func(r *ateletpb.UploadPausedCheckpointRequest) { r.DestinationSnapshotUri = "not-a-uri" }, true}, - {"unspecified fidelity", func(r *ateletpb.UploadPausedCheckpointRequest) { - r.DesiredFidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED - }, true}, - {"invalid fidelity", func(r *ateletpb.UploadPausedCheckpointRequest) { - r.DesiredFidelity = ateletpb.SnapshotFidelity(23) - }, true}, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - req := validUploadPausedCheckpointRequest() - tc.mutate(req) - if err := validateUploadPausedCheckpointRequest(req); (err != nil) != tc.wantErr { - t.Errorf("validateUploadPausedCheckpointRequest err = %v, wantErr %v", err, tc.wantErr) - } - }) - } -} - func TestShouldHaveSnapshots(t *testing.T) { tests := []struct { name string @@ -1764,7 +1531,7 @@ func TestShouldHaveSnapshots(t *testing.T) { Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES, Spec: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "durable", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, + {Name: "durable", DurableDir: &ateletpb.DurableDirVolume{}}, }, }, }, @@ -1776,7 +1543,7 @@ func TestShouldHaveSnapshots(t *testing.T) { Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES, Spec: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "csi", Source: &ateletpb.Volume_External{External: &ateletpb.ExternalVolumeSource{}}}, + {Name: "csi", External: &ateletpb.ExternalVolumeSource{}}, }, }, }, @@ -1788,8 +1555,8 @@ func TestShouldHaveSnapshots(t *testing.T) { Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES, Spec: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "durable", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, - {Name: "csi", Source: &ateletpb.Volume_External{External: &ateletpb.ExternalVolumeSource{}}}, + {Name: "durable", DurableDir: &ateletpb.DurableDirVolume{}}, + {Name: "csi", External: &ateletpb.ExternalVolumeSource{}}, }, }, }, diff --git a/cmd/atelet/systeminfovolume.go b/cmd/atelet/systeminfovolume.go index ebded77046..05f1c444e3 100644 --- a/cmd/atelet/systeminfovolume.go +++ b/cmd/atelet/systeminfovolume.go @@ -168,17 +168,17 @@ func (r *systemInfoVolumeRefresher) DeregisterOwned(owner *registeredActor) { // collectData builds the volume's contents keyed by volume-relative path. func (r *systemInfoVolumeRefresher) collectData(ref resources.ActorRef, actorUID string, si *ateletpb.SystemInfoVolume) (payload map[string][]byte, err error) { payload = map[string][]byte{} - for _, dataSourceAny := range si.GetDataSources() { - switch dataSource := dataSourceAny.GetDataSource().(type) { - case *ateletpb.SystemInfoDataSource_TrustBundle: - tb := dataSource.TrustBundle + for _, dataSource := range si.GetDataSources() { + switch { + case dataSource.GetTrustBundle() != nil: + tb := dataSource.GetTrustBundle() pemBundle, err := r.bundles.Combined(tb.GetNames()) if err != nil { return nil, fmt.Errorf("system-info projection %q: %w", tb.GetPath(), err) } payload[tb.GetPath()] = pemBundle - case *ateletpb.SystemInfoDataSource_ActorMetadata: - for _, item := range dataSource.ActorMetadata.GetItems() { + case dataSource.GetActorMetadata() != nil: + for _, item := range dataSource.GetActorMetadata().GetItems() { var value string switch item.GetField() { case ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_NAME: diff --git a/cmd/atelet/systeminfovolume_test.go b/cmd/atelet/systeminfovolume_test.go index d08928d77c..c5286c3b33 100644 --- a/cmd/atelet/systeminfovolume_test.go +++ b/cmd/atelet/systeminfovolume_test.go @@ -111,9 +111,7 @@ func (s *ctbStore) remove(t *testing.T) { func trustVolumeSpec(relPath string) *ateletpb.SystemInfoVolume { return &ateletpb.SystemInfoVolume{ DataSources: []*ateletpb.SystemInfoDataSource{ - {DataSource: &ateletpb.SystemInfoDataSource_TrustBundle{ - TrustBundle: &ateletpb.TrustBundleDataSource{Names: []string{trustbundle.EgressName}, Path: relPath}, - }}, + {TrustBundle: &ateletpb.TrustBundleDataSource{Names: []string{trustbundle.EgressName}, Path: relPath}}, }, } } @@ -121,13 +119,11 @@ func trustVolumeSpec(relPath string) *ateletpb.SystemInfoVolume { func metadataVolumeSpec() *ateletpb.SystemInfoVolume { return &ateletpb.SystemInfoVolume{ DataSources: []*ateletpb.SystemInfoDataSource{ - {DataSource: &ateletpb.SystemInfoDataSource_ActorMetadata{ - 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"}, - }, + {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"}, }, }}, }, @@ -687,9 +683,9 @@ func TestSystemInfoVolumeRefresher_FailedReplacementDropsRegistration(t *testing func TestSystemInfoVolumesFor(t *testing.T) { spec := &ateletpb.WorkloadSpec{Volumes: []*ateletpb.Volume{ - {Name: "data", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, - {Name: "trust", Source: &ateletpb.Volume_SystemInfo{SystemInfo: trustVolumeSpec("ca.pem")}}, - {Name: "meta", Source: &ateletpb.Volume_SystemInfo{SystemInfo: metadataVolumeSpec()}}, + {Name: "data", DurableDir: &ateletpb.DurableDirVolume{}}, + {Name: "trust", SystemInfo: trustVolumeSpec("ca.pem")}, + {Name: "meta", SystemInfo: metadataVolumeSpec()}, }} got := systemInfoVolumesFor("uid-1", spec) if len(got) != 2 || got[0].Name != "trust" || got[1].Name != "meta" { diff --git a/cmd/atelet/volumes_test.go b/cmd/atelet/volumes_test.go index e3b13ac9b3..cab35adcb2 100644 --- a/cmd/atelet/volumes_test.go +++ b/cmd/atelet/volumes_test.go @@ -91,27 +91,21 @@ func TestUnmountExternalVolumes(t *testing.T) { extVol1 := &ateletpb.Volume{ Name: "vol-1", - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: "mock-vol-1", - VolumeType: "mock-driver", - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: "mock-vol-1", + VolumeType: "mock-driver", }, } extVol2 := &ateletpb.Volume{ Name: "vol-2", - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: "mock-vol-2", - VolumeType: "mock-driver", - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: "mock-vol-2", + VolumeType: "mock-driver", }, } durableVol := &ateletpb.Volume{ - Name: "durable-1", - Source: &ateletpb.Volume_DurableDir{ - DurableDir: &ateletpb.DurableDirVolume{}, - }, + Name: "durable-1", + DurableDir: &ateletpb.DurableDirVolume{}, } t.Run("success", func(t *testing.T) { @@ -177,29 +171,23 @@ func TestMountExternalVolumes(t *testing.T) { extVol1 := &ateletpb.Volume{ Name: "vol-1", - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: "mock-vol-1", - VolumeType: "mock-driver", - VolumeContext: map[string]string{"key": "val1"}, - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: "mock-vol-1", + VolumeType: "mock-driver", + VolumeContext: map[string]string{"key": "val1"}, }, } extVol2 := &ateletpb.Volume{ Name: "vol-2", - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: "mock-vol-2", - VolumeType: "mock-driver", - VolumeContext: map[string]string{"key": "val2"}, - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: "mock-vol-2", + VolumeType: "mock-driver", + VolumeContext: map[string]string{"key": "val2"}, }, } durableVol := &ateletpb.Volume{ - Name: "durable-1", - Source: &ateletpb.Volume_DurableDir{ - DurableDir: &ateletpb.DurableDirVolume{}, - }, + Name: "durable-1", + DurableDir: &ateletpb.DurableDirVolume{}, } t.Run("success mounts external volumes and creates mount directory", func(t *testing.T) { @@ -275,11 +263,9 @@ func TestMountExternalVolumes(t *testing.T) { unknownVol := &ateletpb.Volume{ Name: "vol-unknown", - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: "mock-vol-unknown", - VolumeType: "unknown-driver", - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: "mock-vol-unknown", + VolumeType: "unknown-driver", }, } @@ -394,20 +380,16 @@ func TestVolumeHostDirectoryCleanup(t *testing.T) { extVol1 := &ateletpb.Volume{ Name: "vol-1", - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: "mock-vol-1", - VolumeType: "mock-driver", - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: "mock-vol-1", + VolumeType: "mock-driver", }, } extVol2 := &ateletpb.Volume{ Name: "vol-2", - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: "mock-vol-2", - VolumeType: "mock-driver", - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: "mock-vol-2", + VolumeType: "mock-driver", }, } diff --git a/cmd/ateom-gvisor/stats_test.go b/cmd/ateom-gvisor/stats_test.go index 42f80e92da..c1f086a1fe 100644 --- a/cmd/ateom-gvisor/stats_test.go +++ b/cmd/ateom-gvisor/stats_test.go @@ -661,11 +661,12 @@ func TestDeadSandboxCheckDuringDrain(t *testing.T) { } // TestDrainNeverKillsPauseContainer: gracefulShutdown kills the names -// containerNames takes from the spec. atelet rejects a spec that names the -// pause container, so the drain cannot stop the sandbox itself. +// containerNames takes from the spec. atelet requires container names to be +// DNS-1123 labels (k8s-short-name) and the pause container's name is not one, +// so no spec can name it and the drain cannot stop the sandbox itself. func TestDrainNeverKillsPauseContainer(t *testing.T) { - if err := resources.ValidateContainerNames([]string{ocispec.PauseContainer}); err == nil { - t.Fatalf("ValidateContainerNames accepted %q; a spec could then put it on the drain's kill list", ocispec.PauseContainer) + if resources.IsValidResourceName(ocispec.PauseContainer) { + t.Fatalf("%q is a valid container name; a spec could then put it on the drain's kill list", ocispec.PauseContainer) } spec := []*ateompb.Container{{Name: "app"}, {Name: "sidecar"}} for _, name := range containerNames(spec) { diff --git a/cmd/benchmarking/isolate/fake-atelet/herder_test.go b/cmd/benchmarking/isolate/fake-atelet/herder_test.go index 4c90f158f3..33234f4899 100644 --- a/cmd/benchmarking/isolate/fake-atelet/herder_test.go +++ b/cmd/benchmarking/isolate/fake-atelet/herder_test.go @@ -98,8 +98,8 @@ func herderCalls(h *herder) map[string]func(context.Context) error { func externalCheckpoint(uri string) *ateletpb.CheckpointRequest { return &ateletpb.CheckpointRequest{ - Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL, - Config: &ateletpb.CheckpointRequest_ExternalConfig{ExternalConfig: &ateletpb.ExternalCheckpointConfiguration{SnapshotUri: uri}}, + Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL, + ExternalConfig: &ateletpb.ExternalCheckpointConfiguration{SnapshotUri: uri}, } } diff --git a/internal/proto/ateletpb/atelet.pb.go b/internal/proto/ateletpb/atelet.pb.go index af0456c727..2181e83c54 100644 --- a/internal/proto/ateletpb/atelet.pb.go +++ b/internal/proto/ateletpb/atelet.pb.go @@ -42,7 +42,7 @@ const ( ActorMetadataField_ACTOR_METADATA_FIELD_UNSPECIFIED ActorMetadataField = 0 ActorMetadataField_ACTOR_METADATA_FIELD_NAME ActorMetadataField = 1 ActorMetadataField_ACTOR_METADATA_FIELD_ATESPACE ActorMetadataField = 2 - ActorMetadataField_ACTOR_METADATA_FIELD_UID ActorMetadataField = 3 + ActorMetadataField_ACTOR_METADATA_FIELD_UID ActorMetadataField = 3 // Keep this in sync with the maximums on fields of this type. ) // Enum value maps for ActorMetadataField. @@ -96,7 +96,7 @@ const ( // Save snapshot only in local filesystem CheckpointType_CHECKPOINT_TYPE_LOCAL CheckpointType = 1 // Save snapshot to object storage - CheckpointType_CHECKPOINT_TYPE_EXTERNAL CheckpointType = 2 + CheckpointType_CHECKPOINT_TYPE_EXTERNAL CheckpointType = 2 // Keep this in sync with the maximums on fields of this type. ) // Enum value maps for CheckpointType. @@ -156,7 +156,7 @@ const ( SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS SnapshotFidelity = 2 // Capture process memory plus the full filesystem delta on top of the OCI // image (including any attached DurableDir volumes). - SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY SnapshotFidelity = 3 + SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY SnapshotFidelity = 3 // Keep this in sync with the maximums on fields of this type. ) // Enum value maps for SnapshotFidelity. @@ -927,16 +927,36 @@ func (x *MintActorCertificateResponse) GetActorCertificates() [][]byte { type TerminateRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // If unset, the atelet will just cleanup node resources for the actor. - WorkerPodUid string `protobuf:"bytes,1,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` - Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` - ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` - ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` - ActorTemplateAtespace string `protobuf:"bytes,5,opt,name=actor_template_atespace,json=actorTemplateAtespace,proto3" json:"actor_template_atespace,omitempty"` - ActorTemplateName string `protobuf:"bytes,6,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` - Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The UID of the worker pod whose ateom hosts the actor. If unset, atelet + // only cleans up the actor's node resources; it does not reach an ateom. + // + // +k8s:optional + // +k8s:format=k8s-uuid + WorkerPodUid string `protobuf:"bytes,1,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` + // +k8s:required + // +k8s:format=k8s-uuid + ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` + // The template identity is carried for metrics attribution. + // + // +k8s:optional + // +k8s:format=k8s-short-name + ActorTemplateAtespace string `protobuf:"bytes,5,opt,name=actor_template_atespace,json=actorTemplateAtespace,proto3" json:"actor_template_atespace,omitempty"` + // +k8s:optional + // +k8s:format=k8s-short-name + ActorTemplateName string `protobuf:"bytes,6,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` + // A nil spec is tolerated: the delete flow terminates template-less + // actors with a fallback spec. + // + // +k8s:optional + Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *TerminateRequest) Reset() { @@ -1055,24 +1075,51 @@ func (*TerminateResponse) Descriptor() ([]byte, []int) { } type RunRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - WorkerPodUid string `protobuf:"bytes,1,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` - Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` - ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` - ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` - ActorTemplateAtespace string `protobuf:"bytes,5,opt,name=actor_template_atespace,json=actorTemplateAtespace,proto3" json:"actor_template_atespace,omitempty"` - ActorTemplateName string `protobuf:"bytes,6,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` - Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // The UID of the worker pod whose ateom hosts the actor. + // + // +k8s:required + // +k8s:format=k8s-uuid + WorkerPodUid string `protobuf:"bytes,1,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` + // +k8s:required + // +k8s:format=k8s-uuid + ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` + // The template identity is carried for metrics attribution. + // + // +k8s:optional + // +k8s:format=k8s-short-name + ActorTemplateAtespace string `protobuf:"bytes,5,opt,name=actor_template_atespace,json=actorTemplateAtespace,proto3" json:"actor_template_atespace,omitempty"` + // +k8s:optional + // +k8s:format=k8s-short-name + ActorTemplateName string `protobuf:"bytes,6,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` + // +k8s:required + Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` // The sandbox binaries to use for booting this actor from scratch. atelet // fetches the relevant assets and records them with the actor's on-node state // so a later Checkpoint can pin the same version into the snapshot manifest. + // + // +k8s:required SandboxAssets *SandboxAssets `protobuf:"bytes,8,opt,name=sandbox_assets,json=sandboxAssets,proto3" json:"sandbox_assets,omitempty"` // When absent the actor has no egress: its TCP is captured and refused. + // + // +k8s:optional EgressGateway *EgressGateway `protobuf:"bytes,9,opt,name=egress_gateway,json=egressGateway,proto3,oneof" json:"egress_gateway,omitempty"` // The actor's declared size, from the ActorTemplate's resource limits. atelet // passes these through to the sandbox so it is sized to the actor (not the // whole host or worker pod). Zero means "unset": keep the runtime default. - CpuMilli int64 `protobuf:"varint,10,opt,name=cpu_milli,json=cpuMilli,proto3" json:"cpu_milli,omitempty"` // CPU limit in millicores (1000 = one core). + // + // +k8s:optional + // +k8s:minimum=0 + // +k8s:maximum=999999 # the control plane caps cpu limits strictly below 1000 cores + CpuMilli int64 `protobuf:"varint,10,opt,name=cpu_milli,json=cpuMilli,proto3" json:"cpu_milli,omitempty"` // CPU limit in millicores (1000 = one core). + // +k8s:optional + // +k8s:minimum=0 MemoryBytes int64 `protobuf:"varint,11,opt,name=memory_bytes,json=memoryBytes,proto3" json:"memory_bytes,omitempty"` // Memory limit in bytes. unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1190,6 +1237,10 @@ type EgressGateway struct { state protoimpl.MessageState `protogen:"open.v1"` // address is required and identifies the remote gateway as an IP address or // DNS name followed by its port. + // + // +k8s:required + // +k8s:maxLength=261 # a bracketed or 253-character host, ':' and a 5-digit port + // +k8s:customValidation # host:port shape Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1241,8 +1292,15 @@ type AssetFile struct { // the plugin cannot serve it. // Masked in logged requests, since a signed URL carries its credential in // the query. + // + // +k8s:required + // +k8s:maxLength=2048 # matches the SandboxConfig asset url's bound Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` // Lower-case hex SHA256; names the cached file and verifies its integrity. + // + // +k8s:required + // +k8s:maxLength=64 + // +k8s:customValidation # 64 hex characters Sha256 string `protobuf:"bytes,2,opt,name=sha256,proto3" json:"sha256,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1295,8 +1353,11 @@ func (x *AssetFile) GetSha256() string { // ArchAssets is the set of assets for a single architecture, keyed by asset // name (a wrapper message because proto map values cannot themselves be maps). type ArchAssets struct { - state protoimpl.MessageState `protogen:"open.v1"` - Files map[string]*AssetFile `protobuf:"bytes,1,rep,name=files,proto3" json:"files,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // asset name -> file + state protoimpl.MessageState `protogen:"open.v1"` + // +k8s:required + // +k8s:maxProperties=16 + // +k8s:eachKey=+k8s:maxLength=64 + Files map[string]*AssetFile `protobuf:"bytes,1,rep,name=files,proto3" json:"files,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // asset name -> file unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1344,14 +1405,27 @@ func (x *ArchAssets) GetFiles() map[string]*AssetFile { // atelet's backend code interprets the asset names (gVisor expects "gvisor", // the release tarball; legacy "runsc", a bare binary, is still accepted). type SandboxAssets struct { - state protoimpl.MessageState `protogen:"open.v1"` - SandboxClass string `protobuf:"bytes,1,opt,name=sandbox_class,json=sandboxClass,proto3" json:"sandbox_class,omitempty"` // e.g. "gvisor" - Assets map[string]*ArchAssets `protobuf:"bytes,2,rep,name=assets,proto3" json:"assets,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // arch -> {name -> file} + state protoimpl.MessageState `protogen:"open.v1"` + // TODO: send this as an enum, like ateapi's and ateom's SandboxClass, so + // the tags can bound it and the custom validation goes away. + // + // +k8s:required + // +k8s:maxLength=63 # guardrail; the class names are a few characters + // +k8s:customValidation # gvisor or microvm + SandboxClass string `protobuf:"bytes,1,opt,name=sandbox_class,json=sandboxClass,proto3" json:"sandbox_class,omitempty"` // e.g. "gvisor" + // +k8s:required + // +k8s:maxProperties=8 # one entry per architecture + // +k8s:eachKey=+k8s:maxLength=16 + Assets map[string]*ArchAssets `protobuf:"bytes,2,rep,name=assets,proto3" json:"assets,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // arch -> {name -> file} // pause_image is the image for the sandbox's root container. Like the // binaries above it is sandbox configuration, not workload configuration, // and atelet pins it into the snapshot manifest so a restore rebuilds the // sandbox from the same image. Empty for sandboxes without a pause // container (microvm). + // + // +k8s:optional + // +k8s:maxLength=512 + // +k8s:customValidation # must be a well-formed image reference, pinned by digest PauseImage string `protobuf:"bytes,3,opt,name=pause_image,json=pauseImage,proto3" json:"pause_image,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1409,10 +1483,20 @@ func (x *SandboxAssets) GetPauseImage() string { } // WorkloadSpec parallels Pod, but with far fewer configurable fields. +// +// +k8s:customValidation # every volume mount names a declared volume type WorkloadSpec struct { - state protoimpl.MessageState `protogen:"open.v1"` - Containers []*Container `protobuf:"bytes,1,rep,name=containers,proto3" json:"containers,omitempty"` - Volumes []*Volume `protobuf:"bytes,2,rep,name=volumes,proto3" json:"volumes,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // +k8s:optional + // +k8s:maxItems=10 # matches the template's containers bound + // +k8s:listType=map + // +k8s:listMapKey=name + Containers []*Container `protobuf:"bytes,1,rep,name=containers,proto3" json:"containers,omitempty"` + // +k8s:optional + // +k8s:maxItems=32 # matches the template's volumes bound + // +k8s:listType=map + // +k8s:listMapKey=name + Volumes []*Volume `protobuf:"bytes,2,rep,name=volumes,proto3" json:"volumes,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1498,12 +1582,29 @@ func (*DurableDirVolume) Descriptor() ([]byte, []int) { } type ExternalVolumeSource struct { - state protoimpl.MessageState `protogen:"open.v1"` - StorageVolumeId string `protobuf:"bytes,1,opt,name=storage_volume_id,json=storageVolumeId,proto3" json:"storage_volume_id,omitempty"` - VolumeType string `protobuf:"bytes,2,opt,name=volume_type,json=volumeType,proto3" json:"volume_type,omitempty"` - VolumeContext map[string]string `protobuf:"bytes,3,rep,name=volume_context,json=volumeContext,proto3" json:"volume_context,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + state protoimpl.MessageState `protogen:"open.v1"` + // Required, unlike the actor status counterpart: the control plane only + // sends volumes that finished provisioning. + // + // +k8s:required + // +k8s:maxLength=256 # matches ExternalVolume.storage_volume_id's bound + // +k8s:customValidation # no control characters + StorageVolumeId string `protobuf:"bytes,1,opt,name=storage_volume_id,json=storageVolumeId,proto3" json:"storage_volume_id,omitempty"` + // +k8s:optional + // +k8s:maxLength=253 # matches ExternalVolume.volume_type's bound + // +k8s:customValidation # optional "substrate.io/" prefix + DNS subdomain + VolumeType string `protobuf:"bytes,2,opt,name=volume_type,json=volumeType,proto3" json:"volume_type,omitempty"` + // volume_context is the driver's provisioning metadata. Its entries are the + // driver's own, so the map is only bounded, not validated. + // + // +k8s:optional + // +k8s:customValidation # at most 40 KiB in total, ten times the CSI limit for map fields + VolumeContext map[string]string `protobuf:"bytes,3,rep,name=volume_context,json=volumeContext,proto3" json:"volume_context,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // publish_context is the driver's attachment metadata for this node, which - // the node plugin needs to complete the mount. + // the node plugin needs to complete the mount. Bounded like volume_context. + // + // +k8s:optional + // +k8s:customValidation # at most 40 KiB in total, ten times the CSI limit for map fields PublishContext map[string]string `protobuf:"bytes,4,rep,name=publish_context,json=publishContext,proto3" json:"publish_context,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1568,8 +1669,13 @@ func (x *ExternalVolumeSource) GetPublishContext() map[string]string { } type ImageVolumeSource struct { - state protoimpl.MessageState `protogen:"open.v1"` - Reference string `protobuf:"bytes,1,opt,name=reference,proto3" json:"reference,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // reference is the OCI image reference, pinned by digest. + // + // +k8s:required + // +k8s:maxLength=512 # matches the template ImageVolumeSource.reference's bound + // +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 } @@ -1614,9 +1720,19 @@ func (x *ImageVolumeSource) GetReference() string { // ActorMetadataItem projects one actor identity field to one file at the // given path, relative to the root of the enclosing system-info volume. type ActorMetadataItem struct { - state protoimpl.MessageState `protogen:"open.v1"` - Field ActorMetadataField `protobuf:"varint,1,opt,name=field,proto3,enum=atelet.ActorMetadataField" json:"field,omitempty"` - Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3 # keep this in sync with the ActorMetadataField enum + Field ActorMetadataField `protobuf:"varint,1,opt,name=field,proto3,enum=atelet.ActorMetadataField" json:"field,omitempty"` + // path must be a clean relative Unix path; the rule is shared with the + // control plane through internal/resources. + // + // +k8s:required + // +k8s:minLength=1 + // +k8s:maxLength=255 + // +k8s:customValidation # clean relative path + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1668,8 +1784,16 @@ func (x *ActorMetadataItem) GetPath() string { // ActorMetadataDataSource projects the actor's identity fields to files, one // per item. Values are written raw with no trailing newline. type ActorMetadataDataSource struct { - state protoimpl.MessageState `protogen:"open.v1"` - Items []*ActorMetadataItem `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // items must not project the same field twice. + // + // +k8s:required + // +k8s:minItems=1 + // +k8s:maxItems=8 # matches the template's items bound + // +k8s:listType=atomic + // +k8s:unique=map + // +k8s:listMapKey=field + Items []*ActorMetadataItem `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1713,9 +1837,22 @@ func (x *ActorMetadataDataSource) GetItems() []*ActorMetadataItem { // TrustBundleDataSource corresponds to TrustBundleDataSource in ateapi.proto type TrustBundleDataSource struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Names []string `protobuf:"bytes,3,rep,name=names,proto3" json:"names,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // path must be a clean relative Unix path; the rule is shared with the + // control plane through internal/resources. + // + // +k8s:required + // +k8s:minLength=1 + // +k8s:maxLength=255 + // +k8s:customValidation # clean relative path + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + // +k8s:required + // +k8s:minItems=1 + // +k8s:maxItems=8 # matches the template's names bound + // +k8s:listType=set + // +k8s:eachVal=+k8s:minLength=1 + // +k8s:eachVal=+k8s:maxLength=253 # matches the template's bundle-name bound + Names []string `protobuf:"bytes,3,rep,name=names,proto3" json:"names,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1764,13 +1901,18 @@ func (x *TrustBundleDataSource) GetNames() []string { return nil } +// SystemInfoDataSource selects exactly one projection to place in the +// volume. type SystemInfoDataSource struct { state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to DataSource: + // Exactly one of actor_metadata / trust_bundle must be set. // - // *SystemInfoDataSource_ActorMetadata - // *SystemInfoDataSource_TrustBundle - DataSource isSystemInfoDataSource_DataSource `protobuf_oneof:"data_source"` + // +k8s:optional + // +k8s:unionMember + ActorMetadata *ActorMetadataDataSource `protobuf:"bytes,1,opt,name=actor_metadata,json=actorMetadata,proto3" json:"actor_metadata,omitempty"` + // +k8s:optional + // +k8s:unionMember + TrustBundle *TrustBundleDataSource `protobuf:"bytes,2,opt,name=trust_bundle,json=trustBundle,proto3" json:"trust_bundle,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1805,52 +1947,29 @@ func (*SystemInfoDataSource) Descriptor() ([]byte, []int) { return file_atelet_proto_rawDescGZIP(), []int{26} } -func (x *SystemInfoDataSource) GetDataSource() isSystemInfoDataSource_DataSource { - if x != nil { - return x.DataSource - } - return nil -} - func (x *SystemInfoDataSource) GetActorMetadata() *ActorMetadataDataSource { if x != nil { - if x, ok := x.DataSource.(*SystemInfoDataSource_ActorMetadata); ok { - return x.ActorMetadata - } + return x.ActorMetadata } return nil } func (x *SystemInfoDataSource) GetTrustBundle() *TrustBundleDataSource { if x != nil { - if x, ok := x.DataSource.(*SystemInfoDataSource_TrustBundle); ok { - return x.TrustBundle - } + return x.TrustBundle } return nil } -type isSystemInfoDataSource_DataSource interface { - isSystemInfoDataSource_DataSource() -} - -type SystemInfoDataSource_ActorMetadata struct { - ActorMetadata *ActorMetadataDataSource `protobuf:"bytes,1,opt,name=actor_metadata,json=actorMetadata,proto3,oneof"` -} - -type SystemInfoDataSource_TrustBundle struct { - TrustBundle *TrustBundleDataSource `protobuf:"bytes,2,opt,name=trust_bundle,json=trustBundle,proto3,oneof"` -} - -func (*SystemInfoDataSource_ActorMetadata) isSystemInfoDataSource_DataSource() {} - -func (*SystemInfoDataSource_TrustBundle) isSystemInfoDataSource_DataSource() {} - // SystemInfoVolume is a read-only volume whose files are generated by atelet // on every Run/Restore, so they carry the values of the actor actually being // started, whatever checkpointed state it boots from. type SystemInfoVolume struct { - state protoimpl.MessageState `protogen:"open.v1"` + state protoimpl.MessageState `protogen:"open.v1"` + // +k8s:optional + // +k8s:maxItems=8 # matches the template's data_sources bound + // +k8s:listType=atomic + // +k8s:customValidation # at most one actor_metadata; paths unique across entries DataSources []*SystemInfoDataSource `protobuf:"bytes,1,rep,name=data_sources,json=dataSources,proto3" json:"data_sources,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1893,16 +2012,26 @@ func (x *SystemInfoVolume) GetDataSources() []*SystemInfoDataSource { return nil } +// Volume names one volume and selects exactly one source for it. type Volume struct { state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Types that are valid to be assigned to Source: - // - // *Volume_DurableDir - // *Volume_External - // *Volume_SystemInfo - // *Volume_Image - Source isVolume_Source `protobuf_oneof:"source"` + // +k8s:required + // +k8s:format=k8s-short-name + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Exactly one of durable_dir / external / system_info / image must be set. + // + // +k8s:optional + // +k8s:unionMember + DurableDir *DurableDirVolume `protobuf:"bytes,2,opt,name=durable_dir,json=durableDir,proto3" json:"durable_dir,omitempty"` + // +k8s:optional + // +k8s:unionMember + External *ExternalVolumeSource `protobuf:"bytes,3,opt,name=external,proto3" json:"external,omitempty"` + // +k8s:optional + // +k8s:unionMember + SystemInfo *SystemInfoVolume `protobuf:"bytes,4,opt,name=system_info,json=systemInfo,proto3" json:"system_info,omitempty"` + // +k8s:optional + // +k8s:unionMember + Image *ImageVolumeSource `protobuf:"bytes,5,opt,name=image,proto3" json:"image,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1944,81 +2073,47 @@ func (x *Volume) GetName() string { return "" } -func (x *Volume) GetSource() isVolume_Source { - if x != nil { - return x.Source - } - return nil -} - func (x *Volume) GetDurableDir() *DurableDirVolume { if x != nil { - if x, ok := x.Source.(*Volume_DurableDir); ok { - return x.DurableDir - } + return x.DurableDir } return nil } func (x *Volume) GetExternal() *ExternalVolumeSource { if x != nil { - if x, ok := x.Source.(*Volume_External); ok { - return x.External - } + return x.External } return nil } func (x *Volume) GetSystemInfo() *SystemInfoVolume { if x != nil { - if x, ok := x.Source.(*Volume_SystemInfo); ok { - return x.SystemInfo - } + return x.SystemInfo } return nil } func (x *Volume) GetImage() *ImageVolumeSource { if x != nil { - if x, ok := x.Source.(*Volume_Image); ok { - return x.Image - } + return x.Image } return nil } -type isVolume_Source interface { - isVolume_Source() -} - -type Volume_DurableDir struct { - DurableDir *DurableDirVolume `protobuf:"bytes,2,opt,name=durable_dir,json=durableDir,proto3,oneof"` -} - -type Volume_External struct { - External *ExternalVolumeSource `protobuf:"bytes,3,opt,name=external,proto3,oneof"` -} - -type Volume_SystemInfo struct { - SystemInfo *SystemInfoVolume `protobuf:"bytes,4,opt,name=system_info,json=systemInfo,proto3,oneof"` -} - -type Volume_Image struct { - Image *ImageVolumeSource `protobuf:"bytes,5,opt,name=image,proto3,oneof"` -} - -func (*Volume_DurableDir) isVolume_Source() {} - -func (*Volume_External) isVolume_Source() {} - -func (*Volume_SystemInfo) isVolume_Source() {} - -func (*Volume_Image) isVolume_Source() {} - type VolumeMount struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - MountPath string `protobuf:"bytes,2,opt,name=mount_path,json=mountPath,proto3" json:"mount_path,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // name must match the name of a Volume. + // + // +k8s:required + // +k8s:format=k8s-short-name + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // mount_path within the container. Must be a clean absolute Unix path. + // + // +k8s:required + // +k8s:maxLength=4096 + // +k8s:customValidation # clean-absolute-path shape; no regex/pattern tag exists + MountPath string `protobuf:"bytes,2,opt,name=mount_path,json=mountPath,proto3" json:"mount_path,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2068,17 +2163,46 @@ func (x *VolumeMount) GetMountPath() string { } type Container struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Image string `protobuf:"bytes,2,opt,name=image,proto3" json:"image,omitempty"` - Command []string `protobuf:"bytes,3,rep,name=command,proto3" json:"command,omitempty"` - Args []string `protobuf:"bytes,7,rep,name=args,proto3" json:"args,omitempty"` - Env []*EnvEntry `protobuf:"bytes,4,rep,name=env,proto3" json:"env,omitempty"` - WakeupProbe *WakeupProbe `protobuf:"bytes,5,opt,name=wakeup_probe,json=wakeupProbe,proto3" json:"wakeup_probe,omitempty"` - VolumeMounts []*VolumeMount `protobuf:"bytes,6,rep,name=volume_mounts,json=volumeMounts,proto3" json:"volume_mounts,omitempty"` - SecurityContext *SecurityContext `protobuf:"bytes,8,opt,name=security_context,json=securityContext,proto3" json:"security_context,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // +k8s:required + // +k8s:format=k8s-short-name + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // +k8s:required + // +k8s:maxLength=512 # matches the template Container.image's bound + // +k8s:customValidation # must be a well-formed image reference, pinned by digest + Image string `protobuf:"bytes,2,opt,name=image,proto3" json:"image,omitempty"` + // +k8s:optional + // +k8s:maxItems=64 + // +k8s:listType=atomic + // +k8s:eachVal=+k8s:maxLength=4096 # argv strings; guardrail, not a contract + Command []string `protobuf:"bytes,3,rep,name=command,proto3" json:"command,omitempty"` + // +k8s:optional + // +k8s:maxItems=64 + // +k8s:listType=atomic + // +k8s:eachVal=+k8s:maxLength=4096 # argv strings; guardrail, not a contract + Args []string `protobuf:"bytes,7,rep,name=args,proto3" json:"args,omitempty"` + // +k8s:optional + // +k8s:maxItems=32 # matches the template's env bound + // +k8s:listType=map # each variable is set at most once + // +k8s:listMapKey=name + Env []*EnvEntry `protobuf:"bytes,4,rep,name=env,proto3" json:"env,omitempty"` + // +k8s:optional + WakeupProbe *WakeupProbe `protobuf:"bytes,5,opt,name=wakeup_probe,json=wakeupProbe,proto3" json:"wakeup_probe,omitempty"` + // Keyed by mount_path: each path hosts exactly one mount, while a volume + // may be mounted at multiple paths. + // + // +k8s:optional + // +k8s:maxItems=32 # matches the template's volume_mounts bound + // +k8s:listType=map + // +k8s:listMapKey=mount_path + // +k8s:customValidation # mounts must not nest + VolumeMounts []*VolumeMount `protobuf:"bytes,6,rep,name=volume_mounts,json=volumeMounts,proto3" json:"volume_mounts,omitempty"` + // +k8s:optional + SecurityContext *SecurityContext `protobuf:"bytes,8,opt,name=security_context,json=securityContext,proto3" json:"security_context,omitempty"` // resources are the cgroup limits for this container, resolved by // ate-api-server from the ActorTemplate. Unset means no limits. + // + // +k8s:optional Resources *ResourceLimits `protobuf:"bytes,9,opt,name=resources,proto3" json:"resources,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2179,8 +2303,9 @@ func (x *Container) GetResources() *ResourceLimits { // SecurityContext holds security settings for a container's process. type SecurityContext struct { - state protoimpl.MessageState `protogen:"open.v1"` - Capabilities *Capabilities `protobuf:"bytes,1,opt,name=capabilities,proto3" json:"capabilities,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // +k8s:optional + Capabilities *Capabilities `protobuf:"bytes,1,opt,name=capabilities,proto3" json:"capabilities,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2225,9 +2350,17 @@ func (x *SecurityContext) GetCapabilities() *Capabilities { // Capabilities adjusts a container's Linux capabilities relative to the default // set. Names carry no "CAP_" prefix; drop applies before add. type Capabilities struct { - state protoimpl.MessageState `protogen:"open.v1"` - Add []string `protobuf:"bytes,1,rep,name=add,proto3" json:"add,omitempty"` - Drop []string `protobuf:"bytes,2,rep,name=drop,proto3" json:"drop,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // +k8s:optional + // +k8s:maxItems=64 + // +k8s:listType=set + // +k8s:customValidation # capability grammar; "ALL" not accepted + Add []string `protobuf:"bytes,1,rep,name=add,proto3" json:"add,omitempty"` + // +k8s:optional + // +k8s:maxItems=64 + // +k8s:listType=set + // +k8s:customValidation # capability grammar + Drop []string `protobuf:"bytes,2,rep,name=drop,proto3" json:"drop,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2281,8 +2414,15 @@ func (x *Capabilities) GetDrop() []string { type ResourceLimits struct { state protoimpl.MessageState `protogen:"open.v1"` // memory_bytes is the memory limit in bytes. 0 means unset. + // + // +k8s:optional + // +k8s:minimum=0 MemoryBytes int64 `protobuf:"varint,1,opt,name=memory_bytes,json=memoryBytes,proto3" json:"memory_bytes,omitempty"` // cpu_millis is the CPU limit in milli-cores (1000 = one core). 0 means unset. + // + // +k8s:optional + // +k8s:minimum=0 + // +k8s:maximum=999999 # the control plane caps cpu limits strictly below 1000 cores CpuMillis int64 `protobuf:"varint,2,opt,name=cpu_millis,json=cpuMillis,proto3" json:"cpu_millis,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2333,9 +2473,16 @@ func (x *ResourceLimits) GetCpuMillis() int64 { } type EnvEntry struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // name may be any printable ASCII character except '='. + // + // +k8s:required + // +k8s:maxLength=256 # guardrail + // +k8s:customValidation # printable ASCII except '=' + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // +k8s:optional + // +k8s:maxLength=32768 # guardrail + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2387,9 +2534,14 @@ func (x *EnvEntry) GetValue() string { // WakeupProbe describes how to check that a container is ready to serve. // Only HTTP is supported today. type WakeupProbe struct { - state protoimpl.MessageState `protogen:"open.v1"` - HttpGet *HttpGetAction `protobuf:"bytes,1,opt,name=http_get,json=httpGet,proto3" json:"http_get,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // +k8s:required + HttpGet *HttpGetAction `protobuf:"bytes,1,opt,name=http_get,json=httpGet,proto3" json:"http_get,omitempty"` // How long to keep polling before giving up and failing the actor start. + // + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3600 # matches the template wakeup probe's bound TimeoutSeconds int32 `protobuf:"varint,2,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2443,8 +2595,16 @@ func (x *WakeupProbe) GetTimeoutSeconds() int32 { type HttpGetAction struct { state protoimpl.MessageState `protogen:"open.v1"` // Path to access on the HTTP server. + // + // +k8s:required + // +k8s:maxLength=1024 # matches the template probe path's bound + // +k8s:customValidation # RFC 3986 path shape; no regex/pattern tag exists Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` // TCP port to connect to (1..65535). + // + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=65535 Port int32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2536,6 +2696,9 @@ type LocalCheckpointConfiguration struct { // data will be stored. atelet decides where that directory lives, from the // actor's UID, so this is a bare name and must not contain a path separator. // The structure of the checkpoint should generally be treated as opaque. + // + // +k8s:required + // +k8s:format=k8s-short-name SnapshotName string `protobuf:"bytes,1,opt,name=snapshot_name,json=snapshotName,proto3" json:"snapshot_name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2582,6 +2745,10 @@ type ExternalCheckpointConfiguration struct { state protoimpl.MessageState `protogen:"open.v1"` // The object storage URI of the snapshot to write. Object names are appended // to it, so it addresses the snapshot as a whole rather than any one object. + // + // +k8s:required + // +k8s:maxLength=2048 + // +k8s:customValidation # a parseable snapshot URI SnapshotUri string `protobuf:"bytes,1,opt,name=snapshot_uri,json=snapshotUri,proto3" json:"snapshot_uri,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2632,6 +2799,10 @@ type ExternalRestoreConfiguration struct { // The object storage URI of the snapshot to read. Object names are // appended to it, so it addresses the snapshot as a whole rather than any // one object. + // + // +k8s:required + // +k8s:maxLength=2048 + // +k8s:customValidation # a parseable snapshot URI SnapshotUri string `protobuf:"bytes,1,opt,name=snapshot_uri,json=snapshotUri,proto3" json:"snapshot_uri,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2674,27 +2845,56 @@ func (x *ExternalRestoreConfiguration) GetSnapshotUri() string { return "" } +// +k8s:customValidation # the set config matches type type CheckpointRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - WorkerPodUid string `protobuf:"bytes,1,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` - Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` - ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` - ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` - ActorTemplateAtespace string `protobuf:"bytes,5,opt,name=actor_template_atespace,json=actorTemplateAtespace,proto3" json:"actor_template_atespace,omitempty"` - ActorTemplateName string `protobuf:"bytes,6,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // The UID of the worker pod whose ateom hosts the actor. + // + // +k8s:required + // +k8s:format=k8s-uuid + WorkerPodUid string `protobuf:"bytes,1,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` + // +k8s:required + // +k8s:format=k8s-uuid + ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` + // The template identity is carried for metrics attribution. + // + // +k8s:optional + // +k8s:format=k8s-short-name + ActorTemplateAtespace string `protobuf:"bytes,5,opt,name=actor_template_atespace,json=actorTemplateAtespace,proto3" json:"actor_template_atespace,omitempty"` + // +k8s:optional + // +k8s:format=k8s-short-name + ActorTemplateName string `protobuf:"bytes,6,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` // Sandbox binary config is not sent on checkpoint: atelet uses the version the // actor is currently running (recorded with the actor's on-node state at // Run/Restore) and records it into the snapshot manifest. - Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` + // + // +k8s:required + Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=2 # keep this in sync with the CheckpointType enum Type CheckpointType `protobuf:"varint,8,opt,name=type,proto3,enum=atelet.CheckpointType" json:"type,omitempty"` - // The checkpoint configuration, depending on the type. + // The checkpoint configuration: local_config when type is LOCAL, + // external_config when it is EXTERNAL. Exactly one is set. // - // Types that are valid to be assigned to Config: + // +k8s:optional + // +k8s:unionMember + LocalConfig *LocalCheckpointConfiguration `protobuf:"bytes,9,opt,name=local_config,json=localConfig,proto3" json:"local_config,omitempty"` + // +k8s:optional + // +k8s:unionMember + ExternalConfig *ExternalCheckpointConfiguration `protobuf:"bytes,10,opt,name=external_config,json=externalConfig,proto3" json:"external_config,omitempty"` + // Fidelity the checkpoint captures. Which levels a runtime can serve is the + // runtime's call: atelet forwards it and ateom rejects what it cannot capture. // - // *CheckpointRequest_LocalConfig - // *CheckpointRequest_ExternalConfig - Config isCheckpointRequest_Config `protobuf_oneof:"config"` - // Fidelity the checkpoint captures. + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3 # keep this in sync with the SnapshotFidelity enum Fidelity SnapshotFidelity `protobuf:"varint,11,opt,name=fidelity,proto3,enum=atelet.SnapshotFidelity" json:"fidelity,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2786,27 +2986,16 @@ func (x *CheckpointRequest) GetType() CheckpointType { return CheckpointType_CHECKPOINT_TYPE_UNSPECIFIED } -func (x *CheckpointRequest) GetConfig() isCheckpointRequest_Config { - if x != nil { - return x.Config - } - return nil -} - func (x *CheckpointRequest) GetLocalConfig() *LocalCheckpointConfiguration { if x != nil { - if x, ok := x.Config.(*CheckpointRequest_LocalConfig); ok { - return x.LocalConfig - } + return x.LocalConfig } return nil } func (x *CheckpointRequest) GetExternalConfig() *ExternalCheckpointConfiguration { if x != nil { - if x, ok := x.Config.(*CheckpointRequest_ExternalConfig); ok { - return x.ExternalConfig - } + return x.ExternalConfig } return nil } @@ -2818,22 +3007,6 @@ func (x *CheckpointRequest) GetFidelity() SnapshotFidelity { return SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED } -type isCheckpointRequest_Config interface { - isCheckpointRequest_Config() -} - -type CheckpointRequest_LocalConfig struct { - LocalConfig *LocalCheckpointConfiguration `protobuf:"bytes,9,opt,name=local_config,json=localConfig,proto3,oneof"` -} - -type CheckpointRequest_ExternalConfig struct { - ExternalConfig *ExternalCheckpointConfiguration `protobuf:"bytes,10,opt,name=external_config,json=externalConfig,proto3,oneof"` -} - -func (*CheckpointRequest_LocalConfig) isCheckpointRequest_Config() {} - -func (*CheckpointRequest_ExternalConfig) isCheckpointRequest_Config() {} - type CheckpointResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -2871,22 +3044,47 @@ func (*CheckpointResponse) Descriptor() ([]byte, []int) { } type UploadPausedCheckpointRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Atespace string `protobuf:"bytes,1,opt,name=atespace,proto3" json:"atespace,omitempty"` - ActorName string `protobuf:"bytes,2,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` - ActorUid string `protobuf:"bytes,3,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // Golden actors are never paused, so the golden atespace is rejected. + // + // +k8s:required + // +k8s:format=k8s-short-name + // +k8s:customValidation # not the golden atespace + Atespace string `protobuf:"bytes,1,opt,name=atespace,proto3" json:"atespace,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + ActorName string `protobuf:"bytes,2,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` + // +k8s:required + // +k8s:format=k8s-uuid + ActorUid string `protobuf:"bytes,3,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` // For metrics attribution, like on CheckpointRequest. + // + // +k8s:optional + // +k8s:format=k8s-short-name ActorTemplateAtespace string `protobuf:"bytes,4,opt,name=actor_template_atespace,json=actorTemplateAtespace,proto3" json:"actor_template_atespace,omitempty"` - ActorTemplateName string `protobuf:"bytes,5,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` + // +k8s:optional + // +k8s:format=k8s-short-name + ActorTemplateName string `protobuf:"bytes,5,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` // The local checkpoint to upload: the uuid of the actor's pause snapshot - // (ateapi Snapshot.uuid). + // (ateapi Snapshot.uuid, validated as a short name there too). + // + // +k8s:required + // +k8s:format=k8s-short-name LocalSnapshotName string `protobuf:"bytes,6,opt,name=local_snapshot_name,json=localSnapshotName,proto3" json:"local_snapshot_name,omitempty"` // Destination object-storage URI (the actor's in-progress snapshot URI). + // + // +k8s:required + // +k8s:maxLength=2048 + // +k8s:customValidation # a parseable snapshot URI DestinationSnapshotUri string `protobuf:"bytes,7,opt,name=destination_snapshot_uri,json=destinationSnapshotUri,proto3" json:"destination_snapshot_uri,omitempty"` // Fidelity the uploaded snapshot must have. The fidelity the pause // checkpoint captured is not sent: atelet reads it from the local // snapshot's own manifest, which is authoritative, and rejects the upload // if the two differ. + // + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3 # keep this in sync with the SnapshotFidelity enum DesiredFidelity SnapshotFidelity `protobuf:"varint,8,opt,name=desired_fidelity,json=desiredFidelity,proto3,enum=atelet.SnapshotFidelity" json:"desired_fidelity,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -3014,35 +3212,72 @@ func (*UploadPausedCheckpointResponse) Descriptor() ([]byte, []int) { return file_atelet_proto_rawDescGZIP(), []int{44} } +// +k8s:customValidation # the set config matches type type RestoreRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - WorkerPodUid string `protobuf:"bytes,1,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` - Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` - ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` - ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` - ActorTemplateAtespace string `protobuf:"bytes,5,opt,name=actor_template_atespace,json=actorTemplateAtespace,proto3" json:"actor_template_atespace,omitempty"` - ActorTemplateName string `protobuf:"bytes,6,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` - Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` - Type CheckpointType `protobuf:"varint,8,opt,name=type,proto3,enum=atelet.CheckpointType" json:"type,omitempty"` - // The checkpoint configuration, depending on the type. - // - // Types that are valid to be assigned to Config: - // - // *RestoreRequest_LocalConfig - // *RestoreRequest_ExternalConfig - Config isRestoreRequest_Config `protobuf_oneof:"config"` + state protoimpl.MessageState `protogen:"open.v1"` + // The UID of the worker pod whose ateom hosts the actor. + // + // +k8s:required + // +k8s:format=k8s-uuid + WorkerPodUid string `protobuf:"bytes,1,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` + // +k8s:required + // +k8s:format=k8s-uuid + ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` + // The template identity is carried for metrics attribution. + // + // +k8s:optional + // +k8s:format=k8s-short-name + ActorTemplateAtespace string `protobuf:"bytes,5,opt,name=actor_template_atespace,json=actorTemplateAtespace,proto3" json:"actor_template_atespace,omitempty"` + // +k8s:optional + // +k8s:format=k8s-short-name + ActorTemplateName string `protobuf:"bytes,6,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` + // +k8s:required + Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=2 # keep this in sync with the CheckpointType enum + Type CheckpointType `protobuf:"varint,8,opt,name=type,proto3,enum=atelet.CheckpointType" json:"type,omitempty"` + // The checkpoint configuration: local_config when type is LOCAL, + // external_config when it is EXTERNAL. Exactly one is set. + // + // +k8s:optional + // +k8s:unionMember + LocalConfig *LocalCheckpointConfiguration `protobuf:"bytes,9,opt,name=local_config,json=localConfig,proto3" json:"local_config,omitempty"` + // +k8s:optional + // +k8s:unionMember + ExternalConfig *ExternalRestoreConfiguration `protobuf:"bytes,10,opt,name=external_config,json=externalConfig,proto3" json:"external_config,omitempty"` // Fidelity to restore from the checkpoint. + // + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3 # keep this in sync with the SnapshotFidelity enum Fidelity SnapshotFidelity `protobuf:"varint,11,opt,name=fidelity,proto3,enum=atelet.SnapshotFidelity" json:"fidelity,omitempty"` // When absent the actor has no egress: its TCP is captured and refused. + // + // +k8s:optional EgressGateway *EgressGateway `protobuf:"bytes,13,opt,name=egress_gateway,json=egressGateway,proto3,oneof" json:"egress_gateway,omitempty"` // The actor's declared size, from the ActorTemplate's resource limits. For // gVisor and micro-VM VOLUMES restores the sandbox is (re)sized to these; // for a MEMORY micro-VM restore the size baked into the snapshot wins. Zero // means "unset": keep the runtime default. - CpuMilli int64 `protobuf:"varint,14,opt,name=cpu_milli,json=cpuMilli,proto3" json:"cpu_milli,omitempty"` // CPU limit in millicores (1000 = one core). + // + // +k8s:optional + // +k8s:minimum=0 + // +k8s:maximum=999999 # the control plane caps cpu limits strictly below 1000 cores + CpuMilli int64 `protobuf:"varint,14,opt,name=cpu_milli,json=cpuMilli,proto3" json:"cpu_milli,omitempty"` // CPU limit in millicores (1000 = one core). + // +k8s:optional + // +k8s:minimum=0 MemoryBytes int64 `protobuf:"varint,15,opt,name=memory_bytes,json=memoryBytes,proto3" json:"memory_bytes,omitempty"` // Memory limit in bytes. // The sandbox binaries and pause image to restore with, resolved from the - // ActorTemplate's SandboxConfig. Required. + // ActorTemplate's SandboxConfig. + // + // +k8s:required SandboxAssets *SandboxAssets `protobuf:"bytes,16,opt,name=sandbox_assets,json=sandboxAssets,proto3" json:"sandbox_assets,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -3134,27 +3369,16 @@ func (x *RestoreRequest) GetType() CheckpointType { return CheckpointType_CHECKPOINT_TYPE_UNSPECIFIED } -func (x *RestoreRequest) GetConfig() isRestoreRequest_Config { - if x != nil { - return x.Config - } - return nil -} - func (x *RestoreRequest) GetLocalConfig() *LocalCheckpointConfiguration { if x != nil { - if x, ok := x.Config.(*RestoreRequest_LocalConfig); ok { - return x.LocalConfig - } + return x.LocalConfig } return nil } func (x *RestoreRequest) GetExternalConfig() *ExternalRestoreConfiguration { if x != nil { - if x, ok := x.Config.(*RestoreRequest_ExternalConfig); ok { - return x.ExternalConfig - } + return x.ExternalConfig } return nil } @@ -3194,22 +3418,6 @@ func (x *RestoreRequest) GetSandboxAssets() *SandboxAssets { return nil } -type isRestoreRequest_Config interface { - isRestoreRequest_Config() -} - -type RestoreRequest_LocalConfig struct { - LocalConfig *LocalCheckpointConfiguration `protobuf:"bytes,9,opt,name=local_config,json=localConfig,proto3,oneof"` -} - -type RestoreRequest_ExternalConfig struct { - ExternalConfig *ExternalRestoreConfiguration `protobuf:"bytes,10,opt,name=external_config,json=externalConfig,proto3,oneof"` -} - -func (*RestoreRequest_LocalConfig) isRestoreRequest_Config() {} - -func (*RestoreRequest_ExternalConfig) isRestoreRequest_Config() {} - type RestoreResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -3362,22 +3570,20 @@ const file_atelet_proto_rawDesc = "" + "\x05items\x18\x01 \x03(\v2\x19.atelet.ActorMetadataItemR\x05items\"M\n" + "\x15TrustBundleDataSource\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12\x14\n" + - "\x05names\x18\x03 \x03(\tR\x05namesJ\x04\b\x02\x10\x03R\x04name\"\xb3\x01\n" + - "\x14SystemInfoDataSource\x12H\n" + - "\x0eactor_metadata\x18\x01 \x01(\v2\x1f.atelet.ActorMetadataDataSourceH\x00R\ractorMetadata\x12B\n" + - "\ftrust_bundle\x18\x02 \x01(\v2\x1d.atelet.TrustBundleDataSourceH\x00R\vtrustBundleB\r\n" + - "\vdata_source\"S\n" + + "\x05names\x18\x03 \x03(\tR\x05namesJ\x04\b\x02\x10\x03R\x04name\"\xa0\x01\n" + + "\x14SystemInfoDataSource\x12F\n" + + "\x0eactor_metadata\x18\x01 \x01(\v2\x1f.atelet.ActorMetadataDataSourceR\ractorMetadata\x12@\n" + + "\ftrust_bundle\x18\x02 \x01(\v2\x1d.atelet.TrustBundleDataSourceR\vtrustBundle\"S\n" + "\x10SystemInfoVolume\x12?\n" + - "\fdata_sources\x18\x01 \x03(\v2\x1c.atelet.SystemInfoDataSourceR\vdataSources\"\x8f\x02\n" + + "\fdata_sources\x18\x01 \x03(\v2\x1c.atelet.SystemInfoDataSourceR\vdataSources\"\xfd\x01\n" + "\x06Volume\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12;\n" + - "\vdurable_dir\x18\x02 \x01(\v2\x18.atelet.DurableDirVolumeH\x00R\n" + - "durableDir\x12:\n" + - "\bexternal\x18\x03 \x01(\v2\x1c.atelet.ExternalVolumeSourceH\x00R\bexternal\x12;\n" + - "\vsystem_info\x18\x04 \x01(\v2\x18.atelet.SystemInfoVolumeH\x00R\n" + - "systemInfo\x121\n" + - "\x05image\x18\x05 \x01(\v2\x19.atelet.ImageVolumeSourceH\x00R\x05imageB\b\n" + - "\x06source\"@\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x129\n" + + "\vdurable_dir\x18\x02 \x01(\v2\x18.atelet.DurableDirVolumeR\n" + + "durableDir\x128\n" + + "\bexternal\x18\x03 \x01(\v2\x1c.atelet.ExternalVolumeSourceR\bexternal\x129\n" + + "\vsystem_info\x18\x04 \x01(\v2\x18.atelet.SystemInfoVolumeR\n" + + "systemInfo\x12/\n" + + "\x05image\x18\x05 \x01(\v2\x19.atelet.ImageVolumeSourceR\x05image\"@\n" + "\vVolumeMount\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" + "\n" + @@ -3416,7 +3622,7 @@ const file_atelet_proto_rawDesc = "" + "\x1fExternalCheckpointConfiguration\x12!\n" + "\fsnapshot_uri\x18\x01 \x01(\tR\vsnapshotUri\"A\n" + "\x1cExternalRestoreConfiguration\x12!\n" + - "\fsnapshot_uri\x18\x01 \x01(\tR\vsnapshotUri\"\xae\x04\n" + + "\fsnapshot_uri\x18\x01 \x01(\tR\vsnapshotUri\"\xa0\x04\n" + "\x11CheckpointRequest\x12$\n" + "\x0eworker_pod_uid\x18\x01 \x01(\tR\fworkerPodUid\x12\x1a\n" + "\batespace\x18\x02 \x01(\tR\batespace\x12\x1d\n" + @@ -3426,12 +3632,11 @@ const file_atelet_proto_rawDesc = "" + "\x17actor_template_atespace\x18\x05 \x01(\tR\x15actorTemplateAtespace\x12.\n" + "\x13actor_template_name\x18\x06 \x01(\tR\x11actorTemplateName\x12(\n" + "\x04spec\x18\a \x01(\v2\x14.atelet.WorkloadSpecR\x04spec\x12*\n" + - "\x04type\x18\b \x01(\x0e2\x16.atelet.CheckpointTypeR\x04type\x12I\n" + - "\flocal_config\x18\t \x01(\v2$.atelet.LocalCheckpointConfigurationH\x00R\vlocalConfig\x12R\n" + + "\x04type\x18\b \x01(\x0e2\x16.atelet.CheckpointTypeR\x04type\x12G\n" + + "\flocal_config\x18\t \x01(\v2$.atelet.LocalCheckpointConfigurationR\vlocalConfig\x12P\n" + "\x0fexternal_config\x18\n" + - " \x01(\v2'.atelet.ExternalCheckpointConfigurationH\x00R\x0eexternalConfig\x124\n" + - "\bfidelity\x18\v \x01(\x0e2\x18.atelet.SnapshotFidelityR\bfidelityB\b\n" + - "\x06config\"\x14\n" + + " \x01(\v2'.atelet.ExternalCheckpointConfigurationR\x0eexternalConfig\x124\n" + + "\bfidelity\x18\v \x01(\x0e2\x18.atelet.SnapshotFidelityR\bfidelity\"\x14\n" + "\x12CheckpointResponse\"\x8e\x03\n" + "\x1dUploadPausedCheckpointRequest\x12\x1a\n" + "\batespace\x18\x01 \x01(\tR\batespace\x12\x1d\n" + @@ -3443,7 +3648,7 @@ const file_atelet_proto_rawDesc = "" + "\x13local_snapshot_name\x18\x06 \x01(\tR\x11localSnapshotName\x128\n" + "\x18destination_snapshot_uri\x18\a \x01(\tR\x16destinationSnapshotUri\x12C\n" + "\x10desired_fidelity\x18\b \x01(\x0e2\x18.atelet.SnapshotFidelityR\x0fdesiredFidelity\" \n" + - "\x1eUploadPausedCheckpointResponse\"\xfc\x05\n" + + "\x1eUploadPausedCheckpointResponse\"\xee\x05\n" + "\x0eRestoreRequest\x12$\n" + "\x0eworker_pod_uid\x18\x01 \x01(\tR\fworkerPodUid\x12\x1a\n" + "\batespace\x18\x02 \x01(\tR\batespace\x12\x1d\n" + @@ -3453,16 +3658,15 @@ const file_atelet_proto_rawDesc = "" + "\x17actor_template_atespace\x18\x05 \x01(\tR\x15actorTemplateAtespace\x12.\n" + "\x13actor_template_name\x18\x06 \x01(\tR\x11actorTemplateName\x12(\n" + "\x04spec\x18\a \x01(\v2\x14.atelet.WorkloadSpecR\x04spec\x12*\n" + - "\x04type\x18\b \x01(\x0e2\x16.atelet.CheckpointTypeR\x04type\x12I\n" + - "\flocal_config\x18\t \x01(\v2$.atelet.LocalCheckpointConfigurationH\x00R\vlocalConfig\x12O\n" + + "\x04type\x18\b \x01(\x0e2\x16.atelet.CheckpointTypeR\x04type\x12G\n" + + "\flocal_config\x18\t \x01(\v2$.atelet.LocalCheckpointConfigurationR\vlocalConfig\x12M\n" + "\x0fexternal_config\x18\n" + - " \x01(\v2$.atelet.ExternalRestoreConfigurationH\x00R\x0eexternalConfig\x124\n" + + " \x01(\v2$.atelet.ExternalRestoreConfigurationR\x0eexternalConfig\x124\n" + "\bfidelity\x18\v \x01(\x0e2\x18.atelet.SnapshotFidelityR\bfidelity\x12A\n" + - "\x0eegress_gateway\x18\r \x01(\v2\x15.atelet.EgressGatewayH\x01R\regressGateway\x88\x01\x01\x12\x1b\n" + + "\x0eegress_gateway\x18\r \x01(\v2\x15.atelet.EgressGatewayH\x00R\regressGateway\x88\x01\x01\x12\x1b\n" + "\tcpu_milli\x18\x0e \x01(\x03R\bcpuMilli\x12!\n" + "\fmemory_bytes\x18\x0f \x01(\x03R\vmemoryBytes\x12<\n" + - "\x0esandbox_assets\x18\x10 \x01(\v2\x15.atelet.SandboxAssetsR\rsandboxAssetsB\b\n" + - "\x06configB\x11\n" + + "\x0esandbox_assets\x18\x10 \x01(\v2\x15.atelet.SandboxAssetsR\rsandboxAssetsB\x11\n" + "\x0f_egress_gateway\"\x11\n" + "\x0fRestoreResponse*\x9a\x01\n" + "\x12ActorMetadataField\x12$\n" + @@ -3639,24 +3843,7 @@ func file_atelet_proto_init() { return } file_atelet_proto_msgTypes[14].OneofWrappers = []any{} - file_atelet_proto_msgTypes[26].OneofWrappers = []any{ - (*SystemInfoDataSource_ActorMetadata)(nil), - (*SystemInfoDataSource_TrustBundle)(nil), - } - file_atelet_proto_msgTypes[28].OneofWrappers = []any{ - (*Volume_DurableDir)(nil), - (*Volume_External)(nil), - (*Volume_SystemInfo)(nil), - (*Volume_Image)(nil), - } - file_atelet_proto_msgTypes[41].OneofWrappers = []any{ - (*CheckpointRequest_LocalConfig)(nil), - (*CheckpointRequest_ExternalConfig)(nil), - } - file_atelet_proto_msgTypes[45].OneofWrappers = []any{ - (*RestoreRequest_LocalConfig)(nil), - (*RestoreRequest_ExternalConfig)(nil), - } + file_atelet_proto_msgTypes[45].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/internal/proto/ateletpb/atelet.proto b/internal/proto/ateletpb/atelet.proto index 3a192351d4..6c13cb4e31 100644 --- a/internal/proto/ateletpb/atelet.proto +++ b/internal/proto/ateletpb/atelet.proto @@ -262,16 +262,39 @@ service AteomHerder { } message TerminateRequest { - // If unset, the atelet will just cleanup node resources for the actor. + // The UID of the worker pod whose ateom hosts the actor. If unset, atelet + // only cleans up the actor's node resources; it does not reach an ateom. + // + // +k8s:optional + // +k8s:format=k8s-uuid string worker_pod_uid = 1; + // +k8s:required + // +k8s:format=k8s-short-name string atespace = 2; + + // +k8s:required + // +k8s:format=k8s-short-name string actor_name = 3; + + // +k8s:required + // +k8s:format=k8s-uuid string actor_uid = 4; + // The template identity is carried for metrics attribution. + // + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_atespace = 5; + + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_name = 6; + // A nil spec is tolerated: the delete flow terminates template-less + // actors with a fallback spec. + // + // +k8s:optional WorkloadSpec spec = 7; } @@ -279,29 +302,60 @@ message TerminateResponse { } message RunRequest { + // The UID of the worker pod whose ateom hosts the actor. + // + // +k8s:required + // +k8s:format=k8s-uuid string worker_pod_uid = 1; + // +k8s:required + // +k8s:format=k8s-short-name string atespace = 2; + + // +k8s:required + // +k8s:format=k8s-short-name string actor_name = 3; + + // +k8s:required + // +k8s:format=k8s-uuid string actor_uid = 4; + // The template identity is carried for metrics attribution. + // + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_atespace = 5; + + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_name = 6; + // +k8s:required WorkloadSpec spec = 7; // The sandbox binaries to use for booting this actor from scratch. atelet // fetches the relevant assets and records them with the actor's on-node state // so a later Checkpoint can pin the same version into the snapshot manifest. + // + // +k8s:required SandboxAssets sandbox_assets = 8; // When absent the actor has no egress: its TCP is captured and refused. + // + // +k8s:optional optional EgressGateway egress_gateway = 9; // The actor's declared size, from the ActorTemplate's resource limits. atelet // passes these through to the sandbox so it is sized to the actor (not the // whole host or worker pod). Zero means "unset": keep the runtime default. - int64 cpu_milli = 10; // CPU limit in millicores (1000 = one core). + // + // +k8s:optional + // +k8s:minimum=0 + // +k8s:maximum=999999 # the control plane caps cpu limits strictly below 1000 cores + int64 cpu_milli = 10; // CPU limit in millicores (1000 = one core). + + // +k8s:optional + // +k8s:minimum=0 int64 memory_bytes = 11; // Memory limit in bytes. } @@ -309,6 +363,10 @@ message RunRequest { message EgressGateway { // address is required and identifies the remote gateway as an IP address or // DNS name followed by its port. + // + // +k8s:required + // +k8s:maxLength=261 # a bracketed or 253-character host, ':' and a 5-digit port + // +k8s:customValidation # host:port shape string address = 1; } @@ -320,14 +378,25 @@ message AssetFile { // the plugin cannot serve it. // Masked in logged requests, since a signed URL carries its credential in // the query. + // + // +k8s:required + // +k8s:maxLength=2048 # matches the SandboxConfig asset url's bound string url = 1 [ debug_redact = true ]; + // Lower-case hex SHA256; names the cached file and verifies its integrity. + // + // +k8s:required + // +k8s:maxLength=64 + // +k8s:customValidation # 64 hex characters string sha256 = 2; } // ArchAssets is the set of assets for a single architecture, keyed by asset // name (a wrapper message because proto map values cannot themselves be maps). message ArchAssets { + // +k8s:required + // +k8s:maxProperties=16 + // +k8s:eachKey=+k8s:maxLength=64 map files = 1; // asset name -> file } @@ -337,7 +406,17 @@ message ArchAssets { // atelet's backend code interprets the asset names (gVisor expects "gvisor", // the release tarball; legacy "runsc", a bare binary, is still accepted). message SandboxAssets { - string sandbox_class = 1; // e.g. "gvisor" + // TODO: send this as an enum, like ateapi's and ateom's SandboxClass, so + // the tags can bound it and the custom validation goes away. + // + // +k8s:required + // +k8s:maxLength=63 # guardrail; the class names are a few characters + // +k8s:customValidation # gvisor or microvm + string sandbox_class = 1; // e.g. "gvisor" + + // +k8s:required + // +k8s:maxProperties=8 # one entry per architecture + // +k8s:eachKey=+k8s:maxLength=16 map assets = 2; // arch -> {name -> file} // pause_image is the image for the sandbox's root container. Like the @@ -345,12 +424,27 @@ message SandboxAssets { // and atelet pins it into the snapshot manifest so a restore rebuilds the // sandbox from the same image. Empty for sandboxes without a pause // container (microvm). + // + // +k8s:optional + // +k8s:maxLength=512 + // +k8s:customValidation # must be a well-formed image reference, pinned by digest string pause_image = 3; } // WorkloadSpec parallels Pod, but with far fewer configurable fields. +// +// +k8s:customValidation # every volume mount names a declared volume message WorkloadSpec { + // +k8s:optional + // +k8s:maxItems=10 # matches the template's containers bound + // +k8s:listType=map + // +k8s:listMapKey=name repeated Container containers = 1; + + // +k8s:optional + // +k8s:maxItems=32 # matches the template's volumes bound + // +k8s:listType=map + // +k8s:listMapKey=name repeated Volume volumes = 2; } @@ -358,16 +452,40 @@ message DurableDirVolume { } message ExternalVolumeSource { + // Required, unlike the actor status counterpart: the control plane only + // sends volumes that finished provisioning. + // + // +k8s:required + // +k8s:maxLength=256 # matches ExternalVolume.storage_volume_id's bound + // +k8s:customValidation # no control characters string storage_volume_id = 1; + + // +k8s:optional + // +k8s:maxLength=253 # matches ExternalVolume.volume_type's bound + // +k8s:customValidation # optional "substrate.io/" prefix + DNS subdomain string volume_type = 2; + + // volume_context is the driver's provisioning metadata. Its entries are the + // driver's own, so the map is only bounded, not validated. + // + // +k8s:optional + // +k8s:customValidation # at most 40 KiB in total, ten times the CSI limit for map fields map volume_context = 3; // publish_context is the driver's attachment metadata for this node, which - // the node plugin needs to complete the mount. + // the node plugin needs to complete the mount. Bounded like volume_context. + // + // +k8s:optional + // +k8s:customValidation # at most 40 KiB in total, ten times the CSI limit for map fields map publish_context = 4; } message ImageVolumeSource { + // reference is the OCI image reference, pinned by digest. + // + // +k8s:required + // +k8s:maxLength=512 # matches the template ImageVolumeSource.reference's bound + // +k8s:customValidation # must be a well-formed image reference, pinned by digest string reference = 1; } @@ -377,83 +495,197 @@ enum ActorMetadataField { ACTOR_METADATA_FIELD_NAME = 1; ACTOR_METADATA_FIELD_ATESPACE = 2; ACTOR_METADATA_FIELD_UID = 3; + // Keep this in sync with the maximums on fields of this type. } // ActorMetadataItem projects one actor identity field to one file at the // given path, relative to the root of the enclosing system-info volume. message ActorMetadataItem { + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3 # keep this in sync with the ActorMetadataField enum ActorMetadataField field = 1; + + // path must be a clean relative Unix path; the rule is shared with the + // control plane through internal/resources. + // + // +k8s:required + // +k8s:minLength=1 + // +k8s:maxLength=255 + // +k8s:customValidation # clean relative path string path = 2; } // ActorMetadataDataSource projects the actor's identity fields to files, one // per item. Values are written raw with no trailing newline. message ActorMetadataDataSource { + // items must not project the same field twice. + // + // +k8s:required + // +k8s:minItems=1 + // +k8s:maxItems=8 # matches the template's items bound + // +k8s:listType=atomic + // +k8s:unique=map + // +k8s:listMapKey=field repeated ActorMetadataItem items = 1; } // TrustBundleDataSource corresponds to TrustBundleDataSource in ateapi.proto message TrustBundleDataSource { + // path must be a clean relative Unix path; the rule is shared with the + // control plane through internal/resources. + // + // +k8s:required + // +k8s:minLength=1 + // +k8s:maxLength=255 + // +k8s:customValidation # clean relative path string path = 1; + // +k8s:required + // +k8s:minItems=1 + // +k8s:maxItems=8 # matches the template's names bound + // +k8s:listType=set + // +k8s:eachVal=+k8s:minLength=1 + // +k8s:eachVal=+k8s:maxLength=253 # matches the template's bundle-name bound repeated string names = 3; reserved 2; reserved "name"; } +// SystemInfoDataSource selects exactly one projection to place in the +// volume. message SystemInfoDataSource { - oneof data_source { - ActorMetadataDataSource actor_metadata = 1; - TrustBundleDataSource trust_bundle = 2; - } + // Exactly one of actor_metadata / trust_bundle must be set. + // + // +k8s:optional + // +k8s:unionMember + ActorMetadataDataSource actor_metadata = 1; + + // +k8s:optional + // +k8s:unionMember + TrustBundleDataSource trust_bundle = 2; } // SystemInfoVolume is a read-only volume whose files are generated by atelet // on every Run/Restore, so they carry the values of the actor actually being // started, whatever checkpointed state it boots from. message SystemInfoVolume { + // +k8s:optional + // +k8s:maxItems=8 # matches the template's data_sources bound + // +k8s:listType=atomic + // +k8s:customValidation # at most one actor_metadata; paths unique across entries repeated SystemInfoDataSource data_sources = 1; } +// Volume names one volume and selects exactly one source for it. message Volume { + // +k8s:required + // +k8s:format=k8s-short-name string name = 1; - oneof source { - DurableDirVolume durable_dir = 2; - ExternalVolumeSource external = 3; - SystemInfoVolume system_info = 4; - ImageVolumeSource image = 5; - } + // Exactly one of durable_dir / external / system_info / image must be set. + // + // +k8s:optional + // +k8s:unionMember + DurableDirVolume durable_dir = 2; + + // +k8s:optional + // +k8s:unionMember + ExternalVolumeSource external = 3; + + // +k8s:optional + // +k8s:unionMember + SystemInfoVolume system_info = 4; + + // +k8s:optional + // +k8s:unionMember + ImageVolumeSource image = 5; } message VolumeMount { + // name must match the name of a Volume. + // + // +k8s:required + // +k8s:format=k8s-short-name string name = 1; + + // mount_path within the container. Must be a clean absolute Unix path. + // + // +k8s:required + // +k8s:maxLength=4096 + // +k8s:customValidation # clean-absolute-path shape; no regex/pattern tag exists string mount_path = 2; } message Container { + // +k8s:required + // +k8s:format=k8s-short-name string name = 1; + + // +k8s:required + // +k8s:maxLength=512 # matches the template Container.image's bound + // +k8s:customValidation # must be a well-formed image reference, pinned by digest string image = 2; + + // +k8s:optional + // +k8s:maxItems=64 + // +k8s:listType=atomic + // +k8s:eachVal=+k8s:maxLength=4096 # argv strings; guardrail, not a contract repeated string command = 3; + + // +k8s:optional + // +k8s:maxItems=64 + // +k8s:listType=atomic + // +k8s:eachVal=+k8s:maxLength=4096 # argv strings; guardrail, not a contract repeated string args = 7; + + // +k8s:optional + // +k8s:maxItems=32 # matches the template's env bound + // +k8s:listType=map # each variable is set at most once + // +k8s:listMapKey=name repeated EnvEntry env = 4; + + // +k8s:optional WakeupProbe wakeup_probe = 5; + + // Keyed by mount_path: each path hosts exactly one mount, while a volume + // may be mounted at multiple paths. + // + // +k8s:optional + // +k8s:maxItems=32 # matches the template's volume_mounts bound + // +k8s:listType=map + // +k8s:listMapKey=mount_path + // +k8s:customValidation # mounts must not nest repeated VolumeMount volume_mounts = 6; + + // +k8s:optional SecurityContext security_context = 8; // resources are the cgroup limits for this container, resolved by // ate-api-server from the ActorTemplate. Unset means no limits. + // + // +k8s:optional ResourceLimits resources = 9; } // SecurityContext holds security settings for a container's process. message SecurityContext { + // +k8s:optional Capabilities capabilities = 1; } // Capabilities adjusts a container's Linux capabilities relative to the default // set. Names carry no "CAP_" prefix; drop applies before add. message Capabilities { + // +k8s:optional + // +k8s:maxItems=64 + // +k8s:listType=set + // +k8s:customValidation # capability grammar; "ALL" not accepted repeated string add = 1; + + // +k8s:optional + // +k8s:maxItems=64 + // +k8s:listType=set + // +k8s:customValidation # capability grammar repeated string drop = 2; } @@ -461,29 +693,60 @@ message Capabilities { // map so each resource.Quantity is parsed exactly once, in ate-api-server. message ResourceLimits { // memory_bytes is the memory limit in bytes. 0 means unset. + // + // +k8s:optional + // +k8s:minimum=0 int64 memory_bytes = 1; + // cpu_millis is the CPU limit in milli-cores (1000 = one core). 0 means unset. + // + // +k8s:optional + // +k8s:minimum=0 + // +k8s:maximum=999999 # the control plane caps cpu limits strictly below 1000 cores int64 cpu_millis = 2; } message EnvEntry { + // name may be any printable ASCII character except '='. + // + // +k8s:required + // +k8s:maxLength=256 # guardrail + // +k8s:customValidation # printable ASCII except '=' string name = 1; + + // +k8s:optional + // +k8s:maxLength=32768 # guardrail string value = 2 [ debug_redact = true ]; } // WakeupProbe describes how to check that a container is ready to serve. // Only HTTP is supported today. message WakeupProbe { + // +k8s:required HttpGetAction http_get = 1; + // How long to keep polling before giving up and failing the actor start. + // + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3600 # matches the template wakeup probe's bound int32 timeout_seconds = 2; } // HttpGetAction performs an HTTP GET against the container. message HttpGetAction { // Path to access on the HTTP server. + // + // +k8s:required + // +k8s:maxLength=1024 # matches the template probe path's bound + // +k8s:customValidation # RFC 3986 path shape; no regex/pattern tag exists string path = 1; + // TCP port to connect to (1..65535). + // + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=65535 int32 port = 2; } @@ -495,12 +758,19 @@ message LocalCheckpointConfiguration { // data will be stored. atelet decides where that directory lives, from the // actor's UID, so this is a bare name and must not contain a path separator. // The structure of the checkpoint should generally be treated as opaque. + // + // +k8s:required + // +k8s:format=k8s-short-name string snapshot_name = 1; } message ExternalCheckpointConfiguration { // The object storage URI of the snapshot to write. Object names are appended // to it, so it addresses the snapshot as a whole rather than any one object. + // + // +k8s:required + // +k8s:maxLength=2048 + // +k8s:customValidation # a parseable snapshot URI string snapshot_uri = 1; } @@ -511,6 +781,7 @@ enum CheckpointType { CHECKPOINT_TYPE_LOCAL = 1; // Save snapshot to object storage CHECKPOINT_TYPE_EXTERNAL = 2; + // Keep this in sync with the maximums on fields of this type. } // SnapshotFidelity mirrors the public ateapi enum: the state layers a @@ -528,6 +799,7 @@ enum SnapshotFidelity { // Capture process memory plus the full filesystem delta on top of the OCI // image (including any attached DurableDir volumes). SNAPSHOT_FIDELITY_MEMORY = 3; + // Keep this in sync with the maximums on fields of this type. } // ExternalRestoreConfiguration is an external snapshot a restore reads. @@ -537,33 +809,72 @@ message ExternalRestoreConfiguration { // The object storage URI of the snapshot to read. Object names are // appended to it, so it addresses the snapshot as a whole rather than any // one object. + // + // +k8s:required + // +k8s:maxLength=2048 + // +k8s:customValidation # a parseable snapshot URI string snapshot_uri = 1; } +// +k8s:customValidation # the set config matches type message CheckpointRequest { + // The UID of the worker pod whose ateom hosts the actor. + // + // +k8s:required + // +k8s:format=k8s-uuid string worker_pod_uid = 1; + // +k8s:required + // +k8s:format=k8s-short-name string atespace = 2; + + // +k8s:required + // +k8s:format=k8s-short-name string actor_name = 3; + + // +k8s:required + // +k8s:format=k8s-uuid string actor_uid = 4; + // The template identity is carried for metrics attribution. + // + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_atespace = 5; + + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_name = 6; // Sandbox binary config is not sent on checkpoint: atelet uses the version the // actor is currently running (recorded with the actor's on-node state at // Run/Restore) and records it into the snapshot manifest. + // + // +k8s:required WorkloadSpec spec = 7; + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=2 # keep this in sync with the CheckpointType enum CheckpointType type = 8; - // The checkpoint configuration, depending on the type. - oneof config { - LocalCheckpointConfiguration local_config = 9; - ExternalCheckpointConfiguration external_config = 10; - } + // The checkpoint configuration: local_config when type is LOCAL, + // external_config when it is EXTERNAL. Exactly one is set. + // + // +k8s:optional + // +k8s:unionMember + LocalCheckpointConfiguration local_config = 9; + + // +k8s:optional + // +k8s:unionMember + ExternalCheckpointConfiguration external_config = 10; - // Fidelity the checkpoint captures. + // Fidelity the checkpoint captures. Which levels a runtime can serve is the + // runtime's call: atelet forwards it and ateom rejects what it cannot capture. + // + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3 # keep this in sync with the SnapshotFidelity enum SnapshotFidelity fidelity = 11; } @@ -571,66 +882,138 @@ message CheckpointResponse { } message UploadPausedCheckpointRequest { + // Golden actors are never paused, so the golden atespace is rejected. + // + // +k8s:required + // +k8s:format=k8s-short-name + // +k8s:customValidation # not the golden atespace string atespace = 1; + + // +k8s:required + // +k8s:format=k8s-short-name string actor_name = 2; + + // +k8s:required + // +k8s:format=k8s-uuid string actor_uid = 3; // For metrics attribution, like on CheckpointRequest. + // + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_atespace = 4; + + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_name = 5; // The local checkpoint to upload: the uuid of the actor's pause snapshot - // (ateapi Snapshot.uuid). + // (ateapi Snapshot.uuid, validated as a short name there too). + // + // +k8s:required + // +k8s:format=k8s-short-name string local_snapshot_name = 6; // Destination object-storage URI (the actor's in-progress snapshot URI). + // + // +k8s:required + // +k8s:maxLength=2048 + // +k8s:customValidation # a parseable snapshot URI string destination_snapshot_uri = 7; // Fidelity the uploaded snapshot must have. The fidelity the pause // checkpoint captured is not sent: atelet reads it from the local // snapshot's own manifest, which is authoritative, and rejects the upload // if the two differ. + // + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3 # keep this in sync with the SnapshotFidelity enum SnapshotFidelity desired_fidelity = 8; } message UploadPausedCheckpointResponse { } +// +k8s:customValidation # the set config matches type message RestoreRequest { + // The UID of the worker pod whose ateom hosts the actor. + // + // +k8s:required + // +k8s:format=k8s-uuid string worker_pod_uid = 1; + // +k8s:required + // +k8s:format=k8s-short-name string atespace = 2; + + // +k8s:required + // +k8s:format=k8s-short-name string actor_name = 3; + + // +k8s:required + // +k8s:format=k8s-uuid string actor_uid = 4; + // The template identity is carried for metrics attribution. + // + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_atespace = 5; + + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_name = 6; + // +k8s:required WorkloadSpec spec = 7; + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=2 # keep this in sync with the CheckpointType enum CheckpointType type = 8; - // The checkpoint configuration, depending on the type. - oneof config { - LocalCheckpointConfiguration local_config = 9; - ExternalRestoreConfiguration external_config = 10; - } + // The checkpoint configuration: local_config when type is LOCAL, + // external_config when it is EXTERNAL. Exactly one is set. + // + // +k8s:optional + // +k8s:unionMember + LocalCheckpointConfiguration local_config = 9; + + // +k8s:optional + // +k8s:unionMember + ExternalRestoreConfiguration external_config = 10; // Fidelity to restore from the checkpoint. + // + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3 # keep this in sync with the SnapshotFidelity enum SnapshotFidelity fidelity = 11; // When absent the actor has no egress: its TCP is captured and refused. + // + // +k8s:optional optional EgressGateway egress_gateway = 13; // The actor's declared size, from the ActorTemplate's resource limits. For // gVisor and micro-VM VOLUMES restores the sandbox is (re)sized to these; // for a MEMORY micro-VM restore the size baked into the snapshot wins. Zero // means "unset": keep the runtime default. - int64 cpu_milli = 14; // CPU limit in millicores (1000 = one core). + // + // +k8s:optional + // +k8s:minimum=0 + // +k8s:maximum=999999 # the control plane caps cpu limits strictly below 1000 cores + int64 cpu_milli = 14; // CPU limit in millicores (1000 = one core). + + // +k8s:optional + // +k8s:minimum=0 int64 memory_bytes = 15; // Memory limit in bytes. // The sandbox binaries and pause image to restore with, resolved from the - // ActorTemplate's SandboxConfig. Required. + // ActorTemplate's SandboxConfig. + // + // +k8s:required SandboxAssets sandbox_assets = 16; } diff --git a/internal/resources/validate.go b/internal/resources/validate.go index ff3d411a65..2c8f749a21 100644 --- a/internal/resources/validate.go +++ b/internal/resources/validate.go @@ -17,20 +17,26 @@ package resources import ( "encoding/hex" "fmt" + "net" "net/netip" "net/url" "os" "path/filepath" "reflect" + "regexp" + "strconv" "strings" "github.com/agent-substrate/substrate/internal/apierror" "github.com/agent-substrate/substrate/internal/proto/ateompb" + "github.com/agent-substrate/substrate/internal/volumepath" + "github.com/distribution/reference" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/api/validate/content" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -173,29 +179,6 @@ func ValidateRuntimeAssetPath(root, p string, fldPath *field.Path) field.ErrorLi return nil } -// ValidateContainerNames ensures every application container name is safe to -// use as an OCI bundle path component. Each must be a DNS-1123 label (no -// separator or ".."), must not be the reserved "pause" name (which would -// collide with the sandbox-infra bundle and race its concurrent writer), and -// must be unique (duplicates map to the same bundle path and corrupt each -// other). -func ValidateContainerNames(names []string) error { - seen := make(map[string]struct{}) - for _, name := range names { - if errs := content.IsDNS1123Label(name); len(errs) > 0 { - return fmt.Errorf("invalid container name %q: %s", name, strings.Join(errs, "; ")) - } - if name == "pause" { - return fmt.Errorf("invalid container name %q: reserved for sandbox infrastructure", name) - } - if _, dup := seen[name]; dup { - return fmt.Errorf("duplicate container name %q", name) - } - seen[name] = struct{}{} - } - return nil -} - // ValidateRunscHash ensures the runsc SHA-256 hash is exactly 64 hex // characters before it is used to build the on-disk binary path // (static-files/runsc-) and, on a cache hit, returned for ateom to @@ -302,13 +285,21 @@ func ValidateUUID(uuid string, fldPath *field.Path) field.ErrorList { return nil } -// cpuLimitMax bounds cpu limits: they must be less than 1000 cores. -var cpuLimitMax = resource.MustParse("1k") +// cpuLimitMax bounds cpu limits. The wire carries ceil(cores × 1000) +// millicores and caps it at 999999 (strictly below 1000 cores), so a limit +// above 999.999 cores rounds up out of range. +var cpuLimitMax = resource.MustParse("999.999") + +// memoryLimitMax bounds memory limits at what the wire's int64 byte count +// holds; resource.Quantity.Value wraps silently past it. +var memoryLimitMax = resource.MustParse("9223372036854775807") // ValidateLimit validates one resource limit entry at fldPath: only cpu and -// memory are supported, the quantity must be greater than zero, and the cpu -// limit must be less than 1000 cores. An empty quantity is left to the -// required tag. +// memory are supported, the quantity must be greater than zero, and it must +// fit the scalars the control plane converts it to (cpuLimitMax and +// memoryLimitMax). The bounds compare Quantities rather than the converted +// integers, which wrap for very large values. An empty quantity is left to +// the required tag. func ValidateLimit(fldPath *field.Path, name, quantity string) field.ErrorList { if name != ResourceCPU && name != ResourceMemory { return field.ErrorList{field.NotSupported(fldPath.Child("name"), name, []string{ResourceCPU, ResourceMemory})} @@ -324,8 +315,221 @@ func ValidateLimit(fldPath *field.Path, name, quantity string) field.ErrorList { if q.Sign() <= 0 { errs = append(errs, field.Invalid(fldPath.Child("quantity"), quantity, "must be greater than zero")) } - if name == ResourceCPU && q.Cmp(cpuLimitMax) >= 0 { - errs = append(errs, field.Invalid(fldPath.Child("quantity"), quantity, "cpu limit must be less than 1000 cores")) + switch { + case name == ResourceCPU && q.Cmp(cpuLimitMax) > 0: + errs = append(errs, field.Invalid(fldPath.Child("quantity"), quantity, "cpu limit must be at most 999.999 cores")) + case name == ResourceMemory && q.Cmp(memoryLimitMax) > 0: + errs = append(errs, field.Invalid(fldPath.Child("quantity"), quantity, "memory limit must be at most 9223372036854775807 bytes")) + } + return errs +} + +// 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 value == "" { + return nil + } + 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 +} + +// capabilityRE constrains Linux capability names: uppercase, without the +// "CAP_" prefix (which is added when the OCI spec is written; the prefixed +// spelling would silently grant nothing). +var capabilityRE = regexp.MustCompile(`^[A-Z][A-Z0-9_]*$`) + +// ValidateCapabilities checks each capability name. "ALL" is accepted only +// when allowAll is set, which is the case for drop but not add. +func ValidateCapabilities(fldPath *field.Path, caps []string, allowAll bool) field.ErrorList { + var errs field.ErrorList + for i, c := range caps { + p := fldPath.Index(i) + switch { + case c == "ALL" && !allowAll: + errs = append(errs, field.Invalid(p, c, "add does not accept 'ALL'; name the individual capabilities the container needs")) + case c == "ALL": + case len(c) > 63: + errs = append(errs, field.TooLong(p, nil, 63)) + case strings.HasPrefix(c, "CAP_"): + errs = append(errs, field.Invalid(p, c, "must be named without the 'CAP_' prefix (e.g. 'NET_BIND_SERVICE')")) + case !capabilityRE.MatchString(c): + errs = append(errs, field.Invalid(p, c, "must be an uppercase capability name like 'NET_BIND_SERVICE'")) + } + } + return errs +} + +// mountPathBadSegmentRE matches '.' or '..' path segments. +var mountPathBadSegmentRE = regexp.MustCompile(`(^|/)[.][.]?(/|$)`) + +// ValidateMountPath requires a clean absolute Unix path that starts with '/', +// is not '/', and contains no ':', '.' or '..' segments, '//', trailing '/', +// or control characters. +func ValidateMountPath(fldPath *field.Path, p string) field.ErrorList { + if p == "" { + return nil + } + bad := !strings.HasPrefix(p, "/") || len(p) == 1 || + strings.HasSuffix(p, "/") || strings.Contains(p, "//") || + strings.Contains(p, ":") || mountPathBadSegmentRE.MatchString(p) + if !bad { + for _, r := range p { + if r < 0x20 || r == 0x7f { + bad = true + break + } + } + } + if bad { + return field.ErrorList{field.Invalid(fldPath, p, "must be a clean absolute Unix path: must start with '/', not be '/', and contain no ':', '..', '.', '//', trailing '/', or control characters")} + } + return nil +} + +// ValidateNestedMountPaths rejects mount paths that nest under or over one +// another: volumes cannot mount onto other volumes. paths[i] is reported at +// fldPath[i].mount_path. Identical paths are left to the list-key uniqueness +// check. +func ValidateNestedMountPaths(fldPath *field.Path, paths []string) field.ErrorList { + var errs field.ErrorList + for i, path := range paths { + if path == "" { + continue + } + for _, prior := range paths[:i] { + if prior == "" || prior == path { + continue + } + if strings.HasPrefix(path, prior+"/") || strings.HasPrefix(prior, path+"/") { + errs = append(errs, field.Invalid(fldPath.Index(i).Child("mount_path"), path, + fmt.Sprintf("must not nest under or over another mount (%q)", prior))) + } + } + } + return errs +} + +// httpGetPathRE constrains wakeup probe paths to RFC 3986 path-segment +// characters only, with well-formed percent-escapes, and no query string +// or fragment. +var httpGetPathRE = regexp.MustCompile(`^/([A-Za-z0-9\-._~!$&'()*+,;=:@/]|%[0-9A-Fa-f]{2})*$`) + +// ValidateHTTPGetPath checks a wakeup probe's HTTP path. +func ValidateHTTPGetPath(fldPath *field.Path, p string) field.ErrorList { + if p == "" { + return nil + } + if !httpGetPathRE.MatchString(p) { + return field.ErrorList{field.Invalid(fldPath, p, "must be a URL path starting with '/', using only RFC 3986 path-segment characters, without query or fragment")} + } + return nil +} + +// envVarNameRE constrains env var names to any printable ASCII character +// except '='. +var envVarNameRE = regexp.MustCompile(`^[ -<>-~]+$`) + +// ValidateEnvVarName checks an environment variable name. +func ValidateEnvVarName(fldPath *field.Path, name string) field.ErrorList { + if name == "" { + return nil + } + if !envVarNameRE.MatchString(name) { + return field.ErrorList{field.Invalid(fldPath, name, "may contain any printable ASCII character except '='")} + } + return nil +} + +// ValidateProjectedPath applies volumepath.ValidateProjected to a file path +// projected into a system-info volume. atelet re-checks it before writing to +// the host. +func ValidateProjectedPath(fldPath *field.Path, p string) field.ErrorList { + if p == "" { + return nil + } + if err := volumepath.ValidateProjected(p); err != nil { + return field.ErrorList{field.Invalid(fldPath, p, err.Error())} + } + return nil +} + +// ValidateStorageVolumeID rejects control characters (U+0000-U+0008, U+000B, +// U+000C, U+000E-U+001F, U+007F-U+009F) in an external volume's storage ID. +func ValidateStorageVolumeID(fldPath *field.Path, id string) field.ErrorList { + for _, r := range id { + if (r >= 0x0000 && r <= 0x0008) || + r == 0x000B || + r == 0x000C || + (r >= 0x000E && r <= 0x001F) || + (r >= 0x007F && r <= 0x009F) { + return field.ErrorList{field.Invalid(fldPath, id, "must not contain control characters (U+0000-U+0008, U+000B, U+000C, U+000E-U+001F, U+007F-U+009F)")} + } + } + return nil +} + +// ValidateVolumeType allows an optional "substrate.io/" prefix, followed by a +// valid DNS-1123 subdomain. +func ValidateVolumeType(fldPath *field.Path, volumeType string) field.ErrorList { + if volumeType == "" { + return nil + } + var errs field.ErrorList + for _, msg := range validation.IsDNS1123Subdomain(strings.TrimPrefix(volumeType, "substrate.io/")) { + errs = append(errs, field.Invalid(fldPath, volumeType, msg)) + } + return errs +} + +// csiMapMaxBytes bounds a map a CSI driver produced. The CSI spec caps +// map fields at 4 KiB; this allows ten times that so a future +// revision of the limit does not turn compliant drivers away. It is a +// guardrail against unbounded input, not a contract drivers are held to. +const csiMapMaxBytes = 40 * 1024 + +// ValidateCSIMap bounds a map a CSI driver produced (a volume or publish +// context) at csiMapMaxBytes, counting keys and values. The entries are the +// driver's own and are not validated further. +func ValidateCSIMap(fldPath *field.Path, m map[string]string) field.ErrorList { + size := 0 + for k, v := range m { + size += len(k) + len(v) + } + if size > csiMapMaxBytes { + return field.ErrorList{field.TooLong(fldPath, nil, csiMapMaxBytes)} + } + return nil +} + +// ValidateHostPort requires "host:port", where host is an IP address or a DNS +// subdomain name (a trailing dot, marking it fully qualified, is allowed) and +// port is a number in 1..65535. +func ValidateHostPort(fldPath *field.Path, value string) field.ErrorList { + if value == "" { + return nil + } + host, port, err := net.SplitHostPort(value) + if err != nil { + return field.ErrorList{field.Invalid(fldPath, value, "must be host:port")} + } + var errs field.ErrorList + if _, err := netip.ParseAddr(host); err != nil { + for _, msg := range content.IsDNS1123Subdomain(strings.TrimSuffix(host, ".")) { + errs = append(errs, field.Invalid(fldPath, value, "host: "+msg)) + } + } + if n, err := strconv.Atoi(port); err != nil || n < 1 || n > 65535 { + errs = append(errs, field.Invalid(fldPath, value, "port must be a number between 1 and 65535")) } return errs } diff --git a/internal/resources/validate_test.go b/internal/resources/validate_test.go index 7db4c4997a..e2d3e905f6 100644 --- a/internal/resources/validate_test.go +++ b/internal/resources/validate_test.go @@ -15,6 +15,7 @@ package resources import ( + "fmt" "os" "path/filepath" "regexp" @@ -123,32 +124,6 @@ func TestValidateWorkerPodUID(t *testing.T) { } } -func TestValidateContainerNames(t *testing.T) { - tests := []struct { - name string - names []string - wantErr bool - }{ - {"no containers", nil, false}, - {"single valid", []string{"worker"}, false}, - {"multiple valid", []string{"worker", "sidecar"}, false}, - {"separator", []string{"a/b"}, true}, - {"traversal", []string{".."}, true}, - {"empty name", []string{""}, true}, - {"uppercase", []string{"Worker"}, true}, - {"reserved pause", []string{"pause"}, true}, - {"reserved pause among valid", []string{"worker", "pause"}, true}, - {"duplicate", []string{"worker", "worker"}, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := ValidateContainerNames(tt.names); (err != nil) != tt.wantErr { - t.Errorf("ValidateContainerNames(%v) err = %v, wantErr %v", tt.names, err, tt.wantErr) - } - }) - } -} - func TestValidateRunscHash(t *testing.T) { const valid = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" @@ -296,14 +271,20 @@ func TestValidateLimit(t *testing.T) { want field.ErrorList }{ {name: "cpu below the bound", limit: "cpu", quantity: "999"}, + {name: "cpu at the millicore bound", limit: "cpu", quantity: "999.999"}, {name: "memory", limit: "memory", quantity: "1Gi"}, - {name: "memory has no upper bound", limit: "memory", quantity: "1000"}, + {name: "memory at the int64 bound", limit: "memory", quantity: "9223372036854775807"}, {name: "missing quantity left to tags", limit: "cpu"}, {name: "unsupported name", limit: "gpu", quantity: "1", want: field.ErrorList{field.NotSupported[string](path.Child("name"), nil, nil)}}, {name: "malformed quantity", limit: "cpu", quantity: "x", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, {name: "zero quantity", limit: "memory", quantity: "0", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, {name: "negative quantity", limit: "memory", quantity: "-1", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, - {name: "cpu at the bound", limit: "cpu", quantity: "1000", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, + {name: "cpu at 1000 cores", limit: "cpu", quantity: "1000", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, + {name: "cpu rounds up to 1000 cores in millicores", limit: "cpu", quantity: "999.9995", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, + {name: "cpu far beyond int64 millicores", limit: "cpu", quantity: "1e30", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, + {name: "memory past the int64 bound", limit: "memory", quantity: "9223372036854775808", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, + {name: "memory rounds up past the int64 bound", limit: "memory", quantity: "9223372036854775807.5", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, + {name: "memory far beyond int64 bytes", limit: "memory", quantity: "1e30", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -395,6 +376,27 @@ func TestValidateActorDirs(t *testing.T) { } } +const testPinnedImage = "example.com/app@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + +// stringRuleCase is one input to a single-string rule and whether it fails. +type stringRuleCase struct { + name string + value string + wantErr bool +} + +func runStringRule(t *testing.T, rule func(*field.Path, string) field.ErrorList, tests []stringRuleCase) { + t.Helper() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errs := rule(field.NewPath("f"), tt.value) + if (len(errs) > 0) != tt.wantErr { + t.Errorf("value %q: errs = %v, wantErr %v", tt.value, errs, tt.wantErr) + } + }) + } +} + func TestValidateRuntimeAssetPath(t *testing.T) { // on macOS the temp dir is behind the /var symlink root, err := filepath.EvalSymlinks(t.TempDir()) @@ -452,3 +454,178 @@ func TestValidateRuntimeAssetPath(t *testing.T) { }) } } + +func TestValidatePinnedImage(t *testing.T) { + runStringRule(t, ValidatePinnedImage, []stringRuleCase{ + {"empty is left to tags", "", false}, + {"pinned", testPinnedImage, false}, + {"tag only", "example.com/app:v1", true}, + {"malformed digest", "example.com/app@sha256:abc", true}, + {"malformed reference", "Example.com/App", true}, + }) +} + +func TestValidateMountPath(t *testing.T) { + runStringRule(t, ValidateMountPath, []stringRuleCase{ + {"empty is left to tags", "", false}, + {"clean absolute", "/data/cache", false}, + {"relative", "data", true}, + {"root", "/", true}, + {"trailing slash", "/data/", true}, + {"double slash", "/data//x", true}, + {"colon", "/data:x", true}, + {"dot segment", "/data/./x", true}, + {"dot-dot segment", "/data/../x", true}, + {"control character", "/data\x01", true}, + }) +} + +func TestValidateHTTPGetPath(t *testing.T) { + runStringRule(t, ValidateHTTPGetPath, []stringRuleCase{ + {"empty is left to tags", "", false}, + {"simple", "/healthz", false}, + {"percent escape", "/a%20b", false}, + {"no leading slash", "healthz", true}, + {"query", "/healthz?x=1", true}, + {"fragment", "/healthz#x", true}, + {"bad percent escape", "/a%2", true}, + }) +} + +func TestValidateEnvVarName(t *testing.T) { + runStringRule(t, ValidateEnvVarName, []stringRuleCase{ + {"empty is left to tags", "", false}, + {"conventional", "HOME_DIR", false}, + {"printable punctuation", "a.b-c", false}, + {"equals sign", "A=B", true}, + {"control character", "A\tB", true}, + {"non-ASCII", "é", true}, + }) +} + +func TestValidateProjectedPath(t *testing.T) { + runStringRule(t, ValidateProjectedPath, []stringRuleCase{ + {"empty is left to tags", "", false}, + {"relative file", "identity/name", false}, + {"absolute", "/etc/name", true}, + {"escape", "../name", true}, + }) +} + +func TestValidateStorageVolumeID(t *testing.T) { + runStringRule(t, ValidateStorageVolumeID, []stringRuleCase{ + {"empty", "", false}, + {"plain", "projects/p/disks/d-1", false}, + {"newline is allowed", "a\nb", false}, + {"NUL", "a\x00b", true}, + {"DEL", "a\x7fb", true}, + {"C1 control", "a\u0085b", true}, + }) +} + +func TestValidateVolumeType(t *testing.T) { + runStringRule(t, ValidateVolumeType, []stringRuleCase{ + {"empty", "", false}, + {"dns subdomain", "pd.csi.storage.gke.io", false}, + {"substrate prefix", "substrate.io/gcs", false}, + {"uppercase", "PD", true}, + {"other prefix", "example.io/gcs", true}, + }) +} + +func TestValidateCapabilities(t *testing.T) { + tests := []struct { + name string + caps []string + allowAll bool + wantErrs int + }{ + {"valid names", []string{"NET_BIND_SERVICE", "SYS_PTRACE"}, false, 0}, + {"ALL rejected for add", []string{"ALL"}, false, 1}, + {"ALL accepted for drop", []string{"ALL"}, true, 0}, + {"CAP_ prefix", []string{"CAP_NET_ADMIN"}, false, 1}, + {"lowercase", []string{"net_admin"}, false, 1}, + {"too long", []string{strings.Repeat("A", 64)}, false, 1}, + {"each bad entry reported", []string{"CAP_X", "ok", "NET_ADMIN"}, false, 2}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if errs := ValidateCapabilities(field.NewPath("add"), tt.caps, tt.allowAll); len(errs) != tt.wantErrs { + t.Errorf("errs = %v, want %d", errs, tt.wantErrs) + } + }) + } +} + +func TestValidateNestedMountPaths(t *testing.T) { + path := field.NewPath("volume_mounts") + tests := []struct { + name string + paths []string + want field.ErrorList + }{ + {name: "siblings", paths: []string{"/a", "/b", "/ab"}}, + {name: "identical paths are left to the list key", paths: []string{"/a", "/a"}}, + {name: "empty paths are skipped", paths: []string{"", "/a"}}, + { + name: "nested under an earlier mount", + paths: []string{"/a", "/a/b"}, + want: field.ErrorList{field.Invalid(path.Index(1).Child("mount_path"), nil, "")}, + }, + { + name: "nested over an earlier mount", + paths: []string{"/a/b", "/a"}, + want: field.ErrorList{field.Invalid(path.Index(1).Child("mount_path"), nil, "")}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + field.ErrorMatcher{}.ByType().ByField().Test(t, tt.want, ValidateNestedMountPaths(path, tt.paths)) + }) + } +} + +func TestValidateCSIMap(t *testing.T) { + path := field.NewPath("publish_context") + many := make(map[string]string, 100) + for i := range 100 { + many[fmt.Sprintf("key-%03d", i)] = "v" + } + tests := []struct { + name string + m map[string]string + want field.ErrorList + }{ + {name: "nil"}, + {name: "one long value", m: map[string]string{"token": strings.Repeat("x", 4000)}}, + {name: "many small entries", m: many}, + {name: "over the CSI spec limit, within the headroom", m: map[string]string{"k": strings.Repeat("x", 8*1024)}}, + {name: "exactly the bound", m: map[string]string{"k": strings.Repeat("x", 40*1024-1)}}, + {name: "one byte over", m: map[string]string{"k": strings.Repeat("x", 40*1024)}, want: field.ErrorList{field.TooLong(path, nil, 40*1024)}}, + {name: "over across entries", m: map[string]string{"a": strings.Repeat("x", 20*1024), "b": strings.Repeat("y", 20*1024)}, want: field.ErrorList{field.TooLong(path, nil, 40*1024)}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + field.ErrorMatcher{}.ByType().ByField().Test(t, tt.want, ValidateCSIMap(path, tt.m)) + }) + } +} + +func TestValidateHostPort(t *testing.T) { + runStringRule(t, ValidateHostPort, []stringRuleCase{ + {name: "empty is left to presence tags", value: ""}, + {name: "dns name", value: "atenet-egress.ate-system.svc:443"}, + {name: "fully qualified dns name", value: "atenet-egress.ate-system.svc.cluster.local.:443"}, + {name: "ipv4", value: "10.0.0.1:8080"}, + {name: "ipv6", value: "[fd00::1]:443"}, + {name: "missing port", value: "atenet-egress.ate-system.svc", wantErr: true}, + {name: "empty host", value: ":443", wantErr: true}, + {name: "bare dot host", value: ".:443", wantErr: true}, + {name: "uppercase host", value: "Egress.Example:443", wantErr: true}, + {name: "host with a path", value: "egress/../x:443", wantErr: true}, + {name: "port zero", value: "egress.example:0", wantErr: true}, + {name: "port too large", value: "egress.example:65536", wantErr: true}, + {name: "named port", value: "egress.example:https", wantErr: true}, + {name: "unbracketed ipv6", value: "fd00::1:443", wantErr: true}, + }) +} diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index 2bcbb20599..fde3450ce7 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -1049,13 +1049,11 @@ type ExternalVolume struct { Status ExternalVolume_Status `protobuf:"varint,4,opt,name=status,proto3,enum=ateapi.ExternalVolume_Status" json:"status,omitempty"` // volume_context contains metadata returned by the CSI driver during volume // provisioning, needed by the node plugin for mounting (e.g. attachment - // info). Keys and values are the driver's own, so they are only bounded, + // info). Keys and values are the driver's own, so the map is only bounded, // not validated. // // +k8s:optional - // +k8s:maxProperties=32 - // +k8s:eachKey=+k8s:maxLength=128 - // +k8s:eachVal=+k8s:maxLength=256 + // +k8s:customValidation # at most 40 KiB in total, ten times the CSI limit for map fields VolumeContext map[string]string `protobuf:"bytes,5,rep,name=volume_context,json=volumeContext,proto3" json:"volume_context,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -3877,7 +3875,7 @@ type SystemInfoVolumeSource struct { // +k8s:optional // +k8s:maxItems=8 // +k8s:listType=atomic - // +k8s:customValidation # paths unique across entries + // +k8s:customValidation # at most one actor_metadata; paths unique across entries DataSources []*SystemInfoDataSource `protobuf:"bytes,1,rep,name=data_sources,json=dataSources,proto3" json:"data_sources,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 3762751f22..b53d1541e7 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -425,13 +425,11 @@ message ExternalVolume { // volume_context contains metadata returned by the CSI driver during volume // provisioning, needed by the node plugin for mounting (e.g. attachment - // info). Keys and values are the driver's own, so they are only bounded, + // info). Keys and values are the driver's own, so the map is only bounded, // not validated. // // +k8s:optional - // +k8s:maxProperties=32 - // +k8s:eachKey=+k8s:maxLength=128 - // +k8s:eachVal=+k8s:maxLength=256 + // +k8s:customValidation # at most 40 KiB in total, ten times the CSI limit for map fields map volume_context = 5; } @@ -1392,7 +1390,7 @@ message SystemInfoVolumeSource { // +k8s:optional // +k8s:maxItems=8 // +k8s:listType=atomic - // +k8s:customValidation # paths unique across entries + // +k8s:customValidation # at most one actor_metadata; paths unique across entries repeated SystemInfoDataSource data_sources = 1; }