mirror of
https://github.com/containers/kubernetes-mcp-server.git
synced 2025-10-23 01:22:57 +03:00
feat(auth): add local development environment with Kind and Keycloak for OIDC (#354)
* Initial KinD setup Signed-off-by: Matthias Wessendorf <mwessend@redhat.com> * Initial Keycloak container setup Signed-off-by: Matthias Wessendorf <mwessend@redhat.com> * Adding an initial realm setup Signed-off-by: Matthias Wessendorf <mwessend@redhat.com> * Adding OIDC issuer and realm updates, adding cert-manager and handling self-signed certificates Signed-off-by: Matthias Wessendorf <mwessend@redhat.com> * Updates to script b/c of invalid auth config Signed-off-by: Matthias Wessendorf <mwessend@redhat.com> * Adjusting ports and better support for mac/podman Signed-off-by: Matthias Wessendorf <mwessend@redhat.com> * Addressing review comments: * do not expose all internal tasks, just keep the important targets documents * remove the keycloak-forward * move binaries for dev tools to _output * generate a configuration TOML file into the _output folder Signed-off-by: Matthias Wessendorf <mwessend@redhat.com> --------- Signed-off-by: Matthias Wessendorf <mwessend@redhat.com>
This commit is contained in:
committed by
GitHub
parent
0c78a1e89d
commit
7fe604e61d
40
Makefile
40
Makefile
@@ -113,3 +113,43 @@ lint: golangci-lint ## Lint the code
|
|||||||
.PHONY: update-readme-tools
|
.PHONY: update-readme-tools
|
||||||
update-readme-tools: ## Update the README.md file with the latest toolsets
|
update-readme-tools: ## Update the README.md file with the latest toolsets
|
||||||
go run ./internal/tools/update-readme/main.go README.md
|
go run ./internal/tools/update-readme/main.go README.md
|
||||||
|
|
||||||
|
##@ Tools
|
||||||
|
|
||||||
|
.PHONY: tools
|
||||||
|
tools: ## Install all required tools (kind) to ./_output/bin/
|
||||||
|
@echo "Checking and installing required tools to ./_output/bin/ ..."
|
||||||
|
@if [ -f _output/bin/kind ]; then echo "[OK] kind already installed"; else echo "Installing kind..."; $(MAKE) -s kind; fi
|
||||||
|
@echo "All tools ready!"
|
||||||
|
|
||||||
|
##@ Local Development
|
||||||
|
|
||||||
|
.PHONY: local-env-setup
|
||||||
|
local-env-setup: ## Setup complete local development environment with Kind cluster
|
||||||
|
@echo "========================================="
|
||||||
|
@echo "Kubernetes MCP Server - Local Setup"
|
||||||
|
@echo "========================================="
|
||||||
|
$(MAKE) tools
|
||||||
|
$(MAKE) kind-create-cluster
|
||||||
|
$(MAKE) keycloak-install
|
||||||
|
$(MAKE) build
|
||||||
|
@echo ""
|
||||||
|
@echo "========================================="
|
||||||
|
@echo "Local environment ready!"
|
||||||
|
@echo "========================================="
|
||||||
|
@echo ""
|
||||||
|
@echo "Configuration file generated:"
|
||||||
|
@echo " _output/config.toml"
|
||||||
|
@echo ""
|
||||||
|
@echo "Run the MCP server with:"
|
||||||
|
@echo " ./$(BINARY_NAME) --port 8080 --config _output/config.toml"
|
||||||
|
@echo ""
|
||||||
|
@echo "Or run with MCP inspector:"
|
||||||
|
@echo " npx @modelcontextprotocol/inspector@latest \$$(pwd)/$(BINARY_NAME) --config _output/config.toml"
|
||||||
|
|
||||||
|
.PHONY: local-env-teardown
|
||||||
|
local-env-teardown: ## Tear down the local Kind cluster
|
||||||
|
$(MAKE) kind-delete-cluster
|
||||||
|
|
||||||
|
# Include build configuration files
|
||||||
|
-include build/*.mk
|
||||||
|
|||||||
448
build/keycloak.mk
Normal file
448
build/keycloak.mk
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
# Keycloak IdP for development and testing
|
||||||
|
|
||||||
|
KEYCLOAK_NAMESPACE = keycloak
|
||||||
|
KEYCLOAK_ADMIN_USER = admin
|
||||||
|
KEYCLOAK_ADMIN_PASSWORD = admin
|
||||||
|
|
||||||
|
.PHONY: keycloak-install
|
||||||
|
keycloak-install:
|
||||||
|
@echo "Installing Keycloak (dev mode using official image)..."
|
||||||
|
@kubectl apply -f dev/config/keycloak/deployment.yaml
|
||||||
|
@echo "Applying Keycloak ingress (cert-manager will create TLS certificate)..."
|
||||||
|
@kubectl apply -f dev/config/keycloak/ingress.yaml
|
||||||
|
@echo "Extracting cert-manager CA certificate..."
|
||||||
|
@mkdir -p _output/cert-manager-ca
|
||||||
|
@kubectl get secret selfsigned-ca-secret -n cert-manager -o jsonpath='{.data.ca\.crt}' | base64 -d > _output/cert-manager-ca/ca.crt
|
||||||
|
@echo "✅ cert-manager CA certificate extracted to _output/cert-manager-ca/ca.crt (bind-mounted to API server)"
|
||||||
|
@echo "Restarting Kubernetes API server to pick up new CA..."
|
||||||
|
@docker exec kubernetes-mcp-server-control-plane pkill -f kube-apiserver || \
|
||||||
|
podman exec kubernetes-mcp-server-control-plane pkill -f kube-apiserver
|
||||||
|
@echo "Waiting for API server to restart..."
|
||||||
|
@sleep 5
|
||||||
|
@echo "Waiting for API server to be ready..."
|
||||||
|
@for i in $$(seq 1 30); do \
|
||||||
|
if kubectl get --raw /healthz >/dev/null 2>&1; then \
|
||||||
|
echo "✅ Kubernetes API server updated with cert-manager CA"; \
|
||||||
|
break; \
|
||||||
|
fi; \
|
||||||
|
sleep 2; \
|
||||||
|
done
|
||||||
|
@echo "Waiting for Keycloak to be ready..."
|
||||||
|
@kubectl wait --for=condition=ready pod -l app=keycloak -n $(KEYCLOAK_NAMESPACE) --timeout=120s || true
|
||||||
|
@echo "Waiting for Keycloak HTTP endpoint to be available..."
|
||||||
|
@for i in $$(seq 1 30); do \
|
||||||
|
STATUS=$$(curl -sk -o /dev/null -w "%{http_code}" https://keycloak.127-0-0-1.sslip.io:8443/realms/master 2>/dev/null || echo "000"); \
|
||||||
|
if [ "$$STATUS" = "200" ]; then \
|
||||||
|
echo "✅ Keycloak HTTP endpoint ready"; \
|
||||||
|
break; \
|
||||||
|
fi; \
|
||||||
|
echo " Attempt $$i/30: Waiting for Keycloak (status: $$STATUS)..."; \
|
||||||
|
sleep 3; \
|
||||||
|
done
|
||||||
|
@echo ""
|
||||||
|
@echo "Setting up OpenShift realm..."
|
||||||
|
@$(MAKE) -s keycloak-setup-realm
|
||||||
|
@echo ""
|
||||||
|
@echo "✅ Keycloak installed and configured!"
|
||||||
|
@echo "Access at: https://keycloak.127-0-0-1.sslip.io:8443"
|
||||||
|
|
||||||
|
.PHONY: keycloak-uninstall
|
||||||
|
keycloak-uninstall:
|
||||||
|
@kubectl delete -f dev/config/keycloak/deployment.yaml 2>/dev/null || true
|
||||||
|
|
||||||
|
.PHONY: keycloak-status
|
||||||
|
keycloak-status: ## Show Keycloak status and connection info
|
||||||
|
@if kubectl get svc -n $(KEYCLOAK_NAMESPACE) keycloak >/dev/null 2>&1; then \
|
||||||
|
echo "========================================"; \
|
||||||
|
echo "Keycloak Status"; \
|
||||||
|
echo "========================================"; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Status: Installed"; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Admin Console:"; \
|
||||||
|
echo " URL: https://keycloak.127-0-0-1.sslip.io:8443"; \
|
||||||
|
echo " Username: $(KEYCLOAK_ADMIN_USER)"; \
|
||||||
|
echo " Password: $(KEYCLOAK_ADMIN_PASSWORD)"; \
|
||||||
|
echo ""; \
|
||||||
|
echo "OIDC Endpoints (openshift realm):"; \
|
||||||
|
echo " Discovery: https://keycloak.127-0-0-1.sslip.io:8443/realms/openshift/.well-known/openid-configuration"; \
|
||||||
|
echo " Token: https://keycloak.127-0-0-1.sslip.io:8443/realms/openshift/protocol/openid-connect/token"; \
|
||||||
|
echo " Authorize: https://keycloak.127-0-0-1.sslip.io:8443/realms/openshift/protocol/openid-connect/auth"; \
|
||||||
|
echo " UserInfo: https://keycloak.127-0-0-1.sslip.io:8443/realms/openshift/protocol/openid-connect/userinfo"; \
|
||||||
|
echo " JWKS: https://keycloak.127-0-0-1.sslip.io:8443/realms/openshift/protocol/openid-connect/certs"; \
|
||||||
|
echo ""; \
|
||||||
|
echo "========================================"; \
|
||||||
|
else \
|
||||||
|
echo "Keycloak is not installed. Run: make keycloak-install"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
.PHONY: keycloak-logs
|
||||||
|
keycloak-logs: ## Tail Keycloak logs
|
||||||
|
@kubectl logs -n $(KEYCLOAK_NAMESPACE) -l app=keycloak -f --tail=100
|
||||||
|
|
||||||
|
.PHONY: keycloak-setup-realm
|
||||||
|
keycloak-setup-realm:
|
||||||
|
@echo "========================================="
|
||||||
|
@echo "Setting up OpenShift Realm for Token Exchange"
|
||||||
|
@echo "========================================="
|
||||||
|
@echo "Using Keycloak at https://keycloak.127-0-0-1.sslip.io:8443"
|
||||||
|
@echo ""
|
||||||
|
@echo "Getting admin access token..."
|
||||||
|
@RESPONSE=$$(curl -sk -X POST "https://keycloak.127-0-0-1.sslip.io:8443/realms/master/protocol/openid-connect/token" \
|
||||||
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||||
|
-d "username=$(KEYCLOAK_ADMIN_USER)" \
|
||||||
|
-d "password=$(KEYCLOAK_ADMIN_PASSWORD)" \
|
||||||
|
-d "grant_type=password" \
|
||||||
|
-d "client_id=admin-cli"); \
|
||||||
|
TOKEN=$$(echo "$$RESPONSE" | jq -r '.access_token // empty' 2>/dev/null); \
|
||||||
|
if [ -z "$$TOKEN" ] || [ "$$TOKEN" = "null" ]; then \
|
||||||
|
echo "❌ Failed to get access token"; \
|
||||||
|
echo "Response was: $$RESPONSE" | head -c 200; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Check if:"; \
|
||||||
|
echo " - Keycloak is running (make keycloak-install)"; \
|
||||||
|
echo " - Keycloak is accessible at https://keycloak.127-0-0-1.sslip.io:8443"; \
|
||||||
|
echo " - Admin credentials are correct: $(KEYCLOAK_ADMIN_USER)/$(KEYCLOAK_ADMIN_PASSWORD)"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo "✅ Successfully obtained access token"; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Creating OpenShift realm..."; \
|
||||||
|
REALM_RESPONSE=$$(curl -sk -w "%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"realm":"openshift","enabled":true}'); \
|
||||||
|
REALM_CODE=$$(echo "$$REALM_RESPONSE" | tail -c 4); \
|
||||||
|
if [ "$$REALM_CODE" = "201" ] || [ "$$REALM_CODE" = "409" ]; then \
|
||||||
|
if [ "$$REALM_CODE" = "201" ]; then echo "✅ OpenShift realm created"; \
|
||||||
|
else echo "✅ OpenShift realm already exists"; fi; \
|
||||||
|
else \
|
||||||
|
echo "❌ Failed to create OpenShift realm (HTTP $$REALM_CODE)"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Configuring realm events..."; \
|
||||||
|
EVENT_CONFIG_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X PUT "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"realm":"openshift","enabled":true,"eventsEnabled":true,"eventsListeners":["jboss-logging"],"adminEventsEnabled":true,"adminEventsDetailsEnabled":true}'); \
|
||||||
|
EVENT_CONFIG_CODE=$$(echo "$$EVENT_CONFIG_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
|
||||||
|
if [ "$$EVENT_CONFIG_CODE" = "204" ]; then \
|
||||||
|
echo "✅ User and admin event logging enabled"; \
|
||||||
|
else \
|
||||||
|
echo "⚠️ Could not configure event logging (HTTP $$EVENT_CONFIG_CODE)"; \
|
||||||
|
fi; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Creating mcp:openshift client scope..."; \
|
||||||
|
SCOPE_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/client-scopes" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"mcp:openshift","protocol":"openid-connect","attributes":{"display.on.consent.screen":"false","include.in.token.scope":"true"}}'); \
|
||||||
|
SCOPE_CODE=$$(echo "$$SCOPE_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
|
||||||
|
if [ "$$SCOPE_CODE" = "201" ] || [ "$$SCOPE_CODE" = "409" ]; then \
|
||||||
|
if [ "$$SCOPE_CODE" = "201" ]; then echo "✅ mcp:openshift client scope created"; \
|
||||||
|
else echo "✅ mcp:openshift client scope already exists"; fi; \
|
||||||
|
else \
|
||||||
|
echo "❌ Failed to create mcp:openshift scope (HTTP $$SCOPE_CODE)"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Adding audience mapper to mcp:openshift scope..."; \
|
||||||
|
SCOPES_LIST=$$(curl -sk -X GET "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/client-scopes" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Accept: application/json"); \
|
||||||
|
SCOPE_ID=$$(echo "$$SCOPES_LIST" | jq -r '.[] | select(.name == "mcp:openshift") | .id // empty' 2>/dev/null); \
|
||||||
|
if [ -z "$$SCOPE_ID" ]; then \
|
||||||
|
echo "❌ Failed to find mcp:openshift scope"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
MAPPER_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/client-scopes/$$SCOPE_ID/protocol-mappers/models" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"openshift-audience","protocol":"openid-connect","protocolMapper":"oidc-audience-mapper","config":{"included.client.audience":"openshift","id.token.claim":"true","access.token.claim":"true"}}'); \
|
||||||
|
MAPPER_CODE=$$(echo "$$MAPPER_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
|
||||||
|
if [ "$$MAPPER_CODE" = "201" ] || [ "$$MAPPER_CODE" = "409" ]; then \
|
||||||
|
if [ "$$MAPPER_CODE" = "201" ]; then echo "✅ Audience mapper added"; \
|
||||||
|
else echo "✅ Audience mapper already exists"; fi; \
|
||||||
|
else \
|
||||||
|
echo "❌ Failed to create audience mapper (HTTP $$MAPPER_CODE)"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Creating groups client scope..."; \
|
||||||
|
GROUPS_SCOPE_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/client-scopes" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"groups","protocol":"openid-connect","attributes":{"display.on.consent.screen":"false","include.in.token.scope":"true"}}'); \
|
||||||
|
GROUPS_SCOPE_CODE=$$(echo "$$GROUPS_SCOPE_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
|
||||||
|
if [ "$$GROUPS_SCOPE_CODE" = "201" ] || [ "$$GROUPS_SCOPE_CODE" = "409" ]; then \
|
||||||
|
if [ "$$GROUPS_SCOPE_CODE" = "201" ]; then echo "✅ groups client scope created"; \
|
||||||
|
else echo "✅ groups client scope already exists"; fi; \
|
||||||
|
else \
|
||||||
|
echo "❌ Failed to create groups scope (HTTP $$GROUPS_SCOPE_CODE)"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Adding group membership mapper to groups scope..."; \
|
||||||
|
SCOPES_LIST=$$(curl -sk -X GET "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/client-scopes" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Accept: application/json"); \
|
||||||
|
GROUPS_SCOPE_ID=$$(echo "$$SCOPES_LIST" | jq -r '.[] | select(.name == "groups") | .id // empty' 2>/dev/null); \
|
||||||
|
if [ -z "$$GROUPS_SCOPE_ID" ]; then \
|
||||||
|
echo "❌ Failed to find groups scope"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
GROUPS_MAPPER_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/client-scopes/$$GROUPS_SCOPE_ID/protocol-mappers/models" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"groups","protocol":"openid-connect","protocolMapper":"oidc-group-membership-mapper","config":{"claim.name":"groups","full.path":"false","id.token.claim":"true","access.token.claim":"true","userinfo.token.claim":"true"}}'); \
|
||||||
|
GROUPS_MAPPER_CODE=$$(echo "$$GROUPS_MAPPER_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
|
||||||
|
if [ "$$GROUPS_MAPPER_CODE" = "201" ] || [ "$$GROUPS_MAPPER_CODE" = "409" ]; then \
|
||||||
|
if [ "$$GROUPS_MAPPER_CODE" = "201" ]; then echo "✅ Group membership mapper added"; \
|
||||||
|
else echo "✅ Group membership mapper already exists"; fi; \
|
||||||
|
else \
|
||||||
|
echo "❌ Failed to create group mapper (HTTP $$GROUPS_MAPPER_CODE)"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Creating mcp-server client scope..."; \
|
||||||
|
MCP_SERVER_SCOPE_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/client-scopes" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"mcp-server","protocol":"openid-connect","attributes":{"display.on.consent.screen":"false","include.in.token.scope":"true"}}'); \
|
||||||
|
MCP_SERVER_SCOPE_CODE=$$(echo "$$MCP_SERVER_SCOPE_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
|
||||||
|
if [ "$$MCP_SERVER_SCOPE_CODE" = "201" ] || [ "$$MCP_SERVER_SCOPE_CODE" = "409" ]; then \
|
||||||
|
if [ "$$MCP_SERVER_SCOPE_CODE" = "201" ]; then echo "✅ mcp-server client scope created"; \
|
||||||
|
else echo "✅ mcp-server client scope already exists"; fi; \
|
||||||
|
else \
|
||||||
|
echo "❌ Failed to create mcp-server scope (HTTP $$MCP_SERVER_SCOPE_CODE)"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Adding audience mapper to mcp-server scope..."; \
|
||||||
|
SCOPES_LIST=$$(curl -sk -X GET "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/client-scopes" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Accept: application/json"); \
|
||||||
|
MCP_SERVER_SCOPE_ID=$$(echo "$$SCOPES_LIST" | jq -r '.[] | select(.name == "mcp-server") | .id // empty' 2>/dev/null); \
|
||||||
|
if [ -z "$$MCP_SERVER_SCOPE_ID" ]; then \
|
||||||
|
echo "❌ Failed to find mcp-server scope"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
MCP_SERVER_MAPPER_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/client-scopes/$$MCP_SERVER_SCOPE_ID/protocol-mappers/models" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"mcp-server-audience","protocol":"openid-connect","protocolMapper":"oidc-audience-mapper","config":{"included.client.audience":"mcp-server","id.token.claim":"true","access.token.claim":"true"}}'); \
|
||||||
|
MCP_SERVER_MAPPER_CODE=$$(echo "$$MCP_SERVER_MAPPER_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
|
||||||
|
if [ "$$MCP_SERVER_MAPPER_CODE" = "201" ] || [ "$$MCP_SERVER_MAPPER_CODE" = "409" ]; then \
|
||||||
|
if [ "$$MCP_SERVER_MAPPER_CODE" = "201" ]; then echo "✅ mcp-server audience mapper added"; \
|
||||||
|
else echo "✅ mcp-server audience mapper already exists"; fi; \
|
||||||
|
else \
|
||||||
|
echo "❌ Failed to create mcp-server audience mapper (HTTP $$MCP_SERVER_MAPPER_CODE)"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Creating openshift service client..."; \
|
||||||
|
OPENSHIFT_CLIENT_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/clients" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"clientId":"openshift","enabled":true,"publicClient":false,"standardFlowEnabled":true,"directAccessGrantsEnabled":true,"serviceAccountsEnabled":true,"authorizationServicesEnabled":false,"redirectUris":["*"],"defaultClientScopes":["profile","email","groups"],"optionalClientScopes":[]}'); \
|
||||||
|
OPENSHIFT_CLIENT_CODE=$$(echo "$$OPENSHIFT_CLIENT_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
|
||||||
|
if [ "$$OPENSHIFT_CLIENT_CODE" = "201" ] || [ "$$OPENSHIFT_CLIENT_CODE" = "409" ]; then \
|
||||||
|
if [ "$$OPENSHIFT_CLIENT_CODE" = "201" ]; then echo "✅ openshift client created"; \
|
||||||
|
else echo "✅ openshift client already exists"; fi; \
|
||||||
|
else \
|
||||||
|
echo "❌ Failed to create openshift client (HTTP $$OPENSHIFT_CLIENT_CODE)"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Adding username mapper to openshift client..."; \
|
||||||
|
OPENSHIFT_CLIENTS_LIST=$$(curl -sk -X GET "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/clients" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Accept: application/json"); \
|
||||||
|
OPENSHIFT_CLIENT_ID=$$(echo "$$OPENSHIFT_CLIENTS_LIST" | jq -r '.[] | select(.clientId == "openshift") | .id // empty' 2>/dev/null); \
|
||||||
|
OPENSHIFT_USERNAME_MAPPER_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/clients/$$OPENSHIFT_CLIENT_ID/protocol-mappers/models" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{ "name":"username","protocol":"openid-connect","protocolMapper":"oidc-usermodel-property-mapper","config":{"userinfo.token.claim":"true","user.attribute":"username","id.token.claim":"true","access.token.claim":"true","claim.name":"preferred_username","jsonType.label":"String"}}'); \
|
||||||
|
OPENSHIFT_USERNAME_MAPPER_CODE=$$(echo "$$OPENSHIFT_USERNAME_MAPPER_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
|
||||||
|
if [ "$$OPENSHIFT_USERNAME_MAPPER_CODE" = "201" ] || [ "$$OPENSHIFT_USERNAME_MAPPER_CODE" = "409" ]; then \
|
||||||
|
if [ "$$OPENSHIFT_USERNAME_MAPPER_CODE" = "201" ]; then echo "✅ Username mapper added to openshift client"; \
|
||||||
|
else echo "✅ Username mapper already exists on openshift client"; fi; \
|
||||||
|
else \
|
||||||
|
echo "❌ Failed to create username mapper (HTTP $$OPENSHIFT_USERNAME_MAPPER_CODE)"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Creating mcp-client public client..."; \
|
||||||
|
MCP_PUBLIC_CLIENT_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/clients" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"clientId":"mcp-client","enabled":true,"publicClient":true,"standardFlowEnabled":true,"directAccessGrantsEnabled":true,"serviceAccountsEnabled":false,"authorizationServicesEnabled":false,"redirectUris":["*"],"defaultClientScopes":["profile","email"],"optionalClientScopes":["mcp-server"]}'); \
|
||||||
|
MCP_PUBLIC_CLIENT_CODE=$$(echo "$$MCP_PUBLIC_CLIENT_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
|
||||||
|
if [ "$$MCP_PUBLIC_CLIENT_CODE" = "201" ] || [ "$$MCP_PUBLIC_CLIENT_CODE" = "409" ]; then \
|
||||||
|
if [ "$$MCP_PUBLIC_CLIENT_CODE" = "201" ]; then echo "✅ mcp-client public client created"; \
|
||||||
|
else echo "✅ mcp-client public client already exists"; fi; \
|
||||||
|
else \
|
||||||
|
echo "❌ Failed to create mcp-client public client (HTTP $$MCP_PUBLIC_CLIENT_CODE)"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Adding username mapper to mcp-client..."; \
|
||||||
|
MCP_PUBLIC_CLIENTS_LIST=$$(curl -sk -X GET "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/clients" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Accept: application/json"); \
|
||||||
|
MCP_PUBLIC_CLIENT_ID=$$(echo "$$MCP_PUBLIC_CLIENTS_LIST" | jq -r '.[] | select(.clientId == "mcp-client") | .id // empty' 2>/dev/null); \
|
||||||
|
MCP_PUBLIC_USERNAME_MAPPER_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/clients/$$MCP_PUBLIC_CLIENT_ID/protocol-mappers/models" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"username","protocol":"openid-connect","protocolMapper":"oidc-usermodel-property-mapper","config":{"userinfo.token.claim":"true","user.attribute":"username","id.token.claim":"true","access.token.claim":"true","claim.name":"preferred_username","jsonType.label":"String"}}'); \
|
||||||
|
MCP_PUBLIC_USERNAME_MAPPER_CODE=$$(echo "$$MCP_PUBLIC_USERNAME_MAPPER_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
|
||||||
|
if [ "$$MCP_PUBLIC_USERNAME_MAPPER_CODE" = "201" ] || [ "$$MCP_PUBLIC_USERNAME_MAPPER_CODE" = "409" ]; then \
|
||||||
|
if [ "$$MCP_PUBLIC_USERNAME_MAPPER_CODE" = "201" ]; then echo "✅ Username mapper added to mcp-client"; \
|
||||||
|
else echo "✅ Username mapper already exists on mcp-client"; fi; \
|
||||||
|
else \
|
||||||
|
echo "❌ Failed to create username mapper (HTTP $$MCP_PUBLIC_USERNAME_MAPPER_CODE)"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Creating mcp-server client with token exchange..."; \
|
||||||
|
MCP_CLIENT_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/clients" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"clientId":"mcp-server","enabled":true,"publicClient":false,"standardFlowEnabled":true,"directAccessGrantsEnabled":true,"serviceAccountsEnabled":true,"authorizationServicesEnabled":false,"redirectUris":["*"],"defaultClientScopes":["profile","email","groups","mcp-server"],"optionalClientScopes":["mcp:openshift"],"attributes":{"oauth2.device.authorization.grant.enabled":"false","oidc.ciba.grant.enabled":"false","backchannel.logout.session.required":"true","backchannel.logout.revoke.offline.tokens":"false"}}'); \
|
||||||
|
MCP_CLIENT_CODE=$$(echo "$$MCP_CLIENT_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
|
||||||
|
if [ "$$MCP_CLIENT_CODE" = "201" ] || [ "$$MCP_CLIENT_CODE" = "409" ]; then \
|
||||||
|
if [ "$$MCP_CLIENT_CODE" = "201" ]; then echo "✅ mcp-server client created"; \
|
||||||
|
else echo "✅ mcp-server client already exists"; fi; \
|
||||||
|
else \
|
||||||
|
echo "❌ Failed to create mcp-server client (HTTP $$MCP_CLIENT_CODE)"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Enabling standard token exchange for mcp-server..."; \
|
||||||
|
CLIENTS_LIST=$$(curl -sk -X GET "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/clients" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Accept: application/json"); \
|
||||||
|
MCP_CLIENT_ID=$$(echo "$$CLIENTS_LIST" | jq -r '.[] | select(.clientId == "mcp-server") | .id // empty' 2>/dev/null); \
|
||||||
|
if [ -z "$$MCP_CLIENT_ID" ]; then \
|
||||||
|
echo "❌ Failed to find mcp-server client"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
UPDATE_CLIENT_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X PUT "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/clients/$$MCP_CLIENT_ID" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"clientId":"mcp-server","enabled":true,"publicClient":false,"standardFlowEnabled":true,"directAccessGrantsEnabled":true,"serviceAccountsEnabled":true,"authorizationServicesEnabled":false,"redirectUris":["*"],"defaultClientScopes":["profile","email","groups","mcp-server"],"optionalClientScopes":["mcp:openshift"],"attributes":{"oauth2.device.authorization.grant.enabled":"false","oidc.ciba.grant.enabled":"false","backchannel.logout.session.required":"true","backchannel.logout.revoke.offline.tokens":"false","standard.token.exchange.enabled":"true"}}'); \
|
||||||
|
UPDATE_CLIENT_CODE=$$(echo "$$UPDATE_CLIENT_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
|
||||||
|
if [ "$$UPDATE_CLIENT_CODE" = "204" ]; then \
|
||||||
|
echo "✅ Standard token exchange enabled for mcp-server client"; \
|
||||||
|
else \
|
||||||
|
echo "⚠️ Could not enable token exchange (HTTP $$UPDATE_CLIENT_CODE)"; \
|
||||||
|
fi; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Getting mcp-server client secret..."; \
|
||||||
|
SECRET_RESPONSE=$$(curl -sk -X GET "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/clients/$$MCP_CLIENT_ID/client-secret" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Accept: application/json"); \
|
||||||
|
CLIENT_SECRET=$$(echo "$$SECRET_RESPONSE" | jq -r '.value // empty' 2>/dev/null); \
|
||||||
|
if [ -z "$$CLIENT_SECRET" ]; then \
|
||||||
|
echo "❌ Failed to get client secret"; \
|
||||||
|
else \
|
||||||
|
echo "✅ Client secret retrieved"; \
|
||||||
|
fi; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Adding username mapper to mcp-server client..."; \
|
||||||
|
MCP_USERNAME_MAPPER_RESPONSE=$$(curl -sk -w "HTTPCODE:%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/clients/$$MCP_CLIENT_ID/protocol-mappers/models" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"username","protocol":"openid-connect","protocolMapper":"oidc-usermodel-property-mapper","config":{"userinfo.token.claim":"true","user.attribute":"username","id.token.claim":"true","access.token.claim":"true","claim.name":"preferred_username","jsonType.label":"String"}}'); \
|
||||||
|
MCP_USERNAME_MAPPER_CODE=$$(echo "$$MCP_USERNAME_MAPPER_RESPONSE" | grep -o "HTTPCODE:[0-9]*" | cut -d: -f2); \
|
||||||
|
if [ "$$MCP_USERNAME_MAPPER_CODE" = "201" ] || [ "$$MCP_USERNAME_MAPPER_CODE" = "409" ]; then \
|
||||||
|
if [ "$$MCP_USERNAME_MAPPER_CODE" = "201" ]; then echo "✅ Username mapper added to mcp-server client"; \
|
||||||
|
else echo "✅ Username mapper already exists on mcp-server client"; fi; \
|
||||||
|
else \
|
||||||
|
echo "❌ Failed to create username mapper (HTTP $$MCP_USERNAME_MAPPER_CODE)"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Creating test user mcp/mcp..."; \
|
||||||
|
USER_RESPONSE=$$(curl -sk -w "%{http_code}" -X POST "https://keycloak.127-0-0-1.sslip.io:8443/admin/realms/openshift/users" \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"mcp","email":"mcp@example.com","firstName":"MCP","lastName":"User","enabled":true,"emailVerified":true,"credentials":[{"type":"password","value":"mcp","temporary":false}]}'); \
|
||||||
|
USER_CODE=$$(echo "$$USER_RESPONSE" | tail -c 4); \
|
||||||
|
if [ "$$USER_CODE" = "201" ] || [ "$$USER_CODE" = "409" ]; then \
|
||||||
|
if [ "$$USER_CODE" = "201" ]; then echo "✅ mcp user created"; \
|
||||||
|
else echo "✅ mcp user already exists"; fi; \
|
||||||
|
else \
|
||||||
|
echo "❌ Failed to create mcp user (HTTP $$USER_CODE)"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Setting up RBAC for mcp user..."; \
|
||||||
|
kubectl apply -f dev/config/keycloak/rbac.yaml; \
|
||||||
|
echo "✅ RBAC binding created for mcp user"; \
|
||||||
|
echo ""; \
|
||||||
|
echo "🎉 OpenShift realm setup complete!"; \
|
||||||
|
echo ""; \
|
||||||
|
echo "========================================"; \
|
||||||
|
echo "Configuration Summary"; \
|
||||||
|
echo "========================================"; \
|
||||||
|
echo "Realm: openshift"; \
|
||||||
|
echo "Authorization URL: https://keycloak.127-0-0-1.sslip.io:8443/realms/openshift"; \
|
||||||
|
echo "Issuer URL (for config.toml): https://keycloak.127-0-0-1.sslip.io:8443/realms/openshift"; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Test User:"; \
|
||||||
|
echo " Username: mcp"; \
|
||||||
|
echo " Password: mcp"; \
|
||||||
|
echo " Email: mcp@example.com"; \
|
||||||
|
echo " RBAC: cluster-admin (full cluster access)"; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Clients:"; \
|
||||||
|
echo " mcp-client (public, for browser-based auth)"; \
|
||||||
|
echo " Client ID: mcp-client"; \
|
||||||
|
echo " Optional Scopes: mcp-server"; \
|
||||||
|
echo " mcp-server (confidential, token exchange enabled)"; \
|
||||||
|
echo " Client ID: mcp-server"; \
|
||||||
|
echo " Client Secret: $$CLIENT_SECRET"; \
|
||||||
|
echo " openshift (service account)"; \
|
||||||
|
echo " Client ID: openshift"; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Client Scopes:"; \
|
||||||
|
echo " mcp-server (default) - Audience: mcp-server"; \
|
||||||
|
echo " mcp:openshift (optional) - Audience: openshift"; \
|
||||||
|
echo " groups (default) - Group membership mapper"; \
|
||||||
|
echo ""; \
|
||||||
|
echo "TOML Configuration (config.toml):"; \
|
||||||
|
echo " require_oauth = true"; \
|
||||||
|
echo " oauth_audience = \"mcp-server\""; \
|
||||||
|
echo " oauth_scopes = [\"openid\", \"mcp-server\"]"; \
|
||||||
|
echo " validate_token = false"; \
|
||||||
|
echo " authorization_url = \"https://keycloak.127-0-0-1.sslip.io:8443/realms/openshift\""; \
|
||||||
|
echo " sts_client_id = \"mcp-server\""; \
|
||||||
|
echo " sts_client_secret = \"$$CLIENT_SECRET\""; \
|
||||||
|
echo " sts_audience = \"openshift\""; \
|
||||||
|
echo " sts_scopes = [\"mcp:openshift\"]"; \
|
||||||
|
echo " certificate_authority = \"_output/cert-manager-ca/ca.crt\""; \
|
||||||
|
echo "========================================"; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Note: The Kubernetes API server is configured with:"; \
|
||||||
|
echo " --oidc-issuer-url=https://keycloak.127-0-0-1.sslip.io:8443/realms/openshift"; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Important: The cert-manager CA certificate was extracted to:"; \
|
||||||
|
echo " _output/cert-manager-ca/ca.crt"; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Writing configuration to _output/config.toml..."; \
|
||||||
|
mkdir -p _output; \
|
||||||
|
printf '%s\n' \
|
||||||
|
'require_oauth = true' \
|
||||||
|
'oauth_audience = "mcp-server"' \
|
||||||
|
'oauth_scopes = ["openid", "mcp-server"]' \
|
||||||
|
'validate_token = false' \
|
||||||
|
'authorization_url = "https://keycloak.127-0-0-1.sslip.io:8443/realms/openshift"' \
|
||||||
|
'sts_client_id = "mcp-server"' \
|
||||||
|
"sts_client_secret = \"$$CLIENT_SECRET\"" \
|
||||||
|
'sts_audience = "openshift"' \
|
||||||
|
'sts_scopes = ["mcp:openshift"]' \
|
||||||
|
'certificate_authority = "_output/cert-manager-ca/ca.crt"' \
|
||||||
|
> _output/config.toml; \
|
||||||
|
echo "✅ Configuration written to _output/config.toml"
|
||||||
61
build/kind.mk
Normal file
61
build/kind.mk
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Kind cluster management
|
||||||
|
|
||||||
|
KIND_CLUSTER_NAME ?= kubernetes-mcp-server
|
||||||
|
|
||||||
|
# Detect container engine (docker or podman)
|
||||||
|
CONTAINER_ENGINE ?= $(shell command -v docker 2>/dev/null || command -v podman 2>/dev/null)
|
||||||
|
|
||||||
|
.PHONY: kind-create-certs
|
||||||
|
kind-create-certs:
|
||||||
|
@if [ ! -f _output/cert-manager-ca/ca.crt ]; then \
|
||||||
|
echo "Creating placeholder CA certificate for bind mount..."; \
|
||||||
|
./hack/generate-placeholder-ca.sh; \
|
||||||
|
else \
|
||||||
|
echo "✅ Placeholder CA already exists"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
.PHONY: kind-create-cluster
|
||||||
|
kind-create-cluster: kind kind-create-certs
|
||||||
|
@# Set KIND provider for podman on Linux
|
||||||
|
@if [ "$(shell uname -s)" != "Darwin" ] && echo "$(CONTAINER_ENGINE)" | grep -q "podman"; then \
|
||||||
|
export KIND_EXPERIMENTAL_PROVIDER=podman; \
|
||||||
|
fi; \
|
||||||
|
if $(KIND) get clusters 2>/dev/null | grep -q "^$(KIND_CLUSTER_NAME)$$"; then \
|
||||||
|
echo "Kind cluster '$(KIND_CLUSTER_NAME)' already exists, skipping creation"; \
|
||||||
|
else \
|
||||||
|
echo "Creating Kind cluster '$(KIND_CLUSTER_NAME)'..."; \
|
||||||
|
$(KIND) create cluster --name $(KIND_CLUSTER_NAME) --config dev/config/kind/cluster.yaml; \
|
||||||
|
echo "Adding ingress-ready label to control-plane node..."; \
|
||||||
|
kubectl label node $(KIND_CLUSTER_NAME)-control-plane ingress-ready=true --overwrite; \
|
||||||
|
echo "Installing nginx ingress controller..."; \
|
||||||
|
kubectl apply -f dev/config/ingress/nginx-ingress.yaml; \
|
||||||
|
echo "Waiting for ingress controller to be ready..."; \
|
||||||
|
kubectl wait --namespace ingress-nginx --for=condition=ready pod --selector=app.kubernetes.io/component=controller --timeout=90s; \
|
||||||
|
echo "✅ Ingress controller ready"; \
|
||||||
|
echo "Installing cert-manager..."; \
|
||||||
|
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.2/cert-manager.yaml; \
|
||||||
|
echo "Waiting for cert-manager to be ready..."; \
|
||||||
|
kubectl wait --namespace cert-manager --for=condition=available deployment/cert-manager --timeout=120s; \
|
||||||
|
kubectl wait --namespace cert-manager --for=condition=available deployment/cert-manager-cainjector --timeout=120s; \
|
||||||
|
kubectl wait --namespace cert-manager --for=condition=available deployment/cert-manager-webhook --timeout=120s; \
|
||||||
|
echo "✅ cert-manager ready"; \
|
||||||
|
echo "Creating cert-manager ClusterIssuer..."; \
|
||||||
|
sleep 5; \
|
||||||
|
kubectl apply -f dev/config/cert-manager/selfsigned-issuer.yaml; \
|
||||||
|
echo "✅ ClusterIssuer created"; \
|
||||||
|
echo "Adding /etc/hosts entry for Keycloak in control plane..."; \
|
||||||
|
if command -v docker >/dev/null 2>&1 && docker ps --filter "name=$(KIND_CLUSTER_NAME)-control-plane" --format "{{.Names}}" | grep -q "$(KIND_CLUSTER_NAME)-control-plane"; then \
|
||||||
|
docker exec $(KIND_CLUSTER_NAME)-control-plane bash -c 'grep -q "keycloak.127-0-0-1.sslip.io" /etc/hosts || echo "127.0.0.1 keycloak.127-0-0-1.sslip.io" >> /etc/hosts'; \
|
||||||
|
elif command -v podman >/dev/null 2>&1 && podman ps --filter "name=$(KIND_CLUSTER_NAME)-control-plane" --format "{{.Names}}" | grep -q "$(KIND_CLUSTER_NAME)-control-plane"; then \
|
||||||
|
podman exec $(KIND_CLUSTER_NAME)-control-plane bash -c 'grep -q "keycloak.127-0-0-1.sslip.io" /etc/hosts || echo "127.0.0.1 keycloak.127-0-0-1.sslip.io" >> /etc/hosts'; \
|
||||||
|
fi; \
|
||||||
|
echo "✅ /etc/hosts entry added"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
.PHONY: kind-delete-cluster
|
||||||
|
kind-delete-cluster: kind
|
||||||
|
@# Set KIND provider for podman on Linux
|
||||||
|
@if [ "$(shell uname -s)" != "Darwin" ] && echo "$(CONTAINER_ENGINE)" | grep -q "podman"; then \
|
||||||
|
export KIND_EXPERIMENTAL_PROVIDER=podman; \
|
||||||
|
fi; \
|
||||||
|
$(KIND) delete cluster --name $(KIND_CLUSTER_NAME)
|
||||||
20
build/tools.mk
Normal file
20
build/tools.mk
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Tools
|
||||||
|
|
||||||
|
# Platform detection
|
||||||
|
OS := $(shell uname -s | tr '[:upper:]' '[:lower:]')
|
||||||
|
ARCH := $(shell uname -m | tr '[:upper:]' '[:lower:]')
|
||||||
|
ifeq ($(ARCH),x86_64)
|
||||||
|
ARCH = amd64
|
||||||
|
endif
|
||||||
|
ifeq ($(ARCH),aarch64)
|
||||||
|
ARCH = arm64
|
||||||
|
endif
|
||||||
|
|
||||||
|
KIND = _output/bin/kind
|
||||||
|
KIND_VERSION = v0.30.0
|
||||||
|
$(KIND):
|
||||||
|
@mkdir -p _output/bin
|
||||||
|
GOBIN=$(PWD)/_output/bin go install sigs.k8s.io/kind@$(KIND_VERSION)
|
||||||
|
|
||||||
|
.PHONY: kind
|
||||||
|
kind: $(KIND) ## Download kind locally if necessary
|
||||||
31
dev/config/cert-manager/selfsigned-issuer.yaml
Normal file
31
dev/config/cert-manager/selfsigned-issuer.yaml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: ClusterIssuer
|
||||||
|
metadata:
|
||||||
|
name: selfsigned-issuer
|
||||||
|
spec:
|
||||||
|
selfSigned: {}
|
||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: selfsigned-ca
|
||||||
|
namespace: cert-manager
|
||||||
|
spec:
|
||||||
|
isCA: true
|
||||||
|
commonName: selfsigned-ca
|
||||||
|
secretName: selfsigned-ca-secret
|
||||||
|
privateKey:
|
||||||
|
algorithm: ECDSA
|
||||||
|
size: 256
|
||||||
|
issuerRef:
|
||||||
|
name: selfsigned-issuer
|
||||||
|
kind: ClusterIssuer
|
||||||
|
group: cert-manager.io
|
||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: ClusterIssuer
|
||||||
|
metadata:
|
||||||
|
name: selfsigned-ca-issuer
|
||||||
|
spec:
|
||||||
|
ca:
|
||||||
|
secretName: selfsigned-ca-secret
|
||||||
386
dev/config/ingress/nginx-ingress.yaml
Normal file
386
dev/config/ingress/nginx-ingress.yaml
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: ingress-nginx
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/instance: ingress-nginx
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/instance: ingress-nginx
|
||||||
|
app.kubernetes.io/component: controller
|
||||||
|
name: ingress-nginx
|
||||||
|
namespace: ingress-nginx
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/instance: ingress-nginx
|
||||||
|
app.kubernetes.io/component: controller
|
||||||
|
name: ingress-nginx-controller
|
||||||
|
namespace: ingress-nginx
|
||||||
|
data:
|
||||||
|
allow-snippet-annotations: "true"
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/instance: ingress-nginx
|
||||||
|
name: ingress-nginx
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- configmaps
|
||||||
|
- endpoints
|
||||||
|
- nodes
|
||||||
|
- pods
|
||||||
|
- secrets
|
||||||
|
- namespaces
|
||||||
|
verbs:
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- coordination.k8s.io
|
||||||
|
resources:
|
||||||
|
- leases
|
||||||
|
verbs:
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- nodes
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- services
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- networking.k8s.io
|
||||||
|
resources:
|
||||||
|
- ingresses
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- events
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- patch
|
||||||
|
- apiGroups:
|
||||||
|
- networking.k8s.io
|
||||||
|
resources:
|
||||||
|
- ingresses/status
|
||||||
|
verbs:
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- networking.k8s.io
|
||||||
|
resources:
|
||||||
|
- ingressclasses
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- discovery.k8s.io
|
||||||
|
resources:
|
||||||
|
- endpointslices
|
||||||
|
verbs:
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- get
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/instance: ingress-nginx
|
||||||
|
name: ingress-nginx
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: ingress-nginx
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: ingress-nginx
|
||||||
|
namespace: ingress-nginx
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/instance: ingress-nginx
|
||||||
|
app.kubernetes.io/component: controller
|
||||||
|
name: ingress-nginx
|
||||||
|
namespace: ingress-nginx
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- namespaces
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- configmaps
|
||||||
|
- pods
|
||||||
|
- secrets
|
||||||
|
- endpoints
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- services
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- networking.k8s.io
|
||||||
|
resources:
|
||||||
|
- ingresses
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- networking.k8s.io
|
||||||
|
resources:
|
||||||
|
- ingresses/status
|
||||||
|
verbs:
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- networking.k8s.io
|
||||||
|
resources:
|
||||||
|
- ingressclasses
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- coordination.k8s.io
|
||||||
|
resources:
|
||||||
|
- leases
|
||||||
|
resourceNames:
|
||||||
|
- ingress-nginx-leader
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- coordination.k8s.io
|
||||||
|
resources:
|
||||||
|
- leases
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- events
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- patch
|
||||||
|
- apiGroups:
|
||||||
|
- discovery.k8s.io
|
||||||
|
resources:
|
||||||
|
- endpointslices
|
||||||
|
verbs:
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- get
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/instance: ingress-nginx
|
||||||
|
app.kubernetes.io/component: controller
|
||||||
|
name: ingress-nginx
|
||||||
|
namespace: ingress-nginx
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: ingress-nginx
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: ingress-nginx
|
||||||
|
namespace: ingress-nginx
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/instance: ingress-nginx
|
||||||
|
app.kubernetes.io/component: controller
|
||||||
|
name: ingress-nginx-controller
|
||||||
|
namespace: ingress-nginx
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: http
|
||||||
|
appProtocol: http
|
||||||
|
- name: https
|
||||||
|
port: 443
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: https
|
||||||
|
appProtocol: https
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/instance: ingress-nginx
|
||||||
|
app.kubernetes.io/component: controller
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/instance: ingress-nginx
|
||||||
|
app.kubernetes.io/component: controller
|
||||||
|
name: ingress-nginx-controller
|
||||||
|
namespace: ingress-nginx
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/instance: ingress-nginx
|
||||||
|
app.kubernetes.io/component: controller
|
||||||
|
replicas: 1
|
||||||
|
revisionHistoryLimit: 10
|
||||||
|
minReadySeconds: 0
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/instance: ingress-nginx
|
||||||
|
app.kubernetes.io/component: controller
|
||||||
|
spec:
|
||||||
|
dnsPolicy: ClusterFirst
|
||||||
|
containers:
|
||||||
|
- name: controller
|
||||||
|
image: registry.k8s.io/ingress-nginx/controller:v1.11.1
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
lifecycle:
|
||||||
|
preStop:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- /wait-shutdown
|
||||||
|
args:
|
||||||
|
- /nginx-ingress-controller
|
||||||
|
- --election-id=ingress-nginx-leader
|
||||||
|
- --controller-class=k8s.io/ingress-nginx
|
||||||
|
- --ingress-class=nginx
|
||||||
|
- --configmap=$(POD_NAMESPACE)/ingress-nginx-controller
|
||||||
|
- --watch-ingress-without-class=true
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 101
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
add:
|
||||||
|
- NET_BIND_SERVICE
|
||||||
|
env:
|
||||||
|
- name: POD_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.name
|
||||||
|
- name: POD_NAMESPACE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.namespace
|
||||||
|
- name: LD_PRELOAD
|
||||||
|
value: /usr/local/lib/libmimalloc.so
|
||||||
|
livenessProbe:
|
||||||
|
failureThreshold: 5
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 10254
|
||||||
|
scheme: HTTP
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
successThreshold: 1
|
||||||
|
timeoutSeconds: 1
|
||||||
|
readinessProbe:
|
||||||
|
failureThreshold: 3
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 10254
|
||||||
|
scheme: HTTP
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
successThreshold: 1
|
||||||
|
timeoutSeconds: 1
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 80
|
||||||
|
protocol: TCP
|
||||||
|
hostPort: 80
|
||||||
|
- name: https
|
||||||
|
containerPort: 443
|
||||||
|
protocol: TCP
|
||||||
|
hostPort: 443
|
||||||
|
- name: https-alt
|
||||||
|
containerPort: 443
|
||||||
|
protocol: TCP
|
||||||
|
hostPort: 8443
|
||||||
|
- name: webhook
|
||||||
|
containerPort: 8443
|
||||||
|
protocol: TCP
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 90Mi
|
||||||
|
nodeSelector:
|
||||||
|
ingress-ready: "true"
|
||||||
|
kubernetes.io/os: linux
|
||||||
|
serviceAccountName: ingress-nginx
|
||||||
|
terminationGracePeriodSeconds: 0
|
||||||
|
tolerations:
|
||||||
|
- effect: NoSchedule
|
||||||
|
key: node-role.kubernetes.io/master
|
||||||
|
operator: Equal
|
||||||
|
- effect: NoSchedule
|
||||||
|
key: node-role.kubernetes.io/control-plane
|
||||||
|
operator: Equal
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: IngressClass
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/instance: ingress-nginx
|
||||||
|
app.kubernetes.io/component: controller
|
||||||
|
name: nginx
|
||||||
|
spec:
|
||||||
|
controller: k8s.io/ingress-nginx
|
||||||
71
dev/config/keycloak/deployment.yaml
Normal file
71
dev/config/keycloak/deployment.yaml
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: keycloak
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: keycloak
|
||||||
|
namespace: keycloak
|
||||||
|
labels:
|
||||||
|
app: keycloak
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: keycloak
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: keycloak
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: keycloak
|
||||||
|
image: quay.io/keycloak/keycloak:26.4
|
||||||
|
args: ["start-dev"]
|
||||||
|
env:
|
||||||
|
- name: KC_BOOTSTRAP_ADMIN_USERNAME
|
||||||
|
value: "admin"
|
||||||
|
- name: KC_BOOTSTRAP_ADMIN_PASSWORD
|
||||||
|
value: "admin"
|
||||||
|
- name: KC_HOSTNAME
|
||||||
|
value: "https://keycloak.127-0-0-1.sslip.io:8443"
|
||||||
|
- name: KC_HTTP_ENABLED
|
||||||
|
value: "true"
|
||||||
|
- name: KC_HEALTH_ENABLED
|
||||||
|
value: "true"
|
||||||
|
- name: KC_PROXY_HEADERS
|
||||||
|
value: "xforwarded"
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 8080
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health/ready
|
||||||
|
port: 9000
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health/live
|
||||||
|
port: 9000
|
||||||
|
initialDelaySeconds: 60
|
||||||
|
periodSeconds: 30
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: keycloak
|
||||||
|
namespace: keycloak
|
||||||
|
labels:
|
||||||
|
app: keycloak
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: 8080
|
||||||
|
selector:
|
||||||
|
app: keycloak
|
||||||
|
type: ClusterIP
|
||||||
34
dev/config/keycloak/ingress.yaml
Normal file
34
dev/config/keycloak/ingress.yaml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: keycloak
|
||||||
|
namespace: keycloak
|
||||||
|
labels:
|
||||||
|
app: keycloak
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: "selfsigned-ca-issuer"
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||||
|
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
|
||||||
|
# Required for Keycloak 26.2.0+ to include port in issuer URLs
|
||||||
|
nginx.ingress.kubernetes.io/configuration-snippet: |
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
proxy_set_header X-Forwarded-Port 8443;
|
||||||
|
proxy_set_header X-Forwarded-Host $host:8443;
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- keycloak.127-0-0-1.sslip.io
|
||||||
|
secretName: keycloak-tls-cert
|
||||||
|
rules:
|
||||||
|
- host: keycloak.127-0-0-1.sslip.io
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: keycloak
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
20
dev/config/keycloak/rbac.yaml
Normal file
20
dev/config/keycloak/rbac.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# RBAC ClusterRoleBinding for mcp user with OIDC authentication
|
||||||
|
#
|
||||||
|
# IMPORTANT: This requires Kubernetes API server to be configured with OIDC:
|
||||||
|
# --oidc-issuer-url=https://keycloak.127-0-0-1.sslip.io:8443/realms/openshift
|
||||||
|
# --oidc-username-claim=preferred_username
|
||||||
|
#
|
||||||
|
# Without OIDC configuration, this binding will not work.
|
||||||
|
#
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: oidc-mcp-cluster-admin
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: cluster-admin
|
||||||
|
subjects:
|
||||||
|
- apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: User
|
||||||
|
name: https://keycloak.127-0-0-1.sslip.io:8443/realms/openshift#mcp
|
||||||
30
dev/config/kind/cluster.yaml
Normal file
30
dev/config/kind/cluster.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
kind: Cluster
|
||||||
|
apiVersion: kind.x-k8s.io/v1alpha4
|
||||||
|
nodes:
|
||||||
|
- role: control-plane
|
||||||
|
extraMounts:
|
||||||
|
- hostPath: ./_output/cert-manager-ca/ca.crt
|
||||||
|
containerPath: /etc/kubernetes/pki/keycloak-ca.crt
|
||||||
|
readOnly: true
|
||||||
|
kubeadmConfigPatches:
|
||||||
|
- |
|
||||||
|
kind: InitConfiguration
|
||||||
|
nodeRegistration:
|
||||||
|
kubeletExtraArgs:
|
||||||
|
node-labels: "ingress-ready=true"
|
||||||
|
|
||||||
|
kind: ClusterConfiguration
|
||||||
|
apiServer:
|
||||||
|
extraArgs:
|
||||||
|
oidc-issuer-url: https://keycloak.127-0-0-1.sslip.io:8443/realms/openshift
|
||||||
|
oidc-client-id: openshift
|
||||||
|
oidc-username-claim: preferred_username
|
||||||
|
oidc-groups-claim: groups
|
||||||
|
oidc-ca-file: /etc/kubernetes/pki/keycloak-ca.crt
|
||||||
|
extraPortMappings:
|
||||||
|
- containerPort: 80
|
||||||
|
hostPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
- containerPort: 443
|
||||||
|
hostPort: 8443
|
||||||
|
protocol: TCP
|
||||||
22
hack/generate-placeholder-ca.sh
Executable file
22
hack/generate-placeholder-ca.sh
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Generate a placeholder self-signed CA certificate for KIND cluster startup
|
||||||
|
# This will be replaced with the real cert-manager CA after the cluster is created
|
||||||
|
|
||||||
|
CERT_DIR="_output/cert-manager-ca"
|
||||||
|
CA_CERT="$CERT_DIR/ca.crt"
|
||||||
|
CA_KEY="$CERT_DIR/ca.key"
|
||||||
|
|
||||||
|
mkdir -p "$CERT_DIR"
|
||||||
|
|
||||||
|
# Generate a self-signed CA certificate (valid placeholder)
|
||||||
|
openssl req -x509 -newkey rsa:2048 -nodes \
|
||||||
|
-keyout "$CA_KEY" \
|
||||||
|
-out "$CA_CERT" \
|
||||||
|
-days 365 \
|
||||||
|
-subj "/CN=placeholder-ca" \
|
||||||
|
2>/dev/null
|
||||||
|
|
||||||
|
echo "✅ Placeholder CA certificate created at $CA_CERT"
|
||||||
|
echo "⚠️ This will be replaced with cert-manager CA after cluster creation"
|
||||||
Reference in New Issue
Block a user