Skip to content

Commit

Permalink
feat: Add validating webhook for private key retrieval option (#68)
Browse files Browse the repository at this point in the history
- Add e2e tests
- Add webhook test
  • Loading branch information
samirtahir91 authored Nov 14, 2024
1 parent c219192 commit 293d154
Show file tree
Hide file tree
Showing 53 changed files with 1,608 additions and 126 deletions.
77 changes: 77 additions & 0 deletions .github/workflows/e2e-tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: E2E Tests

# Trigger the workflow on pull requests and direct pushes to any branch
on:
push:
pull_request:

jobs:
test:
name: ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os:
- ubuntu-latest
- macos-latest
# Pull requests from the same repository won't trigger this checks as they were already triggered by the push
if: (github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository)
steps:
- name: Clone the code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '~1.22'
- name: Install Helm and Kubectl
if: matrix.os == 'macos-latest'
run: |
brew install helm
brew install kubectl
- name: Setup Minikube cluster
if: matrix.os != 'macos-latest'
uses: medyagh/setup-minikube@latest
# This step is needed as the following one tries to remove
# kustomize for each test but has no permission to do so
- name: Remove pre-installed kustomize
if: matrix.os != 'macos-latest'
run: sudo rm -f /usr/local/bin/kustomize
- name: Perform the E2E test
if: matrix.os != 'macos-latest'
run: |
chmod -R +x scripts
export "GITHUB_PRIVATE_KEY=${{ secrets.GH_TEST_APP_PK }}"
export "GH_APP_ID=${{ secrets.GH_APP_ID }}"
export "GH_INSTALL_ID=${{ secrets.GH_INSTALL_ID }}"
export "VAULT_ADDR=http://vault.default:8200"
export "VAULT_ROLE_AUDIENCE=githubapp"
export "VAULT_ROLE=githubapp"
eval $(minikube docker-env)
# Run tests
make test-e2e || true
# debug
#docker images
#kubectl -n github-app-operator-system describe po
#kubectl -n github-app-operator-system describe deploy
#echo 'kubectl get mutatingwebhookconfiguration cert-manager-webhook -o jsonpath={.webhooks[*].clientConfig.caBundle}'
#kubectl get mutatingwebhookconfiguration cert-manager-webhook -o jsonpath={.webhooks[*].clientConfig.caBundle}
#kubectl -n cert-manager describe deploy,po
#echo "######### gh operator logs ##########"
#kubectl -n github-app-operator-system logs deploy/github-app-operator-controller-manager
#echo "######### cert-manager-webhook logs ##########"
#kubectl -n cert-manager logs deploy/cert-manager-webhook
- name: Report failure
uses: nashmaniac/[email protected]
# Only report failures of pushes (PRs have are visible through the Checks section) to the default branch
if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main'
with:
title: 🐛 Unit tests failed on ${{ matrix.os }} for ${{ github.sha }}
token: ${{ secrets.GITHUB_TOKEN }}
labels: kind/bug
body: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
11 changes: 10 additions & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ jobs:
- name: Remove pre-installed kustomize
if: matrix.os != 'macos-latest'
run: sudo rm -f /usr/local/bin/kustomize
- name: Perform the webhook tests
if: matrix.os != 'macos-latest'
run: |
export "GH_APP_ID=${{ secrets.GH_APP_ID }}"
export "GH_INSTALL_ID=${{ secrets.GH_INSTALL_ID }}"
# Run webhook tests
make test-webhooks
# Install vault to minikube cluster to test vault case with kubernetes auth
- name: Install and configure Vault
if: matrix.os != 'macos-latest'
Expand All @@ -45,7 +53,7 @@ jobs:
cd scripts
chmod +x install_and_setup_vault_k8s.sh
./install_and_setup_vault_k8s.sh
- name: Perform the test
- name: Perform the controller integration tests
if: matrix.os != 'macos-latest'
run: |
export "GITHUB_PRIVATE_KEY=${{ secrets.GH_TEST_APP_PK }}"
Expand All @@ -54,6 +62,7 @@ jobs:
export "VAULT_ADDR=http://localhost:8200"
export "VAULT_ROLE_AUDIENCE=githubapp"
export "VAULT_ROLE=githubapp"
export ENABLE_WEBHOOKS=false
# Run vault port forward in background
kubectl port-forward vault-0 8200:8200 &
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ Dockerfile.cross
# Test binary, built with `go test -c`
*.test

# test data
cluster-keys.json

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

Expand Down
13 changes: 9 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

# Image URL to use all building/pushing image targets
IMG ?= controller:latest
IMG ?= samirtahir91076/github-app-operator:latest
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
ENVTEST_K8S_VERSION = 1.29.0

Expand Down Expand Up @@ -62,7 +62,11 @@ vet: ## Run go vet against code.

.PHONY: test
test: manifests generate fmt vet envtest ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v -E '/e2e|v1|utils|cmd|test_helpers|vault') -coverprofile cover.out
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v -E '/e2e|v1|utils|cmd|test_helpers|vault') -v -ginkgo.v -coverprofile cover.out

.PHONY: test-webhooks
test-webhooks: manifests generate fmt vet envtest ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./api/v1/ -v -ginkgo.v -coverprofile cover.out

# Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors.
.PHONY: test-e2e # Run the e2e tests against a Kind k8s instance that is spun up.
Expand Down Expand Up @@ -194,10 +198,11 @@ HELMIFY ?= $(LOCALBIN)/helmify
.PHONY: helmify
helmify: $(HELMIFY) ## Download helmify locally if necessary.
$(HELMIFY): $(LOCALBIN)
test -s $(LOCALBIN)/helmify || GOBIN=$(LOCALBIN) go install github.com/arttor/helmify/cmd/[email protected].5
test -s $(LOCALBIN)/helmify || GOBIN=$(LOCALBIN) go install github.com/arttor/helmify/cmd/[email protected].14

