Skip to content

Commit

Permalink
feat: validating/mutating webhook configurations for the request cont…
Browse files Browse the repository at this point in the history
…rollers (#48)
  • Loading branch information
diranged authored Dec 2, 2022
1 parent 2add2f4 commit ad6f282
Show file tree
Hide file tree
Showing 37 changed files with 2,231 additions and 166 deletions.
6 changes: 5 additions & 1 deletion Custom.mk
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ lint: revive
$(REVIVE) -config revive.toml -formatter stylish ./...

.PHONY: test-e2e # you will need to have a Kind cluster up and running to run this target
test-e2e:
test-e2e: cert-manager
go test ./test/e2e/ -v -ginkgo.v

.PHONY: cert-manager
cert-manager:
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.10.1/cert-manager.yaml

.PHONY: goreleaser
goreleaser: $(GORELEASER)
$(GORELEASER):
Expand Down
3 changes: 2 additions & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ should provide the most common extensions that will make development easier.

First, spin up an empty [KIND][kind] cluster in your development environment.
We recommend always creating a new KIND environment for every project you work
on.
on. Once it is up, you must also install the `cert-manager` toolkit...

```sh
$ kind create cluster
$ make cert-manager
```

### Running on the cluster
Expand Down
12 changes: 10 additions & 2 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,17 @@ resources:
kind: ExecAccessRequest
path: github.com/diranged/oz/api/v1alpha1
version: v1alpha1
webhooks:
defaulting: true
validation: true
webhookVersion: v1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: wizardofoz.co
group: crds
kind: AccessTemplate
kind: PodAccessTemplate
path: github.com/diranged/oz/api/v1alpha1
version: v1alpha1
- api:
Expand All @@ -40,7 +44,11 @@ resources:
controller: true
domain: wizardofoz.co
group: crds
kind: AccessRequest
kind: PodAccessRequest
path: github.com/diranged/oz/api/v1alpha1
version: v1alpha1
webhooks:
defaulting: true
validation: true
webhookVersion: v1
version: "3"
330 changes: 330 additions & 0 deletions api/v1alpha1/exec_access_request_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
package v1alpha1

import (
"encoding/json"
"fmt"
"strconv"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
admissionv1 "k8s.io/api/admission/v1"
appsv1 "k8s.io/api/apps/v1"
authenticationv1 "k8s.io/api/authentication/v1"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/clientcmd/api"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// The ExecAccessRequest tests are primarily testing the behavior of the
// ExecAccessRequest struct - but because some of our tests are testing against
// the real Kubernetes API, they indirectly are also acting as tests of the
// ExecAccessRequestController Reconciler().
var _ = Describe("ExecAccessRequest", Ordered, func() {
var namespace *corev1.Namespace
var deployment *appsv1.Deployment
var template *ExecAccessTemplate

// These tests create real ExecAccessRequest{} objects in the cluster and
// validate behavior. This indirectly tests both the reconciler code, AND
// directly is testing the Webhook code. These are functional tests to
// ensure basic functionality.
//
// Explicit test-cases for function logic is handled in the next Context()
// below.
Context("Reconciliation / Webhook Tests", func() {
requestName := "test"

It("Creation of the ExecAccessRequest to work - Passes Webhook", func() {
request := &ExecAccessRequest{
ObjectMeta: metav1.ObjectMeta{
Name: requestName,
Namespace: template.Namespace,
},
Spec: ExecAccessRequestSpec{
TemplateName: template.Name,
Duration: "1h",
},
}
err := k8sClient.Create(ctx, request)
Expect(err).To(Not(HaveOccurred()))
})

It("ExecAccessRequest becomes ready - Passes Reconciliation", func() {
request := &ExecAccessRequest{}
Eventually(func() error {
err := k8sClient.Get(ctx, types.NamespacedName{
Name: requestName,
Namespace: template.Namespace,
}, request)
Expect(err).To(Not(HaveOccurred()))
if request.GetStatus().IsReady() {
return nil
}
return fmt.Errorf(
"Failed to reconcile resource: %s",
strconv.FormatBool(request.GetStatus().IsReady()),
)
}, time.Minute, time.Second).Should(HaveOccurred())
})

It("ExecAccessRequest Update - Passes Webhook ValidateUpdate() Call", func() {
// Get the request first
request := &ExecAccessRequest{}
err := k8sClient.Get(ctx, types.NamespacedName{
Name: requestName,
Namespace: template.Namespace,
}, request)
Expect(err).To(Not(HaveOccurred()))

// Update it and push it
request.ObjectMeta.SetAnnotations(map[string]string{"foo": "bar"})
err = k8sClient.Update(ctx, request)
Expect(err).To(Not(HaveOccurred()))
})
It("ExecAccessRequest Delete - Passes Webhook ValidateUpdate() Call", func() {
// Get the request first
request := &ExecAccessRequest{
ObjectMeta: metav1.ObjectMeta{
Name: requestName,
Namespace: template.Namespace,
},
}
err := k8sClient.Delete(ctx, request)
Expect(err).To(Not(HaveOccurred()))
})
})

// This Context() tests specific functions - no real calls against the API
// are made here. Fake data is passed into the functions to verify that
// they each behave correctly under different conditions.
Context("Functional Unit Tests", func() {
var (
err error
requestName = "test"
admissionRequest *admission.Request

request = &ExecAccessRequest{
Spec: ExecAccessRequestSpec{
TemplateName: "",
Duration: "",
},
}

gvr = metav1.GroupVersionResource{
Group: api.SchemeGroupVersion.Group,
Version: api.SchemeGroupVersion.Version,
Resource: "execaccessrequest",
}
gvk = metav1.GroupVersionKind{
Group: api.SchemeGroupVersion.Group,
Version: api.SchemeGroupVersion.Version,
Kind: ExecAccessRequest{}.Kind,
}
)

// TODO: The "Userinfo" checks should move into an authentication
// package so that we can write one set of tests for all of the
// Validate* functions.
It("Create with UserInfo...", func() {
requestBytes, _ := json.Marshal(request)
admissionRequest = &admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Resource: gvr,
RequestKind: &gvk,
RequestResource: &gvr,
Name: requestName,
Namespace: namespace.Name,
Operation: "CREATE",
UserInfo: authenticationv1.UserInfo{
Username: "admin",
UID: "",
Groups: []string{},
Extra: map[string]authenticationv1.ExtraValue{
"": {},
},
},
Object: runtime.RawExtension{
Raw: requestBytes,
},
},
}
err = request.ValidateCreate(*admissionRequest)
Expect(err).To(Not(HaveOccurred()))
})

It("Create without UserInfo...", func() {
requestBytes, _ := json.Marshal(request)
admissionRequest = &admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Resource: gvr,
RequestKind: &gvk,
RequestResource: &gvr,
Name: requestName,
Namespace: namespace.Name,
Operation: "CREATE",
UserInfo: authenticationv1.UserInfo{},
Object: runtime.RawExtension{
Raw: requestBytes,
},
},
}
err = request.ValidateCreate(*admissionRequest)
Expect(err).To(Not(HaveOccurred()))
})

It("Update with UserInfo...", func() {
requestBytes, _ := json.Marshal(request)
admissionRequest = &admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Resource: gvr,
RequestKind: &gvk,
RequestResource: &gvr,
Name: requestName,
Namespace: namespace.Name,
Operation: "UPDATE",
UserInfo: authenticationv1.UserInfo{
Username: "admin",
UID: "",
Groups: []string{},
Extra: map[string]authenticationv1.ExtraValue{
"": {},
},
},
Object: runtime.RawExtension{
Raw: requestBytes,
},
},
}
err = request.ValidateUpdate(*admissionRequest, request)
Expect(err).To(Not(HaveOccurred()))
})

