cri-api: Add the security profile RPCs and the OCI profile type

Add the CRI surface of KEP-6061: the OCI profile type with oci_ref and
base_profile in SecurityProfile, the SecurityProfileBase message, the
PullSecurityProfile, ListSecurityProfiles, and RemoveSecurityProfile
ImageService RPCs with SecurityProfileKind, the seccomp_profile_oci
runtime feature, and the SecurityProfileInvalid well-known error for
permanent pull failures. The kubelet owns the lifecycle of pulled
profiles through the list and remove RPCs. The remote client, the fake
image service, the kubelet's instrumented service, and the e2e CRI
proxies implement the new methods; the kubelet does not call them yet.

Signed-off-by: Sascha Grunert <sgrunert@redhat.com>
This commit is contained in:
Sascha Grunert committed 2026-10-23 13:04:35 +02:00
1 parent 674b5161f0
commit af09bec1f4
12 files changed
+2512 -1085

No files matched your search

@@ -317,6 +317,33 @@ func (in instrumentedImageManagerService) PullImage(ctx context.Context, image *
return imageRef, err
}
func (in instrumentedImageManagerService) PullSecurityProfile(ctx context.Context, image *runtimeapi.ImageSpec, auth *runtimeapi.AuthConfig, podSandboxConfig *runtimeapi.PodSandboxConfig, kind runtimeapi.SecurityProfileKind) (*runtimeapi.PullSecurityProfileResponse, error) {
const operation = "pull_security_profile"
defer recordOperation(operation, time.Now())
resp, err := in.service.PullSecurityProfile(ctx, image, auth, podSandboxConfig, kind)
recordError(operation, err)
return resp, err
}
func (in instrumentedImageManagerService) ListSecurityProfiles(ctx context.Context) ([]*runtimeapi.SecurityProfileInfo, error) {
const operation = "list_security_profiles"
defer recordOperation(operation, time.Now())
out, err := in.service.ListSecurityProfiles(ctx)
recordError(operation, err)
return out, err
}
func (in instrumentedImageManagerService) RemoveSecurityProfile(ctx context.Context, digest string) error {
const operation = "remove_security_profile"
defer recordOperation(operation, time.Now())
err := in.service.RemoveSecurityProfile(ctx, digest)
recordError(operation, err)
return err
}
func (in instrumentedImageManagerService) RemoveImage(ctx context.Context, image *runtimeapi.ImageSpec) error {
const operation = "remove_image"
defer recordOperation(operation, time.Now())
File diff suppressed because it is too large. Load diff
@@ -249,6 +249,34 @@ service ImageService {
rpc ImageStatus(ImageStatusRequest) returns (ImageStatusResponse) {}
// PullImage pulls an image with authentication config.
rpc PullImage(PullImageRequest) returns (PullImageResponse) {}
// PullSecurityProfile pulls a security profile with authentication config
// into the runtime's storage. If the profile is already present, the
// runtime must not contact the registry, unless a signature policy that
// did not apply to the earlier pull requires it. The profile is applied by
// passing its reference in SecurityProfile.oci_ref. Accepted artifact
// formats and their validation are up to the runtime.
// Pulled profiles are not images: they must only be returned by
// ListSecurityProfiles, and the kubelet manages their lifecycle with
// RemoveSecurityProfile.
// Errors use the well-known messages RegistryUnavailable,
// SignatureValidationFailed, or, for permanent rejections of the request
// or the artifact, SecurityProfileInvalid as the prefix of the gRPC status
// message. The kubelet retries all errors with backoff except
// SecurityProfileInvalid and the gRPC code Unimplemented.
// Feature gate: SecurityProfileOCI
// See https://kep.k8s.io/6061 for more details.
rpc PullSecurityProfile(PullSecurityProfileRequest) returns (PullSecurityProfileResponse) {}
// ListSecurityProfiles lists the profiles pulled with PullSecurityProfile.
// Feature gate: SecurityProfileOCI
// See https://kep.k8s.io/6061 for more details.
rpc ListSecurityProfiles(ListSecurityProfilesRequest) returns (ListSecurityProfilesResponse) {}
// RemoveSecurityProfile removes a profile pulled with PullSecurityProfile.
// This call is idempotent, and must not return an error if the profile has
// already been removed. Containers already running with the profile are
// not affected.
// Feature gate: SecurityProfileOCI
// See https://kep.k8s.io/6061 for more details.
rpc RemoveSecurityProfile(RemoveSecurityProfileRequest) returns (RemoveSecurityProfileResponse) {}
// RemoveImage removes the image.
// This call is idempotent, and must not return an error if the image has
// already been removed.
@@ -532,6 +560,14 @@ message SecurityProfile {
Unconfined = 1;
// A pre-defined profile on the node should be used.
Localhost = 2;
// A profile pulled with PullSecurityProfile should be used, merged
// with its base profile (see base_profile). If the profile is not
// present, runtimes must fail the call and must not pull it. Only
// supported for seccomp: runtimes must reject OCI in the apparmor
// field with InvalidArgument.
// Feature gate: SecurityProfileOCI
// See https://kep.k8s.io/6061 for more details.
OCI = 3;
}
// Indicator which `ProfileType` should be applied.
ProfileType profile_type = 1;
@@ -540,6 +576,41 @@ message SecurityProfile {
// For seccomp, it must be an absolute path to the seccomp profile.
// For AppArmor, this field is the AppArmor `<profile name>/`
string localhost_ref = 2;
// oci_ref is the canonical, digest-pinned reference of a profile pulled
// with PullSecurityProfile, as written in the pod spec and passed in
// image.image of the pull (for example
// "registry.example.com/profile@sha256:abc123...").
// Must be set if `ProfileType` is `OCI`, and must not be set for other
// profile types: runtimes reject it with InvalidArgument.
string oci_ref = 3;
// base_profile is the optional base profile of an `OCI` profile. The
// effective profile is the intersection of the OCI profile and the base
// profile, so it permits an operation only if both do. If base_profile is
// not set, the base is the runtime default profile; runtimes must check
// that it is set, because its zero value selects RuntimeDefault as well.
// Must not be set for other profile types: runtimes reject it with
// InvalidArgument.
SecurityProfileBase base_profile = 4;
}
// SecurityProfileBase is the base profile of an OCI security profile. A
// Localhost base takes the place of the runtime default profile, so a node
// administrator can provide a looser or a stricter base than the default.
message SecurityProfileBase {
// Available base profile types.
enum BaseType {
// The container runtime default profile is used as the base.
RuntimeDefault = 0;
// A pre-defined profile on the node is used as the base.
Localhost = 1;
}
// Indicator which `BaseType` should be applied.
BaseType type = 1;
// Absolute path to the base profile on the node, with the same semantics
// as SecurityProfile.localhost_ref.
// Must be set if `BaseType` is `Localhost`, and must not be set for other
// base types: runtimes reject it with InvalidArgument.
string localhost_ref = 2;
}
// LinuxPodSandboxConfig holds platform-specific configurations for Linux
@@ -1832,6 +1903,74 @@ message PullImageResponse {
string image_ref = 1;
}
// SecurityProfileKind identifies the security mechanism of a profile pulled
// with PullSecurityProfile.
enum SecurityProfileKind {
// Unspecified kind. Runtimes reject it like an unsupported kind.
SecurityProfileKindUnspecified = 0;
// A seccomp profile.
Seccomp = 1;
}
message PullSecurityProfileRequest {
// Spec of the profile. image.image and image.user_specified_image both
// hold the canonical, digest-pinned reference from the pod spec, and
// image.runtime_handler selects the runtime handler, as for PullImage.
ImageSpec image = 1;
// Authentication configuration for pulling the profile, resolved by the
// kubelet the same way as for PullImage.
AuthConfig auth = 2;
// Config of the PodSandbox, which is used to pull the profile in
// PodSandbox context.
PodSandboxConfig sandbox_config = 3;
// profile_kind identifies the expected security mechanism. The runtime
// rejects artifacts that do not hold a profile of this kind. Kinds the
// runtime does not support are rejected with gRPC code InvalidArgument
// and the SecurityProfileInvalid well-known error message.
SecurityProfileKind profile_kind = 4;
}
message PullSecurityProfileResponse {
// cached is true if the profile was already present and the runtime made
// no registry request, so the credentials of this request were not
// checked. It is false if the registry served or verified the profile for
// this request. The kubelet calls PullSecurityProfile on every pod sync,
// emits pull events only for actual pulls, and labels its pull duration
// metric with it. The response exists only for a successful call: an
// error, whether RegistryUnavailable, SignatureValidationFailed, or
// SecurityProfileInvalid, means the profile cannot be used for this
// request, whether or not it is present.
bool cached = 1;
}
message ListSecurityProfilesRequest {}
// SecurityProfileInfo describes a profile pulled with PullSecurityProfile.
// The kubelet considers a profile in use if a pod references its digest or
// one of its refs.
message SecurityProfileInfo {
// digest identifies the profile in the runtime's storage (for example
// "sha256:abc123..."). Its algorithm may differ from the digests of refs.
string digest = 1;
// refs are the image.image values of all PullSecurityProfile calls that
// resolved to the profile, including calls that found it already present.
repeated string refs = 2;
// size is the size of the profile in the runtime's storage, in bytes.
uint64 size = 3;
}
message ListSecurityProfilesResponse {
// List of pulled security profiles.
repeated SecurityProfileInfo profiles = 1;
}
message RemoveSecurityProfileRequest {
// digest of the profile to remove, as returned by ListSecurityProfiles.
string digest = 1;
}
message RemoveSecurityProfileResponse {}
message RemoveImageRequest {
// Spec of the image to remove.
ImageSpec image = 1;
@@ -1926,6 +2065,12 @@ message RuntimeFeatures {
// cgroup_mount_mode is set to true if the runtime supports the
// cgroup_mount_mode field of LinuxContainerSecurityContext.
bool cgroup_mount_mode = 4;
// seccomp_profile_oci is set to true if the runtime supports the `OCI`
// profile type in the seccomp fields of sandboxes and containers, and the
// security profile RPCs for the Seccomp kind.
// Feature gate: SecurityProfileOCI
// See https://kep.k8s.io/6061 for more details.
bool seccomp_profile_oci = 5;
}
message StatusResponse {
@@ -1845,12 +1845,15 @@ var RuntimeService_ServiceDesc = grpc.ServiceDesc{
}
const (
ImageService_ListImages_FullMethodName = "/runtime.v1.ImageService/ListImages"
ImageService_StreamImages_FullMethodName = "/runtime.v1.ImageService/StreamImages"
ImageService_ImageStatus_FullMethodName = "/runtime.v1.ImageService/ImageStatus"
ImageService_PullImage_FullMethodName = "/runtime.v1.ImageService/PullImage"
ImageService_RemoveImage_FullMethodName = "/runtime.v1.ImageService/RemoveImage"
ImageService_ImageFsInfo_FullMethodName = "/runtime.v1.ImageService/ImageFsInfo"
ImageService_ListImages_FullMethodName = "/runtime.v1.ImageService/ListImages"
ImageService_StreamImages_FullMethodName = "/runtime.v1.ImageService/StreamImages"
ImageService_ImageStatus_FullMethodName = "/runtime.v1.ImageService/ImageStatus"
ImageService_PullImage_FullMethodName = "/runtime.v1.ImageService/PullImage"
ImageService_PullSecurityProfile_FullMethodName = "/runtime.v1.ImageService/PullSecurityProfile"
ImageService_ListSecurityProfiles_FullMethodName = "/runtime.v1.ImageService/ListSecurityProfiles"
ImageService_RemoveSecurityProfile_FullMethodName = "/runtime.v1.ImageService/RemoveSecurityProfile"
ImageService_RemoveImage_FullMethodName = "/runtime.v1.ImageService/RemoveImage"
ImageService_ImageFsInfo_FullMethodName = "/runtime.v1.ImageService/ImageFsInfo"
)
// ImageServiceClient is the client API for ImageService service.
@@ -1880,6 +1883,34 @@ type ImageServiceClient interface {
ImageStatus(ctx context.Context, in *ImageStatusRequest, opts ...grpc.CallOption) (*ImageStatusResponse, error)
// PullImage pulls an image with authentication config.
PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (*PullImageResponse, error)
// PullSecurityProfile pulls a security profile with authentication config
// into the runtime's storage. If the profile is already present, the
// runtime must not contact the registry, unless a signature policy that
// did not apply to the earlier pull requires it. The profile is applied by
// passing its reference in SecurityProfile.oci_ref. Accepted artifact
// formats and their validation are up to the runtime.
// Pulled profiles are not images: they must only be returned by
// ListSecurityProfiles, and the kubelet manages their lifecycle with
// RemoveSecurityProfile.
// Errors use the well-known messages RegistryUnavailable,
// SignatureValidationFailed, or, for permanent rejections of the request
// or the artifact, SecurityProfileInvalid as the prefix of the gRPC status
// message. The kubelet retries all errors with backoff except
// SecurityProfileInvalid and the gRPC code Unimplemented.
// Feature gate: SecurityProfileOCI
// See https://kep.k8s.io/6061 for more details.
PullSecurityProfile(ctx context.Context, in *PullSecurityProfileRequest, opts ...grpc.CallOption) (*PullSecurityProfileResponse, error)
// ListSecurityProfiles lists the profiles pulled with PullSecurityProfile.
// Feature gate: SecurityProfileOCI
// See https://kep.k8s.io/6061 for more details.
ListSecurityProfiles(ctx context.Context, in *ListSecurityProfilesRequest, opts ...grpc.CallOption) (*ListSecurityProfilesResponse, error)
// RemoveSecurityProfile removes a profile pulled with PullSecurityProfile.
// This call is idempotent, and must not return an error if the profile has
// already been removed. Containers already running with the profile are
// not affected.
// Feature gate: SecurityProfileOCI
// See https://kep.k8s.io/6061 for more details.
RemoveSecurityProfile(ctx context.Context, in *RemoveSecurityProfileRequest, opts ...grpc.CallOption) (*RemoveSecurityProfileResponse, error)
// RemoveImage removes the image.
// This call is idempotent, and must not return an error if the image has
// already been removed.
@@ -1953,6 +1984,36 @@ func (c *imageServiceClient) PullImage(ctx context.Context, in *PullImageRequest
return out, nil
}
func (c *imageServiceClient) PullSecurityProfile(ctx context.Context, in *PullSecurityProfileRequest, opts ...grpc.CallOption) (*PullSecurityProfileResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(PullSecurityProfileResponse)
err := c.cc.Invoke(ctx, ImageService_PullSecurityProfile_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *imageServiceClient) ListSecurityProfiles(ctx context.Context, in *ListSecurityProfilesRequest, opts ...grpc.CallOption) (*ListSecurityProfilesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListSecurityProfilesResponse)
err := c.cc.Invoke(ctx, ImageService_ListSecurityProfiles_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *imageServiceClient) RemoveSecurityProfile(ctx context.Context, in *RemoveSecurityProfileRequest, opts ...grpc.CallOption) (*RemoveSecurityProfileResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RemoveSecurityProfileResponse)
err := c.cc.Invoke(ctx, ImageService_RemoveSecurityProfile_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *imageServiceClient) RemoveImage(ctx context.Context, in *RemoveImageRequest, opts ...grpc.CallOption) (*RemoveImageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RemoveImageResponse)
@@ -2000,6 +2061,34 @@ type ImageServiceServer interface {
ImageStatus(context.Context, *ImageStatusRequest) (*ImageStatusResponse, error)
// PullImage pulls an image with authentication config.
PullImage(context.Context, *PullImageRequest) (*PullImageResponse, error)
// PullSecurityProfile pulls a security profile with authentication config
// into the runtime's storage. If the profile is already present, the
// runtime must not contact the registry, unless a signature policy that
// did not apply to the earlier pull requires it. The profile is applied by
// passing its reference in SecurityProfile.oci_ref. Accepted artifact
// formats and their validation are up to the runtime.
// Pulled profiles are not images: they must only be returned by
// ListSecurityProfiles, and the kubelet manages their lifecycle with
// RemoveSecurityProfile.
// Errors use the well-known messages RegistryUnavailable,
// SignatureValidationFailed, or, for permanent rejections of the request
// or the artifact, SecurityProfileInvalid as the prefix of the gRPC status
// message. The kubelet retries all errors with backoff except
// SecurityProfileInvalid and the gRPC code Unimplemented.
// Feature gate: SecurityProfileOCI
// See https://kep.k8s.io/6061 for more details.
PullSecurityProfile(context.Context, *PullSecurityProfileRequest) (*PullSecurityProfileResponse, error)
// ListSecurityProfiles lists the profiles pulled with PullSecurityProfile.
// Feature gate: SecurityProfileOCI
// See https://kep.k8s.io/6061 for more details.
ListSecurityProfiles(context.Context, *ListSecurityProfilesRequest) (*ListSecurityProfilesResponse, error)
// RemoveSecurityProfile removes a profile pulled with PullSecurityProfile.
// This call is idempotent, and must not return an error if the profile has
// already been removed. Containers already running with the profile are
// not affected.
// Feature gate: SecurityProfileOCI
// See https://kep.k8s.io/6061 for more details.
RemoveSecurityProfile(context.Context, *RemoveSecurityProfileRequest) (*RemoveSecurityProfileResponse, error)
// RemoveImage removes the image.
// This call is idempotent, and must not return an error if the image has
// already been removed.
@@ -2036,6 +2125,15 @@ func (UnimplementedImageServiceServer) ImageStatus(context.Context, *ImageStatus
func (UnimplementedImageServiceServer) PullImage(context.Context, *PullImageRequest) (*PullImageResponse, error) {
return nil, status.Error(codes.Unimplemented, "method PullImage not implemented")
}
func (UnimplementedImageServiceServer) PullSecurityProfile(context.Context, *PullSecurityProfileRequest) (*PullSecurityProfileResponse, error) {
return nil, status.Error(codes.Unimplemented, "method PullSecurityProfile not implemented")
}
func (UnimplementedImageServiceServer) ListSecurityProfiles(context.Context, *ListSecurityProfilesRequest) (*ListSecurityProfilesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListSecurityProfiles not implemented")
}
func (UnimplementedImageServiceServer) RemoveSecurityProfile(context.Context, *RemoveSecurityProfileRequest) (*RemoveSecurityProfileResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RemoveSecurityProfile not implemented")
}
func (UnimplementedImageServiceServer) RemoveImage(context.Context, *RemoveImageRequest) (*RemoveImageResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RemoveImage not implemented")
}
@@ -2128,6 +2226,60 @@ func _ImageService_PullImage_Handler(srv interface{}, ctx context.Context, dec f
return interceptor(ctx, in, info, handler)
}
func _ImageService_PullSecurityProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PullSecurityProfileRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ImageServiceServer).PullSecurityProfile(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ImageService_PullSecurityProfile_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ImageServiceServer).PullSecurityProfile(ctx, req.(*PullSecurityProfileRequest))
}
return interceptor(ctx, in, info, handler)
}
func _ImageService_ListSecurityProfiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListSecurityProfilesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ImageServiceServer).ListSecurityProfiles(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ImageService_ListSecurityProfiles_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ImageServiceServer).ListSecurityProfiles(ctx, req.(*ListSecurityProfilesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _ImageService_RemoveSecurityProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RemoveSecurityProfileRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ImageServiceServer).RemoveSecurityProfile(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ImageService_RemoveSecurityProfile_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ImageServiceServer).RemoveSecurityProfile(ctx, req.(*RemoveSecurityProfileRequest))
}
return interceptor(ctx, in, info, handler)
}
func _ImageService_RemoveImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RemoveImageRequest)
if err := dec(in); err != nil {
@@ -2183,6 +2335,18 @@ var ImageService_ServiceDesc = grpc.ServiceDesc{
MethodName: "PullImage",
Handler: _ImageService_PullImage_Handler,
},
{
MethodName: "PullSecurityProfile",
Handler: _ImageService_PullSecurityProfile_Handler,
},
{
MethodName: "ListSecurityProfiles",
Handler: _ImageService_ListSecurityProfiles_Handler,
},
{
MethodName: "RemoveSecurityProfile",
Handler: _ImageService_RemoveSecurityProfile_Handler,
},
{
MethodName: "RemoveImage",
Handler: _ImageService_RemoveImage_Handler,
@@ -141,6 +141,12 @@ type ImageManagerService interface {
ImageStatus(ctx context.Context, image *runtimeapi.ImageSpec, verbose bool) (*runtimeapi.ImageStatusResponse, error)
// PullImage pulls an image with the authentication config.
PullImage(ctx context.Context, image *runtimeapi.ImageSpec, auth *runtimeapi.AuthConfig, podSandboxConfig *runtimeapi.PodSandboxConfig) (string, error)
// PullSecurityProfile pulls a security profile with the authentication config.
PullSecurityProfile(ctx context.Context, image *runtimeapi.ImageSpec, auth *runtimeapi.AuthConfig, podSandboxConfig *runtimeapi.PodSandboxConfig, kind runtimeapi.SecurityProfileKind) (*runtimeapi.PullSecurityProfileResponse, error)
// ListSecurityProfiles lists the pulled security profiles.
ListSecurityProfiles(ctx context.Context) ([]*runtimeapi.SecurityProfileInfo, error)
// RemoveSecurityProfile removes a pulled security profile by its digest.
RemoveSecurityProfile(ctx context.Context, digest string) error
// RemoveImage removes the image.
RemoveImage(ctx context.Context, image *runtimeapi.ImageSpec) error
// ImageFsInfo returns information of the filesystem(s) used to store the read-only layers and the writeable layer.
@@ -18,12 +18,17 @@ package testing
import (
"context"
"slices"
"strings"
"sync"
"testing"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
crierrors "k8s.io/cri-api/pkg/errors"
)
// FakeImageService fakes the image service.
@@ -38,6 +43,11 @@ type FakeImageService struct {
pulledImages []*pulledImage
// SecurityProfiles maps the references of the pulled security profiles to their kinds.
SecurityProfiles map[string]runtimeapi.SecurityProfileKind
pulledSecurityProfiles []*pulledSecurityProfile
FakeFilesystemUsage []*runtimeapi.FilesystemUsage
FakeContainerFilesystemUsage []*runtimeapi.FilesystemUsage
}
@@ -108,6 +118,8 @@ func NewFakeImageService() *FakeImageService {
Called: make([]string, 0),
Errors: make(map[string][]error),
Images: make(map[string]*runtimeapi.Image),
SecurityProfiles: make(map[string]runtimeapi.SecurityProfileKind),
}
}
@@ -210,6 +222,98 @@ func (r *FakeImageService) PullImage(_ context.Context, image *runtimeapi.ImageS
return imageID, nil
}
// PullSecurityProfile emulates pulling a security profile into the FakeImageService.
func (r *FakeImageService) PullSecurityProfile(_ context.Context, image *runtimeapi.ImageSpec, auth *runtimeapi.AuthConfig, podSandboxConfig *runtimeapi.PodSandboxConfig, kind runtimeapi.SecurityProfileKind) (*runtimeapi.PullSecurityProfileResponse, error) {
r.Lock()
defer r.Unlock()
r.Called = append(r.Called, "PullSecurityProfile")
if err := r.popError("PullSecurityProfile"); err != nil {
return nil, err
}
// Runtimes reject kinds they do not support.
if kind != runtimeapi.SecurityProfileKind_Seccomp {
return nil, status.Errorf(codes.InvalidArgument, "%s: unsupported security profile kind %v", crierrors.ErrSecurityProfileInvalid, kind)
}
// References must be digest-pinned, which also keeps the profiles
// listed and removed by digest apart.
ref := image.GetImage()
if securityProfileDigest(ref) == "" {
return nil, status.Errorf(codes.InvalidArgument, "%s: security profile reference %q is not digest-pinned", crierrors.ErrSecurityProfileInvalid, ref)
}
r.pulledSecurityProfiles = append(r.pulledSecurityProfiles, &pulledSecurityProfile{imageSpec: image, authConfig: auth, kind: kind})
if r.SecurityProfiles == nil {
r.SecurityProfiles = make(map[string]runtimeapi.SecurityProfileKind)
}
_, cached := r.SecurityProfiles[ref]
r.SecurityProfiles[ref] = kind
return &runtimeapi.PullSecurityProfileResponse{Cached: cached}, nil
}
// ListSecurityProfiles lists the security profiles pulled into the
// FakeImageService, grouping their references by digest.
func (r *FakeImageService) ListSecurityProfiles(_ context.Context) ([]*runtimeapi.SecurityProfileInfo, error) {
r.Lock()
defer r.Unlock()
r.Called = append(r.Called, "ListSecurityProfiles")
if err := r.popError("ListSecurityProfiles"); err != nil {
return nil, err
}
byDigest := map[string]*runtimeapi.SecurityProfileInfo{}
for ref := range r.SecurityProfiles {
digest := securityProfileDigest(ref)
info, ok := byDigest[digest]
if !ok {
info = &runtimeapi.SecurityProfileInfo{Digest: digest, Size: r.FakeImageSize}
byDigest[digest] = info
}
info.Refs = append(info.Refs, ref)
}
profiles := make([]*runtimeapi.SecurityProfileInfo, 0, len(byDigest))
for _, info := range byDigest {
slices.Sort(info.Refs)
profiles = append(profiles, info)
}
slices.SortFunc(profiles, func(a, b *runtimeapi.SecurityProfileInfo) int {
return strings.Compare(a.Digest, b.Digest)
})
return profiles, nil
}
// RemoveSecurityProfile removes all references of a security profile digest
// from the FakeImageService.
func (r *FakeImageService) RemoveSecurityProfile(_ context.Context, digest string) error {
r.Lock()
defer r.Unlock()
r.Called = append(r.Called, "RemoveSecurityProfile")
if err := r.popError("RemoveSecurityProfile"); err != nil {
return err
}
if digest == "" {
return status.Error(codes.InvalidArgument, "security profile digest must not be empty")
}
for ref := range r.SecurityProfiles {
if securityProfileDigest(ref) == digest {
delete(r.SecurityProfiles, ref)
}
}
return nil
}
// securityProfileDigest returns the digest of a digest-pinned reference.
func securityProfileDigest(ref string) string {
_, digest, _ := strings.Cut(ref, "@")
return digest
}
// RemoveImage removes image from the FakeImageService.
func (r *FakeImageService) RemoveImage(_ context.Context, image *runtimeapi.ImageSpec) error {
r.Lock()
@@ -260,6 +364,26 @@ type pulledImage struct {
authConfig *runtimeapi.AuthConfig
}
// AssertSecurityProfilePulledWithAuth validates whether the security profile
// was pulled with auth and kind and asserts if it wasn't.
func (r *FakeImageService) AssertSecurityProfilePulledWithAuth(t *testing.T, image *runtimeapi.ImageSpec, auth *runtimeapi.AuthConfig, kind runtimeapi.SecurityProfileKind, failMsg string) {
t.Helper()
r.Lock()
defer r.Unlock()
for _, p := range r.pulledSecurityProfiles {
if proto.Equal(p.imageSpec, image) && proto.Equal(p.authConfig, auth) && p.kind == kind {
return
}
}
t.Errorf("%s: security profile %v was not pulled with auth %v and kind %v", failMsg, image, auth, kind)
}
type pulledSecurityProfile struct {
imageSpec *runtimeapi.ImageSpec
authConfig *runtimeapi.AuthConfig
kind runtimeapi.SecurityProfileKind
}
// Close will shutdown the internal gRPC client connection.
func (r *FakeImageService) Close(_ context.Context) error {
r.Lock()
@@ -24,10 +24,10 @@ import (
)
var (
// ErrRegistryUnavailable - Get http error on the PullImage RPC call.
// ErrRegistryUnavailable - Get http error on the PullImage or PullSecurityProfile RPC call.
ErrRegistryUnavailable = errors.New("RegistryUnavailable")
// ErrSignatureValidationFailed - Unable to validate the image signature on the PullImage RPC call.
// ErrSignatureValidationFailed - Unable to validate the image or profile signature on the PullImage or PullSecurityProfile RPC call.
ErrSignatureValidationFailed = errors.New("SignatureValidationFailed")
// ErrRROUnsupported - Unable to enforce recursive readonly mounts
@@ -35,6 +35,10 @@ var (
// ErrImageVolumeMountFailed - Unable to mount an image volume.
ErrImageVolumeMountFailed = errors.New("ImageVolumeMountFailed")
// ErrSecurityProfileInvalid - The PullSecurityProfile RPC call rejected
// the requested security profile as invalid.
ErrSecurityProfileInvalid = errors.New("SecurityProfileInvalid")
)
// IsNotFound returns a boolean indicating whether the error
@@ -58,6 +58,32 @@ func (f *RemoteRuntime) PullImage(ctx context.Context, req *kubeapi.PullImageReq
}, nil
}
// PullSecurityProfile pulls a security profile with authentication config.
func (f *RemoteRuntime) PullSecurityProfile(ctx context.Context, req *kubeapi.PullSecurityProfileRequest) (*kubeapi.PullSecurityProfileResponse, error) {
return f.ImageService.PullSecurityProfile(ctx, req.Image, req.Auth, req.SandboxConfig, req.ProfileKind)
}
// ListSecurityProfiles lists the pulled security profiles.
func (f *RemoteRuntime) ListSecurityProfiles(ctx context.Context, req *kubeapi.ListSecurityProfilesRequest) (*kubeapi.ListSecurityProfilesResponse, error) {
profiles, err := f.ImageService.ListSecurityProfiles(ctx)
if err != nil {
return nil, err
}
return &kubeapi.ListSecurityProfilesResponse{
Profiles: profiles,
}, nil
}
// RemoveSecurityProfile removes a pulled security profile.
func (f *RemoteRuntime) RemoveSecurityProfile(ctx context.Context, req *kubeapi.RemoveSecurityProfileRequest) (*kubeapi.RemoveSecurityProfileResponse, error) {
if err := f.ImageService.RemoveSecurityProfile(ctx, req.Digest); err != nil {
return nil, err
}
return &kubeapi.RemoveSecurityProfileResponse{}, nil
}
// RemoveImage removes the image.
// This call is idempotent, and must not return an error if the image has
// already been removed.
@@ -332,18 +332,7 @@ func (r *remoteImageService) pullImageV1(ctx context.Context, image *runtimeapi.
if err != nil {
logger := klog.FromContext(ctx)
logger.Error(err, "PullImage from image service failed", "image", image.Image)
// We can strip the code from unknown status errors since they add no value
// and will make them easier to read in the logs/events.
//
// It also ensures that checking custom error types from pkg/kubelet/images/types.go
// works in `imageManager.EnsureImageExists` (pkg/kubelet/images/image_manager.go).
statusErr, ok := status.FromError(err)
if ok && statusErr.Code() == codes.Unknown {
return "", errors.New(statusErr.Message())
}
return "", err
return "", stripUnknownStatusCode(err)
}
if resp.ImageRef == "" {
@@ -356,6 +345,74 @@ func (r *remoteImageService) pullImageV1(ctx context.Context, image *runtimeapi.
return resp.ImageRef, nil
}
// stripUnknownStatusCode strips the code from unknown status errors since it
// adds no value and makes them easier to read in the logs/events.
//
// It also ensures that checking custom error types from pkg/kubelet/images/types.go
// works in `imageManager.EnsureImageExists` (pkg/kubelet/images/image_manager.go).
// Errors with other codes are returned as they are; callers that match the
// well-known error messages of the CRI use the status message for that.
func stripUnknownStatusCode(err error) error {
statusErr, ok := status.FromError(err)
if ok && statusErr.Code() == codes.Unknown {
return errors.New(statusErr.Message())
}
return err
}
// PullSecurityProfile pulls a security profile with authentication config.
// Unlike PullImage, it is bound by the runtime request timeout, because
// profile artifacts are small.
func (r *remoteImageService) PullSecurityProfile(ctx context.Context, image *runtimeapi.ImageSpec, auth *runtimeapi.AuthConfig, podSandboxConfig *runtimeapi.PodSandboxConfig, kind runtimeapi.SecurityProfileKind) (*runtimeapi.PullSecurityProfileResponse, error) {
ctx, cancel := context.WithTimeout(ctx, r.timeout)
defer cancel()
resp, err := r.imageClient.PullSecurityProfile(ctx, &runtimeapi.PullSecurityProfileRequest{
Image: image,
Auth: auth,
SandboxConfig: podSandboxConfig,
ProfileKind: kind,
})
if err != nil {
logger := klog.FromContext(ctx)
logger.Error(err, "PullSecurityProfile from image service failed", "image", image.GetImage(), "kind", kind)
return nil, stripUnknownStatusCode(err)
}
return resp, nil
}
// ListSecurityProfiles lists the pulled security profiles.
func (r *remoteImageService) ListSecurityProfiles(ctx context.Context) ([]*runtimeapi.SecurityProfileInfo, error) {
ctx, cancel := context.WithTimeout(ctx, r.timeout)
defer cancel()
resp, err := r.imageClient.ListSecurityProfiles(ctx, &runtimeapi.ListSecurityProfilesRequest{})
if err != nil {
logger := klog.FromContext(ctx)
logger.Error(err, "ListSecurityProfiles from image service failed")
return nil, err
}
return resp.Profiles, nil
}
// RemoveSecurityProfile removes a pulled security profile by its digest.
func (r *remoteImageService) RemoveSecurityProfile(ctx context.Context, digest string) error {
ctx, cancel := context.WithTimeout(ctx, r.timeout)
defer cancel()
if _, err := r.imageClient.RemoveSecurityProfile(ctx, &runtimeapi.RemoveSecurityProfileRequest{
Digest: digest,
}); err != nil {
logger := klog.FromContext(ctx)
logger.Error(err, "RemoveSecurityProfile from image service failed", "digest", digest)
return err
}
return nil
}
// RemoveImage removes the image.
func (r *remoteImageService) RemoveImage(ctx context.Context, image *runtimeapi.ImageSpec) (err error) {
ctx, cancel := context.WithTimeout(ctx, r.timeout)
@@ -18,15 +18,23 @@ package cri
import (
"context"
"errors"
"fmt"
"os"
"runtime"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
internalapi "k8s.io/cri-api/pkg/apis"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
crierrors "k8s.io/cri-api/pkg/errors"
fakeremote "k8s.io/cri-client/pkg/fake"
"k8s.io/cri-client/pkg/util"
)
@@ -132,3 +140,158 @@ func TestNewRemoteImageServiceUnixSocketEndpoint(t *testing.T) {
require.NoError(t, err)
assert.NotNil(t, info)
}
// newRemoteImageService starts a fake remote runtime and returns it together
// with an image service connected to it. Both are stopped when the test ends.
func newRemoteImageService(t *testing.T) (*fakeremote.RemoteRuntime, internalapi.ImageManagerService) {
t.Helper()
fakeRuntime, endpoint := createAndStartFakeRemoteRuntime(t)
t.Cleanup(func() {
fakeRuntime.Stop()
// clear endpoint file
if addr, _, err := util.GetAddressAndDialer(endpoint); err == nil {
if _, err := os.Stat(addr); err == nil {
if err := os.Remove(addr); err != nil {
t.Errorf("remove %q: %v", addr, err)
}
}
}
})
imgSvc, err := NewRemoteImageServiceBuilder().
WithEndpoint(endpoint).
WithConnectionTimeout(defaultConnectionTimeout).
Build(context.Background())
require.NoError(t, err)
t.Cleanup(func() {
if err := imgSvc.Close(context.Background()); err != nil {
t.Errorf("close image service: %v", err)
}
})
return fakeRuntime, imgSvc
}
func TestPullSecurityProfile(t *testing.T) {
fakeRuntime, imgSvc := newRemoteImageService(t)
ctx := context.Background()
const ref = "registry.example.com/profile@sha256:0000000000000000000000000000000000000000000000000000000000000000"
image := &runtimeapi.ImageSpec{Image: ref}
auth := &runtimeapi.AuthConfig{Username: "user", Password: "pass"}
resp, err := imgSvc.PullSecurityProfile(ctx, image, auth, nil, runtimeapi.SecurityProfileKind_Seccomp)
require.NoError(t, err)
assert.False(t, resp.Cached)
fakeRuntime.ImageService.AssertSecurityProfilePulledWithAuth(t, image, auth, runtimeapi.SecurityProfileKind_Seccomp, "first pull")
fakeRuntime.ImageService.Lock()
assert.Equal(t, runtimeapi.SecurityProfileKind_Seccomp, fakeRuntime.ImageService.SecurityProfiles[ref])
fakeRuntime.ImageService.Unlock()
resp, err = imgSvc.PullSecurityProfile(ctx, image, nil, nil, runtimeapi.SecurityProfileKind_Seccomp)
require.NoError(t, err)
assert.True(t, resp.Cached)
// Unknown codes are stripped like for PullImage, other codes are kept
// with their message, which callers match by its well-known prefix.
for _, tc := range []struct {
name string
injected error
expectedErr string
expectedCode codes.Code
expectedMessage string
}{{
name: "unknown code is stripped",
injected: errors.New("boom"),
expectedErr: "boom",
}, {
name: "unknown code with a well-known prefix is stripped",
injected: fmt.Errorf("%w: wrong config media type", crierrors.ErrSecurityProfileInvalid),
expectedErr: "SecurityProfileInvalid: wrong config media type",
}, {
name: "SecurityProfileInvalid with another code keeps it",
injected: status.Errorf(codes.InvalidArgument, "%s: layer count 2", crierrors.ErrSecurityProfileInvalid),
expectedCode: codes.InvalidArgument,
expectedMessage: "SecurityProfileInvalid: layer count 2",
}, {
name: "RegistryUnavailable with another code keeps it",
injected: status.Errorf(codes.Unavailable, "%s: connection refused", crierrors.ErrRegistryUnavailable),
expectedCode: codes.Unavailable,
expectedMessage: "RegistryUnavailable: connection refused",
}, {
name: "other codes are kept",
injected: status.Error(codes.Unimplemented, "unknown method PullSecurityProfile"),
expectedCode: codes.Unimplemented,
expectedMessage: "unknown method PullSecurityProfile",
}} {
t.Run(tc.name, func(t *testing.T) {
fakeRuntime.ImageService.InjectError("PullSecurityProfile", tc.injected)
_, err := imgSvc.PullSecurityProfile(ctx, image, nil, nil, runtimeapi.SecurityProfileKind_Seccomp)
if tc.expectedErr != "" {
require.EqualError(t, err, tc.expectedErr)
} else {
require.Equal(t, tc.expectedCode, status.Code(err))
require.Equal(t, tc.expectedMessage, status.Convert(err).Message())
}
})
}
// Unsupported kinds and references without a digest are rejected
// permanently, with the code the proto asks for.
for _, tc := range []struct {
name string
image *runtimeapi.ImageSpec
kind runtimeapi.SecurityProfileKind
}{
{name: "unspecified kind", image: image, kind: runtimeapi.SecurityProfileKind_SecurityProfileKindUnspecified},
{name: "unknown kind", image: image, kind: runtimeapi.SecurityProfileKind(42)},
{name: "tag", image: &runtimeapi.ImageSpec{Image: "registry.example.com/profile:latest"}, kind: runtimeapi.SecurityProfileKind_Seccomp},
{name: "no image", image: nil, kind: runtimeapi.SecurityProfileKind_Seccomp},
} {
t.Run(tc.name, func(t *testing.T) {
_, err := imgSvc.PullSecurityProfile(ctx, tc.image, nil, nil, tc.kind)
require.Equal(t, codes.InvalidArgument, status.Code(err))
require.True(t, strings.HasPrefix(status.Convert(err).Message(), crierrors.ErrSecurityProfileInvalid.Error()), err.Error())
})
}
}
func TestListAndRemoveSecurityProfiles(t *testing.T) {
fakeRuntime, imgSvc := newRemoteImageService(t)
ctx := context.Background()
const (
digestA = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
digestB = "sha256:1111111111111111111111111111111111111111111111111111111111111111"
refA = "registry.example.com/a@" + digestA
mirrorA = "mirror.example.com/a@" + digestA
refB = "registry.example.com/b@" + digestB
)
fakeRuntime.ImageService.SetFakeImageSize(42)
for _, ref := range []string{refA, mirrorA, refB} {
_, err := imgSvc.PullSecurityProfile(ctx, &runtimeapi.ImageSpec{Image: ref}, nil, nil, runtimeapi.SecurityProfileKind_Seccomp)
require.NoError(t, err)
}
profiles, err := imgSvc.ListSecurityProfiles(ctx)
require.NoError(t, err)
require.Len(t, profiles, 2)
assert.Equal(t, digestA, profiles[0].Digest)
assert.Equal(t, []string{mirrorA, refA}, profiles[0].Refs)
assert.Equal(t, uint64(42), profiles[0].Size)
assert.Equal(t, digestB, profiles[1].Digest)
// Removing a digest removes all of its references, and removing it again
// is not an error. An empty digest is rejected.
require.Equal(t, codes.InvalidArgument, status.Code(imgSvc.RemoveSecurityProfile(ctx, "")))
require.NoError(t, imgSvc.RemoveSecurityProfile(ctx, digestA))
require.NoError(t, imgSvc.RemoveSecurityProfile(ctx, digestA))
profiles, err = imgSvc.ListSecurityProfiles(ctx)
require.NoError(t, err)
require.Len(t, profiles, 1)
assert.Equal(t, digestB, profiles[0].Digest)
fakeRuntime.ImageService.InjectError("ListSecurityProfiles", status.Error(codes.Unimplemented, "unknown method ListSecurityProfiles"))
_, err = imgSvc.ListSecurityProfiles(ctx)
require.Equal(t, codes.Unimplemented, status.Code(err))
}
+46 -5
View File
@@ -23,11 +23,14 @@ import (
)
const (
ListImages = "ListImages"
ImageStatus = "ImageStatus"
PullImage = "PullImage"
RemoveImage = "RemoveImage"
ImageFsInfo = "ImageFsInfo"
ListImages = "ListImages"
ImageStatus = "ImageStatus"
PullImage = "PullImage"
RemoveImage = "RemoveImage"
ImageFsInfo = "ImageFsInfo"
PullSecurityProfile = "PullSecurityProfile"
ListSecurityProfiles = "ListSecurityProfiles"
RemoveSecurityProfile = "RemoveSecurityProfile"
// Streaming APIs
StreamImages = "StreamImages"
// Per-send injection point for streaming APIs, called before each stream.Send().
@@ -79,6 +82,44 @@ func (p *RemoteRuntime) PullImage(ctx context.Context, req *kubeapi.PullImageReq
}, nil
}
// PullSecurityProfile pulls a security profile with authentication config.
func (p *RemoteRuntime) PullSecurityProfile(ctx context.Context, req *kubeapi.PullSecurityProfileRequest) (*kubeapi.PullSecurityProfileResponse, error) {
if err := p.runInjectors(PullSecurityProfile); err != nil {
return nil, err
}
return p.imageService.PullSecurityProfile(ctx, req.Image, req.Auth, req.SandboxConfig, req.ProfileKind)
}
// ListSecurityProfiles lists the pulled security profiles.
func (p *RemoteRuntime) ListSecurityProfiles(ctx context.Context, req *kubeapi.ListSecurityProfilesRequest) (*kubeapi.ListSecurityProfilesResponse, error) {
if err := p.runInjectors(ListSecurityProfiles); err != nil {
return nil, err
}
profiles, err := p.imageService.ListSecurityProfiles(ctx)
if err != nil {
return nil, err
}
return &kubeapi.ListSecurityProfilesResponse{
Profiles: profiles,
}, nil
}
// RemoveSecurityProfile removes a pulled security profile.
func (p *RemoteRuntime) RemoveSecurityProfile(ctx context.Context, req *kubeapi.RemoveSecurityProfileRequest) (*kubeapi.RemoveSecurityProfileResponse, error) {
if err := p.runInjectors(RemoveSecurityProfile); err != nil {
return nil, err
}
if err := p.imageService.RemoveSecurityProfile(ctx, req.Digest); err != nil {
return nil, err
}
return &kubeapi.RemoveSecurityProfileResponse{}, nil
}
// RemoveImage removes the image.
// This call is idempotent, and must not return an error if the image has
// already been removed.
@@ -23,11 +23,14 @@ import (
)
const (
ListImages = "ListImages"
ImageStatus = "ImageStatus"
PullImage = "PullImage"
RemoveImage = "RemoveImage"
ImageFsInfo = "ImageFsInfo"
ListImages = "ListImages"
ImageStatus = "ImageStatus"
PullImage = "PullImage"
RemoveImage = "RemoveImage"
ImageFsInfo = "ImageFsInfo"
PullSecurityProfile = "PullSecurityProfile"
ListSecurityProfiles = "ListSecurityProfiles"
RemoveSecurityProfile = "RemoveSecurityProfile"
// Streaming APIs
StreamImages = "StreamImages"
// Per-send injection point for streaming APIs, called before each stream.Send().
@@ -79,6 +82,44 @@ func (p *RemoteRuntime) PullImage(ctx context.Context, req *kubeapi.PullImageReq
}, nil
}
// PullSecurityProfile pulls a security profile with authentication config.
func (p *RemoteRuntime) PullSecurityProfile(ctx context.Context, req *kubeapi.PullSecurityProfileRequest) (*kubeapi.PullSecurityProfileResponse, error) {
if err := p.runInjectors(PullSecurityProfile); err != nil {
return nil, err
}
return p.imageService.PullSecurityProfile(ctx, req.Image, req.Auth, req.SandboxConfig, req.ProfileKind)
}
// ListSecurityProfiles lists the pulled security profiles.
func (p *RemoteRuntime) ListSecurityProfiles(ctx context.Context, req *kubeapi.ListSecurityProfilesRequest) (*kubeapi.ListSecurityProfilesResponse, error) {
if err := p.runInjectors(ListSecurityProfiles); err != nil {
return nil, err
}
profiles, err := p.imageService.ListSecurityProfiles(ctx)
if err != nil {
return nil, err
}
return &kubeapi.ListSecurityProfilesResponse{
Profiles: profiles,
}, nil
}
// RemoveSecurityProfile removes a pulled security profile.
func (p *RemoteRuntime) RemoveSecurityProfile(ctx context.Context, req *kubeapi.RemoveSecurityProfileRequest) (*kubeapi.RemoveSecurityProfileResponse, error) {
if err := p.runInjectors(RemoveSecurityProfile); err != nil {
return nil, err
}
if err := p.imageService.RemoveSecurityProfile(ctx, req.Digest); err != nil {
return nil, err
}
return &kubeapi.RemoveSecurityProfileResponse{}, nil
}
// RemoveImage removes the image.
// This call is idempotent, and must not return an error if the image has
// already been removed.