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
|
||||
update-readme-tools: ## Update the README.md file with the latest toolsets
|
||||
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