Compare commits
2 Commits
main
...
afd-caddy-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8426d99ed9 | ||
|
|
3606b5dd90 |
@ -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
|
||||||
@ -42,6 +42,7 @@ from stack_orchestrator.deploy.deployment_context import DeploymentContext
|
|||||||
from stack_orchestrator.deploy.deployment_create import create as deployment_create
|
from stack_orchestrator.deploy.deployment_create import create as deployment_create
|
||||||
from stack_orchestrator.deploy.deployment_create import init as deployment_init
|
from stack_orchestrator.deploy.deployment_create import init as deployment_init
|
||||||
from stack_orchestrator.deploy.deployment_create import setup as deployment_setup
|
from stack_orchestrator.deploy.deployment_create import setup as deployment_setup
|
||||||
|
from stack_orchestrator.deploy.k8s import k8s_command
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
@ -54,6 +55,10 @@ from stack_orchestrator.deploy.deployment_create import setup as deployment_setu
|
|||||||
def command(ctx, include, exclude, env_file, cluster, deploy_to):
|
def command(ctx, include, exclude, env_file, cluster, deploy_to):
|
||||||
'''deploy a stack'''
|
'''deploy a stack'''
|
||||||
|
|
||||||
|
# k8s subcommand doesn't require stack
|
||||||
|
if ctx.invoked_subcommand == "k8s":
|
||||||
|
return
|
||||||
|
|
||||||
# Although in theory for some subcommands (e.g. deploy create) the stack can be inferred,
|
# Although in theory for some subcommands (e.g. deploy create) the stack can be inferred,
|
||||||
# Click doesn't allow us to know that here, so we make providing the stack mandatory
|
# Click doesn't allow us to know that here, so we make providing the stack mandatory
|
||||||
stack = global_options2(ctx).stack
|
stack = global_options2(ctx).stack
|
||||||
@ -460,3 +465,4 @@ def _orchestrate_cluster_config(ctx, cluster_config, deployer, container_exec_en
|
|||||||
command.add_command(deployment_init)
|
command.add_command(deployment_init)
|
||||||
command.add_command(deployment_create)
|
command.add_command(deployment_create)
|
||||||
command.add_command(deployment_setup)
|
command.add_command(deployment_setup)
|
||||||
|
command.add_command(k8s_command.command, "k8s")
|
||||||
|
|||||||
@ -162,10 +162,12 @@ class ClusterInfo:
|
|||||||
)
|
)
|
||||||
|
|
||||||
ingress_annotations = {
|
ingress_annotations = {
|
||||||
"kubernetes.io/ingress.class": "nginx",
|
"kubernetes.io/ingress.class": "caddy",
|
||||||
}
|
}
|
||||||
if not certificate:
|
# Note: Caddy handles TLS automatically via Let's Encrypt, no cert-manager needed
|
||||||
ingress_annotations["cert-manager.io/cluster-issuer"] = cluster_issuer
|
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(
|
ingress = client.V1Ingress(
|
||||||
metadata=client.V1ObjectMeta(
|
metadata=client.V1ObjectMeta(
|
||||||
|
|||||||
@ -210,8 +210,12 @@ class K8sDeployer(Deployer):
|
|||||||
self.skip_cluster_management = skip_cluster_management
|
self.skip_cluster_management = skip_cluster_management
|
||||||
if not opts.o.dry_run:
|
if not opts.o.dry_run:
|
||||||
if self.is_kind() and not self.skip_cluster_management:
|
if self.is_kind() and not self.skip_cluster_management:
|
||||||
# Create the kind cluster
|
# Create the kind cluster (or reuse existing one)
|
||||||
create_cluster(self.kind_cluster_name, self.deployment_dir.joinpath(constants.kind_config_filename))
|
kind_config = self.deployment_dir.joinpath(constants.kind_config_filename)
|
||||||
|
actual_cluster = create_cluster(self.kind_cluster_name, kind_config)
|
||||||
|
if actual_cluster != self.kind_cluster_name:
|
||||||
|
# An existing cluster was found, use it instead
|
||||||
|
self.kind_cluster_name = actual_cluster
|
||||||
# Ensure the referenced containers are copied into kind
|
# Ensure the referenced containers are copied into kind
|
||||||
load_images_into_kind(self.kind_cluster_name, self.cluster_info.image_set)
|
load_images_into_kind(self.kind_cluster_name, self.cluster_info.image_set)
|
||||||
self.connect_api()
|
self.connect_api()
|
||||||
|
|||||||
@ -35,40 +35,106 @@ def _run_command(command: str):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_kind_cluster():
|
||||||
|
"""Get an existing kind cluster, if any.
|
||||||
|
|
||||||
|
Uses `kind get clusters` to find existing clusters.
|
||||||
|
Returns the cluster name or None if no cluster exists.
|
||||||
|
"""
|
||||||
|
result = subprocess.run(
|
||||||
|
"kind get clusters",
|
||||||
|
shell=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
clusters = result.stdout.strip().splitlines()
|
||||||
|
if clusters:
|
||||||
|
return clusters[0] # Return the first cluster found
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def create_cluster(name: str, config_file: str):
|
def create_cluster(name: str, config_file: str):
|
||||||
|
"""Create a kind cluster, or reuse an existing one.
|
||||||
|
|
||||||
|
Checks if any kind cluster already exists. If so, uses that cluster
|
||||||
|
instead of creating a new one. This allows multiple deployments to
|
||||||
|
share the same kind cluster.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The desired cluster name (used only if creating new)
|
||||||
|
config_file: Path to kind config file (used only if creating new)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The name of the cluster being used (either existing or newly created)
|
||||||
|
"""
|
||||||
|
existing = get_kind_cluster()
|
||||||
|
if existing:
|
||||||
|
print(f"Using existing cluster: {existing}")
|
||||||
|
return existing
|
||||||
|
|
||||||
|
print(f"Creating new cluster: {name}")
|
||||||
result = _run_command(f"kind create cluster --name {name} --config {config_file}")
|
result = _run_command(f"kind create cluster --name {name} --config {config_file}")
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise DeployerException(f"kind create cluster failed: {result}")
|
raise DeployerException(f"kind create cluster failed: {result}")
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
def destroy_cluster(name: str):
|
def destroy_cluster(name: str):
|
||||||
_run_command(f"kind delete cluster --name {name}")
|
_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()
|
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):
|
for i in range(20):
|
||||||
warned_waiting = False
|
warned_waiting = False
|
||||||
w = watch.Watch()
|
w = watch.Watch()
|
||||||
for event in w.stream(func=core_v1.list_namespaced_pod,
|
for event in w.stream(func=core_v1.list_namespaced_pod,
|
||||||
namespace="ingress-nginx",
|
namespace=namespace,
|
||||||
label_selector="app.kubernetes.io/component=controller",
|
label_selector=label_selector,
|
||||||
timeout_seconds=30):
|
timeout_seconds=30):
|
||||||
if event['object'].status.container_statuses:
|
if event['object'].status.container_statuses:
|
||||||
if event['object'].status.container_statuses[0].ready is True:
|
if event['object'].status.container_statuses[0].ready is True:
|
||||||
if warned_waiting:
|
if warned_waiting:
|
||||||
print("Ingress controller is ready")
|
print(f"{ingress_type.capitalize()} ingress controller is ready")
|
||||||
return
|
return
|
||||||
print("Waiting for ingress controller to become ready...")
|
print(f"Waiting for {ingress_type} ingress controller to become ready...")
|
||||||
warned_waiting = True
|
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()
|
api_client = client.ApiClient()
|
||||||
|
|
||||||
|
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"))
|
ingress_install = os.path.abspath(get_k8s_dir().joinpath("components", "ingress", "ingress-nginx-kind-deploy.yaml"))
|
||||||
if opts.o.debug:
|
if opts.o.debug:
|
||||||
print("Installing nginx ingress controller in kind cluster")
|
print("Installing nginx ingress controller in kind cluster")
|
||||||
|
|
||||||
utils.create_from_yaml(api_client, yaml_file=ingress_install)
|
utils.create_from_yaml(api_client, yaml_file=ingress_install)
|
||||||
|
|
||||||
|
|
||||||
@ -251,8 +317,8 @@ def _generate_kind_port_mappings_from_services(parsed_pod_files):
|
|||||||
|
|
||||||
def _generate_kind_port_mappings(parsed_pod_files):
|
def _generate_kind_port_mappings(parsed_pod_files):
|
||||||
port_definitions = []
|
port_definitions = []
|
||||||
# For now we just map port 80 for the nginx ingress controller we install in kind
|
# Map port 80 (HTTP) and 443 (HTTPS) for the ingress controller
|
||||||
port_string = "80"
|
for port_string in ["80", "443"]:
|
||||||
port_definitions.append(f" - containerPort: {port_string}\n hostPort: {port_string}\n")
|
port_definitions.append(f" - containerPort: {port_string}\n hostPort: {port_string}\n")
|
||||||
return (
|
return (
|
||||||
"" if len(port_definitions) == 0 else (
|
"" if len(port_definitions) == 0 else (
|
||||||
|
|||||||
43
stack_orchestrator/deploy/k8s/k8s_command.py
Normal file
43
stack_orchestrator/deploy/k8s/k8s_command.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Copyright © 2024 Vulcanize
|
||||||
|
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http:#www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from stack_orchestrator.deploy.k8s.helpers import get_kind_cluster
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.pass_context
|
||||||
|
def command(ctx):
|
||||||
|
'''k8s cluster management commands'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@command.group()
|
||||||
|
@click.pass_context
|
||||||
|
def list(ctx):
|
||||||
|
'''list k8s resources'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@list.command()
|
||||||
|
@click.pass_context
|
||||||
|
def cluster(ctx):
|
||||||
|
'''Show the existing kind cluster'''
|
||||||
|
existing_cluster = get_kind_cluster()
|
||||||
|
if existing_cluster:
|
||||||
|
print(existing_cluster)
|
||||||
|
else:
|
||||||
|
print("No cluster found")
|
||||||
Loading…
Reference in New Issue
Block a user