first commit

This commit is contained in:
2025-12-10 10:43:57 -05:00
commit 15d959a6c8
7 changed files with 1505 additions and 0 deletions

114
Makefile Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}