Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ repos:
name: shellcheck
language: system
entry: shellcheck
args: ['--external-sources']
types: [shell]
- id: kuttl-assert-collectors
name: Validate KUTTL assert collectors
Expand Down
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,18 @@ build: manifests generate fmt vet ## Build manager binary.
go build -o bin/manager cmd/main.go

.PHONY: run
run: export ENABLE_WEBHOOKS ?= false
run: manifests generate fmt vet ## Run a controller from your host.
source ./scripts/env.sh && go run ./cmd/main.go $(ARGS)

.PHONY: run-with-webhook
run-with-webhook: build ## Run locally with a validating webhook for Linux CRC.
OC="$(OC)" bash hack/run_with_local_webhook.sh $(ARGS)

.PHONY: webhook-cleanup
webhook-cleanup: ## Remove the admission webhook used for local development.
OC="$(OC)" bash hack/clean_local_webhook.sh

# If you wish to build the manager image targeting other platforms you can use the --platform flag.
# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
Expand Down Expand Up @@ -231,6 +240,7 @@ $(LOCALBIN):

## Tool Binaries
KUBECTL ?= kubectl
OC ?= oc
KIND ?= kind
KUSTOMIZE ?= $(LOCALBIN)/kustomize
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
Expand Down
3 changes: 3 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ resources:
kind: OpenStackLightspeed
path: github.com/openstack-k8s-operators/lightspeed-operator/api/v1beta1
version: v1beta1
webhooks:
validation: true
webhookVersion: v1
version: "3"
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,19 @@ Note: `--zap-devel` enable verbose (development) logging locally.
This will:

1. Install the CRDs into your cluster.
2. Run the operator locally, connected to your cluster.
2. Run the operator locally, connected to your cluster, with admission webhooks disabled.

Use this for quick development and testing.

To develop with the validating webhook enabled, use a local Linux CRC cluster
without a deployed Lightspeed operator and run:

```bash
make install run-with-webhook
```

Use `make webhook-cleanup` after an unclean shutdown.

*Attention*: In this mode RBACs are ignored, so when changing those please run
the operator in the OpenShift cluster with an image.

Expand Down
14 changes: 14 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import (

lightspeedv1beta1 "github.com/openstack-k8s-operators/lightspeed-operator/api/v1beta1"
"github.com/openstack-k8s-operators/lightspeed-operator/internal/controller"
webhookv1beta1 "github.com/openstack-k8s-operators/lightspeed-operator/internal/webhook/v1beta1"
telemetryv1 "github.com/openstack-k8s-operators/telemetry-operator/api/v1beta1"
// +kubebuilder:scaffold:imports
)
Expand Down Expand Up @@ -80,6 +81,7 @@ func main() {
var metricsAddr string
var metricsCertPath, metricsCertName, metricsCertKey string
var webhookCertPath, webhookCertName, webhookCertKey string
var webhookPort int
var enableLeaderElection bool
var probeAddr string
var secureMetrics bool
Expand All @@ -96,6 +98,7 @@ func main() {
flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.")
flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.")
flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.")
flag.IntVar(&webhookPort, "webhook-port", 9443, "The port the webhook server listens on.")
flag.StringVar(&metricsCertPath, "metrics-cert-path", "",
"The directory that contains the metrics server certificate.")
flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.")
Expand Down Expand Up @@ -158,6 +161,7 @@ func main() {
}

webhookServer := webhook.NewServer(webhook.Options{
Port: webhookPort,
TLSOpts: webhookTLSOpts,
})

Expand Down Expand Up @@ -269,6 +273,16 @@ func main() {
setupLog.Error(err, "unable to create controller", "controller", "OpenStackLightspeed")
os.Exit(1)
}
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
if err := webhookv1beta1.SetupOpenStackLightspeedWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "OpenStackLightspeed")
os.Exit(1)
}
if err := mgr.AddReadyzCheck("webhook", webhookServer.StartedChecker()); err != nil {
setupLog.Error(err, "unable to set up webhook ready check")
os.Exit(1)
}
}
// +kubebuilder:scaffold:builder

