Repository: kubernetes-sigs/security-profiles-operator
Pull request #174 (2020-12-16 10:13:29)

Add SELinux e2e test
This adds a couple of e2e test cases that verify that the base
functionality for the selinux bits work.

Each test is now prefixed with the component that's being tested.

On the other hand, to enable integration with other kube utilities (such as
`kubectl wait`) which would make testing easier, support for conditions
was added in the `SelinuxPolicy` object.

Some fixes related to object creation/deletion and status reporting had
to be done in the selinux-related controllers.

Note that the SELinux tests are being skipped for everything except
OpenShift clusters, since that's where I've been able to test this.
If someone has access to another distro that uses SELinux where we
could test this, we can integrate that too.

```release-note
Conditions were added to the SelinuxPolicy object's status.
```

k8s-ci-robot: Hi @JAORMX. Thanks for your PR.

I'm waiting for a [kubernetes-sigs](https://github.com/orgs/kubernetes-sigs/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://git.k8s.io/community/community-membership.md#member) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fsecurity-profiles-operator).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>

k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/174#" title="Author self-approved">JAORMX</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **saschagrunert** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @saschagrunert` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fsecurity-profiles-operator).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/security-profiles-operator/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["saschagrunert"]} -->
JAORMX: Pull-request updated, HEAD is now bc6f5b8062789918dd1fc2db2e1fd1ad25cd0ab5
pjbgf: /ok-to-test
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/174#" title="Author self-approved">JAORMX</a>*, *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/174#pullrequestreview-553911745" title="LGTM">pjbgf</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fsecurity-profiles-operator).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes-sigs/security-profiles-operator/blob/master/OWNERS)~~ [pjbgf]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/api/v1alpha1/selinuxpolicy_types.go b/api/v1alpha1/selinuxpolicy_types.go
index f80246093..b4697851c 100644
--- a/api/v1alpha1/selinuxpolicy_types.go
+++ b/api/v1alpha1/selinuxpolicy_types.go
@@ -17,6 +17,7 @@ limitations under the License.
 package v1alpha1
 
 import (
+	rcommonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
@@ -42,6 +43,7 @@ const (
 
 // SelinuxPolicyStatus defines the observed state of SelinuxPolicy.
 type SelinuxPolicyStatus struct {
+	rcommonv1.ConditionedStatus `json:",inline"`
 	// Represents the string that the SelinuxPolicy object can be
 	// referenced as in a pod seLinuxOptions section.
 	Usage string `json:"usage,omitempty"`
diff --git a/deploy/base/crd.yaml b/deploy/base/crd.yaml
index 44600c4b8..feb4841e1 100644
--- a/deploy/base/crd.yaml
+++ b/deploy/base/crd.yaml
@@ -245,6 +245,34 @@ spec:
           status:
             description: SelinuxPolicyStatus defines the observed state of SelinuxPolicy.
             properties:
+              conditions:
+                description: Conditions of the resource.
+                items:
+                  description: A Condition that may apply to a resource.
+                  properties:
+                    lastTransitionTime:
+                      description: LastTransitionTime is the last time this condition transitioned from one status to another.
+                      format: date-time
+                      type: string
+                    message:
+                      description: A Message containing details about this condition's last transition from one status to another, if any.
+                      type: string
+                    reason:
+                      description: A Reason for this condition's last transition from one status to another.
+                      type: string
+                    status:
+                      description: Status of this condition; is it currently True, False, or Unknown?
+                      type: string
+                    type:
+                      description: Type of this condition. At most one of each condition type may apply to a resource at any point in time.
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
               state:
                 description: 'Represents the state that the policy is in. Can be: PENDING, IN-PROGRESS, INSTALLED or ERROR'
                 type: string
diff --git a/deploy/namespace-operator.yaml b/deploy/namespace-operator.yaml
index 9753afdc1..96eb0da68 100644
--- a/deploy/namespace-operator.yaml
+++ b/deploy/namespace-operator.yaml
@@ -246,6 +246,34 @@ spec:
           status:
             description: SelinuxPolicyStatus defines the observed state of SelinuxPolicy.
             properties:
+              conditions:
+                description: Conditions of the resource.
+                items:
+                  description: A Condition that may apply to a resource.
+                  properties:
+                    lastTransitionTime:
+                      description: LastTransitionTime is the last time this condition transitioned from one status to another.
+                      format: date-time
+                      type: string
+                    message:
+                      description: A Message containing details about this condition's last transition from one status to another, if any.
+                      type: string
+                    reason:
+                      description: A Reason for this condition's last transition from one status to another.
+                      type: string
+                    status:
+                      description: Status of this condition; is it currently True, False, or Unknown?
+                      type: string
+                    type:
+                      description: Type of this condition. At most one of each condition type may apply to a resource at any point in time.
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
               state:
                 description: 'Represents the state that the policy is in. Can be: PENDING, IN-PROGRESS, INSTALLED or ERROR'
                 type: string
diff --git a/deploy/operator.yaml b/deploy/operator.yaml
index 2a4f57ed1..af49acfa7 100644
--- a/deploy/operator.yaml
+++ b/deploy/operator.yaml
@@ -246,6 +246,34 @@ spec:
           status:
             description: SelinuxPolicyStatus defines the observed state of SelinuxPolicy.
             properties:
+              conditions:
+                description: Conditions of the resource.
+                items:
+                  description: A Condition that may apply to a resource.
+                  properties:
+                    lastTransitionTime:
+                      description: LastTransitionTime is the last time this condition transitioned from one status to another.
+                      format: date-time
+                      type: string
+                    message:
+                      description: A Message containing details about this condition's last transition from one status to another, if any.
+                      type: string
+                    reason:
+                      description: A Reason for this condition's last transition from one status to another.
+                      type: string
+                    status:
+                      description: Status of this condition; is it currently True, False, or Unknown?
+                      type: string
+                    type:
+                      description: Type of this condition. At most one of each condition type may apply to a resource at any point in time.
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
               state:
                 description: 'Represents the state that the policy is in. Can be: PENDING, IN-PROGRESS, INSTALLED or ERROR'
                 type: string
diff --git a/examples/selinuxpolicy.yaml b/examples/selinuxpolicy.yaml
index c2708d98a..f8c92603c 100644
--- a/examples/selinuxpolicy.yaml
+++ b/examples/selinuxpolicy.yaml
@@ -2,7 +2,6 @@ apiVersion: security-profiles-operator.x-k8s.io/v1alpha1
 kind: SelinuxPolicy
 metadata:
   name: errorlogger
-  namespace: default
 spec:
   apply: true
   policy: |
diff --git a/go.mod b/go.mod
index 78beb923b..fea6efce9 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.15
 
 require (
 	github.com/containers/common v0.29.0
-	github.com/crossplane/crossplane-runtime v0.11.0
+	github.com/crossplane/crossplane-runtime v0.12.0
 	github.com/go-logr/logr v0.3.0
 	github.com/pkg/errors v0.9.1
 	github.com/stretchr/testify v1.6.1
diff --git a/go.sum b/go.sum
index 674a02e99..423a79ca2 100644
--- a/go.sum
+++ b/go.sum
@@ -153,8 +153,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/crossplane/crossplane-runtime v0.11.0 h1:hLDWsGYhU/CUVQ1sU7NHF5bnP6WT6wA6Nu2SaBSSe6w=
-github.com/crossplane/crossplane-runtime v0.11.0/go.mod h1:cJl5ZZONisre4v6wTmbrC8Jh3AI+erq/lNaxZzv9tnU=
+github.com/crossplane/crossplane-runtime v0.12.0 h1:GB1Pq5vVBNStPrfiEJNat56gHCQiuliidgr7pNs/FQA=
+github.com/crossplane/crossplane-runtime v0.12.0/go.mod h1:cJl5ZZONisre4v6wTmbrC8Jh3AI+erq/lNaxZzv9tnU=
 github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
 github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
diff --git a/internal/pkg/controllers/selinuxpolicy/configmap_controller.go b/internal/pkg/controllers/selinuxpolicy/configmap_controller.go
index 6a1d5801a..09f3c929b 100644
--- a/internal/pkg/controllers/selinuxpolicy/configmap_controller.go
+++ b/internal/pkg/controllers/selinuxpolicy/configmap_controller.go
@@ -21,7 +21,9 @@ import (
 	"fmt"
 	"time"
 
+	rcommonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
 	"github.com/crossplane/crossplane-runtime/pkg/event"
+	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
@@ -73,17 +75,28 @@ func (r *ReconcileConfigMap) Reconcile(request reconcile.Request) (reconcile.Res
 	if err := r.client.Get(context.TODO(), policyObjKey, policy); err != nil {
 		return reconcile.Result{}, IgnoreNotFound(err)
 	}
-	policyCopy := policy.DeepCopy()
 
-	if policyCopy.Status.State == "" || policyCopy.Status.State == spov1alpha1.PolicyStatePending {
+	if policy.Status.State == "" || policy.Status.State == spov1alpha1.PolicyStatePending {
+		policyCopy := policy.DeepCopy()
 		policyCopy.Status.State = spov1alpha1.PolicyStateInProgress
+		policyCopy.Status.SetConditions(rcommonv1.Creating())
 		if err := r.client.Status().Update(context.TODO(), policyCopy); err != nil {
 			return reconcile.Result{}, errors.Wrap(err, "Updating policy without status")
 		}
 		return reconcile.Result{Requeue: true}, nil
 	}
 
-	reqLogger.Info("Reconciling pods for policy")
+	// object is not being deleted
+	if cminstance.ObjectMeta.DeletionTimestamp.IsZero() {
+		return r.reconcileInstallerPods(policy, cminstance, reqLogger)
+	}
+	return reconcile.Result{}, nil
+}
+
+func (r *ReconcileConfigMap) reconcileInstallerPods(policy *spov1alpha1.SelinuxPolicy, cm *corev1.ConfigMap,
+	logger logr.Logger) (reconcile.Result, error) {
+	var done bool
+	logger.Info("Reconciling pods for policy")
 
 	nodesList := &corev1.NodeList{}
 	if err := r.client.List(context.TODO(), nodesList); err != nil {
@@ -91,8 +104,8 @@ func (r *ReconcileConfigMap) Reconcile(request reconcile.Request) (reconcile.Res
 	}
 	for nodeidx := range nodesList.Items {
 		// Define a new Pod object
-		pod := newPodForPolicy(policyName, policyNamespace, &nodesList.Items[nodeidx])
-		if err := controllerutil.SetControllerReference(cminstance, pod, r.scheme); err != nil {
+		pod := newPodForPolicy(policy.GetName(), policy.GetNamespace(), &nodesList.Items[nodeidx])
+		if err := controllerutil.SetControllerReference(cm, pod, r.scheme); err != nil {
 			log.Error(err, "Failed to set pod ownership", "pod", pod)
 			return reconcile.Result{}, errors.Wrap(err, "Setting controller reference for SELinux installer pod")
 		}
@@ -101,10 +114,12 @@ func (r *ReconcileConfigMap) Reconcile(request reconcile.Request) (reconcile.Res
 		found := &corev1.Pod{}
 		err := r.client.Get(context.TODO(), types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}, found)
 		if err != nil && kerrors.IsNotFound(err) {
-			reqLogger.Info("Creating a new Pod", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name)
+			logger.Info("Creating a new Pod", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name)
 			if err = r.client.Create(context.TODO(), pod); err != nil {
 				return reconcile.Result{}, IgnoreAlreadyExists(err)
 			}
+			// NOTE(jaosorior): We expedite the creation of all
+			// these pods...
 			continue
 		} else if err != nil {
 			return reconcile.Result{}, errors.Wrap(err, "Gettting selinux installer Pod")
@@ -113,21 +128,29 @@ func (r *ReconcileConfigMap) Reconcile(request reconcile.Request) (reconcile.Res
 		exitCode, gotCode := r.getInstallerContainerExitCode(found)
 		// Pod is still running - requeue
 		if !gotCode {
-			reqLogger.Info("Pod still running", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name)
+			logger.Info("Pod still running", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name)
 			return reconcile.Result{Requeue: true, RequeueAfter: defaultRequeueTime}, nil
 		}
 		if exitCode != 0 {
-			reqLogger.Info("Pod failed", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name, "exit-code", exitCode)
+			logger.Info("Pod failed", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name, "exit-code", exitCode)
+			policyCopy := policy.DeepCopy()
 			policyCopy.Status.State = spov1alpha1.PolicyStateError
+			policyCopy.Status.SetConditions(rcommonv1.Unavailable())
 			if err := r.client.Status().Update(context.TODO(), policyCopy); err != nil {
 				return reconcile.Result{}, errors.Wrap(err, "Updating SELinux policy with install error")
 			}
 			return reconcile.Result{}, nil
 		}
+		done = true
 	}
-	policyCopy.Status.State = spov1alpha1.PolicyStateInstalled
-	if err := r.client.Status().Update(context.TODO(), policyCopy); err != nil {
-		return reconcile.Result{}, errors.Wrap(err, "Updating SELinux policy with installation success")
+
+	if done {
+		policyCopy := policy.DeepCopy()
+		policyCopy.Status.State = spov1alpha1.PolicyStateInstalled
+		policyCopy.Status.SetConditions(rcommonv1.Available())
+		if err := r.client.Status().Update(context.TODO(), policyCopy); err != nil {
+			return reconcile.Result{}, errors.Wrap(err, "Updating SELinux policy with installation success")
+		}
 	}
 	return reconcile.Result{}, nil
 }
@@ -168,13 +191,6 @@ func newPodForPolicy(name, ns string, node *corev1.Node) *corev1.Pod {
 					Image:   "quay.io/jaosorior/udica",
 					Command: []string{"/bin/sh"},
 					Args:    []string{"-c", "semodule -vi /tmp/policy/*.cil /usr/share/udica/templates/*cil;"},
-					Lifecycle: &corev1.Lifecycle{
-						PreStop: &corev1.Handler{
-							Exec: &corev1.ExecAction{
-								Command: []string{"/bin/sh", "-c", fmt.Sprintf("semodule -vr '%s'", GetPolicyName(name, ns))},
-							},
-						},
-					},
 					SecurityContext: &corev1.SecurityContext{
 						Privileged: &trueVal,
 					},
@@ -204,7 +220,7 @@ func newPodForPolicy(name, ns string, node *corev1.Node) *corev1.Pod {
 					// more formal registry
 					Image:   "quay.io/jaosorior/udica",
 					Command: []string{"/bin/sh"},
-					Args:    []string{"-c", "while true; do sleep 30; done;"},
+					Args:    []string{"-c", "trap 'exit 0' SIGINT SIGTERM; while true; do sleep infinity; done;"},
 					Lifecycle: &corev1.Lifecycle{
 						PreStop: &corev1.Handler{
 							Exec: &corev1.ExecAction{
diff --git a/internal/pkg/controllers/selinuxpolicy/selinuxpolicy_controller.go b/internal/pkg/controllers/selinuxpolicy/selinuxpolicy_controller.go
index af22e5d56..acac260c2 100644
--- a/internal/pkg/controllers/selinuxpolicy/selinuxpolicy_controller.go
+++ b/internal/pkg/controllers/selinuxpolicy/selinuxpolicy_controller.go
@@ -22,6 +22,7 @@ import (
 	"strings"
 	"text/template"
 
+	rcommonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
 	"github.com/crossplane/crossplane-runtime/pkg/event"
 	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
@@ -71,9 +72,10 @@ func (r *ReconcileSP) Reconcile(request reconcile.Request) (reconcile.Result, er
 	}
 
 	// Set up an initial state
-	policyCopy := instance.DeepCopy()
-	if policyCopy.Status.State == "" {
+	if instance.Status.State == "" {
+		policyCopy := instance.DeepCopy()
 		policyCopy.Status.State = spov1alpha1.PolicyStatePending
+		policyCopy.Status.SetConditions(rcommonv1.Creating())
 		if err := r.client.Status().Update(context.TODO(), policyCopy); err != nil {
 			return reconcile.Result{}, errors.Wrap(err, "Updating SelinuxPolicy status to PENDING")
 		}
@@ -85,6 +87,7 @@ func (r *ReconcileSP) Reconcile(request reconcile.Request) (reconcile.Result, er
 	if !instance.Spec.Apply {
 		policyCopy := instance.DeepCopy()
 		policyCopy.Status.State = spov1alpha1.PolicyStatePending
+		policyCopy.Status.SetConditions(rcommonv1.Unavailable())
 		if err := r.client.Status().Update(context.TODO(), policyCopy); err != nil {
 			return reconcile.Result{}, errors.Wrap(err, "Updating SelinuxPolicy status to PENDING")
 		}
@@ -106,11 +109,24 @@ func (r *ReconcileSP) Reconcile(request reconcile.Request) (reconcile.Result, er
 	}
 
 	// The object is being deleted
+
+	// Set appropriate condition if needed.
+	if !instance.Status.GetCondition(rcommonv1.TypeReady).Equal(rcommonv1.Deleting()) {
+		policyCopy := instance.DeepCopy()
+		policyCopy.Status.SetConditions(rcommonv1.Deleting())
+		if err := r.client.Status().Update(context.TODO(), policyCopy); err != nil {
+			return reconcile.Result{}, errors.Wrap(err, "Updating SelinuxPolicy status condition to indicate deletion")
+		}
+		return reconcile.Result{}, nil
+	}
+
 	if SliceContainsString(instance.ObjectMeta.Finalizers, selinuxFinalizerName) {
-		if err := r.deleteConfigMap(instance, reqLogger); err != nil {
-			return reconcile.Result{}, err
+		res, err := r.reconcileDeleteConfigMap(instance, reqLogger)
+		if res.Requeue || err != nil {
+			return res, err
 		}
 
+		// We only remove the finalizer once the ConfigMap is deleted
 		return r.removeFinalizer(instance)
 	}
 
@@ -165,11 +181,19 @@ func (r *ReconcileSP) reconcileConfigMap(sp *spov1alpha1.SelinuxPolicy, l logr.L
 	return reconcile.Result{}, nil
 }
 
-func (r *ReconcileSP) deleteConfigMap(instance *spov1alpha1.SelinuxPolicy, logger logr.Logger) error {
+func (r *ReconcileSP) reconcileDeleteConfigMap(sp *spov1alpha1.SelinuxPolicy, l logr.Logger) (reconcile.Result, error) {
 	// Define a new ConfigMap object
-	cm := r.newConfigMapForPolicy(instance)
-	logger.Info("Deleting ConfigMap", "ConfigMap.Namespace", cm.Namespace, "ConfigMap.Name", cm.Name)
-	return IgnoreNotFound(r.client.Delete(context.TODO(), cm))
+	cm := r.newConfigMapForPolicy(sp)
+	key := types.NamespacedName{Name: cm.Name, Namespace: cm.Namespace}
+	l.Info("Deleting ConfigMap", "ConfigMap.Namespace", cm.Namespace, "ConfigMap.Name", cm.Name)
+	if getErr := r.client.Get(context.TODO(), key, cm); getErr != nil {
+		if kerrors.IsNotFound(getErr) {
+			// this is good. Nothing to do.
+			return reconcile.Result{}, nil
+		}
+	}
+	// We still need to delete this
+	return reconcile.Result{Requeue: true}, IgnoreNotFound(r.client.Delete(context.TODO(), cm))
 }
 
 func (r *ReconcileSP) newConfigMapForPolicy(cr *spov1alpha1.SelinuxPolicy) *corev1.ConfigMap {
@@ -185,6 +209,9 @@ func (r *ReconcileSP) newConfigMapForPolicy(cr *spov1alpha1.SelinuxPolicy) *core
 			Name:      GetPolicyConfigMapName(cr.Name, cr.Namespace),
 			Namespace: GetOperatorNamespace(),
 			Labels:    labels,
+			Finalizers: []string{
+				metav1.FinalizerDeleteDependents,
+			},
 		},
 		Data: map[string]string{
 			GetPolicyName(cr.Name, cr.Namespace) + ".cil": r.wrapPolicy(cr),


Test patch:
diff --git a/test/e2e_test.go b/test/e2e_test.go
index d1ea173b2..e0a348b25 100644
--- a/test/e2e_test.go
+++ b/test/e2e_test.go
@@ -19,6 +19,8 @@ package e2e_test
 import (
 	"encoding/json"
 	"fmt"
+	"io/ioutil"
+	"os"
 	"strings"
 
 	"sigs.k8s.io/security-profiles-operator/api/v1alpha1"
@@ -31,9 +33,13 @@ const (
 	namespaceDefaultProfiles = "deploy/profiles/namespace-default-profiles.yaml"
 	testNamespace            = "test-ns"
 	defaultNamespace         = "default"
+	// NOTE(jaosorior): We should be able to decrease this once we
+	// migrate to a single daemonset-based implementation for the
+	// SELinux pieces.
+	defaultSelinuxOpTimeout = "180s"
 )
 
-func (e *e2e) TestSeccompOperator() {
+func (e *e2e) TestSecurityProfilesOperator() {
 	// Deploy the operator
 	e.deployOperator(manifest, defaultProfiles)
 	defer e.run("git", "checkout", manifest)
@@ -49,25 +55,33 @@ func (e *e2e) TestSeccompOperator() {
 		fn          func(nodes []string)
 	}{
 		{
-			"Verify default and example profiles",
+			"Seccomp: Verify default and example profiles",
 			e.testCaseDefaultAndExampleProfiles,
 		},
 		{
-			"Run a test pod",
+			"Seccomp: Run a test pod",
 			e.testCaseRunPod,
 		},
 		{
-			"Verify base profile merge",
+			"Seccomp: Verify base profile merge",
 			e.testCaseBaseProfile,
 		},
 		{
-			"Delete profiles",
+			"Seccomp: Delete profiles",
 			e.testCaseDeleteProfiles,
 		},
 		{
-			"Re-deploy the operator",
+			"Seccomp: Re-deploy the operator",
 			e.testCaseReDeployOperator,
 		},
+		{
+			"SELinux: sanity check",
+			e.testCaseSelinuxSanityCheck,
+		},
+		{
+			"SELinux: base case (install policy, run pod and delete)",
+			e.testCaseSelinuxBaseUsage,
+		},
 	}
 	for _, testCase := range testCases {
 		e.logf("> Running testcase: %s", testCase.description)
@@ -107,7 +121,7 @@ func (e *e2e) TestSeccompOperator() {
 func (e *e2e) deployOperator(manifest, profiles string) {
 	// Ensure that we do not accidentally pull the image and use the pre-loaded
 	// ones from the nodes
-	e.logf("Setting imagePullPolicy to 'Never' in manifest: %s", manifest)
+	e.logf("Setting imagePullPolicy to '%s' in manifest: %s", e.pullPolicy, manifest)
 	e.run(
 		"sed", "-i",
 		fmt.Sprintf("s;imagePullPolicy: Always;imagePullPolicy: %s;g", e.pullPolicy),
@@ -169,3 +183,30 @@ func (e *e2e) getCurrentContextNamespace(alt string) string {
 	}
 	return ctxns
 }
+
+func (e *e2e) writeAndCreate(manifest, filePattern string) func() {
+	file, err := ioutil.TempFile(os.TempDir(), filePattern)
+	fileName := file.Name()
+	e.Nil(err)
+	_, err = file.Write([]byte(manifest))
+	e.Nil(err)
+	err = file.Close()
+	e.Nil(err)
+	e.kubectl("create", "-f", fileName)
+	return func() { os.Remove(fileName) }
+}
+
+func (e *e2e) getSELinuxPolicyName(policy string) string {
+	usageStr := e.getSELinuxPolicyUsage(policy)
+	// Udica (the library that helps out generate SELinux policies),
+	// adds .process in the end of the usage. So we need to remove it
+	// to get the module name
+	return strings.TrimSuffix(usageStr, ".process")
+}
+
+func (e *e2e) getSELinuxPolicyUsage(policy string) string {
+	ns := e.getCurrentContextNamespace(defaultNamespace)
+	// This describes the usage string, which is not necessarily
+	// the name of the policy in the node
+	return e.kubectl("get", "selinuxpolicy", "-n", ns, policy, "-o", "jsonpath={.status.usage}")
+}
diff --git a/test/suite_test.go b/test/suite_test.go
index af2a8a6bb..50fa67b1f 100644
--- a/test/suite_test.go
+++ b/test/suite_test.go
@@ -219,9 +219,8 @@ func (e *openShifte2e) SetupSuite() {
 }
 
 func (e *openShifte2e) TearDownSuite() {
-	testImageRef := config.OperatorName + ":latest"
 	e.kubectl(
-		"delete", "imagestream", "-n", "openshift", testImageRef,
+		"delete", "imagestream", "-n", "openshift", config.OperatorName,
 	)
 }
 
@@ -288,3 +287,11 @@ func (e *e2e) kubectlOperatorNS(args ...string) string {
 func (e *e2e) logf(format string, a ...interface{}) {
 	e.logger.Info(fmt.Sprintf(format, a...))
 }
+
+func (e *e2e) selinuxtOnlyTestCase() {
+	// TODO(jaosorior): Come up with better way to assert if a cluster
+	// supports SELinux or not.
+	if !strings.EqualFold(clusterType, "openshift") {
+		e.T().Skip("Skipping SELinux-related test from non-OpenShift cluster")
+	}
+}
diff --git a/test/tc_delete_profiles_test.go b/test/tc_delete_profiles_test.go
index 5696f5d8a..823f88510 100644
--- a/test/tc_delete_profiles_test.go
+++ b/test/tc_delete_profiles_test.go
@@ -18,8 +18,6 @@ package e2e_test
 
 import (
 	"fmt"
-	"io/ioutil"
-	"os"
 	"time"
 
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/controllers/profile"
@@ -153,15 +151,3 @@ spec:
 		e.kubectlFailure("get", "seccompprofile", deleteProfileName)
 	}
 }
-
-func (e *e2e) writeAndCreate(manifest, filePattern string) func() {
-	file, err := ioutil.TempFile(os.TempDir(), filePattern)
-	fileName := file.Name()
-	e.Nil(err)
-	_, err = file.Write([]byte(manifest))
-	e.Nil(err)
-	err = file.Close()
-	e.Nil(err)
-	e.kubectl("create", "-f", fileName)
-	return func() { os.Remove(fileName) }
-}
diff --git a/test/tc_selinux_base_usage_test.go b/test/tc_selinux_base_usage_test.go
new file mode 100644
index 000000000..9fcc29c6d
--- /dev/null
+++ b/test/tc_selinux_base_usage_test.go
@@ -0,0 +1,126 @@
+/*
+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 e2e_test
+
+import (
+	"fmt"
+	"strings"
+)
+
+func (e *e2e) testCaseSelinuxBaseUsage(nodes []string) {
+	e.selinuxtOnlyTestCase()
+
+	// nolint:lll
+	const podWithPolicyFmt = `
+apiVersion: v1
+kind: Pod
+metadata:
+  name: errorlogger
+spec:
+  containers:
+  - name: errorlogger
+    image: "registry.access.redhat.com/ubi8/ubi-minimal:latest"
+    command: ["/bin/bash"]
+    args: ["-c", "set -eux; while true; do echo \"Time: $(date). Some error info.\" >> /var/log/test.log; sleep 2; done"]
+    securityContext:
+      seLinuxOptions:
+        type: %s
+    volumeMounts:
+    - name: varlog
+      mountPath: /var/log
+  restartPolicy: Never
+  volumes:
+  - name: varlog
+    hostPath:
+      path: /var/log
+      type: Directory
+`
+
+	const errorloggerPolicy = `
+apiVersion: security-profiles-operator.x-k8s.io/v1alpha1
+kind: SelinuxPolicy
+metadata:
+  name: errorlogger
+spec:
+  apply: true
+  policy: |
+    (blockinherit container)
+    (allow process var_log_t ( dir ( open read getattr lock search ioctl add_name remove_name write ))) 
+    (allow process var_log_t ( file ( getattr read write append ioctl lock map open create  ))) 
+    (allow process var_log_t ( sock_file ( getattr read write append open  ))) 
+`
+
+	e.logf("The 'errorlogger' workload should be able to use SELinux policy")
+
+	e.logf("creating policy")
+	e.writeAndCreate(errorloggerPolicy, "errorlogger-policy.yml")
+
+	// Let's wait for the policy to be processed
+	e.kubectl("wait", "--timeout", defaultSelinuxOpTimeout,
+		"--for", "condition=ready", "selinuxpolicy", "errorlogger")
+
+	rawPolicyName := e.getSELinuxPolicyName("errorlogger")
+
+	e.logf("assert policy is installed")
+	for _, node := range nodes {
+		var found bool
+		policiesRaw := e.execNode(node, "semodule", "-l")
+		for _, policy := range strings.Split(policiesRaw, "\n") {
+			if policy == rawPolicyName {
+				found = true
+				break
+			}
+		}
+		e.Truef(found, "The SelinuxPolicy errorlogger wasn't found in the %s node with the name %s",
+			node, rawPolicyName)
+	}
+
+	e.logf("creating workload")
+
+	podWithPolicy := fmt.Sprintf(podWithPolicyFmt, e.getSELinuxPolicyUsage("errorlogger"))
+	e.writeAndCreate(podWithPolicy, "pod-w-policy.yml")
+
+	e.kubectl("wait", "--for", "condition=ready", "pod", "errorlogger")
+
+	e.logf("the workload should be running")
+	podWithPolicyPhase := e.kubectl(
+		"get", "pods", "errorlogger", "-o", "jsonpath={.status.phase}")
+	e.Truef(strings.EqualFold(podWithPolicyPhase, "running"),
+		"The pod without policy's phase should be 'Running', instead it's: %s",
+		podWithPolicyPhase)
+
+	e.logf("cleaning up")
+
+	e.logf("removing workload")
+	e.kubectl("delete", "pod", "errorlogger")
+
+	e.logf("removing policy")
+	e.kubectl("delete", "selinuxpolicy", "errorlogger")
+
+	e.logf("assert policy was removed")
+	for _, node := range nodes {
+		var found bool
+		policiesRaw := e.execNode(node, "semodule", "-l")
+		for _, policy := range strings.Split(policiesRaw, "\n") {
+			if policy == rawPolicyName {
+				found = true
+				break
+			}
+		}
+		e.Falsef(found, "The SelinuxPolicy errorlogger should have been removed from %s node", node)
+	}
+}
diff --git a/test/tc_selinux_sanity_check_test.go b/test/tc_selinux_sanity_check_test.go
new file mode 100644
index 000000000..1497eb5b0
--- /dev/null
+++ b/test/tc_selinux_sanity_check_test.go
@@ -0,0 +1,59 @@
+/*
+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 e2e_test
+
+func (e *e2e) testCaseSelinuxSanityCheck([]string) {
+	e.selinuxtOnlyTestCase()
+
+	const podWithoutPolicy = `
+apiVersion: v1
+kind: Pod
+metadata:
+  name: el-no-policy
+spec:
+  initContainers:
+  - name: errorlogger
+    image: "registry.access.redhat.com/ubi8/ubi-minimal:latest"
+    command: ["/bin/bash"]
+    args: ["-c", "echo \"Time: $(date). Some error info.\" >> /var/log/test.log || /bin/true"]
+    volumeMounts:
+    - name: varlog
+      mountPath: /var/log
+  containers:
+  - name: pauser
+    image: "gcr.io/google_containers/pause:latest"
+  restartPolicy: Never
+  volumes:
+  - name: varlog
+    hostPath:
+      path: /var/log
+      type: Directory
+`
+	e.logf("sanity check: The 'errorlogger' workload should be blocked by SELinux")
+
+	e.logf("creating workload")
+	e.writeAndCreate(podWithoutPolicy, "pod-wo-policy.yml")
+
+	e.kubectl("wait", "--for", "condition=ready", "pod", "el-no-policy")
+
+	e.logf("the workload should have errored")
+	expectedLog := "/bin/bash: /var/log/test.log: Permission denied"
+	log := e.kubectl("logs", "el-no-policy", "-c", "errorlogger")
+	e.Equalf(log, expectedLog, "container should have returned a 'Permissions Denied' error")
+
+	e.kubectl("delete", "pod", "el-no-policy")
+}