It("Update without UserInfo...", func() {
requestBytes, _ := json.Marshal(request)
admissionRequest = &admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Resource: gvr,
RequestKind: &gvk,
RequestResource: &gvr,
Name: requestName,
Namespace: namespace.Name,
Operation: "UPDATE",
UserInfo: authenticationv1.UserInfo{},
Object: runtime.RawExtension{
Raw: requestBytes,
},
},
}
err = request.ValidateUpdate(*admissionRequest, request)
Expect(err).To(Not(HaveOccurred()))
})
})

// Setup code below here - this code rarely changes, the tests above are
// much more important.
BeforeAll(func() {
By("Creating the Namespace to perform the tests")
namespace = &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: randomString(8),
},
}
err := k8sClient.Create(ctx, namespace)
Expect(err).To(Not(HaveOccurred()))

By("Creating the Deployment to peform the tests")
deployment = &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-deployment",
Namespace: namespace.Name,
},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"testLabel": "testValue",
},
},
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"Foo": "bar",
},
Labels: map[string]string{
"testLabel": "testValue",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "conta",
Image: "nginx:latest",
},
},
},
},
},
}
err = k8sClient.Create(ctx, deployment)
Expect(err).To(Not(HaveOccurred()))
})

AfterAll(func() {
By("Deleting the Namespace for tests")
err := k8sClient.Delete(ctx, namespace)
Expect(err).To(Not(HaveOccurred()))
})

BeforeEach(func() {
var err error

// Create the execaccesstemplate
template = &ExecAccessTemplate{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: namespace.Name,
},
Spec: ExecAccessTemplateSpec{
AccessConfig: AccessConfig{
AllowedGroups: []string{"admins"},
DefaultDuration: "1h",
MaxDuration: "24h",
},
ControllerTargetRef: &CrossVersionObjectReference{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: deployment.Name,
},
},
}
err = k8sClient.Create(ctx, template)
Expect(err).To(Not(HaveOccurred()))

// Verify the template is ready
Eventually(func() error {
err = k8sClient.Get(ctx, types.NamespacedName{
Name: template.Name,
Namespace: template.Namespace,
}, template)
Expect(err).To(Not(HaveOccurred()))

if template.GetStatus().IsReady() {
return nil
}
return fmt.Errorf(
"Failed to reconcile resource: %s",
strconv.FormatBool(template.GetStatus().IsReady()),
)
}, time.Minute, time.Second).Should(HaveOccurred())
})

AfterEach(func() {
// Clear out the Template after each test
err := k8sClient.Delete(ctx, template)
Expect(err).To(Not(HaveOccurred()))
})
})
Loading

0 comments on commit ad6f282

Please sign in to comment.