if metricsCertWatcher != nil {
Expand Down
17 changes: 9 additions & 8 deletions config/default/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ resources:
- ../crd
- ../rbac
- ../manager
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
#- ../webhook
- ../webhook
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
#- ../certmanager
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
Expand Down Expand Up @@ -50,11 +48,14 @@ patches:
target:
kind: Deployment

# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
#- path: manager_webhook_patch.yaml
# target:
# kind: Deployment
- path: manager_webhook_patch.yaml
- patch: |-
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: validating-webhook-configuration
annotations:
service.beta.openshift.io/inject-cabundle: "true"

# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
# Uncomment the following replacements to add the cert-manager CA injection annotations
Expand Down
23 changes: 23 additions & 0 deletions config/default/manager_webhook_patch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# OpenShift service-ca supplies the certificate for direct deployments.
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
spec:
template:
spec:
containers:
- name: manager
ports:
- containerPort: 9443
name: webhook-server
protocol: TCP
volumeMounts:
- name: webhook-certs
mountPath: /tmp/k8s-webhook-server/serving-certs
readOnly: true
volumes:
- name: webhook-certs
secret:
secretName: webhook-server-cert
39 changes: 19 additions & 20 deletions config/manifests/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,22 @@ resources:
- ../samples
- ../scorecard

# [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix.
# Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager.
# These patches remove the unnecessary "cert" volume and its manager container volumeMount.
#patches:
#- target:
# group: apps
# version: v1
# kind: Deployment
# name: controller-manager
# namespace: system
# patch: |-
# # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs.
# # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment.
# - op: remove

# path: /spec/template/spec/containers/0/volumeMounts/0
# # Remove the "cert" volume, since OLM will create and mount a set of certs.
# # Update the indices in this path if adding or removing volumes in the manager's Deployment.
# - op: remove
# path: /spec/template/spec/volumes/0
# OLM supplies and mounts its own webhook certificate at the same path.
patches:
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
spec:
template:
spec:
containers:
- name: manager
volumeMounts:
- mountPath: /tmp/k8s-webhook-server/serving-certs
$patch: delete
volumes:
- name: webhook-certs
$patch: delete
6 changes: 6 additions & 0 deletions config/webhook/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
resources:
- manifests.yaml
- service.yaml

configurations:
- kustomizeconfig.yaml
13 changes: 13 additions & 0 deletions config/webhook/kustomizeconfig.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
nameReference:
- kind: Service
version: v1
fieldSpecs:
- kind: ValidatingWebhookConfiguration
group: admissionregistration.k8s.io
path: webhooks/clientConfig/service/name

namespace:
- kind: ValidatingWebhookConfiguration
group: admissionregistration.k8s.io
path: webhooks/clientConfig/service/namespace
create: true
25 changes: 25 additions & 0 deletions config/webhook/manifests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: validating-webhook-configuration
webhooks:
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validate-lightspeed-openstack-org-v1beta1-openstacklightspeed
failurePolicy: Fail
name: vopenstacklightspeed-v1beta1.kb.io
rules:
- apiGroups:
- lightspeed.openstack.org
apiVersions:
- v1beta1
operations:
- CREATE
resources:
- openstacklightspeeds
sideEffects: None
15 changes: 15 additions & 0 deletions config/webhook/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: webhook-service
namespace: system
annotations:
service.beta.openshift.io/serving-cert-secret-name: webhook-server-cert
spec:
ports:
- port: 443
protocol: TCP
targetPort: 9443
selector:
control-plane: controller-manager
app.kubernetes.io/name: openstack-lightspeed-operator
6 changes: 6 additions & 0 deletions docs/install_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ spec:
This deploys the full stack: the AI engine (lightspeed-stack and
OGX), PostgreSQL, OKP, and the console plugin.

> [!NOTE]
> A single OpenStackLightspeed instance is supported
> in the openstack-lightspeed namespace. The validating webhook rejects
> additional instances at creation time. To replace an instance, delete it
> and wait for its removal before creating another.

## Verifying the deployment

```bash
Expand Down
6 changes: 6 additions & 0 deletions hack/clean_local_webhook.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
set -euo pipefail

# Only remove the configuration created by run_with_local_webhook.sh.
"${OC:-oc}" delete validatingwebhookconfiguration \
openstack-lightspeed-local-webhook --ignore-not-found
98 changes: 98 additions & 0 deletions hack/run_with_local_webhook.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/bin/bash
set -euo pipefail

cd "$(dirname "${BASH_SOURCE[0]}")/.."
# shellcheck source=scripts/env.sh
source scripts/env.sh

OC=${OC:-oc}
WEBHOOK_PORT=${WEBHOOK_PORT:-9443}
HEALTH_PORT=${HEALTH_PORT:-8081}
WEBHOOK_CERT_DIR=${WEBHOOK_CERT_DIR:-"$PWD/bin/local-webhook"}
webhook_name=openstack-lightspeed-local-webhook

for command in "$OC" openssl jq curl ip; do
command -v "$command" >/dev/null || { echo "Required command not found: $command" >&2; exit 1; }
done

# Use the host-side CRC bridge address reachable from the VM.
crc_host_ip=$(ip -o -4 addr show dev crc 2>/dev/null | awk '{split($4, address, "/"); print address[1]; exit}' || true)
if [[ -z "$crc_host_ip" ]]; then
echo "No IPv4 address found on the crc interface. This helper requires a local Linux CRC cluster." >&2
exit 1
fi
if [[ -z "$WATCH_NAMESPACE" || "$WATCH_NAMESPACE" == *,* ]]; then
echo "Local webhook development requires one WATCH_NAMESPACE." >&2
exit 1
fi

# Avoid running alongside another Lightspeed webhook, including one managed by OLM.
existing=$("$OC" get validatingwebhookconfigurations -o json | jq -r '
.items[] | select(.metadata.name == "openstack-lightspeed-local-webhook" or
any(.webhooks[]; .name == "vopenstacklightspeed-v1beta1.kb.io")) | .metadata.name')
if [[ -n "$existing" ]]; then
echo "A Lightspeed webhook is already installed: $existing" >&2
echo "Use make webhook-cleanup for a stale local webhook; uninstall the deployed operator before running locally." >&2
exit 1
fi

umask 077
mkdir -p "$WEBHOOK_CERT_DIR"
openssl req -newkey rsa:2048 -nodes -x509 -days 30 \
-subj "/CN=lightspeed-local-webhook" -addext "subjectAltName=IP:$crc_host_ip" \
-keyout "$WEBHOOK_CERT_DIR/tls.key" -out "$WEBHOOK_CERT_DIR/tls.crt"

# The API server needs only the public certificate, never the private key.
ca_bundle=$(openssl base64 -A -in "$WEBHOOK_CERT_DIR/tls.crt")
"$OC" create --dry-run=client --validate=false -f config/webhook/manifests.yaml -o json | \
jq --arg name "$webhook_name" --arg url "https://$crc_host_ip:$WEBHOOK_PORT" \
--arg ca "$ca_bundle" --arg namespace "$WATCH_NAMESPACE" '
.metadata.name = $name |
.webhooks |= map(
.clientConfig = {url: ($url + .clientConfig.service.path), caBundle: $ca} |
.namespaceSelector = {matchLabels: {"kubernetes.io/metadata.name": $namespace}}
)' > "$WEBHOOK_CERT_DIR/webhook.json"

manager_pid=""
registered=false
# Invoked indirectly by the EXIT trap.
# shellcheck disable=SC2317,SC2329
cleanup() {
local status=$?
trap - EXIT INT TERM
if [[ "$registered" == true ]]; then
bash hack/clean_local_webhook.sh || status=1
fi
if [[ -n "$manager_pid" ]]; then
kill "$manager_pid" 2>/dev/null || true
wait "$manager_pid" 2>/dev/null || true
fi
exit "$status"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM

export ENABLE_WEBHOOKS=true OC
./bin/manager "$@" --webhook-port="$WEBHOOK_PORT" --webhook-cert-path="$WEBHOOK_CERT_DIR" \
--health-probe-bind-address="127.0.0.1:$HEALTH_PORT" &
manager_pid=$!

# Register only after the local server is ready to answer admission requests.
for ((attempt=0; attempt<60; attempt++)); do
if ! kill -0 "$manager_pid" 2>/dev/null; then
wait "$manager_pid"
exit 1
fi
if curl --noproxy '*' --fail --silent --max-time 1 "http://127.0.0.1:$HEALTH_PORT/readyz/webhook" >/dev/null; then
registered=true
"$OC" apply -f "$WEBHOOK_CERT_DIR/webhook.json"
echo "Local webhook registered at https://$crc_host_ip:$WEBHOOK_PORT for namespace $WATCH_NAMESPACE."
echo "The API server must be able to reach this address and port. Press Ctrl+C to stop and clean up."
wait "$manager_pid"
exit 0
fi
sleep 1
done
echo "Timed out waiting for the local webhook server to become ready." >&2
exit 1
Loading
Loading