helm: manifests kustomize helmify
$(KUSTOMIZE) build config/default | $(HELMIFY) charts/github-app-operator
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default | $(HELMIFY) -cert-manager-as-subchart charts/github-app-operator
##################################

# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
Expand Down
3 changes: 3 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,7 @@ resources:
kind: GithubApp
path: github-app-operator/api/v1
version: v1
webhooks:
validation: true
webhookVersion: v1
version: "3"
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The `github-app-operator` is a Kubernetes operator that generates an access toke

### Private Key Retrieval Options
> [!TIP]
> There is a sample constraint template and constraint for Gatekeeper to restrict the type of private key source in the `gatekeeper-policy` folder since we can't restrict it to be unique in the GithubApp CRD.
> There is a sample constraint template and constraint for Gatekeeper to restrict the type of private key source in the `gatekeeper-policy` folder if you dont want to use the validating webhook built-in.

#### 1. Using a Kubernetes Secret
Expand Down Expand Up @@ -216,8 +216,17 @@ EOF

### To deploy with Helm using public Docker image
A helm chart is generated using `make helm` when a new tag is pushed, i.e a release.
You can pull the helm chart from this repos packages
- See the [packages page](https://github.com/samirtahir91/github-app-operator/pkgs/container/github-app-operator%2Fhelm-charts%2Fgithub-app-operator)
This chart will have webhooks and cert manager enabled.
If you want to install without webhooks and cert manager required use the local manual chart.
```sh
cd charts/github-app-operator
helm upgrade --install -n github-app-operator-system <release_name> . --create-namespace \
--set webhook.enabled=false \
--set controllerManager.manager.env.enableWebhooks="false"
```

You can pull the automatically built helm chart from this repos packages
- See the [packages](https://github.com/samirtahir91/github-app-operator/pkgs/container/github-app-operator%2Fhelm-charts%2Fgithub-app-operator)
- Pull with helm:
- ```sh
helm pull oci://ghcr.io/samirtahir91/github-app-operator/helm-charts/github-app-operator --version <TAG>
Expand Down Expand Up @@ -282,6 +291,7 @@ export GH_INSTALL_ID=<YOUR GITHUB APP INSTALL ID>
export "VAULT_ADDR=http://localhost:8200" # this can be local k8s Vault or some other Vault
export "VAULT_ROLE_AUDIENCE=githubapp"
export "VAULT_ROLE=githubapp"
export "ENABLE_WEBHOOKS=false"
```
- This uses Vault, you can spin up a simple Vault server using this script.
- It will use Helm and configure the Vault server with a test private key as per the env var ${GITHUB_PRIVATE_KEY}.
Expand All @@ -305,6 +315,7 @@ export GITHUB_PRIVATE_KEY=<YOUR_BASE64_ENCODED_GH_APP_PRIVATE_KEY>
export GH_APP_ID=<YOUR GITHUB APP ID>
export GH_INSTALL_ID=<YOUR GITHUB APP INSTALL ID>
USE_EXISTING_CLUSTER=false make test
USE_EXISTING_CLUSTER=false make test-webhooks
```

**Generate coverage html report:**
Expand Down
101 changes: 101 additions & 0 deletions api/v1/githubapp_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
Copyright 2024.
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 v1

import (
"fmt"

"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// log is for logging in this package.
var githubapplog = logf.Log.WithName("githubapp-resource")

// SetupWebhookWithManager will setup the manager to manage the webhooks
func (r *GithubApp) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(r).
Complete()
}

// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here.
// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook.
// +kubebuilder:webhook:path=/validate-githubapp-samir-io-v1-githubapp,mutating=false,failurePolicy=fail,sideEffects=None,groups=githubapp.samir.io,resources=githubapps,verbs=create;update,versions=v1,name=vgithubapp.kb.io,admissionReviewVersions=v1

var _ webhook.Validator = &GithubApp{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *GithubApp) ValidateCreate() (admission.Warnings, error) {
githubapplog.Info("validate create", "name", r.Name)

// Ensure only one of googlePrivateKeySecret, privateKeySecret, or vaultPrivateKey is specified
err := validateGithubAppSpec(r)
if err != nil {
return nil, err
}

return nil, nil
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *GithubApp) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
githubapplog.Info("validate update", "name", r.Name)

// Ensure only one of googlePrivateKeySecret, privateKeySecret, or vaultPrivateKey is specified
err := validateGithubAppSpec(r)
if err != nil {
return nil, err
}

return nil, nil
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *GithubApp) ValidateDelete() (admission.Warnings, error) {
githubapplog.Info("validate delete", "name", r.Name)

// TODO(user): fill in your validation logic upon object deletion.
return nil, nil
}

// validateGithubAppSpec validates that only one of googlePrivateKeySecret, privateKeySecret, or vaultPrivateKey is specified
func validateGithubAppSpec(r *GithubApp) error {
count := 0

if r.Spec.GcpPrivateKeySecret != "" {
count++
}
if r.Spec.PrivateKeySecret != "" {
count++
}
if r.Spec.VaultPrivateKey != nil {
count++
}

if count != 1 {
return fmt.Errorf("exactly one of googlePrivateKeySecret, privateKeySecret, or vaultPrivateKey must be specified")
}

return nil
}
Loading

0 comments on commit 293d154

Please sign in to comment.