first commit
This commit is contained in:
114
Makefile
Normal file
114
Makefile
Normal file
@@ -0,0 +1,114 @@
|
||||
.PHONY: build push deploy certs test clean
|
||||
|
||||
IMAGE_NAME ?= ewr.vultrcr.com/chansey/irsa-webhook
|
||||
IMAGE_TAG ?= latest
|
||||
NAMESPACE ?= irsa-system
|
||||
|
||||
# Build the Go binary
|
||||
build:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o webhook main.go
|
||||
|
||||
# Build Docker image
|
||||
docker-build:
|
||||
docker build -t $(IMAGE_NAME):$(IMAGE_TAG) .
|
||||
|
||||
# Push Docker image
|
||||
docker-push:
|
||||
docker push $(IMAGE_NAME):$(IMAGE_TAG)
|
||||
|
||||
# Generate TLS certificates
|
||||
certs:
|
||||
chmod +x generate-certs.sh
|
||||
./generate-certs.sh
|
||||
|
||||
# Deploy to Kubernetes
|
||||
deploy:
|
||||
kubectl apply -f deploy.yaml
|
||||
|
||||
# Undeploy from Kubernetes
|
||||
undeploy:
|
||||
kubectl delete -f deploy.yaml
|
||||
|
||||
# View logs
|
||||
logs:
|
||||
kubectl logs -n $(NAMESPACE) -l app=irsa-webhook -f
|
||||
|
||||
# Test with example pod
|
||||
test-example:
|
||||
kubectl apply -f example.yaml
|
||||
@echo "Waiting for pod to be ready..."
|
||||
@kubectl wait --for=condition=Ready pod/example-app --timeout=60s || true
|
||||
@echo ""
|
||||
@echo "Pod logs:"
|
||||
@kubectl logs example-app
|
||||
|
||||
# Clean up test resources
|
||||
clean-example:
|
||||
kubectl delete -f example.yaml --ignore-not-found
|
||||
|
||||
# Run Go tests
|
||||
go-test:
|
||||
go test -v ./...
|
||||
|
||||
# Format Go code
|
||||
fmt:
|
||||
go fmt ./...
|
||||
|
||||
# Run Go vet
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
# Download Go dependencies
|
||||
deps:
|
||||
go mod download
|
||||
go mod tidy
|
||||
|
||||
# Complete build and deploy pipeline
|
||||
all: deps build docker-build docker-push certs deploy
|
||||
|
||||
# Check webhook status
|
||||
status:
|
||||
@echo "=== Webhook Deployment ==="
|
||||
@kubectl get deployment -n $(NAMESPACE) irsa-webhook
|
||||
@echo ""
|
||||
@echo "=== Webhook Pods ==="
|
||||
@kubectl get pods -n $(NAMESPACE) -l app=irsa-webhook
|
||||
@echo ""
|
||||
@echo "=== Webhook Service ==="
|
||||
@kubectl get svc -n $(NAMESPACE) irsa-webhook
|
||||
@echo ""
|
||||
@echo "=== MutatingWebhookConfiguration ==="
|
||||
@kubectl get mutatingwebhookconfiguration irsa-webhook
|
||||
|
||||
# Clean all resources
|
||||
clean:
|
||||
kubectl delete -f deploy.yaml --ignore-not-found
|
||||
kubectl delete namespace $(NAMESPACE) --ignore-not-found
|
||||
rm -f webhook
|
||||
|
||||
# Restart webhook deployment
|
||||
restart:
|
||||
kubectl rollout restart deployment -n $(NAMESPACE) irsa-webhook
|
||||
kubectl rollout status deployment -n $(NAMESPACE) irsa-webhook
|
||||
|
||||
# Help
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo " build - Build Go binary"
|
||||
@echo " docker-build - Build Docker image"
|
||||
@echo " docker-push - Push Docker image"
|
||||
@echo " certs - Generate TLS certificates"
|
||||
@echo " deploy - Deploy to Kubernetes"
|
||||
@echo " undeploy - Remove from Kubernetes"
|
||||
@echo " logs - View webhook logs"
|
||||
@echo " test-example - Deploy and test example pod"
|
||||
@echo " clean-example - Remove example pod"
|
||||
@echo " go-test - Run Go tests"
|
||||
@echo " fmt - Format Go code"
|
||||
@echo " vet - Run Go vet"
|
||||
@echo " deps - Download dependencies"
|
||||
@echo " all - Complete build and deploy"
|
||||
@echo " status - Check webhook status"
|
||||
@echo " clean - Remove all resources"
|
||||
@echo " restart - Restart webhook deployment"
|
||||
@echo " help - Show this help"
|
||||
268
README.md
Normal file
268
README.md
Normal file
@@ -0,0 +1,268 @@
|
||||
# IRSA Mutating Admission Webhook for Kubernetes
|
||||
|
||||
This webhook implements IAM Roles for Service Accounts (IRSA) for Kubernetes clusters, allowing pods to assume AWS IAM roles using projected service account tokens.
|
||||
|
||||
## Features
|
||||
|
||||
- Automatically injects AWS credentials configuration into pods
|
||||
- Uses projected service account tokens with custom audience
|
||||
- Supports multiple containers and init containers
|
||||
- Follows security best practices
|
||||
- No external dependencies beyond Kubernetes API
|
||||
|
||||
## How It Works
|
||||
|
||||
When a pod is created, the webhook:
|
||||
|
||||
1. Extracts the ServiceAccount name from the pod spec
|
||||
2. Fetches the ServiceAccount from the Kubernetes API
|
||||
3. Checks for the `vultr.com/role-arn` annotation
|
||||
4. If present, mutates the pod to inject:
|
||||
- **Environment Variables:**
|
||||
- `AWS_ROLE_ARN`: The IAM role ARN from the annotation
|
||||
- `AWS_WEB_IDENTITY_TOKEN_FILE`: Path to the projected token
|
||||
- `AWS_STS_REGIONAL_ENDPOINTS`: Set to "regional"
|
||||
- **Volume:** A projected ServiceAccount token volume with audience "vultr"
|
||||
- **Volume Mounts:** Mounts the token at `/var/run/secrets/vultr.com/serviceaccount`
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Kubernetes 1.20+ (for projected service account tokens)
|
||||
- `kubectl` configured to access your cluster
|
||||
- OpenSSL (for certificate generation)
|
||||
- Go 1.21+ (for building from source)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Build the Docker Image
|
||||
|
||||
```bash
|
||||
docker build -t your-registry/irsa-webhook:latest .
|
||||
docker push your-registry/irsa-webhook:latest
|
||||
```
|
||||
|
||||
Update `deploy.yaml` with your image location.
|
||||
|
||||
### 2. Generate TLS Certificates
|
||||
|
||||
The webhook requires TLS certificates to communicate with the Kubernetes API server:
|
||||
|
||||
```bash
|
||||
chmod +x generate-certs.sh
|
||||
./generate-certs.sh
|
||||
```
|
||||
|
||||
This script will:
|
||||
- Generate a self-signed CA and certificate
|
||||
- Create a Kubernetes secret with the certificates
|
||||
- Update the MutatingWebhookConfiguration with the CA bundle
|
||||
|
||||
### 3. Deploy the Webhook
|
||||
|
||||
```bash
|
||||
kubectl apply -f deploy.yaml
|
||||
```
|
||||
|
||||
This creates:
|
||||
- Namespace: `irsa-system`
|
||||
- ServiceAccount with RBAC permissions
|
||||
- Deployment with 2 replicas
|
||||
- Service
|
||||
- MutatingWebhookConfiguration
|
||||
|
||||
### 4. Verify Deployment
|
||||
|
||||
```bash
|
||||
kubectl get pods -n irsa-system
|
||||
kubectl logs -n irsa-system -l app=irsa-webhook
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Annotate ServiceAccount
|
||||
|
||||
To enable IRSA for a ServiceAccount, add the `vultr.com/role-arn` annotation:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: my-app
|
||||
namespace: default
|
||||
annotations:
|
||||
vultr.com/role-arn: "arn:aws:iam::123456789012:role/my-app-role"
|
||||
```
|
||||
|
||||
### Deploy a Pod
|
||||
|
||||
Any pod using this ServiceAccount will automatically receive the AWS configuration:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: my-app
|
||||
namespace: default
|
||||
spec:
|
||||
serviceAccountName: my-app
|
||||
containers:
|
||||
- name: app
|
||||
image: amazon/aws-cli:latest
|
||||
command: ["sleep", "3600"]
|
||||
```
|
||||
|
||||
### Verify Injection
|
||||
|
||||
Check that the pod has the injected configuration:
|
||||
|
||||
```bash
|
||||
# Check environment variables
|
||||
kubectl exec my-app -- env | grep AWS
|
||||
|
||||
# Check volume mount
|
||||
kubectl exec my-app -- ls -la /var/run/secrets/vultr.com/serviceaccount
|
||||
|
||||
# Test AWS credentials
|
||||
kubectl exec my-app -- aws sts get-caller-identity
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
The webhook supports the following environment variables:
|
||||
|
||||
- `TLS_CERT_PATH`: Path to TLS certificate (default: `/etc/webhook/certs/tls.crt`)
|
||||
- `TLS_KEY_PATH`: Path to TLS private key (default: `/etc/webhook/certs/tls.key`)
|
||||
- `PORT`: HTTPS port to listen on (default: `8443`)
|
||||
|
||||
### Webhook Configuration
|
||||
|
||||
Edit the `MutatingWebhookConfiguration` in `deploy.yaml`:
|
||||
|
||||
- **failurePolicy**: Set to `Fail` for production to block pods if webhook is unavailable
|
||||
- **timeoutSeconds**: Adjust timeout based on cluster performance
|
||||
- **namespaceSelector**: Control which namespaces are affected
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Least Privilege**: The webhook ServiceAccount only has permissions to read ServiceAccounts
|
||||
2. **TLS**: All communication is encrypted using TLS 1.2+
|
||||
3. **Non-root**: Container runs as non-root user (65532)
|
||||
4. **Read-only filesystem**: Container has read-only root filesystem
|
||||
5. **No privilege escalation**: Security context prevents privilege escalation
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Webhook Not Mutating Pods
|
||||
|
||||
1. Check webhook logs:
|
||||
```bash
|
||||
kubectl logs -n irsa-system -l app=irsa-webhook
|
||||
```
|
||||
|
||||
2. Verify MutatingWebhookConfiguration:
|
||||
```bash
|
||||
kubectl get mutatingwebhookconfiguration irsa-webhook -o yaml
|
||||
```
|
||||
|
||||
3. Check if ServiceAccount has the annotation:
|
||||
```bash
|
||||
kubectl get sa <service-account-name> -o yaml
|
||||
```
|
||||
|
||||
### Certificate Issues
|
||||
|
||||
If you see TLS errors, regenerate certificates:
|
||||
|
||||
```bash
|
||||
./generate-certs.sh
|
||||
kubectl rollout restart deployment -n irsa-system irsa-webhook
|
||||
```
|
||||
|
||||
### RBAC Permissions
|
||||
|
||||
If webhook can't fetch ServiceAccounts, verify RBAC:
|
||||
|
||||
```bash
|
||||
kubectl auth can-i get serviceaccounts --as=system:serviceaccount:irsa-system:irsa-webhook --all-namespaces
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Local Testing
|
||||
|
||||
You can test the webhook logic locally:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func TestGeneratePatches(t *testing.T) {
|
||||
ws := &WebhookServer{}
|
||||
pod := &corev1.Pod{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{Name: "test"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
patches, err := ws.generatePatches(pod, "arn:aws:iam::123456789012:role/test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate patches: %v", err)
|
||||
}
|
||||
|
||||
if len(patches) == 0 {
|
||||
t.Error("Expected patches to be generated")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Building from Source
|
||||
|
||||
```bash
|
||||
go mod download
|
||||
go build -o webhook main.go
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Kubernetes │
|
||||
│ API Server │
|
||||
└──────┬──────┘
|
||||
│
|
||||
│ AdmissionReview Request
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ IRSA Webhook │
|
||||
│ │
|
||||
│ 1. Parse Pod │
|
||||
│ 2. Get SA │◄────┐
|
||||
│ 3. Check Anno │ │
|
||||
│ 4. Gen Patches │ │
|
||||
└─────────────────┘ │
|
||||
│
|
||||
┌─────┴──────┐
|
||||
│ K8s API │
|
||||
│ (Get SA) │
|
||||
└────────────┘
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions welcome! Please ensure:
|
||||
- Code follows Go best practices
|
||||
- Add tests for new functionality
|
||||
- Update documentation as needed
|
||||
390
TROUBLESHOOTING.md
Normal file
390
TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,390 @@
|
||||
# Troubleshooting Guide
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### 1. Webhook Not Responding
|
||||
|
||||
**Symptoms:**
|
||||
- Pods fail to create with timeout errors
|
||||
- Events show webhook timeout
|
||||
- `kubectl get pods` hangs
|
||||
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# Check webhook pod status
|
||||
kubectl get pods -n irsa-system
|
||||
|
||||
# View webhook logs
|
||||
kubectl logs -n irsa-system -l app=irsa-webhook
|
||||
|
||||
# Check webhook service
|
||||
kubectl get svc -n irsa-system irsa-webhook
|
||||
kubectl get endpoints -n irsa-system irsa-webhook
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Webhook pods not running:**
|
||||
```bash
|
||||
kubectl describe pods -n irsa-system -l app=irsa-webhook
|
||||
# Fix image pull issues, resource constraints, etc.
|
||||
```
|
||||
|
||||
2. **TLS certificate issues:**
|
||||
```bash
|
||||
# Regenerate certificates
|
||||
./generate-certs.sh
|
||||
kubectl rollout restart deployment -n irsa-system irsa-webhook
|
||||
```
|
||||
|
||||
3. **Service not routing correctly:**
|
||||
```bash
|
||||
# Check if service selectors match pod labels
|
||||
kubectl get svc -n irsa-system irsa-webhook -o yaml
|
||||
kubectl get pods -n irsa-system -l app=irsa-webhook --show-labels
|
||||
```
|
||||
|
||||
### 2. Pods Not Being Mutated
|
||||
|
||||
**Symptoms:**
|
||||
- Pods create successfully but don't have injected configuration
|
||||
- Environment variables missing
|
||||
- Volume not mounted
|
||||
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# Check if ServiceAccount has annotation
|
||||
kubectl get sa <service-account-name> -o yaml | grep vultr.com/role-arn
|
||||
|
||||
# Check webhook configuration
|
||||
kubectl get mutatingwebhookconfiguration irsa-webhook -o yaml
|
||||
|
||||
# View webhook logs for the specific pod creation
|
||||
kubectl logs -n irsa-system -l app=irsa-webhook --tail=100
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **ServiceAccount annotation missing:**
|
||||
```bash
|
||||
kubectl annotate sa <service-account-name> \
|
||||
vultr.com/role-arn="arn:aws:iam::123456789012:role/your-role"
|
||||
```
|
||||
|
||||
2. **Namespace excluded from webhook:**
|
||||
Check the `namespaceSelector` in the MutatingWebhookConfiguration:
|
||||
```bash
|
||||
kubectl get mutatingwebhookconfiguration irsa-webhook -o yaml
|
||||
```
|
||||
|
||||
3. **Webhook not receiving requests:**
|
||||
```bash
|
||||
# Check webhook logs for incoming requests
|
||||
kubectl logs -n irsa-system -l app=irsa-webhook --tail=50
|
||||
|
||||
# Verify webhook configuration matches service
|
||||
kubectl get mutatingwebhookconfiguration irsa-webhook -o jsonpath='{.webhooks[0].clientConfig}'
|
||||
```
|
||||
|
||||
### 3. RBAC Permission Errors
|
||||
|
||||
**Symptoms:**
|
||||
- Webhook logs show "forbidden" or "unauthorized" errors
|
||||
- Error fetching ServiceAccounts
|
||||
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# Check webhook ServiceAccount permissions
|
||||
kubectl auth can-i get serviceaccounts \
|
||||
--as=system:serviceaccount:irsa-system:irsa-webhook \
|
||||
--all-namespaces
|
||||
|
||||
# View RBAC resources
|
||||
kubectl get clusterrole irsa-webhook -o yaml
|
||||
kubectl get clusterrolebinding irsa-webhook -o yaml
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Missing RBAC permissions:**
|
||||
```bash
|
||||
# Reapply RBAC configuration
|
||||
kubectl apply -f deploy.yaml
|
||||
```
|
||||
|
||||
2. **ServiceAccount not bound to role:**
|
||||
```bash
|
||||
kubectl get clusterrolebinding irsa-webhook -o yaml
|
||||
# Verify subjects include the correct ServiceAccount
|
||||
```
|
||||
|
||||
### 4. TLS/Certificate Issues
|
||||
|
||||
**Symptoms:**
|
||||
- "x509: certificate signed by unknown authority"
|
||||
- "TLS handshake error"
|
||||
- Webhook returns 401 or 403
|
||||
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# Check certificate in secret
|
||||
kubectl get secret -n irsa-system irsa-webhook-certs -o yaml
|
||||
|
||||
# Verify CA bundle in webhook config
|
||||
kubectl get mutatingwebhookconfiguration irsa-webhook \
|
||||
-o jsonpath='{.webhooks[0].clientConfig.caBundle}' | base64 -d
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Regenerate certificates:**
|
||||
```bash
|
||||
./generate-certs.sh
|
||||
```
|
||||
|
||||
2. **Manually update CA bundle:**
|
||||
```bash
|
||||
CA_BUNDLE=$(kubectl get secret -n irsa-system irsa-webhook-certs \
|
||||
-o jsonpath='{.data.ca\.crt}')
|
||||
|
||||
kubectl patch mutatingwebhookconfiguration irsa-webhook \
|
||||
--type='json' \
|
||||
-p="[{'op': 'replace', 'path': '/webhooks/0/clientConfig/caBundle', 'value':'${CA_BUNDLE}'}]"
|
||||
```
|
||||
|
||||
3. **Verify certificate SANs:**
|
||||
```bash
|
||||
kubectl get secret -n irsa-system irsa-webhook-certs \
|
||||
-o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -text -noout
|
||||
```
|
||||
|
||||
### 5. AWS Credential Issues
|
||||
|
||||
**Symptoms:**
|
||||
- Pods can't authenticate with AWS
|
||||
- "Unable to locate credentials" error
|
||||
- "InvalidIdentityToken" error from AWS STS
|
||||
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# Check injected environment variables
|
||||
kubectl exec <pod-name> -- env | grep AWS
|
||||
|
||||
# Verify token file exists
|
||||
kubectl exec <pod-name> -- ls -la /var/run/secrets/vultr.com/serviceaccount/
|
||||
|
||||
# Check token contents (first 50 chars)
|
||||
kubectl exec <pod-name> -- head -c 50 /var/run/secrets/vultr.com/serviceaccount/token
|
||||
|
||||
# Test AWS STS
|
||||
kubectl exec <pod-name> -- aws sts get-caller-identity
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Token not mounted:**
|
||||
- Verify pod has the volume and volume mount
|
||||
- Check webhook logs for mutation
|
||||
- Delete and recreate the pod
|
||||
|
||||
2. **IAM role trust policy issue:**
|
||||
Ensure your IAM role has the correct trust policy:
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"Federated": "arn:aws:iam::YOUR_ACCOUNT_ID:oidc-provider/YOUR_OIDC_PROVIDER"
|
||||
},
|
||||
"Action": "sts:AssumeRoleWithWebIdentity",
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"YOUR_OIDC_PROVIDER:aud": "vultr"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
3. **Wrong audience in token:**
|
||||
- Verify the projected token has audience "vultr"
|
||||
- Check webhook configuration uses correct tokenAudience constant
|
||||
|
||||
### 6. Performance Issues
|
||||
|
||||
**Symptoms:**
|
||||
- Pod creation is slow
|
||||
- Webhook timeout warnings
|
||||
- High resource usage
|
||||
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# Check webhook resource usage
|
||||
kubectl top pods -n irsa-system
|
||||
|
||||
# View webhook latency in logs
|
||||
kubectl logs -n irsa-system -l app=irsa-webhook | grep "Processing pod"
|
||||
|
||||
# Check for throttling
|
||||
kubectl describe pods -n irsa-system -l app=irsa-webhook
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Increase webhook timeout:**
|
||||
```bash
|
||||
kubectl patch mutatingwebhookconfiguration irsa-webhook \
|
||||
--type='json' \
|
||||
-p='[{"op": "replace", "path": "/webhooks/0/timeoutSeconds", "value": 30}]'
|
||||
```
|
||||
|
||||
2. **Scale webhook deployment:**
|
||||
```bash
|
||||
kubectl scale deployment -n irsa-system irsa-webhook --replicas=3
|
||||
```
|
||||
|
||||
3. **Increase resource limits:**
|
||||
Edit deploy.yaml and increase CPU/memory limits:
|
||||
```yaml
|
||||
resources:
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 512Mi
|
||||
```
|
||||
|
||||
### 7. JSON Patch Generation Errors
|
||||
|
||||
**Symptoms:**
|
||||
- "Failed to generate patches" in webhook logs
|
||||
- Malformed patch errors
|
||||
- Array index out of bounds
|
||||
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# Enable verbose logging (add to deployment)
|
||||
# Set LOG_LEVEL=debug in environment
|
||||
|
||||
# Check specific pod that failed
|
||||
kubectl logs -n irsa-system -l app=irsa-webhook --tail=100 | grep -A 10 "Failed"
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Review pod specification:**
|
||||
- Ensure pod spec is valid JSON
|
||||
- Check for unusual container configurations
|
||||
|
||||
2. **Update webhook logic:**
|
||||
- Fix any bugs in generatePatches function
|
||||
- Add error handling for edge cases
|
||||
|
||||
### 8. Multiple Webhooks Conflict
|
||||
|
||||
**Symptoms:**
|
||||
- Pod mutations from other webhooks interfering
|
||||
- Unexpected pod configuration
|
||||
- Volume/env var conflicts
|
||||
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# List all mutating webhooks
|
||||
kubectl get mutatingwebhookconfigurations
|
||||
|
||||
# Check webhook order
|
||||
kubectl get mutatingwebhookconfigurations -o yaml | grep -A 5 "name:"
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Adjust webhook order:**
|
||||
Webhooks are processed alphabetically by name. Rename if needed:
|
||||
```bash
|
||||
# Add a prefix to control order
|
||||
kubectl patch mutatingwebhookconfiguration irsa-webhook \
|
||||
--type='json' \
|
||||
-p='[{"op": "replace", "path": "/metadata/name", "value": "01-irsa-webhook"}]'
|
||||
```
|
||||
|
||||
2. **Add reinvocationPolicy:**
|
||||
```yaml
|
||||
webhooks:
|
||||
- name: irsa.vultr.com
|
||||
reinvocationPolicy: IfNeeded # or Never
|
||||
```
|
||||
|
||||
## Debug Commands Cheat Sheet
|
||||
|
||||
```bash
|
||||
# View all webhook-related resources
|
||||
kubectl get all -n irsa-system
|
||||
kubectl get mutatingwebhookconfiguration irsa-webhook
|
||||
kubectl get clusterrole irsa-webhook
|
||||
kubectl get clusterrolebinding irsa-webhook
|
||||
|
||||
# Test webhook directly
|
||||
kubectl run test-pod --image=nginx --dry-run=client -o yaml | \
|
||||
kubectl create -f - --namespace=default
|
||||
|
||||
# Watch webhook logs in real-time
|
||||
kubectl logs -n irsa-system -l app=irsa-webhook -f
|
||||
|
||||
# Check webhook pod health
|
||||
kubectl get pods -n irsa-system -l app=irsa-webhook -o wide
|
||||
kubectl describe pods -n irsa-system -l app=irsa-webhook
|
||||
|
||||
# View recent events
|
||||
kubectl get events -n irsa-system --sort-by='.lastTimestamp'
|
||||
|
||||
# Test ServiceAccount annotation
|
||||
kubectl get sa -A -o jsonpath='{range .items[?(@.metadata.annotations.vultr\.com/role-arn)]}{.metadata.namespace}{" "}{.metadata.name}{" "}{.metadata.annotations.vultr\.com/role-arn}{"\n"}{end}'
|
||||
|
||||
# Validate webhook configuration
|
||||
kubectl get mutatingwebhookconfiguration irsa-webhook -o yaml | grep -E "(caBundle|service|path|port)"
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you're still experiencing issues:
|
||||
|
||||
1. **Collect diagnostic information:**
|
||||
```bash
|
||||
# Run this script and save output
|
||||
kubectl get all -n irsa-system > diagnostics.txt
|
||||
kubectl logs -n irsa-system -l app=irsa-webhook --tail=200 >> diagnostics.txt
|
||||
kubectl get mutatingwebhookconfiguration irsa-webhook -o yaml >> diagnostics.txt
|
||||
kubectl get events -n irsa-system >> diagnostics.txt
|
||||
```
|
||||
|
||||
2. **Check webhook version:**
|
||||
```bash
|
||||
kubectl get deployment -n irsa-system irsa-webhook -o jsonpath='{.spec.template.spec.containers[0].image}'
|
||||
```
|
||||
|
||||
3. **Review logs with timestamps:**
|
||||
```bash
|
||||
kubectl logs -n irsa-system -l app=irsa-webhook --timestamps=true --tail=100
|
||||
```
|
||||
|
||||
4. **Test in isolation:**
|
||||
- Create a separate test namespace
|
||||
- Deploy a simple test pod
|
||||
- Monitor webhook behavior
|
||||
|
||||
## Prevention
|
||||
|
||||
**Best Practices to Avoid Issues:**
|
||||
|
||||
1. Always test in a non-production cluster first
|
||||
2. Set `failurePolicy: Ignore` during initial deployment
|
||||
3. Monitor webhook performance and logs
|
||||
4. Keep certificates up to date (rotate every 365 days)
|
||||
5. Use resource limits to prevent webhook from consuming too much
|
||||
6. Implement readiness and liveness probes
|
||||
7. Scale webhook deployment for high-traffic clusters
|
||||
8. Document all ServiceAccount annotations
|
||||
139
example.yaml
Normal file
139
example.yaml
Normal file
@@ -0,0 +1,139 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: irsa-system
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: irsa-webhook
|
||||
namespace: irsa-system
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: irsa-webhook
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["serviceaccounts"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: irsa-webhook
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: irsa-webhook
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: irsa-webhook
|
||||
namespace: irsa-system
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: irsa-webhook
|
||||
namespace: irsa-system
|
||||
spec:
|
||||
selector:
|
||||
app: irsa-webhook
|
||||
ports:
|
||||
- port: 443
|
||||
targetPort: 8443
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: irsa-webhook
|
||||
namespace: irsa-system
|
||||
labels:
|
||||
app: irsa-webhook
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: irsa-webhook
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: irsa-webhook
|
||||
spec:
|
||||
serviceAccountName: irsa-webhook
|
||||
containers:
|
||||
- name: webhook
|
||||
image: your-registry/irsa-webhook:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8443
|
||||
name: webhook
|
||||
env:
|
||||
- name: TLS_CERT_PATH
|
||||
value: /etc/webhook/certs/tls.crt
|
||||
- name: TLS_KEY_PATH
|
||||
value: /etc/webhook/certs/tls.key
|
||||
volumeMounts:
|
||||
- name: webhook-certs
|
||||
mountPath: /etc/webhook/certs
|
||||
readOnly: true
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8443
|
||||
scheme: HTTPS
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8443
|
||||
scheme: HTTPS
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65532
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
volumes:
|
||||
- name: webhook-certs
|
||||
secret:
|
||||
secretName: irsa-webhook-certs
|
||||
---
|
||||
apiVersion: admissionregistration.k8s.io/v1
|
||||
kind: MutatingWebhookConfiguration
|
||||
metadata:
|
||||
name: irsa-webhook
|
||||
webhooks:
|
||||
- name: irsa.vultr.com
|
||||
clientConfig:
|
||||
service:
|
||||
name: irsa-webhook
|
||||
namespace: irsa-system
|
||||
path: /mutate
|
||||
caBundle: ${CA_BUNDLE} # Replace with base64-encoded CA certificate
|
||||
rules:
|
||||
- operations: ["CREATE"]
|
||||
apiGroups: [""]
|
||||
apiVersions: ["v1"]
|
||||
resources: ["pods"]
|
||||
admissionReviewVersions: ["v1"]
|
||||
sideEffects: None
|
||||
timeoutSeconds: 10
|
||||
failurePolicy: Ignore # Change to Fail for production if needed
|
||||
namespaceSelector:
|
||||
matchExpressions:
|
||||
- key: irsa-webhook
|
||||
operator: NotIn
|
||||
values: ["disabled"]
|
||||
57
go.mod
Normal file
57
go.mod
Normal file
@@ -0,0 +1,57 @@
|
||||
module github.com/example/irsa-webhook
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
k8s.io/api v0.34.3
|
||||
k8s.io/apimachinery v0.34.3
|
||||
k8s.io/client-go v0.34.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.4 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.4 // indirect
|
||||
github.com/go-openapi/swag v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/loading v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/netutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/gnostic-models v0.7.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/term v0.38.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
167
go.sum
Normal file
167
go.sum
Normal file
@@ -0,0 +1,167 @@
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
|
||||
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
|
||||
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
|
||||
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
|
||||
github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU=
|
||||
github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
|
||||
github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4=
|
||||
github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
|
||||
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
|
||||
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
|
||||
github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y=
|
||||
github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
|
||||
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
|
||||
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
|
||||
github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48=
|
||||
github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg=
|
||||
github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0=
|
||||
github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg=
|
||||
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
|
||||
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
|
||||
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
|
||||
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
||||
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
|
||||
github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4=
|
||||
k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
|
||||
k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
|
||||
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
|
||||
k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ=
|
||||
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
370
main.go
Normal file
370
main.go
Normal file
@@ -0,0 +1,370 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
const (
|
||||
roleArnAnnotation = "vultr.com/role-arn"
|
||||
tokenVolumeName = "vultr-irsa-token"
|
||||
tokenMountPath = "/var/run/secrets/vultr.com/serviceaccount"
|
||||
tokenFileName = "token"
|
||||
tokenAudience = "vultr"
|
||||
envAWSRoleArn = "AWS_ROLE_ARN"
|
||||
envAWSWebIdentityToken = "AWS_WEB_IDENTITY_TOKEN_FILE"
|
||||
envAWSSTSRegionalEndpoint = "AWS_STS_REGIONAL_ENDPOINTS"
|
||||
)
|
||||
|
||||
var (
|
||||
tokenExpirationSeconds = int64(86400) // 24 hours
|
||||
)
|
||||
|
||||
type WebhookServer struct {
|
||||
client *kubernetes.Clientset
|
||||
}
|
||||
|
||||
// JSONPatch represents a JSON Patch operation
|
||||
type JSONPatch struct {
|
||||
Op string `json:"op"`
|
||||
Path string `json:"path"`
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Create in-cluster Kubernetes client
|
||||
config, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create in-cluster config: %v", err)
|
||||
}
|
||||
|
||||
clientset, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create Kubernetes client: %v", err)
|
||||
}
|
||||
|
||||
server := &WebhookServer{
|
||||
client: clientset,
|
||||
}
|
||||
|
||||
// Setup HTTP handlers
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/mutate", server.handleMutate)
|
||||
mux.HandleFunc("/health", handleHealth)
|
||||
|
||||
// TLS configuration
|
||||
tlsCertPath := getEnv("TLS_CERT_PATH", "/etc/webhook/certs/tls.crt")
|
||||
tlsKeyPath := getEnv("TLS_KEY_PATH", "/etc/webhook/certs/tls.key")
|
||||
port := getEnv("PORT", "8443")
|
||||
|
||||
// Load TLS certificates
|
||||
cert, err := tls.LoadX509KeyPair(tlsCertPath, tlsKeyPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load TLS certificates: %v", err)
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
httpServer := &http.Server{
|
||||
Addr: fmt.Sprintf(":%s", port),
|
||||
TLSConfig: tlsConfig,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
log.Printf("Starting webhook server on port %s", port)
|
||||
if err := httpServer.ListenAndServeTLS("", ""); err != nil {
|
||||
log.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WebhookServer) handleMutate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Read request body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Printf("Failed to read request body: %v", err)
|
||||
http.Error(w, "Failed to read request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
// Parse AdmissionReview request
|
||||
admissionReview := &admissionv1.AdmissionReview{}
|
||||
if err := json.Unmarshal(body, admissionReview); err != nil {
|
||||
log.Printf("Failed to unmarshal admission review: %v", err)
|
||||
http.Error(w, "Failed to parse admission review", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if admissionReview.Request == nil {
|
||||
log.Printf("Admission review request is nil")
|
||||
http.Error(w, "Invalid admission review request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Process the request and create response
|
||||
admissionResponse := ws.mutate(admissionReview.Request)
|
||||
|
||||
// Create response AdmissionReview
|
||||
responseAdmissionReview := &admissionv1.AdmissionReview{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "admission.k8s.io/v1",
|
||||
Kind: "AdmissionReview",
|
||||
},
|
||||
Response: admissionResponse,
|
||||
}
|
||||
responseAdmissionReview.Response.UID = admissionReview.Request.UID
|
||||
|
||||
// Marshal and send response
|
||||
responseBytes, err := json.Marshal(responseAdmissionReview)
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal admission response: %v", err)
|
||||
http.Error(w, "Failed to create response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(responseBytes)
|
||||
}
|
||||
|
||||
func (ws *WebhookServer) mutate(request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse {
|
||||
// Create base response
|
||||
response := &admissionv1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
}
|
||||
|
||||
// Only handle Pod objects
|
||||
if request.Kind.Kind != "Pod" {
|
||||
log.Printf("Skipping non-Pod object: %s", request.Kind.Kind)
|
||||
return response
|
||||
}
|
||||
|
||||
// Parse Pod from request
|
||||
pod := &corev1.Pod{}
|
||||
if err := json.Unmarshal(request.Object.Raw, pod); err != nil {
|
||||
log.Printf("Failed to unmarshal pod: %v", err)
|
||||
response.Allowed = false
|
||||
response.Result = &metav1.Status{
|
||||
Message: fmt.Sprintf("Failed to parse pod: %v", err),
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
log.Printf("Processing pod: %s/%s, ServiceAccount: %s",
|
||||
request.Namespace, pod.Name, pod.Spec.ServiceAccountName)
|
||||
|
||||
// Get ServiceAccount name (default to "default" if not specified)
|
||||
serviceAccountName := pod.Spec.ServiceAccountName
|
||||
if serviceAccountName == "" {
|
||||
serviceAccountName = "default"
|
||||
}
|
||||
|
||||
// Fetch ServiceAccount from Kubernetes API
|
||||
serviceAccount, err := ws.client.CoreV1().ServiceAccounts(request.Namespace).Get(
|
||||
context.Background(),
|
||||
serviceAccountName,
|
||||
metav1.GetOptions{},
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch ServiceAccount %s/%s: %v",
|
||||
request.Namespace, serviceAccountName, err)
|
||||
// Don't block pod creation if we can't fetch the SA
|
||||
return response
|
||||
}
|
||||
|
||||
// Check for role ARN annotation
|
||||
roleArn, exists := serviceAccount.Annotations[roleArnAnnotation]
|
||||
if !exists || roleArn == "" {
|
||||
log.Printf("ServiceAccount %s/%s does not have %s annotation, skipping mutation",
|
||||
request.Namespace, serviceAccountName, roleArnAnnotation)
|
||||
return response
|
||||
}
|
||||
|
||||
log.Printf("Found role ARN annotation: %s", roleArn)
|
||||
|
||||
// Generate JSON patches to mutate the pod
|
||||
patches, err := ws.generatePatches(pod, roleArn)
|
||||
if err != nil {
|
||||
log.Printf("Failed to generate patches: %v", err)
|
||||
response.Allowed = false
|
||||
response.Result = &metav1.Status{
|
||||
Message: fmt.Sprintf("Failed to generate patches: %v", err),
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// Marshal patches to JSON
|
||||
patchBytes, err := json.Marshal(patches)
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal patches: %v", err)
|
||||
response.Allowed = false
|
||||
response.Result = &metav1.Status{
|
||||
Message: fmt.Sprintf("Failed to marshal patches: %v", err),
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
response.Patch = patchBytes
|
||||
patchType := admissionv1.PatchTypeJSONPatch
|
||||
response.PatchType = &patchType
|
||||
|
||||
log.Printf("Successfully generated %d patches for pod %s/%s",
|
||||
len(patches), request.Namespace, pod.Name)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func (ws *WebhookServer) generatePatches(pod *corev1.Pod, roleArn string) ([]JSONPatch, error) {
|
||||
var patches []JSONPatch
|
||||
|
||||
// 1. Add projected volume for service account token
|
||||
tokenVolume := corev1.Volume{
|
||||
Name: tokenVolumeName,
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Projected: &corev1.ProjectedVolumeSource{
|
||||
Sources: []corev1.VolumeProjection{
|
||||
{
|
||||
ServiceAccountToken: &corev1.ServiceAccountTokenProjection{
|
||||
Audience: tokenAudience,
|
||||
ExpirationSeconds: &tokenExpirationSeconds,
|
||||
Path: tokenFileName,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Check if volumes array exists
|
||||
if pod.Spec.Volumes == nil {
|
||||
// Initialize volumes array
|
||||
patches = append(patches, JSONPatch{
|
||||
Op: "add",
|
||||
Path: "/spec/volumes",
|
||||
Value: []corev1.Volume{tokenVolume},
|
||||
})
|
||||
} else {
|
||||
// Append to existing volumes
|
||||
patches = append(patches, JSONPatch{
|
||||
Op: "add",
|
||||
Path: "/spec/volumes/-",
|
||||
Value: tokenVolume,
|
||||
})
|
||||
}
|
||||
|
||||
// 2. Process each container
|
||||
for i := range pod.Spec.Containers {
|
||||
containerPatches := ws.generateContainerPatches(i, roleArn, &pod.Spec.Containers[i])
|
||||
patches = append(patches, containerPatches...)
|
||||
}
|
||||
|
||||
// 3. Process init containers if they exist
|
||||
for i := range pod.Spec.InitContainers {
|
||||
containerPatches := ws.generateContainerPatches(i, roleArn, &pod.Spec.InitContainers[i])
|
||||
// Adjust paths for init containers
|
||||
for j := range containerPatches {
|
||||
containerPatches[j].Path = "/spec/initContainers" +
|
||||
containerPatches[j].Path[len("/spec/containers"):]
|
||||
}
|
||||
patches = append(patches, containerPatches...)
|
||||
}
|
||||
|
||||
return patches, nil
|
||||
}
|
||||
|
||||
func (ws *WebhookServer) generateContainerPatches(index int, roleArn string, container *corev1.Container) []JSONPatch {
|
||||
var patches []JSONPatch
|
||||
basePath := fmt.Sprintf("/spec/containers/%d", index)
|
||||
|
||||
// Add volume mount
|
||||
volumeMount := corev1.VolumeMount{
|
||||
Name: tokenVolumeName,
|
||||
MountPath: tokenMountPath,
|
||||
ReadOnly: true,
|
||||
}
|
||||
|
||||
if container.VolumeMounts == nil {
|
||||
// Initialize volume mounts array
|
||||
patches = append(patches, JSONPatch{
|
||||
Op: "add",
|
||||
Path: basePath + "/volumeMounts",
|
||||
Value: []corev1.VolumeMount{volumeMount},
|
||||
})
|
||||
} else {
|
||||
// Append to existing volume mounts
|
||||
patches = append(patches, JSONPatch{
|
||||
Op: "add",
|
||||
Path: basePath + "/volumeMounts/-",
|
||||
Value: volumeMount,
|
||||
})
|
||||
}
|
||||
|
||||
// Add environment variables
|
||||
envVars := []corev1.EnvVar{
|
||||
{
|
||||
Name: envAWSRoleArn,
|
||||
Value: roleArn,
|
||||
},
|
||||
{
|
||||
Name: envAWSWebIdentityToken,
|
||||
Value: fmt.Sprintf("%s/%s", tokenMountPath, tokenFileName),
|
||||
},
|
||||
{
|
||||
Name: envAWSSTSRegionalEndpoint,
|
||||
Value: "regional",
|
||||
},
|
||||
}
|
||||
|
||||
if container.Env == nil {
|
||||
// Initialize env array
|
||||
patches = append(patches, JSONPatch{
|
||||
Op: "add",
|
||||
Path: basePath + "/env",
|
||||
Value: envVars,
|
||||
})
|
||||
} else {
|
||||
// Append to existing env vars
|
||||
for _, envVar := range envVars {
|
||||
patches = append(patches, JSONPatch{
|
||||
Op: "add",
|
||||
Path: basePath + "/env/-",
|
||||
Value: envVar,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return patches
|
||||
}
|
||||
|
||||
func handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
Reference in New Issue
Block a user