From fdecb9041c8e35f523c98a258a6abb3a558d1b12 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Wed, 2 Sep 2020 22:16:29 -0700 Subject: [PATCH] Implement SeccompProfile CRD and controller This change implements a SeccompProfile custom resource API. A new SeccompProfile type is created and the controller Reconciler is modified to handle either a SeccompProfile Kind or a ConfigMap describing a seccomp profile. The CRD manifest is generated by the new type. --- Makefile | 12 +- api/v1alpha1/groupversion_info.go | 36 ++++ api/v1alpha1/seccompprofile_types.go | 109 +++++++++++ api/v1alpha1/zz_generated.deepcopy.go | 177 ++++++++++++++++++ cmd/seccomp-operator/main.go | 5 + deploy/base/crd.yaml | 160 ++++++++++++++++ deploy/base/kustomization.yaml | 1 + deploy/base/rbac.yaml | 3 + deploy/namespace-operator.yaml | 169 +++++++++++++++++ deploy/operator.yaml | 169 +++++++++++++++++ examples/seccompprofile.yaml | 21 +++ go.sum | 6 + internal/pkg/config/config.go | 4 + internal/pkg/controllers/profile/profile.go | 154 ++++++++++----- .../pkg/controllers/profile/profile_test.go | 50 ++++- test/tc_default_profiles_test.go | 2 +- test/tc_invalid_profile_test.go | 2 +- 17 files changed, 1021 insertions(+), 59 deletions(-) create mode 100644 api/v1alpha1/groupversion_info.go create mode 100644 api/v1alpha1/seccompprofile_types.go create mode 100644 api/v1alpha1/zz_generated.deepcopy.go create mode 100644 deploy/base/crd.yaml create mode 100644 examples/seccompprofile.yaml diff --git a/Makefile b/Makefile index bd1af5d5f8..52515fa46b 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,8 @@ LDFLAGS := -s -w -linkmode external -extldflags "-static" $(LDVARS) CONTAINER_RUNTIME ?= docker IMAGE ?= $(PROJECT):latest +CRD_OPTIONS ?= "crd:crdVersions=v1" + GOLANGCI_LINT_VERSION = v1.30.0 REPO_INFRA_VERSION = v0.0.10 @@ -82,7 +84,7 @@ go-mod: ## Cleanup and verify go modules $(GO) mod verify .PHONY: deployments -deployments: ## Generate the deployment files with kustomize +deployments: manifests ## Generate the deployment files with kustomize kustomize build --reorder=none deploy/overlays/cluster -o deploy/operator.yaml kustomize build --reorder=none deploy/overlays/namespaced -o deploy/namespace-operator.yaml @@ -135,3 +137,11 @@ test-unit: $(BUILD_DIR) ## Run the unit tests .PHONY: test-e2e test-e2e: ## Run the end-to-end tests $(GO) test -timeout 20m -tags e2e -count=1 ./test/... -v + +# Generate CRD manifest +manifests: + $(GO) run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen $(CRD_OPTIONS) paths="./api/..." output:crd:stdout > deploy/base/crd.yaml + +# Generate deepcopy code +generate: + $(GO) run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen object:headerFile="hack/boilerplate/boilerplate.go.txt",year=$(shell date -u "+%Y") paths="./..." diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go new file mode 100644 index 0000000000..cccff93dc0 --- /dev/null +++ b/api/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha1 contains API Schema definitions for the seccomp-operator v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=seccomp-operator.k8s-sigs.io +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "seccomp-operator.k8s-sigs.io", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/v1alpha1/seccompprofile_types.go b/api/v1alpha1/seccompprofile_types.go new file mode 100644 index 0000000000..157b94af0e --- /dev/null +++ b/api/v1alpha1/seccompprofile_types.go @@ -0,0 +1,109 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "github.com/containers/common/pkg/seccomp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// SeccompProfileSpec defines the desired state of SeccompProfile. +type SeccompProfileSpec struct { + // Properties from containers/common/pkg/seccomp.Seccomp type + + // the default action for seccomp + //nolint:lll + // +kubebuilder:validation:Enum=SCMP_ACT_KILL;SCMP_ACT_KILL_PROCESS;SCMP_ACT_KILL_THREAD;SCMP_ACT_TRAP;SCMP_ACT_ERRNO;SCMP_ACT_TRACE;SCMP_ACT_ALLOW;SCMP_ACT_LOG + DefaultAction seccomp.Action `json:"defaultAction"` + // the architecture used for system calls + Architectures []*Arch `json:"architectures,omitempty"` + // match a syscall in seccomp. While this property is OPTIONAL, some values + // of defaultAction are not useful without syscalls entries. For example, + // if defaultAction is SCMP_ACT_KILL and syscalls is empty or unset, the + // kernel will kill the container process on its first syscall + Syscalls []*Syscall `json:"syscalls,omitempty"` + + // Additional properties from OCI runtime spec + + // list of flags to use with seccomp(2) + Flags []*Flag `json:"flags,omitempty"` +} + +//nolint:lll +// +kubebuilder:validation:Enum=SCMP_ARCH_X86;SCMP_ARCH_X86_64;SCMP_ARCH_X32;SCMP_ARCH_ARM;SCMP_ARCH_AARCH64;SCMP_ARCH_MIPS;SCMP_ARCH_MIPS64;SCMP_ARCH_MIPS64N32;SCMP_ARCH_MIPSEL;SCMP_ARCH_MIPSEL64;SCMP_ARCH_MIPSEL64N32;SCMP_ARCH_PPC;SCMP_ARCH_PPC64;SCMP_ARCH_PPC64LE;SCMP_ARCH_S390;SCMP_ARCH_S390X;SCMP_ARCH_PARISC;SCMP_ARCH_PARISC64;SCMP_ARCH_RISCV64 +type Arch string + +// +kubebuilder:validation:Enum=SECCOMP_FILTER_FLAG_TSYNC;SECCOMP_FILTER_FLAG_LOG;SECCOMP_FILTER_FLAG_SPEC_ALLOW +type Flag string + +// Syscall defines a syscall in seccomp. +type Syscall struct { + // the names of the syscalls + Names []string `json:"names"` + // the action for seccomp rules + //nolint:lll + // +kubebuilder:validation:Enum=SCMP_ACT_KILL;SCMP_ACT_KILL_PROCESS;SCMP_ACT_KILL_THREAD;SCMP_ACT_TRAP;SCMP_ACT_ERRNO;SCMP_ACT_TRACE;SCMP_ACT_ALLOW;SCMP_ACT_LOG + Action seccomp.Action `json:"action"` + // the errno return code to use. Some actions like SCMP_ACT_ERRNO and + // SCMP_ACT_TRACE allow to specify the errno code to return + ErrnoRet string `json:"errnoRet,omitempty"` + // the specific syscall in seccomp + // +kubebuilder:validation:MaxItems=6 + Args []*Arg `json:"args,omitempty"` +} + +// Arg defines the specific syscall in seccomp. +type Arg struct { + // the index for syscall arguments in seccomp + // +kubebuilder:validation:Minimum=0 + Index uint `json:"index"` + // the value for syscall arguments in seccomp + // +kubebuilder:validation:Minimum=0 + Value uint64 `json:"value,omitempty"` + // the value for syscall arguments in seccomp + // +kubebuilder:validation:Minimum=0 + ValueTwo uint64 `json:"valueTwo,omitempty"` + // the operator for syscall arguments in seccomp + //nolint:lll + // +kubebuilder:validation:Enum=SCMP_CMP_NE;SCMP_CMP_LT;SCMP_CMP_LE;SCMP_CMP_EQ;SCMP_CMP_GE;SCMP_CMP_GT;SCMP_CMP_MASKED_EQ + Op seccomp.Operator `json:"op"` +} + +// +kubebuilder:object:root=true + +// SeccompProfile is a cluster level specification for a seccomp profile. +// See https://github.com/opencontainers/runtime-spec/blob/master/config-linux.md#seccomp +// +kubebuilder:resource:shortName=sp +type SeccompProfile struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec SeccompProfileSpec `json:"spec,omitempty"` +} + +// +kubebuilder:object:root=true + +// SeccompProfileList contains a list of SeccompProfile. +type SeccompProfileList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []SeccompProfile `json:"items"` +} + +func init() { //nolint:gochecknoinits + SchemeBuilder.Register(&SeccompProfile{}, &SeccompProfileList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000000..374dd64324 --- /dev/null +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,177 @@ +// +build !ignore_autogenerated + +/* +Copyright 2020 The Kubernetes Authors. + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Arg) DeepCopyInto(out *Arg) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Arg. +func (in *Arg) DeepCopy() *Arg { + if in == nil { + return nil + } + out := new(Arg) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeccompProfile) DeepCopyInto(out *SeccompProfile) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeccompProfile. +func (in *SeccompProfile) DeepCopy() *SeccompProfile { + if in == nil { + return nil + } + out := new(SeccompProfile) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SeccompProfile) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeccompProfileList) DeepCopyInto(out *SeccompProfileList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]SeccompProfile, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeccompProfileList. +func (in *SeccompProfileList) DeepCopy() *SeccompProfileList { + if in == nil { + return nil + } + out := new(SeccompProfileList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SeccompProfileList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeccompProfileSpec) DeepCopyInto(out *SeccompProfileSpec) { + *out = *in + if in.Architectures != nil { + in, out := &in.Architectures, &out.Architectures + *out = make([]*Arch, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Arch) + **out = **in + } + } + } + if in.Syscalls != nil { + in, out := &in.Syscalls, &out.Syscalls + *out = make([]*Syscall, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Syscall) + (*in).DeepCopyInto(*out) + } + } + } + if in.Flags != nil { + in, out := &in.Flags, &out.Flags + *out = make([]*Flag, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Flag) + **out = **in + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeccompProfileSpec. +func (in *SeccompProfileSpec) DeepCopy() *SeccompProfileSpec { + if in == nil { + return nil + } + out := new(SeccompProfileSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Syscall) DeepCopyInto(out *Syscall) { + *out = *in + if in.Names != nil { + in, out := &in.Names, &out.Names + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make([]*Arg, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Arg) + **out = **in + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Syscall. +func (in *Syscall) DeepCopy() *Syscall { + if in == nil { + return nil + } + out := new(Syscall) + in.DeepCopyInto(out) + return out +} diff --git a/cmd/seccomp-operator/main.go b/cmd/seccomp-operator/main.go index 6a077841e6..e3dfcd857b 100644 --- a/cmd/seccomp-operator/main.go +++ b/cmd/seccomp-operator/main.go @@ -25,6 +25,7 @@ import ( "k8s.io/klog/v2/klogr" ctrl "sigs.k8s.io/controller-runtime" + seccompoperatorv1alpha1 "sigs.k8s.io/seccomp-operator/api/v1alpha1" "sigs.k8s.io/seccomp-operator/internal/pkg/config" "sigs.k8s.io/seccomp-operator/internal/pkg/controllers/profile" "sigs.k8s.io/seccomp-operator/internal/pkg/version" @@ -114,6 +115,10 @@ func run(*cli.Context) error { return errors.Wrap(err, "create manager") } + if err := seccompoperatorv1alpha1.AddToScheme(mgr.GetScheme()); err != nil { + return errors.Wrap(err, "add core seccomp APIs to scheme") + } + if err := profile.Setup(mgr, ctrl.Log.WithName("profile")); err != nil { return errors.Wrap(err, "setup profile controller") } diff --git a/deploy/base/crd.yaml b/deploy/base/crd.yaml new file mode 100644 index 0000000000..946d28602c --- /dev/null +++ b/deploy/base/crd.yaml @@ -0,0 +1,160 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.2.4 + creationTimestamp: null + name: seccompprofiles.seccomp-operator.k8s-sigs.io +spec: + group: seccomp-operator.k8s-sigs.io + names: + kind: SeccompProfile + listKind: SeccompProfileList + plural: seccompprofiles + shortNames: + - sp + singular: seccompprofile + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: SeccompProfile is a cluster level specification for a seccomp profile. See https://github.com/opencontainers/runtime-spec/blob/master/config-linux.md#seccomp + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SeccompProfileSpec defines the desired state of SeccompProfile. + properties: + architectures: + description: the architecture used for system calls + items: + enum: + - SCMP_ARCH_X86 + - SCMP_ARCH_X86_64 + - SCMP_ARCH_X32 + - SCMP_ARCH_ARM + - SCMP_ARCH_AARCH64 + - SCMP_ARCH_MIPS + - SCMP_ARCH_MIPS64 + - SCMP_ARCH_MIPS64N32 + - SCMP_ARCH_MIPSEL + - SCMP_ARCH_MIPSEL64 + - SCMP_ARCH_MIPSEL64N32 + - SCMP_ARCH_PPC + - SCMP_ARCH_PPC64 + - SCMP_ARCH_PPC64LE + - SCMP_ARCH_S390 + - SCMP_ARCH_S390X + - SCMP_ARCH_PARISC + - SCMP_ARCH_PARISC64 + - SCMP_ARCH_RISCV64 + type: string + type: array + defaultAction: + description: the default action for seccomp + enum: + - SCMP_ACT_KILL + - SCMP_ACT_KILL_PROCESS + - SCMP_ACT_KILL_THREAD + - SCMP_ACT_TRAP + - SCMP_ACT_ERRNO + - SCMP_ACT_TRACE + - SCMP_ACT_ALLOW + - SCMP_ACT_LOG + type: string + flags: + description: list of flags to use with seccomp(2) + items: + enum: + - SECCOMP_FILTER_FLAG_TSYNC + - SECCOMP_FILTER_FLAG_LOG + - SECCOMP_FILTER_FLAG_SPEC_ALLOW + type: string + type: array + syscalls: + description: match a syscall in seccomp. While this property is OPTIONAL, some values of defaultAction are not useful without syscalls entries. For example, if defaultAction is SCMP_ACT_KILL and syscalls is empty or unset, the kernel will kill the container process on its first syscall + items: + description: Syscall defines a syscall in seccomp. + properties: + action: + description: the action for seccomp rules + enum: + - SCMP_ACT_KILL + - SCMP_ACT_KILL_PROCESS + - SCMP_ACT_KILL_THREAD + - SCMP_ACT_TRAP + - SCMP_ACT_ERRNO + - SCMP_ACT_TRACE + - SCMP_ACT_ALLOW + - SCMP_ACT_LOG + type: string + args: + description: the specific syscall in seccomp + items: + description: Arg defines the specific syscall in seccomp. + properties: + index: + description: the index for syscall arguments in seccomp + minimum: 0 + type: integer + op: + description: the operator for syscall arguments in seccomp + enum: + - SCMP_CMP_NE + - SCMP_CMP_LT + - SCMP_CMP_LE + - SCMP_CMP_EQ + - SCMP_CMP_GE + - SCMP_CMP_GT + - SCMP_CMP_MASKED_EQ + type: string + value: + description: the value for syscall arguments in seccomp + format: int64 + minimum: 0 + type: integer + valueTwo: + description: the value for syscall arguments in seccomp + format: int64 + minimum: 0 + type: integer + required: + - index + - op + type: object + maxItems: 6 + type: array + errnoRet: + description: the errno return code to use. Some actions like SCMP_ACT_ERRNO and SCMP_ACT_TRACE allow to specify the errno code to return + type: string + names: + description: the names of the syscalls + items: + type: string + type: array + required: + - action + - names + type: object + type: array + required: + - defaultAction + type: object + type: object + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/deploy/base/kustomization.yaml b/deploy/base/kustomization.yaml index 4a610e60b5..4f3e2a37f7 100644 --- a/deploy/base/kustomization.yaml +++ b/deploy/base/kustomization.yaml @@ -19,6 +19,7 @@ commonLabels: resources: - rbac.yaml - operator.yaml + - crd.yaml configMapGenerator: - name: seccomp-operator-profile diff --git a/deploy/base/rbac.yaml b/deploy/base/rbac.yaml index aaded99fad..5aecd3133a 100644 --- a/deploy/base/rbac.yaml +++ b/deploy/base/rbac.yaml @@ -22,6 +22,9 @@ rules: - apiGroups: [""] resources: ["events"] verbs: ["create", "patch"] +- apiGroups: ["seccomp-operator.k8s-sigs.io"] + resources: ["seccompprofiles"] + verbs: ["get", "watch", "list"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/deploy/namespace-operator.yaml b/deploy/namespace-operator.yaml index 197bf44661..51240ef24c 100644 --- a/deploy/namespace-operator.yaml +++ b/deploy/namespace-operator.yaml @@ -36,6 +36,14 @@ rules: verbs: - create - patch +- apiGroups: + - seccomp-operator.k8s-sigs.io + resources: + - seccompprofiles + verbs: + - get + - watch + - list --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding @@ -183,6 +191,167 @@ spec: name: seccomp-operator-profile name: profile-configmap-volume --- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.2.4 + creationTimestamp: null + labels: + app: seccomp-operator + name: seccompprofiles.seccomp-operator.k8s-sigs.io +spec: + group: seccomp-operator.k8s-sigs.io + names: + kind: SeccompProfile + listKind: SeccompProfileList + plural: seccompprofiles + shortNames: + - sp + singular: seccompprofile + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: SeccompProfile is a cluster level specification for a seccomp profile. See https://github.com/opencontainers/runtime-spec/blob/master/config-linux.md#seccomp + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SeccompProfileSpec defines the desired state of SeccompProfile. + properties: + architectures: + description: the architecture used for system calls + items: + enum: + - SCMP_ARCH_X86 + - SCMP_ARCH_X86_64 + - SCMP_ARCH_X32 + - SCMP_ARCH_ARM + - SCMP_ARCH_AARCH64 + - SCMP_ARCH_MIPS + - SCMP_ARCH_MIPS64 + - SCMP_ARCH_MIPS64N32 + - SCMP_ARCH_MIPSEL + - SCMP_ARCH_MIPSEL64 + - SCMP_ARCH_MIPSEL64N32 + - SCMP_ARCH_PPC + - SCMP_ARCH_PPC64 + - SCMP_ARCH_PPC64LE + - SCMP_ARCH_S390 + - SCMP_ARCH_S390X + - SCMP_ARCH_PARISC + - SCMP_ARCH_PARISC64 + - SCMP_ARCH_RISCV64 + type: string + type: array + defaultAction: + description: the default action for seccomp + enum: + - SCMP_ACT_KILL + - SCMP_ACT_KILL_PROCESS + - SCMP_ACT_KILL_THREAD + - SCMP_ACT_TRAP + - SCMP_ACT_ERRNO + - SCMP_ACT_TRACE + - SCMP_ACT_ALLOW + - SCMP_ACT_LOG + type: string + flags: + description: list of flags to use with seccomp(2) + items: + enum: + - SECCOMP_FILTER_FLAG_TSYNC + - SECCOMP_FILTER_FLAG_LOG + - SECCOMP_FILTER_FLAG_SPEC_ALLOW + type: string + type: array + syscalls: + description: match a syscall in seccomp. While this property is OPTIONAL, some values of defaultAction are not useful without syscalls entries. For example, if defaultAction is SCMP_ACT_KILL and syscalls is empty or unset, the kernel will kill the container process on its first syscall + items: + description: Syscall defines a syscall in seccomp. + properties: + action: + description: the action for seccomp rules + enum: + - SCMP_ACT_KILL + - SCMP_ACT_KILL_PROCESS + - SCMP_ACT_KILL_THREAD + - SCMP_ACT_TRAP + - SCMP_ACT_ERRNO + - SCMP_ACT_TRACE + - SCMP_ACT_ALLOW + - SCMP_ACT_LOG + type: string + args: + description: the specific syscall in seccomp + items: + description: Arg defines the specific syscall in seccomp. + properties: + index: + description: the index for syscall arguments in seccomp + minimum: 0 + type: integer + op: + description: the operator for syscall arguments in seccomp + enum: + - SCMP_CMP_NE + - SCMP_CMP_LT + - SCMP_CMP_LE + - SCMP_CMP_EQ + - SCMP_CMP_GE + - SCMP_CMP_GT + - SCMP_CMP_MASKED_EQ + type: string + value: + description: the value for syscall arguments in seccomp + format: int64 + minimum: 0 + type: integer + valueTwo: + description: the value for syscall arguments in seccomp + format: int64 + minimum: 0 + type: integer + required: + - index + - op + type: object + maxItems: 6 + type: array + errnoRet: + description: the errno return code to use. Some actions like SCMP_ACT_ERRNO and SCMP_ACT_TRACE allow to specify the errno code to return + type: string + names: + description: the names of the syscalls + items: + type: string + type: array + required: + - action + - names + type: object + type: array + required: + - defaultAction + type: object + type: object + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- apiVersion: v1 data: seccomp-operator.json: | diff --git a/deploy/operator.yaml b/deploy/operator.yaml index 311020eb30..8f2c9b47c6 100644 --- a/deploy/operator.yaml +++ b/deploy/operator.yaml @@ -36,6 +36,14 @@ rules: verbs: - create - patch +- apiGroups: + - seccomp-operator.k8s-sigs.io + resources: + - seccompprofiles + verbs: + - get + - watch + - list --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding @@ -181,6 +189,167 @@ spec: name: seccomp-operator-profile name: profile-configmap-volume --- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.2.4 + creationTimestamp: null + labels: + app: seccomp-operator + name: seccompprofiles.seccomp-operator.k8s-sigs.io +spec: + group: seccomp-operator.k8s-sigs.io + names: + kind: SeccompProfile + listKind: SeccompProfileList + plural: seccompprofiles + shortNames: + - sp + singular: seccompprofile + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: SeccompProfile is a cluster level specification for a seccomp profile. See https://github.com/opencontainers/runtime-spec/blob/master/config-linux.md#seccomp + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SeccompProfileSpec defines the desired state of SeccompProfile. + properties: + architectures: + description: the architecture used for system calls + items: + enum: + - SCMP_ARCH_X86 + - SCMP_ARCH_X86_64 + - SCMP_ARCH_X32 + - SCMP_ARCH_ARM + - SCMP_ARCH_AARCH64 + - SCMP_ARCH_MIPS + - SCMP_ARCH_MIPS64 + - SCMP_ARCH_MIPS64N32 + - SCMP_ARCH_MIPSEL + - SCMP_ARCH_MIPSEL64 + - SCMP_ARCH_MIPSEL64N32 + - SCMP_ARCH_PPC + - SCMP_ARCH_PPC64 + - SCMP_ARCH_PPC64LE + - SCMP_ARCH_S390 + - SCMP_ARCH_S390X + - SCMP_ARCH_PARISC + - SCMP_ARCH_PARISC64 + - SCMP_ARCH_RISCV64 + type: string + type: array + defaultAction: + description: the default action for seccomp + enum: + - SCMP_ACT_KILL + - SCMP_ACT_KILL_PROCESS + - SCMP_ACT_KILL_THREAD + - SCMP_ACT_TRAP + - SCMP_ACT_ERRNO + - SCMP_ACT_TRACE + - SCMP_ACT_ALLOW + - SCMP_ACT_LOG + type: string + flags: + description: list of flags to use with seccomp(2) + items: + enum: + - SECCOMP_FILTER_FLAG_TSYNC + - SECCOMP_FILTER_FLAG_LOG + - SECCOMP_FILTER_FLAG_SPEC_ALLOW + type: string + type: array + syscalls: + description: match a syscall in seccomp. While this property is OPTIONAL, some values of defaultAction are not useful without syscalls entries. For example, if defaultAction is SCMP_ACT_KILL and syscalls is empty or unset, the kernel will kill the container process on its first syscall + items: + description: Syscall defines a syscall in seccomp. + properties: + action: + description: the action for seccomp rules + enum: + - SCMP_ACT_KILL + - SCMP_ACT_KILL_PROCESS + - SCMP_ACT_KILL_THREAD + - SCMP_ACT_TRAP + - SCMP_ACT_ERRNO + - SCMP_ACT_TRACE + - SCMP_ACT_ALLOW + - SCMP_ACT_LOG + type: string + args: + description: the specific syscall in seccomp + items: + description: Arg defines the specific syscall in seccomp. + properties: + index: + description: the index for syscall arguments in seccomp + minimum: 0 + type: integer + op: + description: the operator for syscall arguments in seccomp + enum: + - SCMP_CMP_NE + - SCMP_CMP_LT + - SCMP_CMP_LE + - SCMP_CMP_EQ + - SCMP_CMP_GE + - SCMP_CMP_GT + - SCMP_CMP_MASKED_EQ + type: string + value: + description: the value for syscall arguments in seccomp + format: int64 + minimum: 0 + type: integer + valueTwo: + description: the value for syscall arguments in seccomp + format: int64 + minimum: 0 + type: integer + required: + - index + - op + type: object + maxItems: 6 + type: array + errnoRet: + description: the errno return code to use. Some actions like SCMP_ACT_ERRNO and SCMP_ACT_TRACE allow to specify the errno code to return + type: string + names: + description: the names of the syscalls + items: + type: string + type: array + required: + - action + - names + type: object + type: array + required: + - defaultAction + type: object + type: object + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- apiVersion: v1 data: seccomp-operator.json: | diff --git a/examples/seccompprofile.yaml b/examples/seccompprofile.yaml new file mode 100644 index 0000000000..7a7987a47d --- /dev/null +++ b/examples/seccompprofile.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: seccomp-operator.k8s-sigs.io/v1alpha1 +kind: SeccompProfile +metadata: + name: profile-block +spec: + defaultAction: "SCMP_ACT_ERRNO" +--- +apiVersion: seccomp-operator.k8s-sigs.io/v1alpha1 +kind: SeccompProfile +metadata: + name: profile-complain +spec: + defaultAction: "SCMP_ACT_LOG" +--- +apiVersion: seccomp-operator.k8s-sigs.io/v1alpha1 +kind: SeccompProfile +metadata: + name: profile-allow +spec: + defaultAction: "SCMP_ACT_ALLOW" diff --git a/go.sum b/go.sum index 55c7aa4412..450855dafd 100644 --- a/go.sum +++ b/go.sum @@ -142,6 +142,7 @@ github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M= github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -231,6 +232,7 @@ github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Il github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= +github.com/gobuffalo/flect v0.1.5 h1:xpKq9ap8MbYfhuPCF0dBH854Gp9CxZjr/IocxELFflo= github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= @@ -398,10 +400,12 @@ github.com/maratori/testpackage v1.0.1/go.mod h1:ddKdw+XG0Phzhx8BFDTKgpWP4i7MpAp github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= @@ -575,6 +579,7 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -1007,6 +1012,7 @@ sigs.k8s.io/controller-runtime v0.6.1 h1:LcK2+nk0kmaOnKGN+vBcWHqY5WDJNJNB/c5pW+s sigs.k8s.io/controller-runtime v0.6.1/go.mod h1:XRYBPdbf5XJu9kpS84VJiZ7h/u1hF3gEORz0efEja7A= sigs.k8s.io/controller-runtime v0.6.2 h1:jkAnfdTYBpFwlmBn3pS5HFO06SfxvnTZ1p5PeEF/zAA= sigs.k8s.io/controller-runtime v0.6.2/go.mod h1:vhcq/rlnENJ09SIRp3EveTaZ0yqH526hjf9iJdbUJ/E= +sigs.k8s.io/controller-tools v0.2.4 h1:la1h46EzElvWefWLqfsXrnsO3lZjpkI0asTpX6h8PLA= sigs.k8s.io/controller-tools v0.2.4/go.mod h1:m/ztfQNocGYBgTTCmFdnK94uVvgxeZeE3LtJvd/jIzA= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca h1:6dsH6AYQWbyZmtttJNe8Gq1cXOeS1BdV3eW37zHilAQ= diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index c4d99c2ac2..f3ee117d70 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -32,6 +32,10 @@ const ( // profiles. DefaultProfilesConfigMapName = "default-profiles" + // CustomProfilesDirectoryName is the directory where profiles from the + // SeccompProfile CRD are stored. + CustomProfilesDirectoryName = "custom-profiles" + // NodeNameEnvKey is the default environment variable key for retrieving // the name of the current node. NodeNameEnvKey = "NODE_NAME" diff --git a/internal/pkg/controllers/profile/profile.go b/internal/pkg/controllers/profile/profile.go index 77fce118fe..403e67e319 100644 --- a/internal/pkg/controllers/profile/profile.go +++ b/internal/pkg/controllers/profile/profile.go @@ -39,6 +39,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" + seccompoperatorv1alpha1 "sigs.k8s.io/seccomp-operator/api/v1alpha1" "sigs.k8s.io/seccomp-operator/internal/pkg/config" ) @@ -50,6 +51,7 @@ const ( errGetProfile = "cannot get profile" errConfigMapNil = "config map cannot be nil" + errSeccompProfileNil = "seccomp profile cannot be nil" errSavingProfile = "cannot save profile" errCreatingOperatorDir = "cannot create operator directory" @@ -85,7 +87,7 @@ func isProfile(obj runtime.Object) bool { func Setup(mgr ctrl.Manager, l logr.Logger) error { const name = "profile" - return ctrl.NewControllerManagedBy(mgr). + if err := ctrl.NewControllerManagedBy(mgr). Named(name). For(&corev1.ConfigMap{}). WithEventFilter(resource.NewPredicates(isProfile)). @@ -93,34 +95,58 @@ func Setup(mgr ctrl.Manager, l logr.Logger) error { client: mgr.GetClient(), log: l, record: event.NewAPIRecorder(mgr.GetEventRecorderFor("profile")), + save: saveProfileOnDisk, + }); err != nil { + return err + } + return ctrl.NewControllerManagedBy(mgr). + Named(name). + For(&seccompoperatorv1alpha1.SeccompProfile{}). + Complete(&Reconciler{ + client: mgr.GetClient(), + log: l, + record: event.NewAPIRecorder(mgr.GetEventRecorderFor("profile")), + save: saveProfileOnDisk, }) } +type saver func(string, []byte) error + // A Reconciler reconciles seccomp profiles. type Reconciler struct { client client.Client log logr.Logger record event.Recorder + save saver } -// Reconcile reconciles a ConfigMap representing a seccomp profile. +// Reconcile reconciles a SeccompProfile or a ConfigMap representing a seccomp profile. func (r *Reconciler) Reconcile(req reconcile.Request) (reconcile.Result, error) { logger := r.log.WithValues("profile", req.Name, "namespace", req.Namespace) ctx, cancel := context.WithTimeout(context.Background(), reconcileTimeout) defer cancel() + // Look for the SeccompProfile Kind first + seccompProfile := &seccompoperatorv1alpha1.SeccompProfile{} + if err := r.client.Get(ctx, req.NamespacedName, seccompProfile); err != nil { + logger.Error(err, "unable to fetch SeccompProfile") + seccompProfile = nil + } + // If no SeccompProfile, look for a ConfigMap configMap := &corev1.ConfigMap{} - if err := r.client.Get(ctx, types.NamespacedName{Name: req.Name, Namespace: req.Namespace}, configMap); err != nil { - // Returning an error means we will be requeued implicitly. - return reconcile.Result{}, errors.Wrap(ignoreNotFound(err), errGetProfile) + if seccompProfile == nil { + if err := r.client.Get(ctx, types.NamespacedName{Name: req.Name, Namespace: req.Namespace}, configMap); err != nil { + // Returning an error means we will be requeued implicitly. + return reconcile.Result{}, errors.Wrap(ignoreNotFound(err), errGetProfile) + } } // Pre-check if the node supports seccomp if !seccomp.IsSupported() { err := errors.New("profile not added") logger.Error(err, fmt.Sprintf("node %q does not support seccomp", os.Getenv(config.NodeNameEnvKey))) - r.record.Event(configMap, + r.record.Event(seccompProfile, event.Warning(reasonSeccompNotSupported, err, os.Getenv(config.NodeNameEnvKey), "node does not support seccomp")) @@ -130,78 +156,108 @@ func (r *Reconciler) Reconcile(req reconcile.Request) (reconcile.Result, error) return reconcile.Result{}, nil } - for profileName, profileContent := range configMap.Data { + if seccompProfile != nil { + return r.reconcileSeccompProfile(seccompProfile, logger) + } + + return r.reconcileConfigMap(configMap, logger) +} + +func (r *Reconciler) reconcileSeccompProfile( + sp *seccompoperatorv1alpha1.SeccompProfile, l logr.Logger) (reconcile.Result, error) { + if sp == nil { + return reconcile.Result{}, errors.New(errSeccompProfileNil) + } + profileName := sp.Name + + profileContent, err := json.Marshal(sp.Spec) + if err != nil { + l.Error(err, "cannot validate profile "+profileName) + r.record.Event(sp, event.Warning(reasonInvalidSeccompProfile, err)) + return reconcile.Result{RequeueAfter: wait}, nil + } + + profilePath, err := GetProfilePath(profileName, sp.ObjectMeta.Namespace, config.CustomProfilesDirectoryName) + if err != nil { + l.Error(err, "cannot get profile path") + r.record.Event(sp, event.Warning(reasonCannotGetProfilePath, err)) + return reconcile.Result{RequeueAfter: wait}, nil + } + if err = r.save(profilePath, profileContent); err != nil { + l.Error(err, "cannot save profile into disk") + r.record.Event(sp, event.Warning(reasonCannotSaveProfile, err)) + return reconcile.Result{RequeueAfter: wait}, nil + } + l.Info( + "Reconciled profile from SeccompProfile", + "resource version", sp.GetResourceVersion(), + "name", sp.GetName(), + ) + r.record.Event(sp, event.Normal(reasonSavedProfile, "Successfully saved profile to disk")) + return reconcile.Result{}, nil +} + +func (r *Reconciler) reconcileConfigMap(cm *corev1.ConfigMap, l logr.Logger) (reconcile.Result, error) { + if cm == nil { + return reconcile.Result{}, errors.New(errConfigMapNil) + } + for profileName, profileContent := range cm.Data { if err := validateProfile(profileContent); err != nil { - logger.Error(err, "cannot validate profile "+profileName) - r.record.Event(configMap, - event.Warning(reasonInvalidSeccompProfile, err, - "name", profileName, - ), - ) - - // it might be possible that other profiles in the configMap are + l.Error(err, "cannot validate profile "+profileName) + r.record.Event(cm, event.Warning(reasonInvalidSeccompProfile, err)) + + // it might be possible that other profiles in the cm are // valid continue } - profilePath, err := GetProfilePath(profileName, configMap) + profilePath, err := GetProfilePath(profileName, cm.ObjectMeta.Namespace, cm.ObjectMeta.Name) if err != nil { - logger.Error(err, "cannot get profile path") - r.record.Event(configMap, - event.Warning(reasonCannotGetProfilePath, err, - "name", profileName, - ), - ) + l.Error(err, "cannot get profile path") + r.record.Event(cm, event.Warning(reasonCannotGetProfilePath, err)) return reconcile.Result{RequeueAfter: wait}, nil } - if err = saveProfileOnDisk(profilePath, profileContent); err != nil { - logger.Error(err, "cannot save profile into disk") - r.record.Event(configMap, - event.Warning(reasonCannotSaveProfile, err, - "name", profileName, - ), - ) + if err = r.save(profilePath, []byte(profileContent)); err != nil { + l.Error(err, "cannot save profile into disk") + r.record.Event(cm, event.Warning(reasonCannotSaveProfile, err)) + return reconcile.Result{RequeueAfter: wait}, nil + } + if err = r.save(profilePath, []byte(profileContent)); err != nil { + l.Error(err, "cannot save profile into disk") + r.record.Event(cm, event.Warning(reasonCannotSaveProfile, err)) return reconcile.Result{RequeueAfter: wait}, nil } - - r.record.Event(configMap, - event.Normal(reasonSavedProfile, - "Successfully saved profile to disk", - "name", profileName, - ), - ) } - - logger.Info( - "Reconciled profile", - "resource version", configMap.GetResourceVersion(), - "name", configMap.GetName(), + l.Info( + "Reconciled profile from ConfigMap", + "resource version", cm.GetResourceVersion(), + "name", cm.GetName(), ) + r.record.Event(cm, event.Normal(reasonSavedProfile, "Successfully saved profile to disk")) return reconcile.Result{}, nil } -func saveProfileOnDisk(fileName, contents string) error { +func saveProfileOnDisk(fileName string, contents []byte) error { if err := os.MkdirAll(path.Dir(fileName), dirPermissionMode); err != nil { return errors.Wrap(err, errCreatingOperatorDir) } - if err := ioutil.WriteFile(fileName, []byte(contents), filePermissionMode); err != nil { + if err := ioutil.WriteFile(fileName, contents, filePermissionMode); err != nil { return errors.Wrap(err, errSavingProfile) } return nil } // GetProfilePath returns the full path for the provided profile name and config. -func GetProfilePath(profileName string, cfg *corev1.ConfigMap) (string, error) { - if cfg == nil { - return "", errors.New(errConfigMapNil) +func GetProfilePath(profileName, namespace, subdir string) (string, error) { + if filepath.Ext(profileName) == "" { + profileName += ".json" } - return path.Join( config.ProfilesRootPath, - filepath.Base(cfg.ObjectMeta.Namespace), - filepath.Base(cfg.ObjectMeta.Name), + filepath.Base(namespace), + filepath.Base(subdir), filepath.Base(profileName), ), nil } diff --git a/internal/pkg/controllers/profile/profile_test.go b/internal/pkg/controllers/profile/profile_test.go index 2a437c90b6..c6d946cac0 100644 --- a/internal/pkg/controllers/profile/profile_test.go +++ b/internal/pkg/controllers/profile/profile_test.go @@ -17,6 +17,7 @@ limitations under the License. package profile import ( + "context" "io/ioutil" "os" "path" @@ -94,7 +95,7 @@ func TestReconcile(t *testing.T) { wantResult: reconcile.Result{}, wantErr: nil, }, - "ErrGetProfile": { + "ConfigMapErrGetProfile": { rec: &Reconciler{ client: &test.MockClient{ MockGet: test.NewMockGetFn(errOops), @@ -105,13 +106,51 @@ func TestReconcile(t *testing.T) { wantResult: reconcile.Result{}, wantErr: errors.Wrap(errOops, errGetProfile), }, - "GotProfile": { + "ConfigMapGotProfile": { + rec: &Reconciler{ + client: &test.MockClient{ + MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error { + switch o.(type) { + case *corev1.ConfigMap: + return nil + default: + return kerrors.NewNotFound(schema.GroupResource{}, name) + } + }, + }, + log: log.Log, + record: event.NewNopRecorder(), + }, + req: reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}}, + wantResult: reconcile.Result{}, + wantErr: nil, + }, + "CRDErrGetProfile": { + rec: &Reconciler{ + client: &test.MockClient{ + MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error { + switch o.(type) { + case *corev1.ConfigMap: + return errOops + default: + return kerrors.NewNotFound(schema.GroupResource{}, name) + } + }, + }, + log: log.Log, + }, + req: reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}}, + wantResult: reconcile.Result{}, + wantErr: errors.Wrap(errOops, errGetProfile), + }, + "CRDGotProfile": { rec: &Reconciler{ client: &test.MockClient{ MockGet: test.NewMockGetFn(nil), }, log: log.Log, record: event.NewNopRecorder(), + save: func(_ string, _ []byte) error { return nil }, }, req: reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}}, wantResult: reconcile.Result{}, @@ -184,7 +223,7 @@ func TestSaveProfileOnDisk(t *testing.T) { tc.setup() } - gotErr := saveProfileOnDisk(tc.fileName, tc.contents) + gotErr := saveProfileOnDisk(tc.fileName, []byte(tc.contents)) file, _ := os.Stat(tc.fileName) // nolint: errcheck gotFileCreated := file != nil @@ -245,14 +284,11 @@ func TestGetProfilePath(t *testing.T) { }, }, }, - "ConfigMapCannotBeNil": { - wantErr: errConfigMapNil, - }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { - got, gotErr := GetProfilePath(tc.profileName, tc.config) + got, gotErr := GetProfilePath(tc.profileName, tc.config.ObjectMeta.Namespace, tc.config.ObjectMeta.Name) if tc.wantErr == "" { require.NoError(t, gotErr) } else { diff --git a/test/tc_default_profiles_test.go b/test/tc_default_profiles_test.go index 0dbd101b56..6902cf239a 100644 --- a/test/tc_default_profiles_test.go +++ b/test/tc_default_profiles_test.go @@ -62,7 +62,7 @@ func (e *e2e) testCaseDefaultAndExampleProfiles(nodes []string) { func (e *e2e) verifyProfilesContent(node string, cm *v1.ConfigMap) { e.logf("Verifying %s profile on node %s", cm.Name, node) for name, content := range cm.Data { - profilePath, err := profile.GetProfilePath(name, cm) + profilePath, err := profile.GetProfilePath(name, cm.ObjectMeta.Namespace, cm.ObjectMeta.Name) e.Nil(err) catOutput := e.execNode(node, "cat", profilePath) e.Contains(catOutput, content) diff --git a/test/tc_invalid_profile_test.go b/test/tc_invalid_profile_test.go index d36bc03293..d694fce8a5 100644 --- a/test/tc_invalid_profile_test.go +++ b/test/tc_invalid_profile_test.go @@ -75,7 +75,7 @@ data: // Check that the profile is not reconciled to the node e.logf("Verifying node content") configMap := e.getConfigMap(configMapName, "default") - profilePath, err := profile.GetProfilePath(profileName, configMap) + profilePath, err := profile.GetProfilePath(profileName, configMap.ObjectMeta.Namespace, configMap.ObjectMeta.Name) e.Nil(err) for _, node := range nodes { e.execNode(node, "bash", "-c", fmt.Sprintf("[ ! -f %s ]", profilePath))