From 3606b5dd90a956d20f4aabc880dfb46e9ee26cf2 Mon Sep 17 00:00:00 2001 From: "A. F. Dudley" Date: Tue, 20 Jan 2026 02:39:01 -0500 Subject: [PATCH] Add Caddy ingress controller support for kind deployments Replace nginx with Caddy as the default ingress controller for kind deployments. Caddy provides automatic HTTPS via Let's Encrypt without requiring cert-manager. Changes: - Add ingress-caddy-kind-deploy.yaml manifest with full RBAC setup - Modify helpers.py to support configurable ingress_type parameter - Update cluster_info.py to use caddy ingress class - Add port 443 mapping for HTTPS support in kind config Co-Authored-By: Claude Opus 4.5 --- .../ingress/ingress-caddy-kind-deploy.yaml | 250 ++++++++++++++++++ stack_orchestrator/deploy/k8s/cluster_info.py | 8 +- stack_orchestrator/deploy/k8s/helpers.py | 51 +++- 3 files changed, 293 insertions(+), 16 deletions(-) create mode 100644 stack_orchestrator/data/k8s/components/ingress/ingress-caddy-kind-deploy.yaml diff --git a/stack_orchestrator/data/k8s/components/ingress/ingress-caddy-kind-deploy.yaml b/stack_orchestrator/data/k8s/components/ingress/ingress-caddy-kind-deploy.yaml new file mode 100644 index 00000000..efe29cd5 --- /dev/null +++ b/stack_orchestrator/data/k8s/components/ingress/ingress-caddy-kind-deploy.yaml @@ -0,0 +1,250 @@ +# Caddy Ingress Controller for kind +# Based on: https://github.com/caddyserver/ingress +# Provides automatic HTTPS with Let's Encrypt +apiVersion: v1 +kind: Namespace +metadata: + name: caddy-system + labels: + app.kubernetes.io/name: caddy-ingress-controller + app.kubernetes.io/instance: caddy-ingress +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: caddy-ingress-controller + namespace: caddy-system + labels: + app.kubernetes.io/name: caddy-ingress-controller + app.kubernetes.io/instance: caddy-ingress +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: caddy-ingress-controller + labels: + app.kubernetes.io/name: caddy-ingress-controller + app.kubernetes.io/instance: caddy-ingress +rules: + - apiGroups: + - "" + resources: + - configmaps + - endpoints + - nodes + - pods + - secrets + - namespaces + - services + verbs: + - list + - watch + - get + - apiGroups: + - "" + resources: + - nodes + verbs: + - get + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + - 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 + verbs: + - get + - create + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: caddy-ingress-controller + labels: + app.kubernetes.io/name: caddy-ingress-controller + app.kubernetes.io/instance: caddy-ingress +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: caddy-ingress-controller +subjects: + - kind: ServiceAccount + name: caddy-ingress-controller + namespace: caddy-system +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: caddy-ingress-controller-configmap + namespace: caddy-system + labels: + app.kubernetes.io/name: caddy-ingress-controller + app.kubernetes.io/instance: caddy-ingress +data: + # Caddy global options + acmeCA: "https://acme-v02.api.letsencrypt.org/directory" + email: "" +--- +apiVersion: v1 +kind: Service +metadata: + name: caddy-ingress-controller + namespace: caddy-system + labels: + app.kubernetes.io/name: caddy-ingress-controller + app.kubernetes.io/instance: caddy-ingress + app.kubernetes.io/component: controller +spec: + type: NodePort + ports: + - name: http + port: 80 + targetPort: http + protocol: TCP + - name: https + port: 443 + targetPort: https + protocol: TCP + selector: + app.kubernetes.io/name: caddy-ingress-controller + app.kubernetes.io/instance: caddy-ingress + app.kubernetes.io/component: controller +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: caddy-ingress-controller + namespace: caddy-system + labels: + app.kubernetes.io/name: caddy-ingress-controller + app.kubernetes.io/instance: caddy-ingress + app.kubernetes.io/component: controller +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: caddy-ingress-controller + app.kubernetes.io/instance: caddy-ingress + app.kubernetes.io/component: controller + template: + metadata: + labels: + app.kubernetes.io/name: caddy-ingress-controller + app.kubernetes.io/instance: caddy-ingress + app.kubernetes.io/component: controller + spec: + serviceAccountName: caddy-ingress-controller + terminationGracePeriodSeconds: 60 + nodeSelector: + ingress-ready: "true" + kubernetes.io/os: linux + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Equal + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Equal + containers: + - name: caddy-ingress-controller + image: ghcr.io/caddyserver/ingress:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 80 + hostPort: 80 + protocol: TCP + - name: https + containerPort: 443 + hostPort: 443 + protocol: TCP + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + args: + - -config-map=caddy-system/caddy-ingress-controller-configmap + - -class-name=caddy + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 1000m + memory: 512Mi + readinessProbe: + httpGet: + path: /healthz + port: 9765 + initialDelaySeconds: 3 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /healthz + port: 9765 + initialDelaySeconds: 3 + periodSeconds: 10 + securityContext: + allowPrivilegeEscalation: true + capabilities: + add: + - NET_BIND_SERVICE + drop: + - ALL + runAsUser: 0 + runAsGroup: 0 + volumeMounts: + - name: caddy-data + mountPath: /data + - name: caddy-config + mountPath: /config + volumes: + - name: caddy-data + emptyDir: {} + - name: caddy-config + emptyDir: {} +--- +apiVersion: networking.k8s.io/v1 +kind: IngressClass +metadata: + name: caddy + labels: + app.kubernetes.io/name: caddy-ingress-controller + app.kubernetes.io/instance: caddy-ingress + annotations: + ingressclass.kubernetes.io/is-default-class: "true" +spec: + controller: caddy.io/ingress-controller diff --git a/stack_orchestrator/deploy/k8s/cluster_info.py b/stack_orchestrator/deploy/k8s/cluster_info.py index be1b2e3d..4e21ae80 100644 --- a/stack_orchestrator/deploy/k8s/cluster_info.py +++ b/stack_orchestrator/deploy/k8s/cluster_info.py @@ -162,10 +162,12 @@ class ClusterInfo: ) ingress_annotations = { - "kubernetes.io/ingress.class": "nginx", + "kubernetes.io/ingress.class": "caddy", } - if not certificate: - ingress_annotations["cert-manager.io/cluster-issuer"] = cluster_issuer + # Note: Caddy handles TLS automatically via Let's Encrypt, no cert-manager needed + if not certificate and cluster_issuer: + # Only add cert-manager annotation if using nginx ingress with cert-manager + pass # Caddy handles certificates automatically ingress = client.V1Ingress( metadata=client.V1ObjectMeta( diff --git a/stack_orchestrator/deploy/k8s/helpers.py b/stack_orchestrator/deploy/k8s/helpers.py index 80fb9c6a..fcb6a4bf 100644 --- a/stack_orchestrator/deploy/k8s/helpers.py +++ b/stack_orchestrator/deploy/k8s/helpers.py @@ -45,30 +45,55 @@ def destroy_cluster(name: str): _run_command(f"kind delete cluster --name {name}") -def wait_for_ingress_in_kind(): +def wait_for_ingress_in_kind(ingress_type="caddy"): + """Wait for ingress controller to become ready. + + Args: + ingress_type: "caddy" or "nginx" - determines which namespace and labels to check + """ core_v1 = client.CoreV1Api() + + if ingress_type == "caddy": + namespace = "caddy-system" + label_selector = "app.kubernetes.io/component=controller" + else: + namespace = "ingress-nginx" + label_selector = "app.kubernetes.io/component=controller" + for i in range(20): warned_waiting = False w = watch.Watch() for event in w.stream(func=core_v1.list_namespaced_pod, - namespace="ingress-nginx", - label_selector="app.kubernetes.io/component=controller", + namespace=namespace, + label_selector=label_selector, timeout_seconds=30): if event['object'].status.container_statuses: if event['object'].status.container_statuses[0].ready is True: if warned_waiting: - print("Ingress controller is ready") + print(f"{ingress_type.capitalize()} ingress controller is ready") return - print("Waiting for ingress controller to become ready...") + print(f"Waiting for {ingress_type} ingress controller to become ready...") warned_waiting = True - error_exit("ERROR: Timed out waiting for ingress to become ready") + error_exit(f"ERROR: Timed out waiting for {ingress_type} ingress to become ready") -def install_ingress_for_kind(): +def install_ingress_for_kind(ingress_type="caddy"): + """Install ingress controller in kind cluster. + + Args: + ingress_type: "caddy" or "nginx" - determines which ingress controller to install + """ api_client = client.ApiClient() - ingress_install = os.path.abspath(get_k8s_dir().joinpath("components", "ingress", "ingress-nginx-kind-deploy.yaml")) - if opts.o.debug: - print("Installing nginx ingress controller in kind cluster") + + if ingress_type == "caddy": + ingress_install = os.path.abspath(get_k8s_dir().joinpath("components", "ingress", "ingress-caddy-kind-deploy.yaml")) + if opts.o.debug: + print("Installing Caddy ingress controller in kind cluster") + else: + ingress_install = os.path.abspath(get_k8s_dir().joinpath("components", "ingress", "ingress-nginx-kind-deploy.yaml")) + if opts.o.debug: + print("Installing nginx ingress controller in kind cluster") + utils.create_from_yaml(api_client, yaml_file=ingress_install) @@ -251,9 +276,9 @@ def _generate_kind_port_mappings_from_services(parsed_pod_files): def _generate_kind_port_mappings(parsed_pod_files): port_definitions = [] - # For now we just map port 80 for the nginx ingress controller we install in kind - port_string = "80" - port_definitions.append(f" - containerPort: {port_string}\n hostPort: {port_string}\n") + # Map port 80 (HTTP) and 443 (HTTPS) for the ingress controller + for port_string in ["80", "443"]: + port_definitions.append(f" - containerPort: {port_string}\n hostPort: {port_string}\n") return ( "" if len(port_definitions) == 0 else ( " extraPortMappings:\n"