From 2e8ffda2702de71025f6331eb9a27706a6578272 Mon Sep 17 00:00:00 2001 From: Jan Schoone Date: Mon, 23 Feb 2026 17:10:58 +0000 Subject: [PATCH 01/11] refactor(build): Replace build tooling with script-based approach Replace the previous build system (csctl, Makefile, Python scripts, Go release notes) with a streamlined set of bash scripts and a justfile. New tooling: - hack/build.sh: build + publish + --install-cso flag with next-steps output - hack/update.sh: unified version/addon updater with image-manager maintenance - hack/generate-resources.sh: ClusterStack + Cluster YAML generator with .release/ auto-detection - hack/generate-image-manifests.sh: ORC and image-manager format generator - hack/show-matrix.sh: version/addon matrix display with --markdown flag - justfile: task runner with dev, install-cso, generate-*, matrix recipes - Containerfile + flake.nix: reproducible tooling environment Removed: - hack/ensure-*.sh, hack/generate_version.py, hack/kind-dev.sh - hack/generate_openstack_image_manager_yaml.sh - hack/tools/release/notes.go Assisted-by: Claude Code Signed-off-by: Jan Schoone --- Containerfile | 53 ++ flake.lock | 61 ++ flake.nix | 88 +++ hack/build.sh | 552 +++++++++++++++ hack/ensure-connected-to-mgt-cluster.sh | 49 -- hack/ensure-env-variables.sh | 32 - hack/generate-image-manifests.sh | 355 ++++++++++ hack/generate-resources.sh | 194 ++++++ hack/generate_openstack_image_manager_yaml.sh | 59 -- hack/generate_version.py | 309 --------- hack/kind-dev.sh | 62 -- hack/show-matrix.sh | 203 ++++++ hack/tools/release/notes.go | 209 ------ hack/update.sh | 652 ++++++++++++++++++ justfile | 181 +++++ 15 files changed, 2339 insertions(+), 720 deletions(-) create mode 100644 Containerfile create mode 100644 flake.lock create mode 100644 flake.nix create mode 100755 hack/build.sh delete mode 100755 hack/ensure-connected-to-mgt-cluster.sh delete mode 100755 hack/ensure-env-variables.sh create mode 100755 hack/generate-image-manifests.sh create mode 100755 hack/generate-resources.sh delete mode 100755 hack/generate_openstack_image_manager_yaml.sh delete mode 100755 hack/generate_version.py delete mode 100755 hack/kind-dev.sh create mode 100755 hack/show-matrix.sh delete mode 100644 hack/tools/release/notes.go create mode 100755 hack/update.sh create mode 100644 justfile diff --git a/Containerfile b/Containerfile new file mode 100644 index 00000000..4abb6d29 --- /dev/null +++ b/Containerfile @@ -0,0 +1,53 @@ +FROM alpine:3.21 + +LABEL org.opencontainers.image.source="https://github.com/SovereignCloudStack/cluster-stacks" +LABEL org.opencontainers.image.description="Cluster Stack Build Tools" +LABEL org.opencontainers.image.licenses="Apache-2.0" + +# Install system dependencies +RUN apk add --no-cache \ + bash \ + git \ + curl \ + tar \ + gzip \ + gawk \ + python3 \ + py3-yaml \ + jq \ + ca-certificates + +# Install helm +RUN HELM_VERSION=v3.17.3 && \ + curl -fsSL "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz" | \ + tar -xz -C /usr/local/bin --strip-components=1 linux-amd64/helm + +# Install yq (mikefarah) +RUN YQ_VERSION=v4.45.4 && \ + curl -fsSL "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" \ + -o /usr/local/bin/yq && chmod +x /usr/local/bin/yq + +# Install oras +RUN ORAS_VERSION=1.2.2 && \ + curl -fsSL "https://github.com/oras-project/oras/releases/download/v${ORAS_VERSION}/oras_${ORAS_VERSION}_linux_amd64.tar.gz" | \ + tar -xz -C /usr/local/bin oras + +# Install just +RUN JUST_VERSION=1.40.0 && \ + curl -fsSL "https://github.com/casey/just/releases/download/${JUST_VERSION}/just-${JUST_VERSION}-x86_64-unknown-linux-musl.tar.gz" | \ + tar -xz -C /usr/local/bin just + +WORKDIR /workspace + +# Verify installations +RUN bash --version | head -1 && \ + helm version --short && \ + yq --version && \ + oras version && \ + just --version && \ + git --version + +# Allow git operations inside mounted volumes +RUN git config --global --add safe.directory /workspace + +CMD ["/bin/bash"] diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..6714924d --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1768886240, + "narHash": "sha256-C2TjvwYZ2VDxYWeqvvJ5XPPp6U7H66zeJlRaErJKoEM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "80e4adbcf8992d3fd27ad4964fbb84907f9478b0", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..8db9a34d --- /dev/null +++ b/flake.nix @@ -0,0 +1,88 @@ +{ + description = "Cluster Stacks - Build tools for SCS Kubernetes cluster stacks"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + devShells.default = pkgs.mkShell { + name = "cluster-stacks-dev"; + + buildInputs = with pkgs; [ + # Core tools + bash + git + curl + + # Container tools + docker + podman + + # Kubernetes tools + kubectl + kubernetes-helm + kind + kustomize + + # Build tools + just + python3 + python3Packages.pyyaml + jq + yq-go + + # OCI/Registry tools + oras + ]; + + shellHook = '' + echo "Cluster Stacks development environment" + echo "" + echo "Available tools:" + echo " just - Run 'just --list' to see available commands" + echo " helm - Kubernetes package manager" + echo " kubectl - Kubernetes CLI" + echo " kind - Local Kubernetes clusters" + echo " yq - YAML processor" + echo " oras - OCI registry client" + echo " python3 - With PyYAML" + echo "" + + # Generate shell completions into a cache directory + comp_dir="''${XDG_CACHE_HOME:-$HOME/.cache}/cluster-stacks/completions" + mkdir -p "$comp_dir" + + user_shell=$(getent passwd "$(whoami)" 2>/dev/null | cut -d: -f7) + shell_name=$(basename "''${user_shell:-bash}") + + # Regenerate completions if the directory is empty or tools were updated + if [ ! -f "$comp_dir/.$shell_name-generated" ]; then + kubectl completion "$shell_name" > "$comp_dir/_kubectl" 2>/dev/null || true + helm completion "$shell_name" > "$comp_dir/_helm" 2>/dev/null || true + just --completions "$shell_name" > "$comp_dir/_just" 2>/dev/null || true + kind completion "$shell_name" > "$comp_dir/_kind" 2>/dev/null || true + oras completion "$shell_name" > "$comp_dir/_oras" 2>/dev/null || true + touch "$comp_dir/.$shell_name-generated" + fi + + # Make completions available + export FPATH="$comp_dir:$FPATH" + + # Start user's preferred shell instead of bash. + # nix develop always drops into bash; this detects the user's + # real login shell from /etc/passwd and exec's into it. + if [ -n "$user_shell" ] && [ "$shell_name" != "bash" ] && [ -x "$user_shell" ]; then + exec "$user_shell" + fi + ''; + }; + } + ); +} diff --git a/hack/build.sh b/hack/build.sh new file mode 100755 index 00000000..3ae57af5 --- /dev/null +++ b/hack/build.sh @@ -0,0 +1,552 @@ +#!/usr/bin/env bash +# Build and optionally publish cluster-stack release artifacts. +# +# Usage: +# ./hack/build.sh [stack-dir] [options] +# +# The is the base directory containing per-minor-version subdirs +# (e.g., providers/openstack/scs). If omitted, it is derived from $PROVIDER +# and $CLUSTER_STACK (default: providers/openstack/scs). +# +# Options: +# --version Build for a specific K8s minor version (e.g., 1.34) +# --all Build for all K8s versions (all 1-* subdirs) +# --publish Push to OCI registry after building +# --install-cso Install/upgrade the Cluster Stack Operator with matching OCI config +# --validate Validate addon bundle structure against clusteraddon.yaml +# +# Without --version or --all, builds the highest 1-* subdirectory. +# +# Each 1-XX/ subdir must contain a stack.yaml with at minimum: +# provider: openstack +# clusterStackName: scs +# kubernetesVersion: 1.34 # minor-only or with patch (1.34.3) +# +# Addon versions are read directly from cluster-addon/*/Chart.yaml as +# maintained by `just update addons`. The build does not resolve or +# modify addon versions. +# +# Environment: +# PROVIDER Provider name (default: openstack) +# CLUSTER_STACK Cluster stack name (default: scs) +# OCI_REGISTRY OCI registry (default: ttl.sh) +# OCI_REPOSITORY OCI repository (auto-generated for ttl.sh) +# OCI_USERNAME OCI auth username (optional) +# OCI_PASSWORD OCI auth password (optional) +# OCI_ACCESS_TOKEN OCI auth token (optional, alternative to user/pass) +# OUTPUT_DIR Output directory (default: .release) +# CSO_CHART CSO Helm chart reference (default: oci://registry.scs.community/cluster-stacks/cso) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# ============================================ +# Argument parsing +# ============================================ + +BASE_DIR="" +TARGET_VERSION="" +BUILD_ALL=false +PUBLISH=false +INSTALL_CSO=false +VALIDATE=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --version) TARGET_VERSION="$2"; shift 2 ;; + --all) BUILD_ALL=true; shift ;; + --publish) PUBLISH=true; shift ;; + --install-cso) INSTALL_CSO=true; shift ;; + --validate) VALIDATE=true; shift ;; + -*) echo "Unknown option: $1"; exit 1 ;; + *) + if [[ -z "$BASE_DIR" ]]; then + BASE_DIR="$1"; shift + else + echo "Unexpected argument: $1"; exit 1 + fi + ;; + esac +done + +if [[ -z "$BASE_DIR" ]]; then + BASE_DIR="providers/${PROVIDER:-openstack}/${CLUSTER_STACK:-scs}" +fi + +if [[ ! -d "$BASE_DIR" ]]; then + echo "Stack base directory not found: $BASE_DIR" + exit 1 +fi + +OUTPUT_DIR="${OUTPUT_DIR:-.release}" + +# ============================================ +# Resolve which minor-version directories to build +# ============================================ + +collect_version_dirs() { + if [[ "$BUILD_ALL" == "true" ]]; then + # All 1-* subdirectories, sorted + for d in "$BASE_DIR"/1-*/; do + [[ -d "$d" ]] && echo "$d" + done | sort -t- -k2 -n + elif [[ -n "$TARGET_VERSION" ]]; then + # --version 1.34 → look for 1-34/ + local minor_dash="${TARGET_VERSION//./-}" + local target_dir="$BASE_DIR/$minor_dash" + if [[ ! -d "$target_dir" ]]; then + echo "Version directory not found: $target_dir" >&2 + echo "Available:" >&2 + ls -d "$BASE_DIR"/1-*/ 2>/dev/null | sed 's|.*/||; s|/$||; s/^/ /' >&2 + return 1 + fi + echo "$target_dir" + else + # Default: highest 1-* directory + local highest + highest=$(ls -d "$BASE_DIR"/1-*/ 2>/dev/null | sort -t- -k2 -n | tail -1) + if [[ -z "$highest" ]]; then + echo "No version directories found in: $BASE_DIR" >&2 + return 1 + fi + echo "$highest" + fi +} + +VERSION_DIRS=$(collect_version_dirs) + +if [[ -z "$VERSION_DIRS" ]]; then + echo "No version directories to build" + exit 1 +fi + +# ============================================ +# OCI registry setup +# ============================================ + +setup_oci() { + if [[ -z "${OCI_REGISTRY:-}" ]]; then + DATE_YYYYMMDD="${OCI_DATE:-$(date +%Y%m%d)}" + export OCI_REGISTRY="ttl.sh" + export OCI_REPOSITORY="clusterstacks-${DATE_YYYYMMDD}" + echo "Auto-configured ttl.sh: $OCI_REGISTRY/$OCI_REPOSITORY (expires in 24h)" + fi +} + +OCI_SETUP_DONE=false +ensure_oci() { + if [[ "$OCI_SETUP_DONE" != "true" ]]; then + setup_oci + OCI_SETUP_DONE=true + fi +} + +# ============================================ +# CSO installation +# ============================================ + +CSO_CHART="${CSO_CHART:-oci://registry.scs.community/cluster-stacks/cso}" + +install_cso() { + if ! command -v helm >/dev/null 2>&1; then + echo "helm not found — install from https://helm.sh/docs/intro/install/" + exit 1 + fi + + echo "Installing/upgrading CSO..." + echo " Chart: $CSO_CHART" + echo " OCI config: $OCI_REGISTRY/$OCI_REPOSITORY" + echo "" + + helm upgrade -i cso "$CSO_CHART" \ + --namespace cso-system --create-namespace \ + --set controllerManager.manager.source=oci \ + --set "clusterStackVariables.ociRegistry=${OCI_REGISTRY}" \ + --set "clusterStackVariables.ociRepository=${OCI_REPOSITORY}" + + echo "" +} + +# Determine release version for a given K8s minor version. +# Stable: queries OCI for highest vN tag, returns v(N+1). Dev: returns v0-. +get_release_version() { + local provider="$1" + local stack_name="$2" + local k8s_dash="$3" + local tag_prefix="${provider}-${stack_name}-${k8s_dash}" + + if [[ "${OCI_REGISTRY:-}" == "ttl.sh" ]] || [[ -z "${OCI_REPOSITORY:-}" ]]; then + # Dev version + local timestamp + timestamp=$(date +%s) + echo "v0-ttl.${timestamp}" + return + fi + + # Query OCI for existing stable versions + local latest=0 + if command -v oras >/dev/null 2>&1; then + local tags + tags=$(oras repo tags "${OCI_REGISTRY}/${OCI_REPOSITORY}" 2>/dev/null || echo "") + if [[ -n "$tags" ]]; then + latest=$(echo "$tags" | grep -oP "^${tag_prefix}-v\K[0-9]+" | sort -n | tail -1 || echo "0") + latest="${latest:-0}" + fi + fi + + echo "v$((latest + 1))" +} + +# ============================================ +# Resolve K8s patch version +# ============================================ + +# Given a version like "1.34" (no patch), resolve to latest patch. +# If already has patch (1.34.3), return as-is. +resolve_k8s_version() { + local version="$1" + local provider="$2" + + # Already has patch version + if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "$version" + return + fi + + # Minor-only: resolve latest patch + if [[ "$provider" == "docker" ]]; then + # Query Docker Hub for kindest/node tags + local latest + latest=$(curl -sfL "https://registry.hub.docker.com/v2/repositories/kindest/node/tags?page_size=100&name=v${version}." 2>/dev/null \ + | jq -r '.results[].name' 2>/dev/null \ + | grep -E "^v${version}\.[0-9]+$" \ + | sed 's/^v//' \ + | sort -V \ + | tail -1) || true + if [[ -n "$latest" ]]; then + echo "$latest" + return + fi + else + # Query GitHub for K8s releases + local github_headers=() + if [[ -n "${GITHUB_TOKEN:-}" ]]; then + github_headers=(-H "Authorization: token $GITHUB_TOKEN") + fi + local latest + latest=$(curl -sfL "${github_headers[@]+"${github_headers[@]}"}" \ + "https://api.github.com/repos/kubernetes/kubernetes/releases?per_page=100" 2>/dev/null \ + | jq -r '.[].tag_name' 2>/dev/null \ + | grep -E "^v${version}\.[0-9]+$" \ + | sed 's/^v//' \ + | sort -V \ + | tail -1) || true + if [[ -n "$latest" ]]; then + echo "$latest" + return + fi + fi + + # Fallback: return with .0 + echo "${version}.0" +} + +# ============================================ +# Generate csctl.yaml for release artifact +# ============================================ + +generate_csctl_yaml() { + local provider="$1" + local stack_name="$2" + local k8s_version="$3" + local output_file="$4" + + cat > "$output_file" </dev/null 2>&1; then + yq -i ".images.controlPlane.name = \"ubuntu-capi-image-v${k8s_version}\"" "$class_values" + yq -i ".images.worker.name = \"ubuntu-capi-image-v${k8s_version}\"" "$class_values" + fi + + # ---- Package cluster-class ---- + echo " Packaging cluster-class..." + rm -rf "$work_dir/cluster-class/charts" + helm package "$work_dir/cluster-class" -d "$release_dir/" > /dev/null + echo " cluster-class packaged" + + # ---- Package cluster-addon bundle ---- + echo " Packaging cluster-addon..." + local addon_temp + addon_temp=$(mktemp -d) + local addon_count=0 + + for addon_dir in "$work_dir"/cluster-addon/*/; do + [[ -d "$addon_dir" ]] || continue + local addon_name + addon_name=$(basename "$addon_dir") + cp -r "$addon_dir" "$addon_temp/$addon_name" + rm -rf "$addon_temp/$addon_name/charts" + addon_count=$((addon_count + 1)) + done + + if [[ $addon_count -eq 0 ]]; then + echo " No addon subdirectories found" + rm -rf "$addon_temp" + exit 1 + fi + + local addon_tgz="${provider}-${stack_name}-${k8s_dash}-cluster-addon-${release_version}.tgz" + (cd "$addon_temp" && tar -czf "$(cd "$REPO_ROOT" && pwd)/$release_dir/$addon_tgz" */) + rm -rf "$addon_temp" + echo " cluster-addon packaged ($addon_count addons)" + + # ---- Validate addon bundle ---- + if [[ "$VALIDATE" == "true" && -f "$version_dir/clusteraddon.yaml" ]]; then + echo " Validating addon bundle..." + local validate_dir + validate_dir=$(mktemp -d) + tar -xzf "$release_dir/$addon_tgz" -C "$validate_dir" + + local expected_addons + expected_addons=$(yq '.addonStages | to_entries | .[].value[].name' "$version_dir/clusteraddon.yaml" 2>/dev/null | sort -u) + + local failed=false + for addon in $expected_addons; do + if [[ ! -d "$validate_dir/$addon" ]]; then + echo " Missing addon: $addon (referenced in clusteraddon.yaml)" + failed=true + fi + done + rm -rf "$validate_dir" + + if [[ "$failed" == "true" ]]; then + exit 1 + fi + echo " Validation passed" + fi + + # ---- Copy clusteraddon.yaml ---- + if [[ -f "$version_dir/clusteraddon.yaml" ]]; then + cp "$version_dir/clusteraddon.yaml" "$release_dir/" + fi + + # ---- Copy generated csctl.yaml ---- + cp "$work_dir/csctl.yaml" "$release_dir/" + + # ---- Generate metadata.yaml ---- + local git_hash + git_hash=$(git rev-parse HEAD 2>/dev/null || echo "unknown") + + cat > "$release_dir/metadata.yaml" < "$release_dir/hashes.json" </dev/null 2>&1; then + echo " oras not found — install from https://oras.land/docs/installation" + exit 1 + fi + + echo "" + echo " Publishing to $OCI_REGISTRY/$OCI_REPOSITORY:$oci_tag" + + local oras_opts=() + if [[ -n "${OCI_USERNAME:-}" && -n "${OCI_PASSWORD:-}" ]]; then + oras_opts+=(--username "$OCI_USERNAME" --password "$OCI_PASSWORD") + elif [[ -n "${OCI_ACCESS_TOKEN:-}" ]]; then + oras_opts+=(--password "$OCI_ACCESS_TOKEN") + fi + + local files=() + for f in "$release_dir"/*; do + [[ -f "$f" ]] && files+=("$(basename "$f")") + done + + (cd "$release_dir" && oras push \ + "$OCI_REGISTRY/$OCI_REPOSITORY:$oci_tag" \ + --artifact-type application/vnd.clusterstack.release \ + "${oras_opts[@]}" \ + "${files[@]}") + + echo " Published: $OCI_REGISTRY/$OCI_REPOSITORY:$oci_tag" + echo " Pull: oras pull $OCI_REGISTRY/$OCI_REPOSITORY:$oci_tag" +} + +# ============================================ +# Main +# ============================================ + +# Resolve OCI config once (needed for --publish and/or --install-cso) +if [[ "$PUBLISH" == "true" || "$INSTALL_CSO" == "true" ]]; then + ensure_oci +fi + +# Install/upgrade CSO if requested +if [[ "$INSTALL_CSO" == "true" ]]; then + install_cso +fi + +echo "Stack base: $BASE_DIR" +echo "Version dirs: $(echo "$VERSION_DIRS" | tr '\n' ' ')" +echo "" + +# Track built versions for "next steps" output +declare -a BUILT_K8S_SHORTS=() +declare -a BUILT_CS_VERSIONS=() +declare -a BUILT_PROVIDERS=() +declare -a BUILT_STACK_NAMES=() + +for version_dir in $VERSION_DIRS; do + build_version_dir "$version_dir" +done + +echo "" +echo "Done." + +# ============================================ +# Next steps (after publish) +# ============================================ + +if [[ "$PUBLISH" == "true" && ${#BUILT_K8S_SHORTS[@]} -gt 0 ]]; then + echo "" + echo "================================================================" + echo "Next steps" + echo "================================================================" + + if [[ "$INSTALL_CSO" != "true" ]]; then + echo "" + echo "1. Install the Cluster Stack Operator (or re-run with --install-cso):" + echo "" + echo " helm upgrade -i cso ${CSO_CHART} \\" + echo " --namespace cso-system --create-namespace \\" + echo " --set controllerManager.manager.source=oci \\" + echo " --set clusterStackVariables.ociRegistry=\"${OCI_REGISTRY}\" \\" + echo " --set clusterStackVariables.ociRepository=\"${OCI_REPOSITORY}\"" + echo "" + echo "2. Apply the ClusterStack resource(s):" + else + echo "" + echo "Apply the ClusterStack resource(s):" + fi + echo "" + for ((i=0; i<${#BUILT_K8S_SHORTS[@]}; i++)); do + local_provider="${BUILT_PROVIDERS[$i]}" + local_stack="${BUILT_STACK_NAMES[$i]}" + local_k8s="${BUILT_K8S_SHORTS[$i]}" + local_version="${BUILT_CS_VERSIONS[$i]}" + echo " CLUSTER_STACK=${local_stack} ./hack/generate-resources.sh --version ${local_k8s} --cs-version ${local_version} | kubectl apply -f -" + done + echo "" +fi diff --git a/hack/ensure-connected-to-mgt-cluster.sh b/hack/ensure-connected-to-mgt-cluster.sh deleted file mode 100755 index 5eda91e7..00000000 --- a/hack/ensure-connected-to-mgt-cluster.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash -# Copyright 2023 The Kubernetes Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -euo pipefail - -context="$(kubectl config current-context 2>/dev/null || true)" - -if [ "$context" = "kind-scs-cluster-stacks" ]; then - exit 0 -fi - -if [ "$context" = "" ]; then - echo "No context set" - exit 1 -fi - - -echo "You are connected to $context. Please set KUBECONFIG to .mgt-cluster-kubeconfig.yaml" -exit 1 - -if [ "$#" -lt 1 ]; then - echo "Usage: $0 VAR1 VAR2 ..." - exit 1 -fi - -missing_vars=() -for varname in "$@"; do - eval varvalue="\$$varname" - if [ -z "$varvalue" ]; then - missing_vars+=("$varname") - fi -done - -if [ ${#missing_vars[@]} -gt 0 ]; then - echo "Missing or empty environment variables: ${missing_vars[*]}" - exit 1 -fi diff --git a/hack/ensure-env-variables.sh b/hack/ensure-env-variables.sh deleted file mode 100755 index 8dc027e4..00000000 --- a/hack/ensure-env-variables.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -# Copyright 2023 The Kubernetes Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -if [ "$#" -lt 1 ]; then - echo "Usage: $0 VAR1 VAR2 ..." - exit 1 -fi - -missing_vars=() -for varname in "$@"; do - eval varvalue="\$$varname" - if [ -z "$varvalue" ]; then - missing_vars+=("$varname") - fi -done - -if [ ${#missing_vars[@]} -gt 0 ]; then - echo "Missing or empty environment variables: ${missing_vars[*]}" - exit 1 -fi diff --git a/hack/generate-image-manifests.sh b/hack/generate-image-manifests.sh new file mode 100755 index 00000000..d1461f83 --- /dev/null +++ b/hack/generate-image-manifests.sh @@ -0,0 +1,355 @@ +#!/usr/bin/env bash +# Generate OpenStack image manifests for Kubernetes CAPI images. +# +# Supports two output formats: +# orc ORC Image CRD (default) — for k-orc.cloud Image resources +# image-manager openstack-image-manager YAML — for OSISM image-manager +# +# Usage: +# ./hack/generate-image-manifests.sh [stack-dir] [options] +# +# Options: +# --version Generate for a specific K8s minor version +# --format Output format: orc (default) or image-manager +# --visibility Image visibility: private (default), public, shared, community +# --output-dir Write to files instead of stdout +# --skip-checksum Skip fetching SHA256 checksums +# +# The is the base directory containing per-minor-version subdirs +# (e.g., providers/openstack/scs). If omitted, it is derived from $PROVIDER +# and $CLUSTER_STACK (default: providers/openstack/scs). +# +# Only relevant for OpenStack-based stacks (Docker stacks have no node images). +# +# Examples: +# ./hack/generate-image-manifests.sh --version 1.34 +# ./hack/generate-image-manifests.sh --format image-manager --visibility public +# ./hack/generate-image-manifests.sh --version 1.34 --visibility shared --skip-checksum +# +# Environment: +# PROVIDER Provider name (default: openstack) +# CLUSTER_STACK Cluster stack name (default: scs) +# IMAGE_BASE_URL Base URL for images (default: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images) +# CLOUD_NAME CloudCredentialsRef cloud name (default: openstack, orc format only) +# SECRET_NAME CloudCredentialsRef secret name (default: openstack, orc format only) + +set -euo pipefail + +# Defaults +BASE_URL="${IMAGE_BASE_URL:-https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images}" +CLOUD_NAME="${CLOUD_NAME:-openstack}" +SECRET_NAME="${SECRET_NAME:-openstack}" + +# ============================================ +# Argument parsing +# ============================================ + +BASE_DIR="" +TARGET_VERSION="" +OUTPUT_DIR="" +OUTPUT_FORMAT="orc" +VISIBILITY="private" +SKIP_CHECKSUM=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --version) TARGET_VERSION="$2"; shift 2 ;; + --format) OUTPUT_FORMAT="$2"; shift 2 ;; + --visibility) VISIBILITY="$2"; shift 2 ;; + --output-dir) OUTPUT_DIR="$2"; shift 2 ;; + --skip-checksum) SKIP_CHECKSUM=true; shift ;; + -*) echo "Unknown option: $1" >&2; exit 1 ;; + *) + if [[ -z "$BASE_DIR" ]]; then + BASE_DIR="$1"; shift + else + echo "Unexpected argument: $1" >&2; exit 1 + fi + ;; + esac +done + +if [[ -z "$BASE_DIR" ]]; then + BASE_DIR="providers/${PROVIDER:-openstack}/${CLUSTER_STACK:-scs}" +fi + +if [[ ! -d "$BASE_DIR" ]]; then + echo "Stack base directory not found: $BASE_DIR" >&2 + exit 1 +fi + +case "$OUTPUT_FORMAT" in + orc|image-manager) ;; + *) echo "Unknown format: $OUTPUT_FORMAT (use 'orc' or 'image-manager')" >&2; exit 1 ;; +esac + +case "$VISIBILITY" in + private|public|shared|community) ;; + *) echo "Unknown visibility: $VISIBILITY (use private, public, shared, or community)" >&2; exit 1 ;; +esac + +# ============================================ +# Ubuntu version mapping +# ============================================ + +# K8s 1.32 and earlier → Ubuntu 22.04 (2204) +# K8s 1.33 and later → Ubuntu 24.04 (2404) +ubuntu_for_minor() { + local minor="$1" + if [[ "$minor" -le 32 ]]; then + echo "2204" + else + echo "2404" + fi +} + +# ============================================ +# Check provider +# ============================================ + +FIRST_STACK=$(ls "$BASE_DIR"/1-*/stack.yaml 2>/dev/null | head -1) +if [[ -z "$FIRST_STACK" ]]; then + echo "No stack.yaml found in $BASE_DIR/1-*/" >&2 + exit 1 +fi + +STACK_PROVIDER=$(yq -r '.provider' "$FIRST_STACK") +if [[ "$STACK_PROVIDER" != "openstack" ]]; then + echo "Image manifests are only relevant for OpenStack-based stacks (provider: $STACK_PROVIDER)." >&2 + exit 0 +fi + +[[ -n "$OUTPUT_DIR" ]] && mkdir -p "$OUTPUT_DIR" + +# ============================================ +# Resolve K8s patch version +# ============================================ + +resolve_k8s_version() { + local version="$1" + + if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "$version" + return + fi + + local github_headers=() + if [[ -n "${GITHUB_TOKEN:-}" ]]; then + github_headers=(-H "Authorization: token $GITHUB_TOKEN") + fi + local latest + latest=$(curl -sfL "${github_headers[@]+"${github_headers[@]}"}" \ + "https://api.github.com/repos/kubernetes/kubernetes/releases?per_page=100" 2>/dev/null \ + | jq -r '.[].tag_name' 2>/dev/null \ + | grep -E "^v${version}\.[0-9]+$" \ + | sed 's/^v//' \ + | sort -V \ + | tail -1) || true + + if [[ -n "$latest" ]]; then + echo "$latest" + else + echo "${version}.0" + fi +} + +# ============================================ +# Collect image data +# ============================================ + +# Arrays to collect data for image-manager format (needs all versions grouped) +declare -a ALL_K8S_VERSIONS=() +declare -a ALL_IMAGE_URLS=() +declare -a ALL_CHECKSUMS=() +declare -a ALL_UBUNTU=() + +GENERATED=0 +FAIL_COUNT=0 + +for version_dir in "$BASE_DIR"/1-*/; do + [[ -d "$version_dir" ]] || continue + stack_yaml="$version_dir/stack.yaml" + [[ -f "$stack_yaml" ]] || continue + + k8s_version_raw=$(yq -r '.kubernetesVersion' "$stack_yaml") + k8s_short=$(echo "$k8s_version_raw" | grep -oP '^\d+\.\d+') + k8s_minor=$(echo "$k8s_short" | cut -d. -f2) + + # Filter by --version if specified + if [[ -n "$TARGET_VERSION" ]]; then + target_short=$(echo "$TARGET_VERSION" | grep -oP '^\d+\.\d+') + if [[ "$k8s_short" != "$target_short" ]]; then + continue + fi + fi + + # Resolve full K8s patch version + k8s_version=$(resolve_k8s_version "$k8s_version_raw") + + UBUNTU=$(ubuntu_for_minor "$k8s_minor") + IMAGE_NAME="ubuntu-${UBUNTU}-kube-v${k8s_version}" + IMAGE_DIR="ubuntu-${UBUNTU}-kube-v${k8s_short}" + IMAGE_URL="${BASE_URL}/${IMAGE_DIR}/${IMAGE_NAME}.qcow2" + + # Fetch checksum + CHECKSUM="" + if [[ "$SKIP_CHECKSUM" != "true" ]]; then + CHECKSUM=$(curl -sf "${IMAGE_URL}.CHECKSUM" | awk '{print $1}' || echo "") + if [[ -z "$CHECKSUM" ]]; then + echo "Failed to fetch checksum for ${k8s_version}: ${IMAGE_URL}.CHECKSUM" >&2 + echo "Use --skip-checksum to generate without hash validation" >&2 + FAIL_COUNT=$((FAIL_COUNT + 1)) + continue + fi + fi + + ALL_K8S_VERSIONS+=("$k8s_version") + ALL_IMAGE_URLS+=("$IMAGE_URL") + ALL_CHECKSUMS+=("$CHECKSUM") + ALL_UBUNTU+=("$UBUNTU") + GENERATED=$((GENERATED + 1)) +done + +# ============================================ +# Output: ORC Image CRD format +# ============================================ + +generate_orc() { + for ((i=0; i<${#ALL_K8S_VERSIONS[@]}; i++)); do + local k8s_version="${ALL_K8S_VERSIONS[$i]}" + local image_url="${ALL_IMAGE_URLS[$i]}" + local checksum="${ALL_CHECKSUMS[$i]}" + local ubuntu="${ALL_UBUNTU[$i]}" + + local hash_block="" + if [[ -n "$checksum" ]]; then + hash_block=" + hash: + algorithm: sha256 + value: ${checksum}" + fi + + local manifest="--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Image +metadata: + name: ubuntu-capi-image-v${k8s_version} +spec: + cloudCredentialsRef: + cloudName: ${CLOUD_NAME} + secretName: ${SECRET_NAME} + managementPolicy: managed + resource: + visibility: ${VISIBILITY} + properties: + hardware: + diskBus: scsi + scsiModel: virtio-scsi + vifModel: virtio + qemuGuestAgent: true + rngModel: virtio + architecture: x86_64 + minDiskGB: 20 + minMemoryMB: 2048 + operatingSystem: + distro: ubuntu + version: \"${ubuntu:0:2}.${ubuntu:2:2}\" + content: + diskFormat: qcow2 + download: + url: ${image_url}${hash_block}" + + if [[ -n "$OUTPUT_DIR" ]]; then + local outfile="${OUTPUT_DIR}/ubuntu-${ubuntu}-kube-v${k8s_version}.yaml" + echo "$manifest" > "$outfile" + echo "Written: $outfile" >&2 + else + echo "$manifest" + fi + done +} + +# ============================================ +# Output: openstack-image-manager format +# ============================================ + +generate_image_manager() { + # Build versions list + local versions_block="" + for ((i=0; i<${#ALL_K8S_VERSIONS[@]}; i++)); do + local k8s_version="${ALL_K8S_VERSIONS[$i]}" + local image_url="${ALL_IMAGE_URLS[$i]}" + local checksum="${ALL_CHECKSUMS[$i]}" + + local checksum_line="" + if [[ -n "$checksum" ]]; then + checksum_line=" + checksum: \"sha256:${checksum}\"" + fi + + versions_block+=" + - version: 'v${k8s_version}' + url: ${image_url}${checksum_line}" + done + + local manifest="--- +images: + - name: ubuntu-capi-image + enable: true + format: raw + login: ubuntu + min_disk: 20 + min_ram: 1024 + status: active + visibility: ${VISIBILITY} + multi: false + separator: \"-\" + meta: + architecture: x86_64 + hw_disk_bus: virtio + hw_rng_model: virtio + hw_scsi_model: virtio-scsi + hw_watchdog_action: reset + hypervisor_type: qemu + os_distro: ubuntu + os_purpose: k8snode + replace_frequency: never + uuid_validity: none + provided_until: none + tags: + - clusterstacks + versions:${versions_block}" + + if [[ -n "$OUTPUT_DIR" ]]; then + local outfile="${OUTPUT_DIR}/kubernetes.yaml" + echo "$manifest" > "$outfile" + echo "Written: $outfile" >&2 + else + echo "$manifest" + fi +} + +# ============================================ +# Generate output +# ============================================ + +if [[ $GENERATED -eq 0 && $FAIL_COUNT -eq 0 ]]; then + echo "No matching versions found" >&2 + exit 1 +fi + +if [[ $FAIL_COUNT -gt 0 && $GENERATED -eq 0 ]]; then + echo "All $FAIL_COUNT version(s) failed checksum fetch" >&2 + exit 1 +fi + +case "$OUTPUT_FORMAT" in + orc) generate_orc ;; + image-manager) generate_image_manager ;; +esac + +if [[ $FAIL_COUNT -gt 0 ]]; then + echo "Generated $GENERATED manifest(s), failed $FAIL_COUNT" >&2 +else + echo "Generated $GENERATED manifest(s)" >&2 +fi diff --git a/hack/generate-resources.sh b/hack/generate-resources.sh new file mode 100755 index 00000000..6a7c2893 --- /dev/null +++ b/hack/generate-resources.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash +# Generate ClusterStack and Cluster YAML resources for testing. +# +# Usage: +# ./hack/generate-resources.sh [stack-dir] --version 1.34 [options] +# +# The is the base directory containing per-minor-version subdirs +# (e.g., providers/openstack/scs). If omitted, it is derived from $PROVIDER +# and $CLUSTER_STACK (default: providers/openstack/scs). +# +# Options: +# --version K8s minor version (required) +# --cs-version Cluster stack version (default: auto-detect, see below) +# --namespace Namespace (default: cluster) +# --cluster-name Workload cluster name (default: cs-cluster) +# --cluster-only Only generate the Cluster resource +# --clusterstack-only Only generate the ClusterStack resource +# +# Auto-detection of --cs-version (in priority order): +# 1. Local .release/ directory — reads metadata.yaml from the latest matching build +# 2. OCI registry — queries tags via oras (requires OCI_REGISTRY + OCI_REPOSITORY) +# 3. Falls back to v1 +# +# Output goes to stdout. Pipe to kubectl apply -f - or redirect to a file. +# +# Examples: +# ./hack/generate-resources.sh --version 1.34 +# ./hack/generate-resources.sh --version 1.34 --cs-version v2 +# ./hack/generate-resources.sh --version 1.34 | kubectl apply -f - +# PROVIDER=docker ./hack/generate-resources.sh --version 1.35 + +set -euo pipefail + +# ============================================ +# Argument parsing +# ============================================ + +BASE_DIR="" +K8S_VERSION="" +CS_VERSION="" +NAMESPACE="cluster" +CLUSTER_NAME="cs-cluster" +CLUSTER_ONLY=false +CLUSTERSTACK_ONLY=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --version) K8S_VERSION="$2"; shift 2 ;; + --cs-version) CS_VERSION="$2"; shift 2 ;; + --namespace) NAMESPACE="$2"; shift 2 ;; + --cluster-name) CLUSTER_NAME="$2"; shift 2 ;; + --cluster-only) CLUSTER_ONLY=true; shift ;; + --clusterstack-only) CLUSTERSTACK_ONLY=true; shift ;; + -*) echo "Unknown option: $1" >&2; exit 1 ;; + *) + if [[ -z "$BASE_DIR" ]]; then + BASE_DIR="$1"; shift + else + echo "Unexpected argument: $1" >&2; exit 1 + fi + ;; + esac +done + +if [[ -z "$BASE_DIR" ]]; then + BASE_DIR="providers/${PROVIDER:-openstack}/${CLUSTER_STACK:-scs}" +fi + +if [[ -z "$K8S_VERSION" ]]; then + echo "Usage: $0 [stack-dir] --version X.Y [--cs-version vN] [--namespace ns] [--cluster-name name]" >&2 + exit 1 +fi + +# Resolve version directory +K8S_DASH="${K8S_VERSION//./-}" +VERSION_DIR="$BASE_DIR/$K8S_DASH" + +if [[ ! -d "$VERSION_DIR" ]]; then + echo "Version directory not found: $VERSION_DIR" >&2 + echo "Available:" >&2 + ls -d "$BASE_DIR"/1-*/ 2>/dev/null | sed 's|.*/||; s|/$||; s/^/ /' >&2 + exit 1 +fi + +STACK_YAML="$VERSION_DIR/stack.yaml" +if [[ ! -f "$STACK_YAML" ]]; then + echo "stack.yaml not found in: $VERSION_DIR" >&2 + exit 1 +fi + +# ============================================ +# Read stack configuration +# ============================================ + +PROVIDER=$(yq -r '.provider' "$STACK_YAML") +CLUSTER_STACK=$(yq -r '.clusterStackName' "$STACK_YAML") +K8S_VERSION_RAW=$(yq -r '.kubernetesVersion' "$STACK_YAML") + +# Use full version from stack.yaml if it has patch, otherwise use the minor +K8S_FULL="$K8S_VERSION_RAW" + +# Auto-detect CS version (if not specified) +# Priority: 1. local .release/ build output 2. OCI registry 3. fall back to v1 +if [[ -z "$CS_VERSION" ]]; then + REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + RELEASE_DIR="${OUTPUT_DIR:-$REPO_ROOT/.release}" + TAG_PREFIX="${PROVIDER}-${CLUSTER_STACK}-${K8S_DASH}" + + # 1. Check .release/ for a matching build (newest first by mtime) + LATEST_BUILD="" + if [[ -d "$RELEASE_DIR" ]]; then + LATEST_BUILD=$(ls -dt "$RELEASE_DIR/${TAG_PREFIX}"-*/metadata.yaml 2>/dev/null | head -1 || true) + fi + + if [[ -n "$LATEST_BUILD" ]]; then + CS_VERSION=$(yq -r '.versions.clusterStack' "$LATEST_BUILD") + echo "# Auto-detected CS version: ${CS_VERSION} (from $(dirname "$LATEST_BUILD"))" >&2 + # 2. Try OCI registry + elif [[ -n "${OCI_REGISTRY:-}" && -n "${OCI_REPOSITORY:-}" ]] && command -v oras >/dev/null 2>&1; then + LATEST=$(oras repo tags "${OCI_REGISTRY}/${OCI_REPOSITORY}" 2>/dev/null | \ + grep -oP "^${TAG_PREFIX}-v\K[0-9]+" | sort -n | tail -1 || echo "") + if [[ -n "$LATEST" ]]; then + CS_VERSION="v${LATEST}" + echo "# Auto-detected CS version: ${CS_VERSION} (from ${OCI_REGISTRY}/${OCI_REPOSITORY})" >&2 + else + CS_VERSION="v1" + echo "# No published versions found, using default: ${CS_VERSION}" >&2 + fi + else + CS_VERSION="v1" + echo "# No .release/ build or OCI registry available, using default: ${CS_VERSION}" >&2 + fi +fi + +# ============================================ +# Generate ClusterStack resource +# ============================================ + +if [[ "$CLUSTER_ONLY" != "true" ]]; then + cat < list: - """ - Read supported versions from file for output or further usage inside this script. - - Parameters: - None - - Returns: - List of supported versions - """ - logger.info("Loading supported versions.") - version_file = SOURCE_PATH.joinpath("versions.yaml") - with open(version_file, encoding="utf-8") as stream: - try: - result = yaml.safe_load(stream) - except yaml.YAMLError as exc: - print(exc) - - return result - - -def get_dash_version(version: str) -> str: - """ - Helper function to convert version from dotted to separated by hyphen. (1.27.14 -> 1-27) - Parameters: - version (string): String containing the full semver version. - - Returns: - String with shortened version separated by a hypgen. - """ - return "-".join(version.split(".")[0:2]) - - -def create_output_dir(version: str) -> PosixPath: - """ - Prepare output directory by creating it and copying the source files over. - This overwrites files existing inside the output directory, as those - should not be edited manually. - - Parameters: - version (string): Semver version string as read from the supported versions list. - - Returns: - PosixPath object of the created directory. - """ - out = get_dash_version(version) - out_dir = DEFAULT_TARGET_PATH.joinpath(out) - - logger.info("Creating output directory at %s", out_dir) - - # TODO: how to handle FileExistsError? - # as the output is being generated, it *should* be safe to overwrite - if out_dir.exists(): - shutil.rmtree(str(out_dir)) - - # Copy whole tree from src dir and remove "versions.yaml" file - shutil.copytree(SOURCE_PATH, out_dir) - out_dir.joinpath("versions.yaml").unlink() - for file in out_dir.joinpath("cluster-addon").rglob("Chart.lock"): - file.unlink() - for folder in out_dir.joinpath("cluster-addon").rglob("charts"): - shutil.rmtree(folder) - - return out_dir - - -def readfile(path: PosixPath): - """ - Helper function to read yaml configuration files. - - Parameters: - path (PosixPath): pathlib object of the file to open. - - Returns: - Content of the yaml configuration file. - """ - # TODO: yaml.safe_load either returns a list or dict, - # depending on the structure of the yaml file. This can be improved / refactored. - with open(path, encoding="utf-8") as stream: - try: - content = yaml.safe_load(stream) - except yaml.YAMLError as exc: - print(exc) - - return content - - -def writefile(path: PosixPath, content): - """ - Helper function to write content to a yaml configuration file. - - Parameters: - path (PosixPath): pathlib object of the target file path. - content: yaml data to be written. This can either be of type list or dict. - - Returns: - None - """ - with open(path, "w", encoding="utf-8") as stream: - yaml.safe_dump(content, stream) - - -def update_cluster_addon( - target: PosixPath, build: bool, build_verbose: bool, **versions -): - """ - Update relevant files inside the cluster-stacks//cluster-addon subdirectory - - Parameters: - target (PosixPath): pathlib object of the relevant file. - build (boolean): Toggle to control if helm dependencies should be build, - build_verbose (boolean): Toggle to control if build output should be printed. - versions (kwargs): Dictionary of version information. - - Returns: - None - """ - logger.info("Updating %s", target) - content = readfile(target) - - for dep in content["dependencies"]: - if dep["name"] == "openstack-cinder-csi": - dep["version"] = versions["cinder_csi"] - - if dep["name"] == "openstack-cloud-controller-manager": - dep["version"] = versions["occm"] - - content["name"] = ( - f"openstack-scs-{get_dash_version(versions['kubernetes'])}-cluster-addon" - ) - - writefile(target, content) - - if build: - logger.info("Building helm dependencies") - cmd = ["helm", "dependency", "build"] - subprocess.run( - cmd, - cwd=str(target).replace("Chart.yaml", ""), - capture_output=build_verbose, - check=False, - ) - - -def update_csctl_conf(target: PosixPath, **versions): - """ - Function to update csctl configuration file. - - Parameters: - target (PosixPath): pathlib object of the relevant file. - versions (kwargs): Dictionary of version information. - - Returns: - None - """ - logger.info("Updating %s", target) - content = readfile(target) - - content["config"]["kubernetesVersion"] = f"v{versions['kubernetes']}" - - writefile(target, content) - - -def update_cluster_class(target: PosixPath, **kwargs): - """ - Update relevant files inside the cluster-stacks//cluster-class subdirectory. - - Parameters: - target (PosixPath): pathlib object of the relevant file. - versions (kwargs): Dictionary of version information. - - Returns: - None - """ - chart_file = target.joinpath("Chart.yaml") - values_file = target.joinpath("values.yaml") - - logger.info("Updating %s", chart_file) - content = readfile(chart_file) - version = get_dash_version(kwargs["kubernetes"]) - content["name"] = f"openstack-scs-{version}-cluster-class" - - writefile(chart_file, content) - - logger.info("Updating %s", values_file) - content = readfile(values_file) - - content["images"]["controlPlane"][ - "name" - ] = f"ubuntu-capi-image-v{kwargs['kubernetes']}" - content["images"]["worker"]["name"] = f"ubuntu-capi-image-v{kwargs['kubernetes']}" - - writefile(values_file, content) - - -def update_node_images(target: PosixPath, **kwargs): - """ - Update relevant files inside the cluster-stacks//node-images subdirectory. - - Parameters: - target (PosixPath): pathlib object of the relevant file. - versions (kwargs): Dictionary of version information. - - Returns: - None - """ - logger.info("Updating %s", target) - content = readfile(target) - - # TODO: can this magic URL be 'removed'? - # pylint: disable=locally-disabled, line-too-long - url = f"https://swift.services.a.regiocloud.tech/swift/v1/AUTH_b182637428444b9aa302bb8d5a5a418c/openstack-k8s-capi-images/ubuntu-2204-kube-v{kwargs['kubernetes'][0:4]}/ubuntu-2204-kube-v{kwargs['kubernetes']}.qcow2" - content["spec"]["resource"]["content"]["download"]["url"] = url - - checksum_url = f"{url}.CHECKSUM" - response = requests.get(checksum_url, timeout=30) - response.raise_for_status() - - checksum_line = response.text.strip() - checksum = checksum_line.split()[0] - - content["spec"]["resource"]["content"]["download"]["hash"]["value"] = checksum - logger.info("Updated checksum: %s", checksum) - - writefile(target, content) - - -if __name__ == "__main__": - LOGFORMAT = "%(asctime)s - %(levelname)s: %(message)s" - logging.basicConfig(level=logging.INFO, encoding="utf-8", format=LOGFORMAT) - # Initialize arg parser - parser = argparse.ArgumentParser() - parser.add_argument( - "-t", - "--target-version", - type=str, - help="Generate files for version specified like 1.XX. See '-l' to list supported versions.", - ) - parser.add_argument( - "-l", "--list", action="store_true", help="List supported versions and exit." - ) - parser.add_argument("--build", action="store_true", help="Build helm dependencies.") - parser.add_argument( - "--build-verbose", action="store_false", help="Show output of helm build" - ) - args = parser.parse_args() - - # Load supported target versions - sup_versions = load_supported_versions() - - if args.list: - print("Supported Kubernetes Versions:") - for v in sup_versions: - print(f"{'.'.join(v['kubernetes'].split('.')[0:2])}") - print("Usage: generate_version.py --target-version VERSION") - sys.exit() - - # filter versions to generate - if args.target_version: - target_versions = [ - v for v in sup_versions if v["kubernetes"].startswith(args.target_version) - ] - else: - target_versions = sup_versions - - for tv in target_versions: - output_dir = create_output_dir(tv["kubernetes"]) - for chart_yaml in output_dir.joinpath("cluster-addon").rglob("Chart.yaml"): - update_cluster_addon( - chart_yaml, - args.build, - args.build_verbose, - **tv, - ) - update_csctl_conf(output_dir.joinpath("csctl.yaml"), **tv) - update_cluster_class(output_dir.joinpath("cluster-class"), **tv) - update_node_images( - output_dir.joinpath("cluster-class", "templates", "image.yaml"), **tv - ) diff --git a/hack/kind-dev.sh b/hack/kind-dev.sh deleted file mode 100755 index 70c2d99e..00000000 --- a/hack/kind-dev.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -# Copyright 2023 The Kubernetes Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -o errexit -set -o pipefail -set -x - -K8S_VERSION=v1.27.2 - -REPO_ROOT=$(git rev-parse --show-toplevel) -cd "${REPO_ROOT}" || exit 1 - -# Creates a kind cluster with the ctlptl tool https://github.com/tilt-dev/ctlptl -function ctlptl_kind-cluster() { - - local CLUSTER_NAME=$1 - local CLUSTER_VERSION=$2 - - cat < is omitted, it is derived from $PROVIDER and $CLUSTER_STACK +# (default: providers/openstack/scs). +# +# Environment: +# PROVIDER Provider name (default: openstack) +# CLUSTER_STACK Cluster stack name (default: scs) +# OCI_REGISTRY OCI registry to query for CS versions (optional) +# OCI_REPOSITORY OCI repository to query for CS versions (optional) + +set -euo pipefail + +MARKDOWN=false +BASE_DIR="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --markdown) MARKDOWN=true; shift ;; + -*) echo "Unknown option: $1" >&2; exit 1 ;; + *) + if [[ -z "$BASE_DIR" ]]; then + BASE_DIR="$1"; shift + else + echo "Unexpected argument: $1" >&2; exit 1 + fi + ;; + esac +done + +if [[ -z "$BASE_DIR" ]]; then + BASE_DIR="providers/${PROVIDER:-openstack}/${CLUSTER_STACK:-scs}" +fi + +if [[ ! -d "$BASE_DIR" ]]; then + echo "Stack base directory not found: $BASE_DIR" >&2 + exit 1 +fi + +# ============================================ +# Collect data from all version directories +# ============================================ + +# First pass: discover all addon names across all versions +declare -A ALL_ADDON_NAMES +declare -A STACK_ADDONS # key: "1-XX:addon_name" → version + +PROVIDER_NAME="" +STACK_NAME="" + +for version_dir in "$BASE_DIR"/1-*/; do + [[ -d "$version_dir" ]] || continue + local_dir=$(basename "$version_dir") + + stack_yaml="$version_dir/stack.yaml" + [[ -f "$stack_yaml" ]] || continue + + if [[ -z "$PROVIDER_NAME" ]]; then + PROVIDER_NAME=$(yq -r '.provider' "$stack_yaml") + STACK_NAME=$(yq -r '.clusterStackName' "$stack_yaml") + fi + + # Collect chart dependency versions + for chart_file in "$version_dir"/cluster-addon/*/Chart.yaml; do + [[ -f "$chart_file" ]] || continue + num_deps=$(yq '.dependencies | length' "$chart_file" 2>/dev/null || echo "0") + for ((i=0; i/dev/null 2>&1 && echo "true" || echo "false") + if [[ "$has_addons" == "true" ]]; then + addon_keys=$(yq -r '.addons | keys | .[]' "$stack_yaml") + for key in $addon_keys; do + value=$(yq -r ".addons.\"${key}\"" "$stack_yaml") + # Map short names to chart names for display + case "$key" in + ccm) chart_key="openstack-cloud-controller-manager" ;; + csi) chart_key="openstack-cinder-csi" ;; + *) chart_key="$key" ;; + esac + ALL_ADDON_NAMES["$chart_key"]=1 + # Mark with range (overrides chart default) + STACK_ADDONS["${local_dir}:${chart_key}"]="$value" + done + fi +done + +if [[ -z "$PROVIDER_NAME" ]]; then + echo "No valid version directories found in: $BASE_DIR" >&2 + exit 1 +fi + +# Sort addon names +SORTED_ADDONS=($(echo "${!ALL_ADDON_NAMES[@]}" | tr ' ' '\n' | sort)) + +# ============================================ +# Collect row data +# ============================================ + +declare -a ROW_VERSIONS=() +declare -a ROW_K8S=() +declare -a ROW_CS=() +declare -A ROW_ADDON_VERSIONS # key: "row_idx:addon_name" → version + +ROW_IDX=0 +for version_dir in "$BASE_DIR"/1-*/; do + [[ -d "$version_dir" ]] || continue + local_dir=$(basename "$version_dir") + stack_yaml="$version_dir/stack.yaml" + [[ -f "$stack_yaml" ]] || continue + + k8s_version=$(yq -r '.kubernetesVersion' "$stack_yaml") + k8s_short=$(echo "$k8s_version" | grep -oP '^\d+\.\d+') + k8s_dash="${k8s_short//./-}" + + # Query OCI for CS version + CS_VERSION="-" + if [[ -n "${OCI_REGISTRY:-}" && -n "${OCI_REPOSITORY:-}" ]] && command -v oras >/dev/null 2>&1; then + TAG_PREFIX="${PROVIDER_NAME}-${STACK_NAME}-${k8s_dash}" + LATEST=$(oras repo tags "${OCI_REGISTRY}/${OCI_REPOSITORY}" 2>/dev/null | \ + grep -oP "^${TAG_PREFIX}-v\K[0-9]+" | sort -n | tail -1 || echo "") + [[ -n "$LATEST" ]] && CS_VERSION="v${LATEST}" + fi + + ROW_VERSIONS+=("$local_dir") + ROW_K8S+=("$k8s_version") + ROW_CS+=("$CS_VERSION") + + for addon in "${SORTED_ADDONS[@]}"; do + ver="${STACK_ADDONS["${local_dir}:${addon}"]:-"-"}" + ROW_ADDON_VERSIONS["${ROW_IDX}:${addon}"]="$ver" + done + ((ROW_IDX++)) || true +done + +# Short display name for an addon +addon_short() { + echo "$1" | sed 's/openstack-cloud-controller-manager/os-ccm/; s/openstack-cinder-csi/os-csi/' +} + +# ============================================ +# Print the matrix +# ============================================ + +if [[ "$MARKDOWN" == true ]]; then + # --- Markdown table output --- + HEADER="| Version | K8s | CS Version" + SEPARATOR="|---------|-----|----------" + for addon in "${SORTED_ADDONS[@]}"; do + HEADER+=" | $(addon_short "$addon")" + SEPARATOR+="|---------" + done + HEADER+=" |" + SEPARATOR+="|" + echo "$HEADER" + echo "$SEPARATOR" + + for ((i=0; i is the base directory containing per-minor-version subdirs +# (e.g., providers/openstack/scs). If omitted, it is derived from $PROVIDER +# and $CLUSTER_STACK (default: providers/openstack/scs). +# +# Options: +# --dry-run Preview changes without modifying files +# --all Run against all stacks in providers/*/*/ +# -h, --help Show this help +# +# Environment: +# PROVIDER Provider name (default: openstack) +# CLUSTER_STACK Cluster stack name (default: scs) +# GITHUB_TOKEN Optional. GitHub personal access token for higher API rate limits. +# IMAGE_BASE_URL Base URL for CAPI images (default: https://nbg1.your-objectstorage.com/...) +# IMAGE_VISIBILITY Image visibility in image-manager.yaml (default: private) + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# ============================================ +# Defaults +# ============================================ + +SUBCOMMAND="" # versions, addons, or "" (both) +BASE_DIR="" +DRY_RUN=false +RUN_ALL=false + +# Image-manager defaults (for OpenStack image manifest generation) +IMAGE_BASE_URL="${IMAGE_BASE_URL:-https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images}" +IMAGE_VISIBILITY="${IMAGE_VISIBILITY:-private}" + +# ============================================ +# Ubuntu version mapping +# ============================================ + +# K8s 1.32 and earlier → Ubuntu 22.04 (2204) +# K8s 1.33 and later → Ubuntu 24.04 (2404) +ubuntu_for_minor() { + local minor="$1" + if [[ "$minor" -le 32 ]]; then + echo "2204" + else + echo "2404" + fi +} + +# ============================================ +# Output helpers +# ============================================ + +info() { echo " $*"; } +ok() { echo " ✓ $*"; } +warn() { echo " ! $*"; } +change() { echo " → $*"; } + +# ============================================ +# Argument parsing +# ============================================ + +usage() { + sed -n '3,/^$/s/^# \?//p' "$0" + exit "${1:-1}" +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + versions|addons) + if [[ -n "$SUBCOMMAND" ]]; then + echo "Error: multiple subcommands given" >&2; usage + fi + SUBCOMMAND="$1"; shift + ;; + --dry-run) DRY_RUN=true; shift ;; + --all) RUN_ALL=true; shift ;; + -h|--help) usage 0 ;; + -*) echo "Unknown option: $1" >&2; usage ;; + *) + if [[ -z "$BASE_DIR" ]]; then + BASE_DIR="$1"; shift + else + echo "Unexpected argument: $1" >&2; usage + fi + ;; + esac + done +} + +resolve_base_dir() { + local dir="$1" + + if [[ -z "$dir" ]]; then + dir="providers/${PROVIDER:-openstack}/${CLUSTER_STACK:-scs}" + fi + + # Resolve relative path + if [[ ! "$dir" = /* ]]; then + dir="$REPO_ROOT/$dir" + fi + + echo "$dir" +} + +# Detect provider from stack dir path +detect_provider() { + local dir="$1" + local rel="${dir#"$REPO_ROOT"/}" + echo "$rel" | cut -d/ -f2 +} + +# ============================================ +# K8s version fetchers (provider-aware) +# ============================================ + +github_curl() { + local url="$1" + local -a headers=() + if [[ -n "${GITHUB_TOKEN:-}" ]]; then + headers=(-H "Authorization: token $GITHUB_TOKEN") + fi + curl -sfL "${headers[@]+"${headers[@]}"}" "$url" +} + +fetch_k8s_versions_github() { + local page=1 + local all_tags="" + + while true; do + local tags + tags=$(github_curl "https://api.github.com/repos/kubernetes/kubernetes/tags?per_page=100&page=$page" \ + | jq -r '.[].name // empty') || { echo "Error fetching K8s tags from GitHub" >&2; return 1; } + + [[ -z "$tags" ]] && break + all_tags+="$tags"$'\n' + page=$((page + 1)) + + if echo "$tags" | grep -qE '^v1\.2[0-9]\.'; then + break + fi + done + + echo "$all_tags" | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sed 's/^v//' | sort -V +} + +fetch_k8s_versions_dockerhub() { + local page=1 + local all_tags="" + + while true; do + local response + response=$(curl -sfL "https://registry.hub.docker.com/v2/repositories/kindest/node/tags?page_size=100&page=$page") \ + || { echo "Error fetching kindest/node tags from Docker Hub" >&2; return 1; } + + local tags + tags=$(echo "$response" | jq -r '.results[].name // empty') + [[ -z "$tags" ]] && break + + all_tags+="$tags"$'\n' + + local next + next=$(echo "$response" | jq -r '.next // empty') + [[ -z "$next" ]] && break + + page=$((page + 1)) + + if echo "$tags" | grep -qE '^v1\.2[0-9]\.'; then + break + fi + done + + echo "$all_tags" | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sed 's/^v//' | sort -V +} + +fetch_k8s_versions() { + local provider="$1" + case "$provider" in + docker) fetch_k8s_versions_dockerhub ;; + *) fetch_k8s_versions_github ;; + esac +} + +# Get the highest patch version for a given minor from a list of versions. +highest_patch() { + local minor="$1" + grep -E "^1\.${minor}\.[0-9]+$" | sort -V | tail -1 +} + +# ============================================ +# Helm repo URL maps for K8s-tied addons +# ============================================ + +declare -A ADDON_HELM_REPOS=( + ["openstack-cloud-controller-manager"]="https://kubernetes.github.io/cloud-provider-openstack" + ["openstack-cinder-csi"]="https://kubernetes.github.io/cloud-provider-openstack" +) + +declare -A ADDON_SHORT_TO_CHART=( + ["ccm"]="openstack-cloud-controller-manager" + ["csi"]="openstack-cinder-csi" +) + +# ============================================ +# Image-manager update (OpenStack only) +# ============================================ + +# Update providers///image-manager.yaml with merge semantics: +# - New minor versions are added +# - Existing minor versions are updated (new patch replaces old) +# - Minor versions no longer in 1-*/ dirs are preserved (never removed) +# +# Args: $1 = base_dir, $2 = name of associative array with resolved patches +update_image_manager() { + local base_dir="${1%/}" # strip trailing slash + local -n patches_ref=$2 # nameref to associative array + + local image_file="$base_dir/image-manager.yaml" + echo "Updating image-manager manifest: $image_file" + + # Build map of existing versions from file (if present), keyed by minor + # Each entry: "version|url|checksum" + declare -A existing_versions=() + if [[ -f "$image_file" ]]; then + local num_versions + num_versions=$(yq '.images[0].versions | length' "$image_file" 2>/dev/null || echo "0") + for ((i=0; i "$image_file" </dev/null || echo "0") + for ((i=0; i/dev/null 2>&1 || true + repos_added["$dep_name"]="$dep_repo" + fi + done + done + done + + if [[ ${#repos_added[@]} -gt 0 ]]; then + info "Updating Helm repos..." + helm repo update > /dev/null 2>&1 + fi + echo "" + + local total_updates=0 + + for version_dir in "$base_dir"/1-*/; do + [[ -d "$version_dir" ]] || continue + local dir_name + dir_name=$(basename "$version_dir") + + echo "=== $dir_name ===" + + for addon_dir in "$version_dir"/cluster-addon/*/; do + [[ -f "$addon_dir/Chart.yaml" ]] || continue + local addon_name chart_file + addon_name=$(basename "$addon_dir") + chart_file="$addon_dir/Chart.yaml" + + local num_deps + num_deps=$(yq '.dependencies | length' "$chart_file" 2>/dev/null || echo "0") + [[ "$num_deps" == "0" || "$num_deps" == "null" ]] && continue + + for ((i=0; i/dev/null) + if [[ -n "$range" && "$range" != "null" ]]; then + is_tied=true + fi + fi + done + fi + + # Get latest version + local latest_version="" + if [[ "$is_tied" == "true" ]]; then + # For K8s-tied addons, match by prefix from stack.yaml range + local k8s_minor + k8s_minor=$(yq -r '.kubernetesVersion' "$stack_yaml" | grep -oP '\.\K\d+') + latest_version=$(helm search repo "$dep_name/$dep_name" --versions -o json 2>/dev/null | \ + jq -r --arg minor "$k8s_minor" \ + '[.[] | select(.version | startswith("2." + $minor + "."))] | .[0].version // empty' 2>/dev/null) || true + else + latest_version=$(helm search repo "$dep_name/$dep_name" -o json 2>/dev/null | \ + jq -r '.[0].version // empty' 2>/dev/null) || true + fi + + if [[ -z "$latest_version" ]]; then + info "$dep_name: could not query upstream" + continue + fi + + if [[ "$current_version" == "$latest_version" ]]; then + ok "$dep_name: $current_version (up to date)" + continue + fi + + change "$dep_name: $current_version → $latest_version" + total_updates=$((total_updates + 1)) + + if [[ "$DRY_RUN" != "true" ]]; then + yq -i ".dependencies[$i].version = \"$latest_version\"" "$chart_file" + info "Updated $chart_file" + fi + done + done + echo "" + done + + if [[ "$DRY_RUN" == "true" ]]; then + if [[ "$total_updates" -gt 0 ]]; then + echo "$total_updates addon update(s) available (dry-run: no changes written)." + else + echo "All addons are up to date." + fi + elif [[ "$total_updates" -gt 0 ]]; then + echo "Applied $total_updates addon update(s)." + else + echo "All addons are up to date." + fi +} + +# ============================================ +# --all mode: iterate all stacks +# ============================================ + +run_all() { + local subcommand="$1" + local found=false + + # Find base dirs: providers/// that contain 1-*/ subdirs + for base_dir in "$REPO_ROOT"/providers/*/*/; do + # Must have at least one 1-*/ subdir + ls "$base_dir"/1-*/stack.yaml >/dev/null 2>&1 || continue + + found=true + echo "==========================================" + echo "$base_dir" + echo "==========================================" + + if [[ -z "$subcommand" ]]; then + cmd_versions "$base_dir" + echo "" + cmd_addons "$base_dir" + elif [[ "$subcommand" == "versions" ]]; then + cmd_versions "$base_dir" + elif [[ "$subcommand" == "addons" ]]; then + cmd_addons "$base_dir" + fi + + echo "" + done + + if [[ "$found" == false ]]; then + echo "No stacks found in providers/*/*/" >&2 + exit 1 + fi +} + +# ============================================ +# Main +# ============================================ + +main() { + parse_args "$@" + + if [[ "$RUN_ALL" == true ]]; then + run_all "$SUBCOMMAND" + return + fi + + BASE_DIR=$(resolve_base_dir "$BASE_DIR") + + if [[ ! -d "$BASE_DIR" ]]; then + echo "Stack base directory not found: $BASE_DIR" >&2 + exit 1 + fi + + if [[ -z "$SUBCOMMAND" ]]; then + cmd_versions "$BASE_DIR" + echo "" + cmd_addons "$BASE_DIR" + elif [[ "$SUBCOMMAND" == "versions" ]]; then + cmd_versions "$BASE_DIR" + elif [[ "$SUBCOMMAND" == "addons" ]]; then + cmd_addons "$BASE_DIR" + fi +} + +main "$@" diff --git a/justfile b/justfile new file mode 100644 index 00000000..e88644f3 --- /dev/null +++ b/justfile @@ -0,0 +1,181 @@ +# Cluster Stacks build system +# Usage: just [args...] +# Config: set PROVIDER and CLUSTER_STACK env vars or in .env (default: openstack/scs) +# +# All hack/ scripts derive the stack base directory from $PROVIDER and $CLUSTER_STACK +# automatically (e.g., providers/openstack/scs). Each base directory contains +# per-minor-version subdirs (1-32, 1-33, etc.) with self-contained stack.yaml. +# +# Container mode: set RUN_IN_CONTAINER=true to transparently run recipes inside +# the tools container. Build the image first with: just container-build + +set dotenv-load +set positional-arguments + +export PROVIDER := env("PROVIDER", "openstack") +export CLUSTER_STACK := env("CLUSTER_STACK", "scs") +RUN_IN_CONTAINER := env("RUN_IN_CONTAINER", "false") +CONTAINER_IMAGE := "cluster-stack-tools" + +# Show available recipes +default: + @just --list + +# ============================================ +# Build & Publish +# ============================================ + +# Build cluster-stack (e.g., just build --version 1.34 or just build --all) +build *FLAGS: + {{ if RUN_IN_CONTAINER == "true" { "just _container-exec build " + FLAGS } else { "./hack/build.sh " + FLAGS } }} + +# Build and publish (e.g., just publish --version 1.34 or just publish --all) +publish *FLAGS: + {{ if RUN_IN_CONTAINER == "true" { "just _container-exec publish " + FLAGS } else { "./hack/build.sh --publish " + FLAGS } }} + +# Build, publish, and generate the ClusterStack resource (e.g., just dev --version 1.35) +dev *FLAGS: + #!/usr/bin/env bash + set -euo pipefail + ./hack/build.sh --publish {{FLAGS}} + # Extract --version from flags for generate-resources + version="" + prev="" + for arg in {{FLAGS}}; do + if [[ "$prev" == "--version" ]]; then version="$arg"; fi + prev="$arg" + done + if [[ -n "$version" ]]; then + echo "" + echo "================================================================" + echo "ClusterStack resource (pipe to kubectl apply -f -)" + echo "================================================================" + ./hack/generate-resources.sh --version "$version" --clusterstack-only + fi + +# Install/upgrade the CSO with OCI config matching current environment +install-cso: + #!/usr/bin/env bash + set -euo pipefail + if [[ -z "${OCI_REGISTRY:-}" ]]; then + export OCI_REGISTRY="ttl.sh" + export OCI_REPOSITORY="clusterstacks-$(date +%Y%m%d)" + echo "Auto-configured ttl.sh: $OCI_REGISTRY/$OCI_REPOSITORY (expires in 24h)" + fi + CSO_CHART="${CSO_CHART:-oci://registry.scs.community/cluster-stacks/cso}" + echo "Installing/upgrading CSO..." + echo " Chart: $CSO_CHART" + echo " OCI config: $OCI_REGISTRY/${OCI_REPOSITORY:-}" + echo "" + helm upgrade -i cso "$CSO_CHART" \ + --namespace cso-system --create-namespace \ + --set controllerManager.manager.source=oci \ + --set "clusterStackVariables.ociRegistry=${OCI_REGISTRY}" \ + --set "clusterStackVariables.ociRepository=${OCI_REPOSITORY}" + +# Clean build artifacts +clean: + rm -rf .release + @echo "Cleaned .release" + +# ============================================ +# Update +# ============================================ + +# Update K8s versions and/or addon charts (subcommands: versions, addons) +update *FLAGS: + {{ if RUN_IN_CONTAINER == "true" { "just _container-exec update " + FLAGS } else { "./hack/update.sh " + FLAGS } }} + +# ============================================ +# Resource Generation +# ============================================ + +# Generate ClusterStack + Cluster YAML (e.g., just generate-resources --version 1.34) +generate-resources *FLAGS: + {{ if RUN_IN_CONTAINER == "true" { "just _container-exec generate-resources " + FLAGS } else { "./hack/generate-resources.sh " + FLAGS } }} + +# Generate only the ClusterStack resource +generate-clusterstack *FLAGS: + {{ if RUN_IN_CONTAINER == "true" { "just _container-exec generate-clusterstack " + FLAGS } else { "./hack/generate-resources.sh --clusterstack-only " + FLAGS } }} + +# Generate only the Cluster resource +generate-cluster *FLAGS: + {{ if RUN_IN_CONTAINER == "true" { "just _container-exec generate-cluster " + FLAGS } else { "./hack/generate-resources.sh --cluster-only " + FLAGS } }} + +# Generate OpenStack Image CRD manifests +generate-image-manifests *FLAGS: + {{ if RUN_IN_CONTAINER == "true" { "just _container-exec generate-image-manifests " + FLAGS } else { "./hack/generate-image-manifests.sh " + FLAGS } }} + +# ============================================ +# Info +# ============================================ + +# Show version matrix for all K8s versions and addons +matrix: + {{ if RUN_IN_CONTAINER == "true" { "just _container-exec matrix" } else { "./hack/show-matrix.sh" } }} + +# Generate configuration docs for all stacks +generate-docs: + #!/usr/bin/env bash + set -euo pipefail + template="hack/config-template.md" + # For each provider/stack, pick the latest version directory and generate docs + for stack_base in providers/*/*; do + [[ -d "$stack_base" ]] || continue + provider=$(basename "$(dirname "$stack_base")") + stack=$(basename "$stack_base") + # Find the latest version dir (highest 1-XX) + latest=$(ls -d "$stack_base"/1-*/ 2>/dev/null | sort -V | tail -1) + [[ -n "$latest" ]] || continue + outdir="docs/providers/${provider}" + outfile="${outdir}/${stack}-configuration.md" + mkdir -p "$outdir" + echo "Generating ${outfile} from ${latest} ..." + python3 ./hack/docugen.py "$latest" \ + --template "$template" \ + --matrix \ + --output "$outfile" + done + echo "Done." + +# ============================================ +# Container +# ============================================ + +# Detect container runtime (podman preferred, fallback to docker) +[private] +container-runtime: + #!/usr/bin/env bash + if command -v podman &>/dev/null; then echo "podman" + elif command -v docker &>/dev/null; then echo "docker" + else echo "ERROR: neither podman nor docker found" >&2; exit 1 + fi + +# Build the tools container image +container-build: + #!/usr/bin/env bash + set -euo pipefail + runtime=$(just container-runtime) + echo "Building {{CONTAINER_IMAGE}} with $runtime..." + $runtime build -t {{CONTAINER_IMAGE}} -f Containerfile . + +# Run a just recipe inside the container (used internally when RUN_IN_CONTAINER=true) +[private] +_container-exec +ARGS: + #!/usr/bin/env bash + set -euo pipefail + runtime=$(just container-runtime) + # Build image if it doesn't exist + if ! $runtime image exists {{CONTAINER_IMAGE}} 2>/dev/null && \ + ! $runtime images --format '{{"{{.Repository}}"}}' | grep -qx '{{CONTAINER_IMAGE}}'; then + echo "Image {{CONTAINER_IMAGE}} not found, building..." + just container-build + fi + $runtime run --rm -it \ + -v "$(pwd):/workspace" \ + -w /workspace \ + -e PROVIDER="$PROVIDER" \ + -e CLUSTER_STACK="$CLUSTER_STACK" \ + -e RUN_IN_CONTAINER=false \ + {{CONTAINER_IMAGE}} \ + just {{ARGS}} From 3032d25d0067066d34c56ef401c8c0db83396785 Mon Sep 17 00:00:00 2001 From: Jan Schoone Date: Mon, 23 Feb 2026 17:13:15 +0000 Subject: [PATCH 02/11] refactor(stacks): Restructure to per-Kubernetes-minor-version directories Reorganize all cluster stacks into self-contained per-minor-version directories: providers///1-XX/ stack.yaml # metadata: provider, name, k8s version, addon pins cluster-class/ # Helm chart producing ClusterClass cluster-addon/ # Helm chart with CNI, CCM, CSI, metrics-server node-images/ # image build definitions (OpenStack only) Each directory is fully independent -- no inheritance or sharing between minor versions. This supports different CAPI API versions (v1beta1 for 1-32, v1beta2 for 1-35) and version-specific addon pins (CCM 2.34.x for K8s 1.34). Stacks created: - openstack/scs: 1-32 (v1beta1, snake_case), 1-33 (v1beta1, camelCase), 1-34 (copy of 1-33), 1-35 (v1beta2, unified variables) - docker/scs: 1-32 through 1-35 Also adds providers/openstack/scs/image-manager.yaml (aggregated image references for all OpenStack minor versions). Removes: - providers/openstack/scs2/ (consolidated into openstack/scs) - Old flat directory structure (csctl.yaml, versions.yaml, etc.) Assisted-by: Claude Code Signed-off-by: Jan Schoone --- .../{ => 1-32}/cluster-addon/cni/Chart.yaml | 2 +- .../{ => 1-32}/cluster-addon/cni/values.yaml | 0 .../cluster-addon/metrics-server/Chart.yaml | 2 +- .../cluster-addon/metrics-server/values.yaml | 0 .../scs/{ => 1-32}/cluster-class/Chart.yaml | 2 +- .../cluster-class/templates/_helpers.tpl | 0 .../templates/cluster-class.yaml | 0 .../templates/docker-cluster-template.yaml | 0 .../templates/docker-machine-template.yaml | 0 ...kubeadm-config-template-worker-docker.yaml | 0 .../kubeadm-control-plane-template.yaml | 0 .../scs/{ => 1-32}/cluster-class/values.yaml | 0 .../docker/scs/{ => 1-32}/clusteraddon.yaml | 0 providers/docker/scs/1-32/stack.yaml | 3 + .../scs/1-33/cluster-addon/cni/Chart.yaml | 9 + .../scs/1-33}/cluster-addon/cni/values.yaml | 0 .../cluster-addon/metrics-server/Chart.yaml | 9 + .../metrics-server/overwrite.yaml | 0 .../cluster-addon/metrics-server/values.yaml | 0 .../docker/scs/1-33/cluster-class/Chart.yaml | 7 + .../1-33/cluster-class/templates/_helpers.tpl | 29 + .../templates/cluster-class.yaml | 185 ++++ .../templates/docker-cluster-template.yaml | 8 + .../templates/docker-machine-template.yaml | 11 + ...kubeadm-config-template-worker-docker.yaml | 14 + .../kubeadm-control-plane-template.yaml | 84 ++ .../docker/scs/1-33/cluster-class/values.yaml | 6 + providers/docker/scs/1-33/clusteraddon.yaml | 18 + providers/docker/scs/1-33/stack.yaml | 3 + .../scs/1-34/cluster-addon/cni/Chart.yaml | 9 + .../scs/1-34/cluster-addon/cni/values.yaml | 14 + .../cluster-addon/metrics-server/Chart.yaml | 9 + .../metrics-server/overwrite.yaml | 0 .../cluster-addon/metrics-server/values.yaml | 0 .../docker/scs/1-34/cluster-class/Chart.yaml | 7 + .../1-34/cluster-class/templates/_helpers.tpl | 29 + .../templates/cluster-class.yaml | 185 ++++ .../templates/docker-cluster-template.yaml | 8 + .../templates/docker-machine-template.yaml | 11 + ...kubeadm-config-template-worker-docker.yaml | 14 + .../kubeadm-control-plane-template.yaml | 84 ++ .../docker/scs/1-34/cluster-class/values.yaml | 6 + providers/docker/scs/1-34/clusteraddon.yaml | 18 + providers/docker/scs/1-34/stack.yaml | 3 + .../scs/1-35/cluster-addon/cni/Chart.yaml | 9 + .../scs/1-35/cluster-addon/cni/values.yaml | 14 + .../cluster-addon/metrics-server/Chart.yaml | 9 + .../metrics-server/overwrite.yaml} | 0 .../cluster-addon/metrics-server/values.yaml | 4 + .../docker/scs/1-35/cluster-class/Chart.yaml | 7 + .../1-35/cluster-class/templates/_helpers.tpl | 51 ++ .../templates/cluster-class.yaml | 314 +++++++ .../templates/docker-cluster-template.yaml | 7 + .../templates/docker-machine-template.yaml | 10 + .../kubeadm-config-template-worker.yaml | 15 + .../kubeadm-control-plane-template.yaml | 99 ++ .../docker/scs/1-35/cluster-class/values.yaml | 20 + providers/docker/scs/1-35/clusteraddon.yaml | 18 + providers/docker/scs/1-35/stack.yaml | 3 + providers/docker/scs/README.md | 89 -- .../docker/scs/cluster-addon/.helmignore | 23 - .../docker/scs/cluster-addon/cni/Chart.lock | 6 - .../cni/charts/cilium-1.16.6.tgz | Bin 204717 -> 0 bytes .../cluster-addon/metrics-server/.helmignore | 23 - .../cluster-addon/metrics-server/Chart.lock | 6 - .../charts/metrics-server-3.12.2.tgz | Bin 10395 -> 0 bytes .../docker/scs/cluster-class/.helmignore | 23 - providers/docker/scs/clusterstack.yaml | 14 - providers/docker/scs/csctl.yaml | 7 - .../{ => 1-32}/cluster-addon/ccm/Chart.yaml | 0 .../cluster-addon/ccm/overwrite.yaml | 0 .../{ => 1-32}/cluster-addon/ccm/values.yaml | 0 .../{ => 1-32}/cluster-addon/cni/Chart.yaml | 8 +- .../{ => 1-32}/cluster-addon/cni/values.yaml | 0 .../scs/1-32/cluster-addon/csi/Chart.yaml | 10 + .../cluster-addon/csi/overwrite.yaml | 0 .../{ => 1-32}/cluster-addon/csi/values.yaml | 0 .../cluster-addon/metrics-server/Chart.yaml | 10 + .../metrics-server/overwrite.yaml | 4 + .../cluster-addon/metrics-server/values.yaml | 4 + .../scs/{ => 1-32}/cluster-class/Chart.yaml | 0 .../cluster-class/templates/_helpers.tpl | 0 .../templates/cluster-class.yaml | 0 .../cluster-class/templates/image.yaml | 0 ...eadm-config-template-worker-openstack.yaml | 0 .../kubeadm-control-plane-template.yaml | 0 .../templates/openstack-cluster-template.yaml | 0 ...nstack-machine-template-control-plane.yaml | 0 .../openstack-machine-template-worker.yaml | 0 .../scs/{ => 1-32}/cluster-class/values.yaml | 0 .../scs/{ => 1-32}/clusteraddon.yaml | 0 providers/openstack/scs/1-32/stack.yaml | 7 + .../1-33}/cluster-addon/ccm/Chart.yaml | 2 +- .../1-33}/cluster-addon/ccm/overwrite.yaml | 0 .../1-33}/cluster-addon/ccm/values.yaml | 0 .../1-33}/cluster-addon/cni/Chart.yaml | 2 +- .../scs/1-33/cluster-addon/cni/values.yaml | 14 + .../1-33}/cluster-addon/csi/Chart.yaml | 2 +- .../1-33}/cluster-addon/csi/overwrite.yaml | 0 .../1-33}/cluster-addon/csi/values.yaml | 0 .../cluster-addon/metrics-server/Chart.yaml | 0 .../metrics-server/overwrite.yaml | 4 + .../cluster-addon/metrics-server/values.yaml | 4 + .../1-33}/cluster-class/Chart.yaml | 4 +- .../cluster-class/templates/_helpers.tpl | 0 .../templates/cluster-class.yaml | 0 ...eadm-config-template-worker-openstack.yaml | 0 .../kubeadm-control-plane-template.yaml | 0 .../templates/openstack-cluster-template.yaml | 0 ...nstack-machine-template-control-plane.yaml | 0 .../openstack-machine-template-worker.yaml | 0 .../1-33}/cluster-class/values.yaml | 0 .../{scs2 => scs/1-33}/clusteraddon.yaml | 0 providers/openstack/scs/1-33/stack.yaml | 7 + .../scs/1-34/cluster-addon/ccm/Chart.yaml | 10 + .../scs/1-34/cluster-addon/ccm/overwrite.yaml | 4 + .../scs/1-34/cluster-addon/ccm/values.yaml | 21 + .../scs/1-34/cluster-addon/cni/Chart.yaml | 10 + .../scs/1-34/cluster-addon/cni/values.yaml | 14 + .../scs/1-34/cluster-addon/csi/Chart.yaml | 10 + .../scs/1-34/cluster-addon/csi/overwrite.yaml | 3 + .../scs/1-34/cluster-addon/csi/values.yaml | 41 + .../cluster-addon/metrics-server/Chart.yaml | 10 + .../metrics-server/overwrite.yaml | 4 + .../cluster-addon/metrics-server/values.yaml | 4 + .../scs/1-34/cluster-class/Chart.yaml | 9 + .../1-34/cluster-class/templates/_helpers.tpl | 62 ++ .../templates/cluster-class.yaml | 844 ++++++++++++++++++ ...eadm-config-template-worker-openstack.yaml | 13 + .../kubeadm-control-plane-template.yaml | 89 ++ .../templates/openstack-cluster-template.yaml | 45 + ...nstack-machine-template-control-plane.yaml | 12 + .../openstack-machine-template-worker.yaml | 12 + .../scs/1-34/cluster-class/values.yaml | 0 .../openstack/scs/1-34/clusteraddon.yaml | 21 + providers/openstack/scs/1-34/stack.yaml | 7 + .../scs/1-35/cluster-addon/ccm/Chart.yaml | 10 + .../scs/1-35/cluster-addon/ccm/overwrite.yaml | 4 + .../scs/1-35/cluster-addon/ccm/values.yaml | 21 + .../scs/1-35/cluster-addon/cni/Chart.yaml | 10 + .../scs/1-35/cluster-addon/cni/values.yaml | 14 + .../scs/1-35/cluster-addon/csi/Chart.yaml | 10 + .../scs/1-35/cluster-addon/csi/overwrite.yaml | 3 + .../scs/1-35/cluster-addon/csi/values.yaml | 41 + .../cluster-addon/metrics-server/Chart.yaml | 10 + .../metrics-server/overwrite.yaml | 4 + .../cluster-addon/metrics-server/values.yaml | 4 + .../scs/1-35/cluster-class/Chart.yaml | 9 + .../1-35/cluster-class/templates/_helpers.tpl | 62 ++ .../templates/cluster-class.yaml | 837 +++++++++++++++++ ...eadm-config-template-worker-openstack.yaml | 15 + .../kubeadm-control-plane-template.yaml | 109 +++ .../templates/openstack-cluster-template.yaml | 91 ++ ...nstack-machine-template-control-plane.yaml | 12 + .../openstack-machine-template-worker.yaml | 12 + .../scs/1-35/cluster-class/values.yaml | 50 ++ .../openstack/scs/1-35/clusteraddon.yaml | 30 + providers/openstack/scs/1-35/stack.yaml | 7 + providers/openstack/scs/README.md | 120 --- .../scs/cluster-addon/ccm/Chart.lock | 6 - ...nstack-cloud-controller-manager-2.32.0.tgz | Bin 18995 -> 0 bytes .../scs/cluster-addon/cni/Chart.lock | 6 - .../cni/charts/cilium-1.17.4.tgz | Bin 219592 -> 0 bytes .../scs/cluster-addon/csi/Chart.lock | 6 - .../scs/cluster-addon/csi/Chart.yaml | 10 - .../charts/openstack-cinder-csi-2.32.0.tgz | Bin 7100 -> 0 bytes .../cluster-addon/metrics-server/.helmignore | 23 - .../cluster-addon/metrics-server/Chart.lock | 6 - .../cluster-addon/metrics-server/Chart.yaml | 10 - .../charts/metrics-server-3.12.2.tgz | Bin 10395 -> 0 bytes .../openstack/scs/cluster-class/.helmignore | 23 - providers/openstack/scs/csctl.yaml | 7 - providers/openstack/scs/image-manager.yaml | 39 + providers/openstack/scs/versions.yaml | 9 - providers/openstack/scs2/README.md | 149 ---- .../openstack/scs2/cluster-class/.helmignore | 23 - providers/openstack/scs2/csctl.yaml | 7 - providers/openstack/scs2/image.yaml | 32 - providers/openstack/scs2/kubernetes.yaml | 55 -- providers/openstack/scs2/versions.yaml | 9 - 180 files changed, 4088 insertions(+), 704 deletions(-) rename providers/docker/scs/{ => 1-32}/cluster-addon/cni/Chart.yaml (89%) rename providers/docker/scs/{ => 1-32}/cluster-addon/cni/values.yaml (100%) rename providers/docker/scs/{ => 1-32}/cluster-addon/metrics-server/Chart.yaml (91%) rename providers/docker/scs/{ => 1-32}/cluster-addon/metrics-server/values.yaml (100%) rename providers/docker/scs/{ => 1-32}/cluster-class/Chart.yaml (67%) rename providers/docker/scs/{ => 1-32}/cluster-class/templates/_helpers.tpl (100%) rename providers/docker/scs/{ => 1-32}/cluster-class/templates/cluster-class.yaml (100%) rename providers/docker/scs/{ => 1-32}/cluster-class/templates/docker-cluster-template.yaml (100%) rename providers/docker/scs/{ => 1-32}/cluster-class/templates/docker-machine-template.yaml (100%) rename providers/docker/scs/{ => 1-32}/cluster-class/templates/kubeadm-config-template-worker-docker.yaml (100%) rename providers/docker/scs/{ => 1-32}/cluster-class/templates/kubeadm-control-plane-template.yaml (100%) rename providers/docker/scs/{ => 1-32}/cluster-class/values.yaml (100%) rename providers/docker/scs/{ => 1-32}/clusteraddon.yaml (100%) create mode 100644 providers/docker/scs/1-32/stack.yaml create mode 100644 providers/docker/scs/1-33/cluster-addon/cni/Chart.yaml rename providers/{openstack/scs2 => docker/scs/1-33}/cluster-addon/cni/values.yaml (100%) create mode 100644 providers/docker/scs/1-33/cluster-addon/metrics-server/Chart.yaml rename providers/{openstack/scs => docker/scs/1-33}/cluster-addon/metrics-server/overwrite.yaml (100%) rename providers/{openstack/scs => docker/scs/1-33}/cluster-addon/metrics-server/values.yaml (100%) create mode 100644 providers/docker/scs/1-33/cluster-class/Chart.yaml create mode 100644 providers/docker/scs/1-33/cluster-class/templates/_helpers.tpl create mode 100644 providers/docker/scs/1-33/cluster-class/templates/cluster-class.yaml create mode 100644 providers/docker/scs/1-33/cluster-class/templates/docker-cluster-template.yaml create mode 100644 providers/docker/scs/1-33/cluster-class/templates/docker-machine-template.yaml create mode 100644 providers/docker/scs/1-33/cluster-class/templates/kubeadm-config-template-worker-docker.yaml create mode 100644 providers/docker/scs/1-33/cluster-class/templates/kubeadm-control-plane-template.yaml create mode 100644 providers/docker/scs/1-33/cluster-class/values.yaml create mode 100644 providers/docker/scs/1-33/clusteraddon.yaml create mode 100644 providers/docker/scs/1-33/stack.yaml create mode 100644 providers/docker/scs/1-34/cluster-addon/cni/Chart.yaml create mode 100644 providers/docker/scs/1-34/cluster-addon/cni/values.yaml create mode 100644 providers/docker/scs/1-34/cluster-addon/metrics-server/Chart.yaml rename providers/{openstack/scs2 => docker/scs/1-34}/cluster-addon/metrics-server/overwrite.yaml (100%) rename providers/{openstack/scs2 => docker/scs/1-34}/cluster-addon/metrics-server/values.yaml (100%) create mode 100644 providers/docker/scs/1-34/cluster-class/Chart.yaml create mode 100644 providers/docker/scs/1-34/cluster-class/templates/_helpers.tpl create mode 100644 providers/docker/scs/1-34/cluster-class/templates/cluster-class.yaml create mode 100644 providers/docker/scs/1-34/cluster-class/templates/docker-cluster-template.yaml create mode 100644 providers/docker/scs/1-34/cluster-class/templates/docker-machine-template.yaml create mode 100644 providers/docker/scs/1-34/cluster-class/templates/kubeadm-config-template-worker-docker.yaml create mode 100644 providers/docker/scs/1-34/cluster-class/templates/kubeadm-control-plane-template.yaml create mode 100644 providers/docker/scs/1-34/cluster-class/values.yaml create mode 100644 providers/docker/scs/1-34/clusteraddon.yaml create mode 100644 providers/docker/scs/1-34/stack.yaml create mode 100644 providers/docker/scs/1-35/cluster-addon/cni/Chart.yaml create mode 100644 providers/docker/scs/1-35/cluster-addon/cni/values.yaml create mode 100644 providers/docker/scs/1-35/cluster-addon/metrics-server/Chart.yaml rename providers/docker/scs/{cluster-addon-values.yaml => 1-35/cluster-addon/metrics-server/overwrite.yaml} (100%) create mode 100644 providers/docker/scs/1-35/cluster-addon/metrics-server/values.yaml create mode 100644 providers/docker/scs/1-35/cluster-class/Chart.yaml create mode 100644 providers/docker/scs/1-35/cluster-class/templates/_helpers.tpl create mode 100644 providers/docker/scs/1-35/cluster-class/templates/cluster-class.yaml create mode 100644 providers/docker/scs/1-35/cluster-class/templates/docker-cluster-template.yaml create mode 100644 providers/docker/scs/1-35/cluster-class/templates/docker-machine-template.yaml create mode 100644 providers/docker/scs/1-35/cluster-class/templates/kubeadm-config-template-worker.yaml create mode 100644 providers/docker/scs/1-35/cluster-class/templates/kubeadm-control-plane-template.yaml create mode 100644 providers/docker/scs/1-35/cluster-class/values.yaml create mode 100644 providers/docker/scs/1-35/clusteraddon.yaml create mode 100644 providers/docker/scs/1-35/stack.yaml delete mode 100644 providers/docker/scs/README.md delete mode 100644 providers/docker/scs/cluster-addon/.helmignore delete mode 100644 providers/docker/scs/cluster-addon/cni/Chart.lock delete mode 100644 providers/docker/scs/cluster-addon/cni/charts/cilium-1.16.6.tgz delete mode 100644 providers/docker/scs/cluster-addon/metrics-server/.helmignore delete mode 100644 providers/docker/scs/cluster-addon/metrics-server/Chart.lock delete mode 100644 providers/docker/scs/cluster-addon/metrics-server/charts/metrics-server-3.12.2.tgz delete mode 100644 providers/docker/scs/cluster-class/.helmignore delete mode 100644 providers/docker/scs/clusterstack.yaml delete mode 100644 providers/docker/scs/csctl.yaml rename providers/openstack/scs/{ => 1-32}/cluster-addon/ccm/Chart.yaml (100%) rename providers/openstack/scs/{ => 1-32}/cluster-addon/ccm/overwrite.yaml (100%) rename providers/openstack/scs/{ => 1-32}/cluster-addon/ccm/values.yaml (100%) rename providers/openstack/scs/{ => 1-32}/cluster-addon/cni/Chart.yaml (54%) rename providers/openstack/scs/{ => 1-32}/cluster-addon/cni/values.yaml (100%) create mode 100644 providers/openstack/scs/1-32/cluster-addon/csi/Chart.yaml rename providers/openstack/scs/{ => 1-32}/cluster-addon/csi/overwrite.yaml (100%) rename providers/openstack/scs/{ => 1-32}/cluster-addon/csi/values.yaml (100%) create mode 100644 providers/openstack/scs/1-32/cluster-addon/metrics-server/Chart.yaml create mode 100644 providers/openstack/scs/1-32/cluster-addon/metrics-server/overwrite.yaml create mode 100644 providers/openstack/scs/1-32/cluster-addon/metrics-server/values.yaml rename providers/openstack/scs/{ => 1-32}/cluster-class/Chart.yaml (100%) rename providers/openstack/scs/{ => 1-32}/cluster-class/templates/_helpers.tpl (100%) rename providers/openstack/scs/{ => 1-32}/cluster-class/templates/cluster-class.yaml (100%) rename providers/openstack/scs/{ => 1-32}/cluster-class/templates/image.yaml (100%) rename providers/openstack/scs/{ => 1-32}/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml (100%) rename providers/openstack/scs/{ => 1-32}/cluster-class/templates/kubeadm-control-plane-template.yaml (100%) rename providers/openstack/scs/{ => 1-32}/cluster-class/templates/openstack-cluster-template.yaml (100%) rename providers/openstack/scs/{ => 1-32}/cluster-class/templates/openstack-machine-template-control-plane.yaml (100%) rename providers/openstack/scs/{ => 1-32}/cluster-class/templates/openstack-machine-template-worker.yaml (100%) rename providers/openstack/scs/{ => 1-32}/cluster-class/values.yaml (100%) rename providers/openstack/scs/{ => 1-32}/clusteraddon.yaml (100%) create mode 100644 providers/openstack/scs/1-32/stack.yaml rename providers/openstack/{scs2 => scs/1-33}/cluster-addon/ccm/Chart.yaml (92%) rename providers/openstack/{scs2 => scs/1-33}/cluster-addon/ccm/overwrite.yaml (100%) rename providers/openstack/{scs2 => scs/1-33}/cluster-addon/ccm/values.yaml (100%) rename providers/openstack/{scs2 => scs/1-33}/cluster-addon/cni/Chart.yaml (88%) create mode 100644 providers/openstack/scs/1-33/cluster-addon/cni/values.yaml rename providers/openstack/{scs2 => scs/1-33}/cluster-addon/csi/Chart.yaml (91%) rename providers/openstack/{scs2 => scs/1-33}/cluster-addon/csi/overwrite.yaml (100%) rename providers/openstack/{scs2 => scs/1-33}/cluster-addon/csi/values.yaml (100%) rename providers/openstack/{scs2 => scs/1-33}/cluster-addon/metrics-server/Chart.yaml (100%) create mode 100644 providers/openstack/scs/1-33/cluster-addon/metrics-server/overwrite.yaml create mode 100644 providers/openstack/scs/1-33/cluster-addon/metrics-server/values.yaml rename providers/openstack/{scs2 => scs/1-33}/cluster-class/Chart.yaml (58%) rename providers/openstack/{scs2 => scs/1-33}/cluster-class/templates/_helpers.tpl (100%) rename providers/openstack/{scs2 => scs/1-33}/cluster-class/templates/cluster-class.yaml (100%) rename providers/openstack/{scs2 => scs/1-33}/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml (100%) rename providers/openstack/{scs2 => scs/1-33}/cluster-class/templates/kubeadm-control-plane-template.yaml (100%) rename providers/openstack/{scs2 => scs/1-33}/cluster-class/templates/openstack-cluster-template.yaml (100%) rename providers/openstack/{scs2 => scs/1-33}/cluster-class/templates/openstack-machine-template-control-plane.yaml (100%) rename providers/openstack/{scs2 => scs/1-33}/cluster-class/templates/openstack-machine-template-worker.yaml (100%) rename providers/openstack/{scs2 => scs/1-33}/cluster-class/values.yaml (100%) rename providers/openstack/{scs2 => scs/1-33}/clusteraddon.yaml (100%) create mode 100644 providers/openstack/scs/1-33/stack.yaml create mode 100644 providers/openstack/scs/1-34/cluster-addon/ccm/Chart.yaml create mode 100644 providers/openstack/scs/1-34/cluster-addon/ccm/overwrite.yaml create mode 100644 providers/openstack/scs/1-34/cluster-addon/ccm/values.yaml create mode 100644 providers/openstack/scs/1-34/cluster-addon/cni/Chart.yaml create mode 100644 providers/openstack/scs/1-34/cluster-addon/cni/values.yaml create mode 100644 providers/openstack/scs/1-34/cluster-addon/csi/Chart.yaml create mode 100644 providers/openstack/scs/1-34/cluster-addon/csi/overwrite.yaml create mode 100644 providers/openstack/scs/1-34/cluster-addon/csi/values.yaml create mode 100644 providers/openstack/scs/1-34/cluster-addon/metrics-server/Chart.yaml create mode 100644 providers/openstack/scs/1-34/cluster-addon/metrics-server/overwrite.yaml create mode 100644 providers/openstack/scs/1-34/cluster-addon/metrics-server/values.yaml create mode 100644 providers/openstack/scs/1-34/cluster-class/Chart.yaml create mode 100644 providers/openstack/scs/1-34/cluster-class/templates/_helpers.tpl create mode 100644 providers/openstack/scs/1-34/cluster-class/templates/cluster-class.yaml create mode 100644 providers/openstack/scs/1-34/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml create mode 100644 providers/openstack/scs/1-34/cluster-class/templates/kubeadm-control-plane-template.yaml create mode 100644 providers/openstack/scs/1-34/cluster-class/templates/openstack-cluster-template.yaml create mode 100644 providers/openstack/scs/1-34/cluster-class/templates/openstack-machine-template-control-plane.yaml create mode 100644 providers/openstack/scs/1-34/cluster-class/templates/openstack-machine-template-worker.yaml create mode 100644 providers/openstack/scs/1-34/cluster-class/values.yaml create mode 100644 providers/openstack/scs/1-34/clusteraddon.yaml create mode 100644 providers/openstack/scs/1-34/stack.yaml create mode 100644 providers/openstack/scs/1-35/cluster-addon/ccm/Chart.yaml create mode 100644 providers/openstack/scs/1-35/cluster-addon/ccm/overwrite.yaml create mode 100644 providers/openstack/scs/1-35/cluster-addon/ccm/values.yaml create mode 100644 providers/openstack/scs/1-35/cluster-addon/cni/Chart.yaml create mode 100644 providers/openstack/scs/1-35/cluster-addon/cni/values.yaml create mode 100644 providers/openstack/scs/1-35/cluster-addon/csi/Chart.yaml create mode 100644 providers/openstack/scs/1-35/cluster-addon/csi/overwrite.yaml create mode 100644 providers/openstack/scs/1-35/cluster-addon/csi/values.yaml create mode 100644 providers/openstack/scs/1-35/cluster-addon/metrics-server/Chart.yaml create mode 100644 providers/openstack/scs/1-35/cluster-addon/metrics-server/overwrite.yaml create mode 100644 providers/openstack/scs/1-35/cluster-addon/metrics-server/values.yaml create mode 100644 providers/openstack/scs/1-35/cluster-class/Chart.yaml create mode 100644 providers/openstack/scs/1-35/cluster-class/templates/_helpers.tpl create mode 100644 providers/openstack/scs/1-35/cluster-class/templates/cluster-class.yaml create mode 100644 providers/openstack/scs/1-35/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml create mode 100644 providers/openstack/scs/1-35/cluster-class/templates/kubeadm-control-plane-template.yaml create mode 100644 providers/openstack/scs/1-35/cluster-class/templates/openstack-cluster-template.yaml create mode 100644 providers/openstack/scs/1-35/cluster-class/templates/openstack-machine-template-control-plane.yaml create mode 100644 providers/openstack/scs/1-35/cluster-class/templates/openstack-machine-template-worker.yaml create mode 100644 providers/openstack/scs/1-35/cluster-class/values.yaml create mode 100644 providers/openstack/scs/1-35/clusteraddon.yaml create mode 100644 providers/openstack/scs/1-35/stack.yaml delete mode 100644 providers/openstack/scs/README.md delete mode 100644 providers/openstack/scs/cluster-addon/ccm/Chart.lock delete mode 100644 providers/openstack/scs/cluster-addon/ccm/charts/openstack-cloud-controller-manager-2.32.0.tgz delete mode 100644 providers/openstack/scs/cluster-addon/cni/Chart.lock delete mode 100644 providers/openstack/scs/cluster-addon/cni/charts/cilium-1.17.4.tgz delete mode 100644 providers/openstack/scs/cluster-addon/csi/Chart.lock delete mode 100644 providers/openstack/scs/cluster-addon/csi/Chart.yaml delete mode 100644 providers/openstack/scs/cluster-addon/csi/charts/openstack-cinder-csi-2.32.0.tgz delete mode 100644 providers/openstack/scs/cluster-addon/metrics-server/.helmignore delete mode 100644 providers/openstack/scs/cluster-addon/metrics-server/Chart.lock delete mode 100644 providers/openstack/scs/cluster-addon/metrics-server/Chart.yaml delete mode 100644 providers/openstack/scs/cluster-addon/metrics-server/charts/metrics-server-3.12.2.tgz delete mode 100644 providers/openstack/scs/cluster-class/.helmignore delete mode 100644 providers/openstack/scs/csctl.yaml create mode 100644 providers/openstack/scs/image-manager.yaml delete mode 100644 providers/openstack/scs/versions.yaml delete mode 100644 providers/openstack/scs2/README.md delete mode 100644 providers/openstack/scs2/cluster-class/.helmignore delete mode 100644 providers/openstack/scs2/csctl.yaml delete mode 100644 providers/openstack/scs2/image.yaml delete mode 100644 providers/openstack/scs2/kubernetes.yaml delete mode 100644 providers/openstack/scs2/versions.yaml diff --git a/providers/docker/scs/cluster-addon/cni/Chart.yaml b/providers/docker/scs/1-32/cluster-addon/cni/Chart.yaml similarity index 89% rename from providers/docker/scs/cluster-addon/cni/Chart.yaml rename to providers/docker/scs/1-32/cluster-addon/cni/Chart.yaml index 69b6be9a..5b9b5dfa 100644 --- a/providers/docker/scs/cluster-addon/cni/Chart.yaml +++ b/providers/docker/scs/1-32/cluster-addon/cni/Chart.yaml @@ -7,4 +7,4 @@ dependencies: - alias: cilium name: cilium repository: https://helm.cilium.io/ - version: 1.16.6 + version: 1.19.1 diff --git a/providers/docker/scs/cluster-addon/cni/values.yaml b/providers/docker/scs/1-32/cluster-addon/cni/values.yaml similarity index 100% rename from providers/docker/scs/cluster-addon/cni/values.yaml rename to providers/docker/scs/1-32/cluster-addon/cni/values.yaml diff --git a/providers/docker/scs/cluster-addon/metrics-server/Chart.yaml b/providers/docker/scs/1-32/cluster-addon/metrics-server/Chart.yaml similarity index 91% rename from providers/docker/scs/cluster-addon/metrics-server/Chart.yaml rename to providers/docker/scs/1-32/cluster-addon/metrics-server/Chart.yaml index e5f961e3..a40982bf 100644 --- a/providers/docker/scs/cluster-addon/metrics-server/Chart.yaml +++ b/providers/docker/scs/1-32/cluster-addon/metrics-server/Chart.yaml @@ -5,6 +5,6 @@ name: yorizonpoc-metrics-server version: v1 dependencies: - name: "metrics-server" - version: "3.12.2" + version: "3.13.0" repository: "https://kubernetes-sigs.github.io/metrics-server/" alias: "metrics-server" diff --git a/providers/docker/scs/cluster-addon/metrics-server/values.yaml b/providers/docker/scs/1-32/cluster-addon/metrics-server/values.yaml similarity index 100% rename from providers/docker/scs/cluster-addon/metrics-server/values.yaml rename to providers/docker/scs/1-32/cluster-addon/metrics-server/values.yaml diff --git a/providers/docker/scs/cluster-class/Chart.yaml b/providers/docker/scs/1-32/cluster-class/Chart.yaml similarity index 67% rename from providers/docker/scs/cluster-class/Chart.yaml rename to providers/docker/scs/1-32/cluster-class/Chart.yaml index 3e2041e9..e24a03aa 100644 --- a/providers/docker/scs/cluster-class/Chart.yaml +++ b/providers/docker/scs/1-32/cluster-class/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v2 description: SCS Cluster Class -name: docker-scs-1-30-cluster-class +name: docker-scs-1-32-cluster-class type: application version: v1 diff --git a/providers/docker/scs/cluster-class/templates/_helpers.tpl b/providers/docker/scs/1-32/cluster-class/templates/_helpers.tpl similarity index 100% rename from providers/docker/scs/cluster-class/templates/_helpers.tpl rename to providers/docker/scs/1-32/cluster-class/templates/_helpers.tpl diff --git a/providers/docker/scs/cluster-class/templates/cluster-class.yaml b/providers/docker/scs/1-32/cluster-class/templates/cluster-class.yaml similarity index 100% rename from providers/docker/scs/cluster-class/templates/cluster-class.yaml rename to providers/docker/scs/1-32/cluster-class/templates/cluster-class.yaml diff --git a/providers/docker/scs/cluster-class/templates/docker-cluster-template.yaml b/providers/docker/scs/1-32/cluster-class/templates/docker-cluster-template.yaml similarity index 100% rename from providers/docker/scs/cluster-class/templates/docker-cluster-template.yaml rename to providers/docker/scs/1-32/cluster-class/templates/docker-cluster-template.yaml diff --git a/providers/docker/scs/cluster-class/templates/docker-machine-template.yaml b/providers/docker/scs/1-32/cluster-class/templates/docker-machine-template.yaml similarity index 100% rename from providers/docker/scs/cluster-class/templates/docker-machine-template.yaml rename to providers/docker/scs/1-32/cluster-class/templates/docker-machine-template.yaml diff --git a/providers/docker/scs/cluster-class/templates/kubeadm-config-template-worker-docker.yaml b/providers/docker/scs/1-32/cluster-class/templates/kubeadm-config-template-worker-docker.yaml similarity index 100% rename from providers/docker/scs/cluster-class/templates/kubeadm-config-template-worker-docker.yaml rename to providers/docker/scs/1-32/cluster-class/templates/kubeadm-config-template-worker-docker.yaml diff --git a/providers/docker/scs/cluster-class/templates/kubeadm-control-plane-template.yaml b/providers/docker/scs/1-32/cluster-class/templates/kubeadm-control-plane-template.yaml similarity index 100% rename from providers/docker/scs/cluster-class/templates/kubeadm-control-plane-template.yaml rename to providers/docker/scs/1-32/cluster-class/templates/kubeadm-control-plane-template.yaml diff --git a/providers/docker/scs/cluster-class/values.yaml b/providers/docker/scs/1-32/cluster-class/values.yaml similarity index 100% rename from providers/docker/scs/cluster-class/values.yaml rename to providers/docker/scs/1-32/cluster-class/values.yaml diff --git a/providers/docker/scs/clusteraddon.yaml b/providers/docker/scs/1-32/clusteraddon.yaml similarity index 100% rename from providers/docker/scs/clusteraddon.yaml rename to providers/docker/scs/1-32/clusteraddon.yaml diff --git a/providers/docker/scs/1-32/stack.yaml b/providers/docker/scs/1-32/stack.yaml new file mode 100644 index 00000000..2efbee8f --- /dev/null +++ b/providers/docker/scs/1-32/stack.yaml @@ -0,0 +1,3 @@ +provider: docker +clusterStackName: scs +kubernetesVersion: 1.32 diff --git a/providers/docker/scs/1-33/cluster-addon/cni/Chart.yaml b/providers/docker/scs/1-33/cluster-addon/cni/Chart.yaml new file mode 100644 index 00000000..7e37d894 --- /dev/null +++ b/providers/docker/scs/1-33/cluster-addon/cni/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: cni +description: Cilium CNI for Docker SCS2 cluster stack +type: application +version: 0.1.0 +dependencies: + - name: cilium + version: 1.19.1 + repository: https://helm.cilium.io/ diff --git a/providers/openstack/scs2/cluster-addon/cni/values.yaml b/providers/docker/scs/1-33/cluster-addon/cni/values.yaml similarity index 100% rename from providers/openstack/scs2/cluster-addon/cni/values.yaml rename to providers/docker/scs/1-33/cluster-addon/cni/values.yaml diff --git a/providers/docker/scs/1-33/cluster-addon/metrics-server/Chart.yaml b/providers/docker/scs/1-33/cluster-addon/metrics-server/Chart.yaml new file mode 100644 index 00000000..764d3b7c --- /dev/null +++ b/providers/docker/scs/1-33/cluster-addon/metrics-server/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: metrics-server +description: Metrics Server for Docker SCS2 cluster stack +type: application +version: 0.1.0 +dependencies: + - name: metrics-server + version: "3.13.0" + repository: https://kubernetes-sigs.github.io/metrics-server/ diff --git a/providers/openstack/scs/cluster-addon/metrics-server/overwrite.yaml b/providers/docker/scs/1-33/cluster-addon/metrics-server/overwrite.yaml similarity index 100% rename from providers/openstack/scs/cluster-addon/metrics-server/overwrite.yaml rename to providers/docker/scs/1-33/cluster-addon/metrics-server/overwrite.yaml diff --git a/providers/openstack/scs/cluster-addon/metrics-server/values.yaml b/providers/docker/scs/1-33/cluster-addon/metrics-server/values.yaml similarity index 100% rename from providers/openstack/scs/cluster-addon/metrics-server/values.yaml rename to providers/docker/scs/1-33/cluster-addon/metrics-server/values.yaml diff --git a/providers/docker/scs/1-33/cluster-class/Chart.yaml b/providers/docker/scs/1-33/cluster-class/Chart.yaml new file mode 100644 index 00000000..6471f5c0 --- /dev/null +++ b/providers/docker/scs/1-33/cluster-class/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: docker-scs-1-33-cluster-class +description: | + SCS Cluster Class (1.33) for the Docker infrastructure provider. + Uses CAPI v1beta1 APIs with camelCase variables (imageRepository, certSANs, oidcConfig). +type: application +version: v1 diff --git a/providers/docker/scs/1-33/cluster-class/templates/_helpers.tpl b/providers/docker/scs/1-33/cluster-class/templates/_helpers.tpl new file mode 100644 index 00000000..62b7a97d --- /dev/null +++ b/providers/docker/scs/1-33/cluster-class/templates/_helpers.tpl @@ -0,0 +1,29 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "cluster-class.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "cluster-class.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "cluster-class.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} diff --git a/providers/docker/scs/1-33/cluster-class/templates/cluster-class.yaml b/providers/docker/scs/1-33/cluster-class/templates/cluster-class.yaml new file mode 100644 index 00000000..dbbbafaa --- /dev/null +++ b/providers/docker/scs/1-33/cluster-class/templates/cluster-class.yaml @@ -0,0 +1,185 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: ClusterClass +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }} + namespace: {{ .Release.Namespace }} +spec: + controlPlane: + ref: + apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + kind: KubeadmControlPlaneTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane + namespace: {{ .Release.Namespace }} + machineInfrastructure: + ref: + kind: DockerMachineTemplate + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + name: {{ .Release.Name }}-{{ .Chart.Version }}-machinetemplate-docker + namespace: {{ .Release.Namespace }} + infrastructure: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: DockerClusterTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-cluster + namespace: {{ .Release.Namespace }} + workers: + machineDeployments: + - class: default-worker + template: + bootstrap: + ref: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 + kind: KubeadmConfigTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-worker-bootstraptemplate-docker + namespace: {{ .Release.Namespace }} + infrastructure: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: DockerMachineTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-machinetemplate-docker + namespace: {{ .Release.Namespace }} + variables: + - name: imageRepository + required: false + schema: + openAPIV3Schema: + type: string + default: "" + example: "registry.k8s.io" + description: "imageRepository sets the container registry to pull images from. If empty, the kubeadm default will be used." + - name: certSANs + required: false + schema: + openAPIV3Schema: + type: array + default: [] + example: ["mydomain.example"] + description: "Extra Subject Alternative Names for the API server TLS certificate." + items: + type: string + - name: oidcConfig + required: false + schema: + openAPIV3Schema: + type: object + properties: + clientID: + type: string + example: "kubectl" + description: "A client id that all tokens must be issued for." + issuerURL: + type: string + example: "https://dex.k8s.scs.community" + description: >- + URL of the provider that allows the API server to + discover public signing keys. Only URLs that use the https:// scheme are + accepted. + usernameClaim: + type: string + example: "preferred_username" + default: "preferred_username" + description: "JWT claim to use as the user name." + groupsClaim: + type: string + example: "groups" + default: "groups" + description: "JWT claim to use as the user's group." + usernamePrefix: + type: string + example: "oidc:" + default: "oidc:" + description: "Prefix prepended to username claims to prevent clashes with existing names." + groupsPrefix: + type: string + example: "oidc:" + default: "oidc:" + description: "Prefix prepended to group claims to prevent clashes with existing names." + patches: + - name: imageRepository + description: "Sets the imageRepository used for the KubeadmControlPlane." + enabledIf: '{{ ne .imageRepository "" }}' + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + kind: KubeadmControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/imageRepository" + valueFrom: + variable: imageRepository + - name: customImageControlPlane + description: "Sets the container image for control plane DockerMachines." + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: DockerMachineTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/customImage" + value: {{ (index .Values.images.controlPlane 0).name }} + - name: customImageWorker + description: "Sets the container image for worker DockerMachines." + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: DockerMachineTemplate + matchResources: + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/customImage" + value: {{ (index .Values.images.worker 0).name }} + - name: certSANs + description: "certSANs sets extra Subject Alternative Names for the API Server signing cert." + enabledIf: {{ `'{{ if .certSANs }}true{{end}}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + kind: KubeadmControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/certSANs" + valueFrom: + variable: certSANs + - name: oidcConfig + description: "Configure API Server to use external authentication service." + enabledIf: {{ `'{{ if and .oidcConfig .oidcConfig.clientID .oidcConfig.issuerURL }}true{{end}}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + kind: KubeadmControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-client-id" + valueFrom: + variable: oidcConfig.clientID + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-issuer-url" + valueFrom: + variable: oidcConfig.issuerURL + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-username-claim" + valueFrom: + variable: oidcConfig.usernameClaim + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-groups-claim" + valueFrom: + variable: oidcConfig.groupsClaim + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-username-prefix" + valueFrom: + variable: oidcConfig.usernamePrefix + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-groups-prefix" + valueFrom: + variable: oidcConfig.groupsPrefix diff --git a/providers/docker/scs/1-33/cluster-class/templates/docker-cluster-template.yaml b/providers/docker/scs/1-33/cluster-class/templates/docker-cluster-template.yaml new file mode 100644 index 00000000..0bdf7120 --- /dev/null +++ b/providers/docker/scs/1-33/cluster-class/templates/docker-cluster-template.yaml @@ -0,0 +1,8 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: DockerClusterTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-cluster + namespace: {{ .Release.Namespace }} +spec: + template: + spec: {} diff --git a/providers/docker/scs/1-33/cluster-class/templates/docker-machine-template.yaml b/providers/docker/scs/1-33/cluster-class/templates/docker-machine-template.yaml new file mode 100644 index 00000000..bc4c6cc6 --- /dev/null +++ b/providers/docker/scs/1-33/cluster-class/templates/docker-machine-template.yaml @@ -0,0 +1,11 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: DockerMachineTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-machinetemplate-docker + namespace: {{ .Release.Namespace }} +spec: + template: + spec: + extraMounts: + - containerPath: "/var/run/docker.sock" + hostPath: "/var/run/docker.sock" diff --git a/providers/docker/scs/1-33/cluster-class/templates/kubeadm-config-template-worker-docker.yaml b/providers/docker/scs/1-33/cluster-class/templates/kubeadm-config-template-worker-docker.yaml new file mode 100644 index 00000000..ff13c65f --- /dev/null +++ b/providers/docker/scs/1-33/cluster-class/templates/kubeadm-config-template-worker-docker.yaml @@ -0,0 +1,14 @@ +apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +kind: KubeadmConfigTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-worker-bootstraptemplate-docker + namespace: {{ .Release.Namespace }} +spec: + template: + spec: + joinConfiguration: + nodeRegistration: + criSocket: unix:///var/run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: "nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%" + fail-swap-on: "false" diff --git a/providers/docker/scs/1-33/cluster-class/templates/kubeadm-control-plane-template.yaml b/providers/docker/scs/1-33/cluster-class/templates/kubeadm-control-plane-template.yaml new file mode 100644 index 00000000..5588759e --- /dev/null +++ b/providers/docker/scs/1-33/cluster-class/templates/kubeadm-control-plane-template.yaml @@ -0,0 +1,84 @@ +apiVersion: controlplane.cluster.x-k8s.io/v1beta1 +kind: KubeadmControlPlaneTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane + namespace: {{ .Release.Namespace }} +spec: + template: + spec: + kubeadmConfigSpec: + clusterConfiguration: + apiServer: + certSANs: [localhost, 127.0.0.1, 0.0.0.0, host.docker.internal] + controllerManager: + extraArgs: + enable-hostpath-provisioner: "true" + bind-address: 0.0.0.0 + secure-port: "10257" + profiling: "false" + terminated-pod-gc-threshold: "100" + scheduler: + extraArgs: + bind-address: 0.0.0.0 + secure-port: "10259" + profiling: "false" + etcd: + local: + dataDir: /var/lib/etcd + extraArgs: + listen-metrics-urls: http://0.0.0.0:2381 + auto-compaction-mode: periodic + auto-compaction-retention: 8h + election-timeout: "2500" + heartbeat-interval: "250" + snapshot-count: "6400" + files: + - content: | + --- + apiVersion: kubeproxy.config.k8s.io/v1alpha1 + kind: KubeProxyConfiguration + metricsBindAddress: "0.0.0.0:10249" + path: /etc/kube-proxy-config.yaml + - content: | + #!/usr/bin/env bash + set -o errexit + set -o nounset + set -o pipefail + + dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + readonly dir + + if [[ ! -f ${dir}/kube-proxy-config.yaml ]]; then + exit 0 + fi + + kubeadm_file="/etc/kubeadm.yml" + if [[ ! -f ${kubeadm_file} ]]; then + kubeadm_file="/run/kubeadm/kubeadm.yaml" + fi + + if [[ ! -f ${kubeadm_file} ]]; then + exit 0 + fi + + cat "${dir}/kube-proxy-config.yaml" >> "${kubeadm_file}" + rm "${dir}/kube-proxy-config.yaml" + + echo success > /tmp/kube-proxy-patch + owner: root:root + path: /etc/kube-proxy-patch.sh + permissions: "0755" + preKubeadmCommands: + - bash /etc/kube-proxy-patch.sh + initConfiguration: + nodeRegistration: + criSocket: unix:///var/run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: "nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%" + fail-swap-on: "false" + joinConfiguration: + nodeRegistration: + criSocket: unix:///var/run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: "nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%" + fail-swap-on: "false" diff --git a/providers/docker/scs/1-33/cluster-class/values.yaml b/providers/docker/scs/1-33/cluster-class/values.yaml new file mode 100644 index 00000000..29fe3394 --- /dev/null +++ b/providers/docker/scs/1-33/cluster-class/values.yaml @@ -0,0 +1,6 @@ +# Node images for kind (patched at build time by build.sh for each K8s version) +images: + controlPlane: + - name: registry.scs.community/docker.io/kindest/node:v1.34.3 + worker: + - name: registry.scs.community/docker.io/kindest/node:v1.34.3 diff --git a/providers/docker/scs/1-33/clusteraddon.yaml b/providers/docker/scs/1-33/clusteraddon.yaml new file mode 100644 index 00000000..9a639968 --- /dev/null +++ b/providers/docker/scs/1-33/clusteraddon.yaml @@ -0,0 +1,18 @@ +apiVersion: clusteraddonconfig.x-k8s.io/v1alpha1 +clusterAddonVersion: clusteraddons.clusterstack.x-k8s.io/v1alpha1 +addonStages: + AfterControlPlaneInitialized: + - name: cni + action: apply + - name: metrics-server + action: apply + BeforeClusterUpgrade: + - name: cni + action: apply + - name: metrics-server + action: apply + AfterClusterUpgrade: + - name: cni + action: apply + - name: metrics-server + action: apply diff --git a/providers/docker/scs/1-33/stack.yaml b/providers/docker/scs/1-33/stack.yaml new file mode 100644 index 00000000..0a5fd917 --- /dev/null +++ b/providers/docker/scs/1-33/stack.yaml @@ -0,0 +1,3 @@ +provider: docker +clusterStackName: scs +kubernetesVersion: 1.33 diff --git a/providers/docker/scs/1-34/cluster-addon/cni/Chart.yaml b/providers/docker/scs/1-34/cluster-addon/cni/Chart.yaml new file mode 100644 index 00000000..7e37d894 --- /dev/null +++ b/providers/docker/scs/1-34/cluster-addon/cni/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: cni +description: Cilium CNI for Docker SCS2 cluster stack +type: application +version: 0.1.0 +dependencies: + - name: cilium + version: 1.19.1 + repository: https://helm.cilium.io/ diff --git a/providers/docker/scs/1-34/cluster-addon/cni/values.yaml b/providers/docker/scs/1-34/cluster-addon/cni/values.yaml new file mode 100644 index 00000000..8a312f0c --- /dev/null +++ b/providers/docker/scs/1-34/cluster-addon/cni/values.yaml @@ -0,0 +1,14 @@ +cilium: + namespaceOverride: kube-system + tls: + secretsNamespace: + name: "kube-system" + sessionAffinity: true + sctp: + enabled: true + ipam: + mode: "kubernetes" + gatewayAPI: + enabled: true + secretsNamespace: + name: "kube-system" diff --git a/providers/docker/scs/1-34/cluster-addon/metrics-server/Chart.yaml b/providers/docker/scs/1-34/cluster-addon/metrics-server/Chart.yaml new file mode 100644 index 00000000..764d3b7c --- /dev/null +++ b/providers/docker/scs/1-34/cluster-addon/metrics-server/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: metrics-server +description: Metrics Server for Docker SCS2 cluster stack +type: application +version: 0.1.0 +dependencies: + - name: metrics-server + version: "3.13.0" + repository: https://kubernetes-sigs.github.io/metrics-server/ diff --git a/providers/openstack/scs2/cluster-addon/metrics-server/overwrite.yaml b/providers/docker/scs/1-34/cluster-addon/metrics-server/overwrite.yaml similarity index 100% rename from providers/openstack/scs2/cluster-addon/metrics-server/overwrite.yaml rename to providers/docker/scs/1-34/cluster-addon/metrics-server/overwrite.yaml diff --git a/providers/openstack/scs2/cluster-addon/metrics-server/values.yaml b/providers/docker/scs/1-34/cluster-addon/metrics-server/values.yaml similarity index 100% rename from providers/openstack/scs2/cluster-addon/metrics-server/values.yaml rename to providers/docker/scs/1-34/cluster-addon/metrics-server/values.yaml diff --git a/providers/docker/scs/1-34/cluster-class/Chart.yaml b/providers/docker/scs/1-34/cluster-class/Chart.yaml new file mode 100644 index 00000000..1360581c --- /dev/null +++ b/providers/docker/scs/1-34/cluster-class/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: docker-scs-1-34-cluster-class +description: | + SCS Cluster Class (1.34) for the Docker infrastructure provider. + Uses CAPI v1beta1 APIs with camelCase variables (imageRepository, certSANs, oidcConfig). +type: application +version: v1 diff --git a/providers/docker/scs/1-34/cluster-class/templates/_helpers.tpl b/providers/docker/scs/1-34/cluster-class/templates/_helpers.tpl new file mode 100644 index 00000000..62b7a97d --- /dev/null +++ b/providers/docker/scs/1-34/cluster-class/templates/_helpers.tpl @@ -0,0 +1,29 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "cluster-class.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "cluster-class.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "cluster-class.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} diff --git a/providers/docker/scs/1-34/cluster-class/templates/cluster-class.yaml b/providers/docker/scs/1-34/cluster-class/templates/cluster-class.yaml new file mode 100644 index 00000000..dbbbafaa --- /dev/null +++ b/providers/docker/scs/1-34/cluster-class/templates/cluster-class.yaml @@ -0,0 +1,185 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: ClusterClass +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }} + namespace: {{ .Release.Namespace }} +spec: + controlPlane: + ref: + apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + kind: KubeadmControlPlaneTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane + namespace: {{ .Release.Namespace }} + machineInfrastructure: + ref: + kind: DockerMachineTemplate + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + name: {{ .Release.Name }}-{{ .Chart.Version }}-machinetemplate-docker + namespace: {{ .Release.Namespace }} + infrastructure: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: DockerClusterTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-cluster + namespace: {{ .Release.Namespace }} + workers: + machineDeployments: + - class: default-worker + template: + bootstrap: + ref: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 + kind: KubeadmConfigTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-worker-bootstraptemplate-docker + namespace: {{ .Release.Namespace }} + infrastructure: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: DockerMachineTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-machinetemplate-docker + namespace: {{ .Release.Namespace }} + variables: + - name: imageRepository + required: false + schema: + openAPIV3Schema: + type: string + default: "" + example: "registry.k8s.io" + description: "imageRepository sets the container registry to pull images from. If empty, the kubeadm default will be used." + - name: certSANs + required: false + schema: + openAPIV3Schema: + type: array + default: [] + example: ["mydomain.example"] + description: "Extra Subject Alternative Names for the API server TLS certificate." + items: + type: string + - name: oidcConfig + required: false + schema: + openAPIV3Schema: + type: object + properties: + clientID: + type: string + example: "kubectl" + description: "A client id that all tokens must be issued for." + issuerURL: + type: string + example: "https://dex.k8s.scs.community" + description: >- + URL of the provider that allows the API server to + discover public signing keys. Only URLs that use the https:// scheme are + accepted. + usernameClaim: + type: string + example: "preferred_username" + default: "preferred_username" + description: "JWT claim to use as the user name." + groupsClaim: + type: string + example: "groups" + default: "groups" + description: "JWT claim to use as the user's group." + usernamePrefix: + type: string + example: "oidc:" + default: "oidc:" + description: "Prefix prepended to username claims to prevent clashes with existing names." + groupsPrefix: + type: string + example: "oidc:" + default: "oidc:" + description: "Prefix prepended to group claims to prevent clashes with existing names." + patches: + - name: imageRepository + description: "Sets the imageRepository used for the KubeadmControlPlane." + enabledIf: '{{ ne .imageRepository "" }}' + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + kind: KubeadmControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/imageRepository" + valueFrom: + variable: imageRepository + - name: customImageControlPlane + description: "Sets the container image for control plane DockerMachines." + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: DockerMachineTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/customImage" + value: {{ (index .Values.images.controlPlane 0).name }} + - name: customImageWorker + description: "Sets the container image for worker DockerMachines." + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: DockerMachineTemplate + matchResources: + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/customImage" + value: {{ (index .Values.images.worker 0).name }} + - name: certSANs + description: "certSANs sets extra Subject Alternative Names for the API Server signing cert." + enabledIf: {{ `'{{ if .certSANs }}true{{end}}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + kind: KubeadmControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/certSANs" + valueFrom: + variable: certSANs + - name: oidcConfig + description: "Configure API Server to use external authentication service." + enabledIf: {{ `'{{ if and .oidcConfig .oidcConfig.clientID .oidcConfig.issuerURL }}true{{end}}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + kind: KubeadmControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-client-id" + valueFrom: + variable: oidcConfig.clientID + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-issuer-url" + valueFrom: + variable: oidcConfig.issuerURL + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-username-claim" + valueFrom: + variable: oidcConfig.usernameClaim + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-groups-claim" + valueFrom: + variable: oidcConfig.groupsClaim + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-username-prefix" + valueFrom: + variable: oidcConfig.usernamePrefix + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-groups-prefix" + valueFrom: + variable: oidcConfig.groupsPrefix diff --git a/providers/docker/scs/1-34/cluster-class/templates/docker-cluster-template.yaml b/providers/docker/scs/1-34/cluster-class/templates/docker-cluster-template.yaml new file mode 100644 index 00000000..0bdf7120 --- /dev/null +++ b/providers/docker/scs/1-34/cluster-class/templates/docker-cluster-template.yaml @@ -0,0 +1,8 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: DockerClusterTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-cluster + namespace: {{ .Release.Namespace }} +spec: + template: + spec: {} diff --git a/providers/docker/scs/1-34/cluster-class/templates/docker-machine-template.yaml b/providers/docker/scs/1-34/cluster-class/templates/docker-machine-template.yaml new file mode 100644 index 00000000..bc4c6cc6 --- /dev/null +++ b/providers/docker/scs/1-34/cluster-class/templates/docker-machine-template.yaml @@ -0,0 +1,11 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: DockerMachineTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-machinetemplate-docker + namespace: {{ .Release.Namespace }} +spec: + template: + spec: + extraMounts: + - containerPath: "/var/run/docker.sock" + hostPath: "/var/run/docker.sock" diff --git a/providers/docker/scs/1-34/cluster-class/templates/kubeadm-config-template-worker-docker.yaml b/providers/docker/scs/1-34/cluster-class/templates/kubeadm-config-template-worker-docker.yaml new file mode 100644 index 00000000..ff13c65f --- /dev/null +++ b/providers/docker/scs/1-34/cluster-class/templates/kubeadm-config-template-worker-docker.yaml @@ -0,0 +1,14 @@ +apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +kind: KubeadmConfigTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-worker-bootstraptemplate-docker + namespace: {{ .Release.Namespace }} +spec: + template: + spec: + joinConfiguration: + nodeRegistration: + criSocket: unix:///var/run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: "nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%" + fail-swap-on: "false" diff --git a/providers/docker/scs/1-34/cluster-class/templates/kubeadm-control-plane-template.yaml b/providers/docker/scs/1-34/cluster-class/templates/kubeadm-control-plane-template.yaml new file mode 100644 index 00000000..5588759e --- /dev/null +++ b/providers/docker/scs/1-34/cluster-class/templates/kubeadm-control-plane-template.yaml @@ -0,0 +1,84 @@ +apiVersion: controlplane.cluster.x-k8s.io/v1beta1 +kind: KubeadmControlPlaneTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane + namespace: {{ .Release.Namespace }} +spec: + template: + spec: + kubeadmConfigSpec: + clusterConfiguration: + apiServer: + certSANs: [localhost, 127.0.0.1, 0.0.0.0, host.docker.internal] + controllerManager: + extraArgs: + enable-hostpath-provisioner: "true" + bind-address: 0.0.0.0 + secure-port: "10257" + profiling: "false" + terminated-pod-gc-threshold: "100" + scheduler: + extraArgs: + bind-address: 0.0.0.0 + secure-port: "10259" + profiling: "false" + etcd: + local: + dataDir: /var/lib/etcd + extraArgs: + listen-metrics-urls: http://0.0.0.0:2381 + auto-compaction-mode: periodic + auto-compaction-retention: 8h + election-timeout: "2500" + heartbeat-interval: "250" + snapshot-count: "6400" + files: + - content: | + --- + apiVersion: kubeproxy.config.k8s.io/v1alpha1 + kind: KubeProxyConfiguration + metricsBindAddress: "0.0.0.0:10249" + path: /etc/kube-proxy-config.yaml + - content: | + #!/usr/bin/env bash + set -o errexit + set -o nounset + set -o pipefail + + dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + readonly dir + + if [[ ! -f ${dir}/kube-proxy-config.yaml ]]; then + exit 0 + fi + + kubeadm_file="/etc/kubeadm.yml" + if [[ ! -f ${kubeadm_file} ]]; then + kubeadm_file="/run/kubeadm/kubeadm.yaml" + fi + + if [[ ! -f ${kubeadm_file} ]]; then + exit 0 + fi + + cat "${dir}/kube-proxy-config.yaml" >> "${kubeadm_file}" + rm "${dir}/kube-proxy-config.yaml" + + echo success > /tmp/kube-proxy-patch + owner: root:root + path: /etc/kube-proxy-patch.sh + permissions: "0755" + preKubeadmCommands: + - bash /etc/kube-proxy-patch.sh + initConfiguration: + nodeRegistration: + criSocket: unix:///var/run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: "nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%" + fail-swap-on: "false" + joinConfiguration: + nodeRegistration: + criSocket: unix:///var/run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: "nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%" + fail-swap-on: "false" diff --git a/providers/docker/scs/1-34/cluster-class/values.yaml b/providers/docker/scs/1-34/cluster-class/values.yaml new file mode 100644 index 00000000..29fe3394 --- /dev/null +++ b/providers/docker/scs/1-34/cluster-class/values.yaml @@ -0,0 +1,6 @@ +# Node images for kind (patched at build time by build.sh for each K8s version) +images: + controlPlane: + - name: registry.scs.community/docker.io/kindest/node:v1.34.3 + worker: + - name: registry.scs.community/docker.io/kindest/node:v1.34.3 diff --git a/providers/docker/scs/1-34/clusteraddon.yaml b/providers/docker/scs/1-34/clusteraddon.yaml new file mode 100644 index 00000000..9a639968 --- /dev/null +++ b/providers/docker/scs/1-34/clusteraddon.yaml @@ -0,0 +1,18 @@ +apiVersion: clusteraddonconfig.x-k8s.io/v1alpha1 +clusterAddonVersion: clusteraddons.clusterstack.x-k8s.io/v1alpha1 +addonStages: + AfterControlPlaneInitialized: + - name: cni + action: apply + - name: metrics-server + action: apply + BeforeClusterUpgrade: + - name: cni + action: apply + - name: metrics-server + action: apply + AfterClusterUpgrade: + - name: cni + action: apply + - name: metrics-server + action: apply diff --git a/providers/docker/scs/1-34/stack.yaml b/providers/docker/scs/1-34/stack.yaml new file mode 100644 index 00000000..1e8da19d --- /dev/null +++ b/providers/docker/scs/1-34/stack.yaml @@ -0,0 +1,3 @@ +provider: docker +clusterStackName: scs +kubernetesVersion: 1.34 diff --git a/providers/docker/scs/1-35/cluster-addon/cni/Chart.yaml b/providers/docker/scs/1-35/cluster-addon/cni/Chart.yaml new file mode 100644 index 00000000..1ee33dee --- /dev/null +++ b/providers/docker/scs/1-35/cluster-addon/cni/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: cni +description: Cilium CNI for Docker bryggan cluster stack +type: application +version: 0.1.0 +dependencies: + - name: cilium + version: 1.19.1 + repository: https://helm.cilium.io/ diff --git a/providers/docker/scs/1-35/cluster-addon/cni/values.yaml b/providers/docker/scs/1-35/cluster-addon/cni/values.yaml new file mode 100644 index 00000000..8a312f0c --- /dev/null +++ b/providers/docker/scs/1-35/cluster-addon/cni/values.yaml @@ -0,0 +1,14 @@ +cilium: + namespaceOverride: kube-system + tls: + secretsNamespace: + name: "kube-system" + sessionAffinity: true + sctp: + enabled: true + ipam: + mode: "kubernetes" + gatewayAPI: + enabled: true + secretsNamespace: + name: "kube-system" diff --git a/providers/docker/scs/1-35/cluster-addon/metrics-server/Chart.yaml b/providers/docker/scs/1-35/cluster-addon/metrics-server/Chart.yaml new file mode 100644 index 00000000..17cec027 --- /dev/null +++ b/providers/docker/scs/1-35/cluster-addon/metrics-server/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: metrics-server +description: Metrics Server for Docker bryggan cluster stack +type: application +version: 0.1.0 +dependencies: + - name: metrics-server + version: "3.13.0" + repository: https://kubernetes-sigs.github.io/metrics-server/ diff --git a/providers/docker/scs/cluster-addon-values.yaml b/providers/docker/scs/1-35/cluster-addon/metrics-server/overwrite.yaml similarity index 100% rename from providers/docker/scs/cluster-addon-values.yaml rename to providers/docker/scs/1-35/cluster-addon/metrics-server/overwrite.yaml diff --git a/providers/docker/scs/1-35/cluster-addon/metrics-server/values.yaml b/providers/docker/scs/1-35/cluster-addon/metrics-server/values.yaml new file mode 100644 index 00000000..a89bf027 --- /dev/null +++ b/providers/docker/scs/1-35/cluster-addon/metrics-server/values.yaml @@ -0,0 +1,4 @@ +metrics-server: + fullnameOverride: metrics-server + args: + - --kubelet-insecure-tls diff --git a/providers/docker/scs/1-35/cluster-class/Chart.yaml b/providers/docker/scs/1-35/cluster-class/Chart.yaml new file mode 100644 index 00000000..5d76f025 --- /dev/null +++ b/providers/docker/scs/1-35/cluster-class/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: docker-scs-1-35-cluster-class +description: | + SCS Cluster Class (1.35) for the Docker infrastructure provider. + Uses CAPI v1beta2 for core/kubeadm resources, v1beta1 for Docker infra resources. +type: application +version: v1 diff --git a/providers/docker/scs/1-35/cluster-class/templates/_helpers.tpl b/providers/docker/scs/1-35/cluster-class/templates/_helpers.tpl new file mode 100644 index 00000000..449a5720 --- /dev/null +++ b/providers/docker/scs/1-35/cluster-class/templates/_helpers.tpl @@ -0,0 +1,51 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "cluster-class.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "cluster-class.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "cluster-class.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "cluster-class.labels" -}} +helm.sh/chart: {{ include "cluster-class.chart" . }} +{{ include "cluster-class.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "cluster-class.selectorLabels" -}} +app.kubernetes.io/name: {{ include "cluster-class.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/providers/docker/scs/1-35/cluster-class/templates/cluster-class.yaml b/providers/docker/scs/1-35/cluster-class/templates/cluster-class.yaml new file mode 100644 index 00000000..a848c31a --- /dev/null +++ b/providers/docker/scs/1-35/cluster-class/templates/cluster-class.yaml @@ -0,0 +1,314 @@ +apiVersion: cluster.x-k8s.io/v1beta2 +kind: ClusterClass +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }} +spec: + controlPlane: + templateRef: + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 + kind: KubeadmControlPlaneTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane + machineInfrastructure: + templateRef: + kind: DockerMachineTemplate + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker + infrastructure: + templateRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: DockerClusterTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-cluster + workers: + machineDeployments: + - class: default-worker + bootstrap: + templateRef: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 + kind: KubeadmConfigTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker + infrastructure: + templateRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: DockerMachineTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker + + # + # Variables + # + variables: + - name: imageRepository + required: false + schema: + openAPIV3Schema: + type: string + default: {{ .Values.variables.imageRepository | quote }} + example: "registry.k8s.io" + description: "imageRepository sets the container registry to pull images from. If empty, the kubeadm default will be used." + - name: certSANs + required: false + schema: + openAPIV3Schema: + type: array + default: {{ .Values.variables.certSANs | toJson }} + example: ["mydomain.example"] + description: "certSANs sets extra Subject Alternative Names for the API Server signing cert." + items: + type: string + - name: oidcConfig + required: false + schema: + openAPIV3Schema: + type: object + properties: + clientID: + type: string + example: "kubectl" + description: "A client id that all tokens must be issued for." + issuerURL: + type: string + example: "https://dex.k8s.scs.community" + description: >- + URL of the provider that allows the API server to discover public signing keys. + Only URLs that use the https:// scheme are accepted. + usernameClaim: + type: string + example: "preferred_username" + default: {{ .Values.variables.oidcConfig.usernameClaim | quote }} + description: "JWT claim to use as the user name." + groupsClaim: + type: string + example: "groups" + default: {{ .Values.variables.oidcConfig.groupsClaim | quote }} + description: "JWT claim to use as the user's group. If the claim is present it must be an array of strings." + usernamePrefix: + type: string + example: "oidc:" + default: {{ .Values.variables.oidcConfig.usernamePrefix | quote }} + description: "Prefix prepended to username claims to prevent clashes with existing names." + groupsPrefix: + type: string + example: "oidc:" + default: {{ .Values.variables.oidcConfig.groupsPrefix | quote }} + description: "Prefix prepended to group claims to prevent clashes with existing names." + - name: registryMirrors + required: false + schema: + openAPIV3Schema: + type: array + default: {{ .Values.variables.registryMirrors | toJson }} + description: "Registry mirrors for upstream container registries. Configures both containerd and CRI-O to pull through a mirror." + items: + type: object + properties: + hostnameUpstream: + type: string + example: "docker.io" + description: "The hostname of the upstream registry." + urlUpstream: + type: string + example: "https://registry-1.docker.io" + description: "The server URL of the upstream registry." + urlMirror: + type: string + example: "https://registry.example.com/v2/dockerhub" + description: "The URL of the mirror registry." + certMirror: + type: string + example: "" + description: "TLS certificate of the mirror in PEM format (optional)." + + # + # Patches + # + patches: + - name: imageRepository + description: "Sets the imageRepository used for the KubeadmControlPlane." + enabledIf: {{ `'{{ ne .imageRepository "" }}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 + kind: KubeadmControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/imageRepository" + valueFrom: + variable: imageRepository + - name: customImage + description: "Sets the container image that is used for running dockerMachines for the control plane." + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: DockerMachineTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/customImage" + value: {{ (index .Values.images.controlPlane 0).name }} + - name: workerImage + description: "Sets the container image that is used for running dockerMachines for the worker machineDeployments." + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: DockerMachineTemplate + matchResources: + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/customImage" + value: {{ (index .Values.images.worker 0).name }} + - name: certSANs + description: "certSANs sets extra Subject Alternative Names for the API Server signing cert." + enabledIf: {{ `'{{ if .certSANs }}true{{end}}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 + kind: KubeadmControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/certSANs" + valueFrom: + variable: certSANs + - name: oidcConfig + description: "Configure API Server to use external authentication service." + enabledIf: {{ `'{{ if and .oidcConfig .oidcConfig.clientID .oidcConfig.issuerURL }}true{{end}}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 + kind: KubeadmControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" + valueFrom: + template: | + name: oidc-client-id + value: {{ `'{{ .oidcConfig.clientID }}'` }} + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" + valueFrom: + template: | + name: oidc-issuer-url + value: {{ `'{{ .oidcConfig.issuerURL }}'` }} + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" + valueFrom: + template: | + name: oidc-username-claim + value: {{ `'{{ .oidcConfig.usernameClaim }}'` }} + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" + valueFrom: + template: | + name: oidc-groups-claim + value: {{ `'{{ .oidcConfig.groupsClaim }}'` }} + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" + valueFrom: + template: | + name: oidc-username-prefix + value: {{ `'{{ .oidcConfig.usernamePrefix }}'` }} + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" + valueFrom: + template: | + name: oidc-groups-prefix + value: {{ `'{{ .oidcConfig.groupsPrefix }}'` }} + # + # Registry mirror patches + # + - name: registryMirrorsControlPlane + description: "Configure registry mirrors on control plane nodes (containerd + CRI-O)." + enabledIf: {{ `'{{ if .registryMirrors }}true{{end}}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 + kind: KubeadmControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/files" + valueFrom: + template: | + {{ `{{- range $r := .registryMirrors }}` }} + - content: | + server = "{{ `{{ $r.urlUpstream }}` }}" + [host."{{ `{{ $r.urlMirror }}` }}"] + capabilities = ["pull","resolve"] + override_path = true + owner: root:root + path: /etc/containerd/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/hosts.toml + permissions: "0644" + - content: | + [[registry]] + prefix = "{{ `{{ $r.hostnameUpstream }}` }}" + location = "{{ `{{ $r.hostnameUpstream }}` }}" + [[registry.mirror]] + location = "{{ `{{ $r.urlMirror }}` }}" + owner: root:root + path: /etc/containers/registries.conf.d/50-mirror-{{ `{{ $r.hostnameUpstream }}` }}.conf + permissions: "0644" + {{ `{{- if $r.certMirror }}` }} + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containerd/certs/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containers/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + {{ `{{- end }}` }} + {{ `{{- end }}` }} + - name: registryMirrorsWorker + description: "Configure registry mirrors on worker nodes (containerd + CRI-O)." + enabledIf: {{ `'{{ if .registryMirrors }}true{{end}}'` }} + definitions: + - selector: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 + kind: KubeadmConfigTemplate + matchResources: + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/files" + valueFrom: + template: | + {{ `{{- range $r := .registryMirrors }}` }} + - content: | + server = "{{ `{{ $r.urlUpstream }}` }}" + [host."{{ `{{ $r.urlMirror }}` }}"] + capabilities = ["pull","resolve"] + override_path = true + owner: root:root + path: /etc/containerd/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/hosts.toml + permissions: "0644" + - content: | + [[registry]] + prefix = "{{ `{{ $r.hostnameUpstream }}` }}" + location = "{{ `{{ $r.hostnameUpstream }}` }}" + [[registry.mirror]] + location = "{{ `{{ $r.urlMirror }}` }}" + owner: root:root + path: /etc/containers/registries.conf.d/50-mirror-{{ `{{ $r.hostnameUpstream }}` }}.conf + permissions: "0644" + {{ `{{- if $r.certMirror }}` }} + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containerd/certs/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containers/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + {{ `{{- end }}` }} + {{ `{{- end }}` }} diff --git a/providers/docker/scs/1-35/cluster-class/templates/docker-cluster-template.yaml b/providers/docker/scs/1-35/cluster-class/templates/docker-cluster-template.yaml new file mode 100644 index 00000000..ca51270d --- /dev/null +++ b/providers/docker/scs/1-35/cluster-class/templates/docker-cluster-template.yaml @@ -0,0 +1,7 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: DockerClusterTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-cluster +spec: + template: + spec: {} diff --git a/providers/docker/scs/1-35/cluster-class/templates/docker-machine-template.yaml b/providers/docker/scs/1-35/cluster-class/templates/docker-machine-template.yaml new file mode 100644 index 00000000..372949f8 --- /dev/null +++ b/providers/docker/scs/1-35/cluster-class/templates/docker-machine-template.yaml @@ -0,0 +1,10 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: DockerMachineTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker +spec: + template: + spec: + extraMounts: + - containerPath: "/var/run/docker.sock" + hostPath: "/var/run/docker.sock" diff --git a/providers/docker/scs/1-35/cluster-class/templates/kubeadm-config-template-worker.yaml b/providers/docker/scs/1-35/cluster-class/templates/kubeadm-config-template-worker.yaml new file mode 100644 index 00000000..8117fedc --- /dev/null +++ b/providers/docker/scs/1-35/cluster-class/templates/kubeadm-config-template-worker.yaml @@ -0,0 +1,15 @@ +apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 +kind: KubeadmConfigTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker +spec: + template: + spec: + joinConfiguration: + nodeRegistration: + criSocket: unix:///var/run/containerd/containerd.sock + kubeletExtraArgs: + - name: eviction-hard + value: "nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%" + - name: fail-swap-on + value: "false" diff --git a/providers/docker/scs/1-35/cluster-class/templates/kubeadm-control-plane-template.yaml b/providers/docker/scs/1-35/cluster-class/templates/kubeadm-control-plane-template.yaml new file mode 100644 index 00000000..c4ee9070 --- /dev/null +++ b/providers/docker/scs/1-35/cluster-class/templates/kubeadm-control-plane-template.yaml @@ -0,0 +1,99 @@ +apiVersion: controlplane.cluster.x-k8s.io/v1beta2 +kind: KubeadmControlPlaneTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane +spec: + template: + spec: + kubeadmConfigSpec: + clusterConfiguration: + apiServer: + certSANs: [localhost, 127.0.0.1, 0.0.0.0, host.docker.internal] + controllerManager: + extraArgs: + - name: bind-address + value: 0.0.0.0 + - name: secure-port + value: "10257" + - name: profiling + value: "false" + - name: terminated-pod-gc-threshold + value: "100" + scheduler: + extraArgs: + - name: bind-address + value: 0.0.0.0 + - name: secure-port + value: "10259" + - name: profiling + value: "false" + etcd: + local: + dataDir: /var/lib/etcd + extraArgs: + - name: listen-metrics-urls + value: http://0.0.0.0:2381 + - name: auto-compaction-mode + value: periodic + - name: auto-compaction-retention + value: 8h + - name: election-timeout + value: "2500" + - name: heartbeat-interval + value: "250" + - name: snapshot-count + value: "6400" + files: + - content: | + --- + apiVersion: kubeproxy.config.k8s.io/v1alpha1 + kind: KubeProxyConfiguration + metricsBindAddress: "0.0.0.0:10249" + path: /etc/kube-proxy-config.yaml + - content: | + #!/usr/bin/env bash + set -o errexit + set -o nounset + set -o pipefail + + dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + readonly dir + + if [[ ! -f ${dir}/kube-proxy-config.yaml ]]; then + exit 0 + fi + + kubeadm_file="/etc/kubeadm.yml" + if [[ ! -f ${kubeadm_file} ]]; then + kubeadm_file="/run/kubeadm/kubeadm.yaml" + fi + + if [[ ! -f ${kubeadm_file} ]]; then + exit 0 + fi + + cat "${dir}/kube-proxy-config.yaml" >> "${kubeadm_file}" + rm "${dir}/kube-proxy-config.yaml" + + echo success > /tmp/kube-proxy-patch + owner: root:root + path: /etc/kube-proxy-patch.sh + permissions: "0755" + preKubeadmCommands: + - bash /etc/kube-proxy-patch.sh + initConfiguration: + nodeRegistration: + criSocket: unix:///var/run/containerd/containerd.sock + kubeletExtraArgs: + - name: eviction-hard + value: "nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%" + - name: fail-swap-on + value: "false" + joinConfiguration: + nodeRegistration: + criSocket: unix:///var/run/containerd/containerd.sock + kubeletExtraArgs: + - name: eviction-hard + value: "nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%" + - name: fail-swap-on + value: "false" diff --git a/providers/docker/scs/1-35/cluster-class/values.yaml b/providers/docker/scs/1-35/cluster-class/values.yaml new file mode 100644 index 00000000..bf6e6ea1 --- /dev/null +++ b/providers/docker/scs/1-35/cluster-class/values.yaml @@ -0,0 +1,20 @@ +# Node images for kind (patched at build time by build.sh for each K8s version) +images: + controlPlane: + - name: registry.scs.community/docker.io/kindest/node:v1.35.1 + worker: + - name: registry.scs.community/docker.io/kindest/node:v1.35.1 + +# ClusterClass variable defaults +# These are referenced by the ClusterClass template and can be overridden per deployment. +variables: + imageRepository: "" + certSANs: [] + registryMirrors: [] + oidcConfig: + clientID: "" + issuerURL: "" + usernameClaim: "preferred_username" + groupsClaim: "groups" + usernamePrefix: "oidc:" + groupsPrefix: "oidc:" diff --git a/providers/docker/scs/1-35/clusteraddon.yaml b/providers/docker/scs/1-35/clusteraddon.yaml new file mode 100644 index 00000000..9a639968 --- /dev/null +++ b/providers/docker/scs/1-35/clusteraddon.yaml @@ -0,0 +1,18 @@ +apiVersion: clusteraddonconfig.x-k8s.io/v1alpha1 +clusterAddonVersion: clusteraddons.clusterstack.x-k8s.io/v1alpha1 +addonStages: + AfterControlPlaneInitialized: + - name: cni + action: apply + - name: metrics-server + action: apply + BeforeClusterUpgrade: + - name: cni + action: apply + - name: metrics-server + action: apply + AfterClusterUpgrade: + - name: cni + action: apply + - name: metrics-server + action: apply diff --git a/providers/docker/scs/1-35/stack.yaml b/providers/docker/scs/1-35/stack.yaml new file mode 100644 index 00000000..75f1e724 --- /dev/null +++ b/providers/docker/scs/1-35/stack.yaml @@ -0,0 +1,3 @@ +provider: docker +clusterStackName: scs +kubernetesVersion: 1.35 diff --git a/providers/docker/scs/README.md b/providers/docker/scs/README.md deleted file mode 100644 index 73e8ae8a..00000000 --- a/providers/docker/scs/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# Cluster Stacks - -## Getting started - -```sh -# Create bootstrap cluster -echo " ---- -kind: Cluster -apiVersion: kind.x-k8s.io/v1alpha4 -networking: - ipFamily: dual -nodes: - - role: control-plane - extraMounts: - - hostPath: /var/run/docker.sock - containerPath: /var/run/docker.sock" | kind create cluster --config - - -# Init Cluster API -export CLUSTER_TOPOLOGY=true -export EXP_CLUSTER_RESOURCE_SET=true -export EXP_RUNTIME_SDK=true -clusterctl init --infrastructure docker - -kubectl -n capi-system rollout status deployment -kubectl -n capd-system rollout status deployment - -# Install CSO and CSPO -helm upgrade -i cso \ --n cso-system \ ---create-namespace \ -oci://registry.scs.community/cluster-stacks/cso \ ---set clusterStackVariables.ociRepository=registry.scs.community/kaas/cluster-stacks - -kubectl create namespace cluster -``` - -clusterstack.yaml - -```yaml -apiVersion: clusterstack.x-k8s.io/v1alpha1 -kind: ClusterStack -metadata: - name: docker - namespace: cluster -spec: - provider: docker - name: scs - kubernetesVersion: "1.30" - channel: custom - autoSubscribe: false - noProvider: true - versions: - - v0-sha.rwvgrna -``` - -Check if ClusterClasses exist - -```sh -kubectl get clusterclass -n cluster -``` - -cluster.yaml - -```yaml -apiVersion: cluster.x-k8s.io/v1beta1 -kind: Cluster -metadata: - name: docker-testcluster - namespace: cluster - labels: - managed-secret: cloud-config -spec: - topology: - class: docker-scs-1-30-v0-sha.rwvgrna - controlPlane: - replicas: 1 - version: v1.30.10 - workers: - machineDeployments: - - class: default-worker - name: md-0 - replicas: 1 -``` - -```sh -clusterctl get kubeconfig -n cluster docker-testcluster > /tmp/kubeconfig -kubectl get nodes --kubeconfig /tmp/kubeconfig -``` diff --git a/providers/docker/scs/cluster-addon/.helmignore b/providers/docker/scs/cluster-addon/.helmignore deleted file mode 100644 index 0e8a0eb3..00000000 --- a/providers/docker/scs/cluster-addon/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/providers/docker/scs/cluster-addon/cni/Chart.lock b/providers/docker/scs/cluster-addon/cni/Chart.lock deleted file mode 100644 index 776d3129..00000000 --- a/providers/docker/scs/cluster-addon/cni/Chart.lock +++ /dev/null @@ -1,6 +0,0 @@ -dependencies: -- name: cilium - repository: https://helm.cilium.io/ - version: 1.16.6 -digest: sha256:e6a746a27a71acab49c5d54cba2d37eed32e04f8b74af5651e2266ae251c55d8 -generated: "2025-02-13T12:55:17.200292016+01:00" diff --git a/providers/docker/scs/cluster-addon/cni/charts/cilium-1.16.6.tgz b/providers/docker/scs/cluster-addon/cni/charts/cilium-1.16.6.tgz deleted file mode 100644 index 3e99630f9fe6bdd8434f4a84901dda2c47fab012..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 204717 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�nbfV{E=1qNoMm6ro>#4Q{!DpiSyJiV&R3=>_9*3;p@|vp?GvxFVXG zu8%Lkv$uNuT>F{mW}xcK1v`(ua+)qriZSEbs-w=<$kk6Z(pI6I zOGFFvf|^#nG%w;rt%8DuNDT}~SMwmFr$0qFzr{-d%%>m=!nG)7^|g$pRk$5x+oaeA zzX`c*Uk%>Yy=>D$a<=_Q{}nrZ#Z-#0lF7jeWg|J3f7Nd8Sx}Gi?T? zlx92zHd2c;|6sI05sDfpcu)e&LZQ*)EZs0FxE#f}saH22RNs79ZQBT(u*Bl2w%*z8 zG&Ta=E>0yoMP{MS%%6a}5ynTPzNO;zMV6K2tIGuq3c#q-I6<5L zpZ@fI%$Q>}ay~vqW+LB@FjrL!bO2J9fNo?kJq#E=y2gVX!$)^8K=gS}aJ3IY=)cgs?HH+o1_$Q3e)jsJ1Z=BB|IK+@9dv-W;(pig2Dsz?X~x7nD@1 zcO}|9V7iK-t9aNn^^I)Etc8x&R%(t%2-vF8tdVjSmH%T%{Q1s_NAM8h4NuyA| z7EZX{J0nul9lX41G}bXCc~)IM5s>Se=B%zX9GWq5cDIb3jnzKKx-vUN8_hQ)1D^G| zu@+5mHlsy2SDsg+&K#k!gRh;41zJWc*Q%v3-J5kAc4z?&W!TnvVi?OF#7;r@Z?jX2 znc}NjnG!x%wH4wa>@_rD&a9jBz1oATb-xOrxS{3o<*Q>(w22F*Uwu?sWSpAMeIQ*o z?w;g~t265c5|nCx>z3NWaP|`CN*UwPmgn>A7A5(!#Mpw2KUB1}WkVqyyP>swS2<|B z#DttkzFgZR;LZd<2%8*2OrtS>2497dF+s?mXHgyES|FW)q~!s*;v( z`|}l>Hb1DUKH7~QD@JM(-K>DB+QQ}Sg=x_X+|IXH7U{YPmQ|aEy{rNC6XtHVpz~2& z2Q0@WCu4d?R?W5f^7iFX)%3!&L?3H8J=mdV`Zx+wK+#-b2^4gJ1<~7SD;MP zV%!SA3QdmA8(2qU1x~|uGlO>>WuPxC*Ju!)2fbE=!xqnp1qtuI;$z(QTBGg{lAYy`c>%4fK| zHoh>?KrGHHpw$CsmDc|h289_FIRS(F9jg?aMc#_tJ$dw8JDFG z*O4AtppVM30^D6GFN@XP=4H`8j5Zjz{oQyR7xP(cFQuofafCVW?#AQu`RrQP{DCzO zFn81rq)i=fv>52tt($>Q&UeLeaFpuY1Li0t2l)Nc_oYJf_tAm+tKg3 zo3&TwM72NDaM@E2)jb2_{Mn%`Z5PElAE0MLZ}b2)IuGe4Je*G$r*b;$g;~{Fog@JC z8ttx%PA?Yw2y53eT1>!e#jN#HFtpVUBA65J>IK^B+Zo+HaYV6lvF{%YuBW|g&G&IR zcmOU3-MAbyaXIL_S*T6Y1Q%BlOX6s^7zx5Dn+rsRm=na(bj}6J^p%#;BH<2fUOuf& z542cdK^N9f0L}0cjfnL&gbPH;5GOOXfRUk+Z~+IUG|o|2k_4SpGXPp`!N7fy6YYth zZ{5&M!ESSsO^7zSVE|Ba$|`dypBGP&VA_*Gp~6W@6m|ub z*lGO>G`@t16)>}^@(h=?djW!X<(EQ|S%Q8nSz!nJ6x#Y!SIspzq^Z_I@mqJ&o}wwc zB@3dB8Ms&?R!o>$nUoa;NwuShp@57EJ}(s&k2odce5q|1Z!0FC+GQlA+T+36w>S|* z!Wug8p{3i$f?`du>bSB!I{YQmuC5s!j|sP=X89i;OTw#lZ!TuF%zX>mxT8x8iX!dP zq7>fhlIBODsOWae&@B~U^VFWw*@k;FvsVo4L?J*c-oOy5Iq20HBJi(#sKiy z02H6<%i>!qDtnKb6Sg4ymQ$%O?LFu)Y#&_X+Nz*QJ))skFC#aWDe}Z0HLs~Eiy@N5 z&(H6LKsEWiahN3^IzpyQAi}E7!j1b+-4#x{qjH=`5KbG09M!)rXpYm=A2vu6H`y^j zr-OEz@VJ`%#;_TadJZ#RI060-$PM>aeNG9~)16`DRZ{}jRkubnC^FA#(jh!z!kY%a?=>{#<9w%Afu zv?kirIjXBv=dwb`0(C%CNvhC9Z5`tdE0-lragHRNk;;|Mv^KtUl8&iy$s-H6f>V=5 za5{y<{FY=6_X0i$4c`^{5|Qy(eYasHL9~QDPwfn=3Dz^(F&k?e2%Q-V`U~|<+c?J) zfifZqpV5LCGxzoJg(-o*pTdz_T_BDr7ZS0N;^6@0DbfL9wgY!oZ6FzVxMOqW^ZDlx zubxm$3mD3;HvO0iX7$ z1H$ks<2!!WvhjBHIqs^ zKf@3XXC_2_0s5HXyDGj<6RkPuP%)p42v_7s3dvQe^)6D46`@%=bAh#1(dm1b{Z2K) zb2;@9R{f~2Ya?xZ4OLr(vq9Mf)k4|ny<17wS94x4q8Cl*;GN2NlCu%c<8KC`?x+Tu zyegM$oN^|FuVg<(qRgp8qowmAflNz<9#D6J-W3FunCkPLjkQ`e7a9RUtZRZ=UobQ{ z2xx-Eh2AF$hy?(^6yFNLIB;Rq$J^=HudOt-8OtEJ6!v3bHFWLtc_$mHdUfDRn~s8w z2dQldIuZ!0Z4JI*oRs-eE8JRKs~T`tmd0fjwj{$@9MbSWBoY#jaw?|AAFzplcBQO` z|5`B;P@;6WEy$xvyETK{P4>VwRLAT-Rcv8K3p%4eS0kZD&v&NDgzl=w6}r}6&K3}R8x1QRV>8bux#wO0#`k`8!j8f zD*%e()1H-nWg^)PUp})V_L*Li|!Euokf`YaxSv4*={gnKi z5njb*S$nS*TI*xc_1)F!G$dy*BMO)5Y}`-ke}43tji#13xfVP|LtFMY;xS3umb-!RaVFiH@J4f;n0kKEB zP!oI06dT1?1Wtpx)|nE36$jOhh7f~c;m z3paM6KP3{H#Fem8rRSUl}lM`dEYdD$#EThsEEOLq(%6H;XB7;W3cOjJ303aq$Zv{n5J zIidHE&1AFWw2)&IT{;kKG840RbXz^+4l*yak$kJ|O8}}sRlj7gF&<&IZ`bS6#3`zZ zq~We=EGAV_lN&Kwvb3Q0odY>93X-ciF#o#O9O$-s4q7M;o(Y4&1~Tv*PnZaZB0LA~ z$O2#;%J%1=ZHls?rDk$c3@Ih$ZcY7nxIU z7+@m?pR<=*1isWpHZyaTUm}Z*h#VU$Tq7p12TbHfGBtWXyZ%vkG?R009I9a007%zP zak8J<{dWo5FX)5TwM85BjpveSA8}%@bHUVRLq)x<-N15QW>}JwYzM)rl7rd(Ls)nyl&v-wKnN=HxCVLY!XMw}x&-%ymgjpeUwg z(0M)#^~AWpkukk{MRMoPOT)H7cTgeDsv4ml5Q1Trw3_*bYd1|ux_5MOT7`Z$Jl<$5 zn|CLg6zDac2=oz2c8PEQk6=ZUkQxIM^HD+M=_~P?=IX51gbyEMnu&M}pIaR@n}vOn z`sY-`Q1}M=R>SM>+-dj`neLyi(LJaUMM|0o*zdh41R8%FfRxDFSN&B7aj7${#m19I8xcEFxec7koR07ar#YKvpL0f6c|kq{Z-+|y z*E~?5x6gFY7)Z+Mk9PdC666^B7xOoVp2eqaI~VYU}1 zFl|~@SLy(G2pN=XA+I&mo^@(x&i36|D!QLxe$&|St(@B(mzt0{F;LxRR_y7|sN(^K z99&W}f7PnGsZlOic6|Ek5`Sag~UsLpHhcR zo|<~bTB5I6b;41jw{ITR}J7GJb*s4v2J^P{GUE;%d~xAT2)=k z>sxn|VTnsD-Mj|&Q(FU5ow*Gj?x;{tT)^k8V^;lk!+hVI`S0+5YjhTF+jThF5Y?(? zEScU3u|0-bWKn|!Rpm*AB!^)@-zMkKIyECUeX)f3>R5YwWfD z)u#=C6x3eoip=wCu5!EylXG0TK{={1$9ES zU;Eo_28|!i^Hz?9*7@pOteBkiVa0aKu}S4hpNQ*3y|e^}8WHn(4i*4c!KlptgPMAg z*iiqR9oyQcrt+n#+mWNILo&1dT;d{Qv)A)Hx3+{D11O5es;m)h*J`_F45x+gmZdi) zrD?Nwh+%Q_rZ#(b+IEu_m|QZ-Nh%$c&!1OS=M_{@#HwxaK36aR?DY&JWtkV8;B=}` zX=FH7b+em z8m(wN8u0-^s*O=h>12A9Vx@GZIGu(WsHzkTRV$_?RP!+Dgt2Z+afngDZA7{4G5Yqve=7Yd>zNSGiby9efFof4H;%Ey|@U5J{ zY1I7&PyJvPU>5GoB!m4q9?>H6E?p`&ka{IT{pe(|Kfk0!2G8jk6vK;DKbO<8K-ERY zIGOo=tg2YgjIFq0-*tkA)D|{V$Gf5uqnwU(CJhm@!U$4zr>)lOcCRB3u_PBPJFcDb z0!UOfr4EQ|Bc_9B4Ig3}2zU36b5c?$>3Nd&21!daVk2dUhHQa(?JT#8YVWDHR3PB7IDS zr#}GXp6KX>)aQiz0*&3HIvY8>JQLeF>mvyVQuws_WrY`LsADhT4=sa^mBUB))=3Oh z1!N5BcF?eW@Zm{t$sW%3lCw%03H72XT~u{~S|#Y_22LE>RV|>Zg#oJmz69swHOQzt zc~q^%IXRew#6^a4R#-EDf7zIi;6z_dye?1_&Fb4r8^O~w33^z}x-Tz>bXR4O^7|Z+ zf@(A!lvt)yAB#-zI5Q>PSUAlefQ0cyy=EoL z*`#4$upX__ly)Q20Mn<)^ddLIlbkG21lF`G`g4;gp*WTrJNmOvp}I7w+V--}j*h2T zOs_~&GB^FI!dQ?-kd^7ywj9K;1rdU>;>d>2`LhmXv5l?sI%_r@-aHpc8^lA?XV4nt)k&P0l9Tt88E@k*XwfwA;`>E4I#drk|Ku3;98@ZG9eYTSCo4L z+WF2nPy^hY3fZztG(xu&G}Xs(K}}k}l~dWM9wy>VH=f6zf-Ud6xxKZsc}?=%d4Ef? zD#aIo)yWHSM{>l^HHX^a8J?RHP0tPpv$G{WuUCx}k8F8}sWv&AfUnnJa-SF8R4L%q zrxx_OaxeV*JUL$VUO9kEC~9vxuVwh6WH=ty{MJZ@r5DDS>_Ta<0&ROy@!IKI4#g?Y zu77;Z`0@Fvb$ReuopuwQ$$30xJa#9ijY}WQLBwz~9})eEh9OyVBEjRg`biL_jr-iz zZ?p0Y+H9Q{j$wor_2VB~Xww=lJH6t!I?A=bNb|uHb9A`|oK~K$T#$yRy?Ln5R)4A^ zVZCf+&T~lc_@{|^fXspg8H7S-R6xu6?#))WX{t#fe!F4xa7*(1O~G!9+Mg&W6{vdg zjjHH*Ej3MWLz=Yb!M8WtZ0K>|nN3yU%9OVgop(*BSQhD&vx5Fi-aHrQEUOH_PKg#d zATGvJ9NTTzR2;SZax~AVY(E}|)#tXcs?laZq>#A_v`;`CmGp}61%V4c?TVq!tiwO= zeY+nuv@`#YsvoJ{OH*@#wAXvtYhx%2RlP%M3U*J*I{l7X{V%# zx~?6k21qCVe&@qjrD<;;7;F%>4S1~IxN0nWiQY=(qcHw8?qcUE>nHB&KPs5Xf?%vlo*6n)y53=mrmdA>IqHMenQjI zVin4lpesVqfA9Pca3H+EG{?yX;dz7=$_6!S2`1UK7&S#|1 zu?*W5)+}qy%3hMIK5UKz-&B!vsUAMU%76m}!7!vr|Va4Ma0CrrYtQsM7D zes2S14L>x%6w`t@ObR)7%y@KRogFP4AMW?57OUBpE;7l{6k~--KVH67Cjnog5_5@Y zfyOzzZFjFK$8(-T-O#i;JtuRX{|CTqoYMWM%mv|y7Gt&w3qiQ5wh{|L_^ATdoe?6? zYD#cMxLB*QcFnh=ywt0y)Mpr%g$TLmGX^Xi9_lff@3oB}Fi_J_#|`xCloc4&56|{` znG67$kzzS$lxq!7L0xlnH6lX>x8{&<@c0I=B-uAm>*(SX-H=tA2Ougb`-b%G9jKPz zk}8pDkaJM)aFi5cGYDB32|mH86VLFSPD(x-1*IgYny9L0_)bZBXCr?Jz?LL$&);3G_cBEvo)n-bEIu0I89S8TIj)UKTI+}g1rjCPqQpdr1)Nybh z>NvOubsP+!j>ZvLvzpmFm(*c2dJZ0|4+0&&(C13^U8}ZG`V>7DB}rACFp_vK*)b%v z(>c>FSay1G0dSpVevGB$jh>qHsym9*=7|DrUm#qTYJleNDphc`qs8IBi3n|0>Hz@M zOcaXk!M_IBdYJ&UG%p;xpl4=TeCVC&Wa?2KtUJns{!t#>bCd_GM|m)8l!l(_&h+0^ zXa6Q%s1D$>9l>8BgVWYlU~>lYC6+9UX$iFK#&RQ^ z&8FS|AlEI34Y;K7NR{?%+-_(C8G9eF|G2mcBK4tR##jwy*hDD->Gb00EF9Jhi9xg! zw=&2TmsANz(7##Ul7i5slL`AN8QjVPBXuD`SYH%2>}iv5tO~^LR56+fpj_}M;v{8- zqDc@8ul2LZHt(yS+sr2ZXhfR)TZ2aT$Ri)^j|oT`Z1`TPLYr^VW%b z;pvY$4Q*n)-fV!7j=66=9CGu#N_Jm$wMGgFX6OM;d2vA3o%K*qa&E;?=Fq+h- z>}JckU6op75;xNL^kS>0W~nvgTEI72|I6<$-~}(!;Zro4OH?p_lcHO~iLH{L3s^;) z+L;vuF^*=86TejwtvT2ML)6%hCS|oCaXxCsSogKR-!k+~X#k~Qd`_@!Z#o~ROa+$;!@Re?k!ZAASowOz=;J0?Ivzs52kjN5=B3RH87%YCg<%4L>E_8 z!;N6<)>>S079^c>DwhgTawn6L(*@1RqzW74EFFtO6m3tLklX6d0V(`-N^8!TbmCvt zpZ@%m<3xR2Pw>ze89F!0W!;0h-)MEBzbCf#!0A?~EplAET$Wgf)wdSpuFPplWv*W( zyd@b@%@NI>sKZ$O86qr|hSP79wt zc>P#0Ue7#;c%F#a+WPwM<91Dtw2ghFDbaeb1-TM>pkr<|ZnKyvJZQC`-Sx?b+-2Djes5~(aZLIG8g?~3?6z=T{8I(!)%lfu<^!(r5G&AxS|l&#-tLQ zj$tMdyj2>8e1`9I@P|S|)$Ih1ULf-xr?aAQ+kDgwz{F9iuimAqgyJsy+V(f#@2_ei zT~qbuPY)IDkzw}`))o11?%)D2m{!1An4!f=`vEVAaae_ z3N3`e#Do@KNi6501)GX^4$_(SEE-~_tvZ+&I&Exr!7`JfKH&tOuhsoj6+r4V$?n7E z%wC|j#T8MAwylSiGr+12qn+N7DXs$`z|bT^4lX8Qus{VXVlRwP z3>}1Jim%SEU=Sw*$58ke*DHK_swpz518FfK+*k#za#^8k0;O`O6wD&s2W~a4Eht$b zF3CSgD(wT4$JN0n+4zGxde#3p)T!R8jMGj+2m@S+*a~`nRfuCYE4j{W6Zr_5HCF#j z>q>oe>Vvz-v{^~*4BwrW7li-7gtSi|yNDT~c82fZ_K$F#QnP6UT@81A6ls=|It}Lc zO0IG(QWi&5%}J^mg8&vXObr0MK9Vzh_lk@O=fp|b`HJL3+RGh_r<%gdRCV#qbmcIr zz2NVGEiJBP-oT3%v6Oj7|GL5YJ(Zjs<8<1fr;?Mnol3mj=sx|iyQT9QZq=b;O066? zfoAvY(lUuNwNN=;hPtnqjwXTTyDKXIK0uL!3>W82UJ{%wK@NIDY!D@s#RZc(d4(2) z;%4wc5JHzSg5U}A7|-Rj4n^|Q{ZC0D&HLeD+iMM>Bfzu)2aq=Gbln@yFaicxr${nzCA4$Ab@UI;j0yJ) zMGFjv92QXr^%uFHlYzY(ALpbpEC2dyA{Oc4^PT6rt=F+TFmR5)kcimVecjZ%iB2_IO?0B9SN<>JdTU`O!q^wc*;r<(_+%1 z6;dqNGPxU%&*!siUHJ!Ainq_SE$LJ@Y2?G}?t-k{!%1fcp^fPS^>g+&CIN_5&CV^QTi*~;#wjl2g2)jqGUK-qfY`ixa^tQO z1?Cu+=S04wMRsMJNLJ>Uv7W_>lc1G)qN{OS+g@9XJ6GYHz=E4XHWRK9vo#fagh|{-f0^$gZ3Br6tCC7ZJwrNQ?Jiq3> z%52&t8;{V-B`W9Il0_xL5^AZ%G9kprHhbSwMCB8Ka4r}snGn>Zfnf{6In4+ncT@nJ zy4R$&uN2lAq?H$tWdDU{d9cbyRM0}nzi2O{ahQL{aU8O}hu(P&m8BIrxTJq0T$?@? zyBjyO$PViX*q9NCGc55TEaHqDBJZhK^VPZOz9itakW_ctsuK%AcYBCFD&*~@Sr2|6 zk(rA9##H=i_|HZ^l3AH!NyK(_cer57ZQaofmk{E*23YU6v$L~vu)h!g-Pze`{&#oh z;Mt#c_n+$eAoY(` za~D}>_;o%URsju`B{>Xezh(chxIRK}2q}SC-Vo!&i3J)lCWYjA_#Bp*u~o|cxqcmTWUo=qA_PP z7^HJyUp$<{eEt2NfBpPFgS{_^{EYm6@7wRb-SPSVo@(%w|9^?!C*%K{>l^@ER!DOX z09=-0yE+A`ZdaTFza#dKkjeZB&2FA6&7y2!i_}0NH!UxSRFWOrE-{xe8{4`awSt2W z&WR{VdI;D5I_0Rw2+|w9bx`a^vSYGzbo`a%SdxjkSn$6Er%ZA>TQPH%0U1o4C5O3a zu1WA2bA=&idH!xLFIZMZovPm>L<%wI1eK~lRWiA9?a^q4Q9FW|bl8+_2V@SVtU)SU zF`s$n2p!;v9w*mkJjqKebjF^jPqbENF#3qZ6z_d^Am%e)Kdi^=gjqUM;BL0Ct)S_q z>NOW+wjlhN%}UG(ivE2!`F1DSiJWN7Ecv$#zUZnbId*LiZ{{Onc5!0kj&4Jcasc@*q0h!_g zjR=~tjE*VElw6jxVghO01iC5MExMi3bZR>iN{m}-534HApaYK<6?REYJgktI7KA6b zEHRYX6-h=!;>N~y3FFWk)~dx1d|IeC-+!LAE)bJPqt_LL=D9!%y@629ZV5+niVLJS zFv?i*L?U6VR!SR*up!k8P27>r(E1d8v)u$Or*naBi6(r=?F;!Fs7#ubf5#jIq?`9j zu9+(-woMRMKA#LsQ2!c)$*c*rYSLiK4y*uAxX9{($7p*l`1Xhv{y7W2g@o6;pHEgD{r9ksJDA1jN$eTp z_0xvN9RH+`>q65f-k+(?FUIF~Sljy44=VJ2Q+rFHm#Rs#=Az=)U!OE51H4u%2M#it zPilM1X44f+6{BcD_=v&dsf~uFKsPjcK!i4tD03_>D z1rg$(chOc&vo{0xpXULz;rG-}9MbbPa94~IoY8;nCG)4?v#wdAtNSh*DHg8iq{Y2A z&#&H}zk7A^{`~0d!~p}!@N3Ry{z`(xe@VuNjfj=TU5sA{jV@rxAQM!D$vE&)x%0M`tHj7e~im04HlNsy)EE&dHnS!U%sqFd_YO(oE-%FHbJdPp(g{-d~+u z{_XVm{dRtGKNA3)(kSGfmg|_iF-w|B;V5RH)kEk zai{%s;&|KcnX}J4^LQWeA}%Bdf>+71oe*oybS&ahxK$MjgCYE+sOPixe; zt@ets_zYH>Kdm-aCgk(>3|Fx(kp!|LEuZ{k(?;^y>)QI^bm|2j(@LseR=(Y_w5A6U zO*8Ai-bbqp`-qq~huV*YZ5E#=a!m_ao4d%c3)#8kz`8CVtOdNj5_L6llKR>bWJu~Hw7O1CneU`Z&q070I^@@cKDSoP zz@La@es9f#-=04mV4z+I{ zLkmL@?F0Vu(|TiKXn|t5%X2;lR~XzN6ava1Ltui~9YTTP^Av1@D4Y@KCO#mRGgqa~ z(x4R%kU0sbK+|msRi5HSX4ikpSCDnqLy2DLHRSVH z`&Ng~Y5GfnR#B-rJV6K9a{o^i;&>JB&uahwgWX-z|9|&j-1&cQB0Y@%e;uQp^=*LE zQ`hMeD&V1B=6&civgAJaC}L0-^`pv_w9s3xvL(fBw2U6!rVfFjj_YS;D7Z*5?T_U+ z6LoxM8#&3Y#bdU>)1{u1n1>~1;WlrMzbZ|9X65U@)AK`chJ#pS3bAvne$V;U7SWxU zn~M$2HD@TDe#0`#AekX)X3f@KQx?_Y|4Cjg|868bQvJV$er!G`lZ)01W&;Nkgz3P+i)~Yi$ch>tC~DG|l+tj6 zi=>OL3a|NzSdpuGA>DrFp+sZmpB0i-jXf1{*`yyU3(l3F@Fh#{GG7BaDE(ROhkh>T5)F z;37{+?n4WD#Az^H9(}bqiq)_p`JWASzZwEah5R4x?eClKzjuavo&4WKvRrb=5a;M~ zyxj-xGJp%u$w_8=E_w4JPH+j3hfO@!g8Rq^UN2}*)p3^GLo@;2k&B)3HLl<_;vYZY zcpo;Pf*~G2Lhjgg0QB5*D%$@l@FNVpzT-nm?{WVhjQ7m_KkoGZ%_PHtLQeb9`DwD@ zb&LmYDUXi`mi+Rc+>u3@aN?aHA_%J^!u4sR=#bu?(IiEnP05X>@nD*eOXp4U?cXG*d_Vhd8na& zBpV>znXNY1BTrTPe;pnG75jg8v@_1*|Lu1Be-o+p{%?LGuRj*dRjbxj&`4p=8p(2- zHFFhS*)v5#v8}6a(V8Z)>iU|2Q8lWr>`q>B!rX=RoUu^5r+z&wxP}g}i*j#$is|wK z{(6MR!b;zB>W%8RuJ{3Xyw#sp2t~ljJ#Js_9am2tX4@FOT5_HEgK(0_pOXB z%cZW|E9TEN$tkGLMq5Rkrjn)U``CdDebCxcGeZ>yLusa(ohhwjU4yPd0%w0~G2E)6 zb|fjZ^eY0VQ_s=8+O3~z5?E60mn`P8RYE9yZksN6FEXMP=hp?xT4cb}k^s?!a!jf| zkfAt`6a-#)2zsL*#{=zuy{xPdZ2HF^fBf-r+PJz@oQm}SShRY9$JPM>Tdx0)Mx)_@ zY5&{Z>-7JPB!e-jX*Vy{O-xoBZfoHcI)BL)*V-PdHKFU?;9B)7RGr{8-NmO>*1p&+ zqNsRq9vwWZoy;}EQCc0VI{?(D1b~_>?-kHbO;pn24PAdzH`>^>7Ji37+98mdYhtY+ zq-7OrPsr~Rhcq?i;j29 z|A?S?A^0eVNfr`x$?||D=ySSt)0BfnO;aBP6a_jIs{XCt7d}64OmHmqnLp1j_-6f? zL$OXEo!bjJ)}OyxewB;K9-kyRUW5cuhQmSJq69%t0z3`PF|~*R0~4)mL)aj&s&t{z zFrPa|!Eg1ud8AqS{#ACx3`1*|+!xxtS^>8doJ3&?`VO@HjnGREGFF-M>?HnaACL z1fWCexXSCu)T$MXl?Fd9#W42i|L>W;C@q(~I zl@)-MRW6iqjfGN73N7N)pd8d{Eo8MtNUGdyMQAWZ;7Wz^r*a#X92hqQ(UCN`0!l{% z#EjzhH-Xo|pWmOp%RsRdLA0y|EGN}!n&agdlH7dsWHWiFeb)$RCtU&S#o9@iavpA{ z!Md>H8yLtd1v}xJfjj7ukt*%KYj^@&;r}rj<@`TKd!z3C??#fv|2xMJYje)(ocH*& zw}kz!>vgZy3f!jxM-^Ic3i;OR_rXIs&si-Z;NTcc0`&{&u!CCoC>i>RdQPiaeNR|4ZghM0PZ~X|Q!vAZ3I5yvZ zkB0l*`>%~8%lSXyAyw!9lv|2Zy~;->&?z|S^G`5hb4(O|gl$-VO#k(FzrK;`}mCp%`0PeXljDiIW;0(Fv*!^cr1tJ3pW(-~i z$fC5^N+_%kh3ozmcmdnr-2y|f1*EC|Ibc{A$ssr4`%B;=A29^}Ap?*g5irmTW^n}*ByKC814#v#EakgdK9Zu!XLh@V?#*6j<`QrWs(nkaRr+eN zCRp3a6ocpdZNnW1;{l3E=Pp}Fi$~5dpor;9RxB2MC1|FqWcY=b%o%pk74!l|^EoJD z@n?BgbIBl!s0R8phxA<}?usnxi9&>Ve|#$EbgTAo1z3PX(>KKgynTPp%Su_8jCN*- zHL}A-O*?bs+{Ag~>Jf?AK@j)x)?>@$g&a3oQ_BV^e!N)%@1O>a3ZKBjWwWM1jhP%H zFrdB|Q#=I_9G{&FZZ3VvDEQA6=9T^@F6%LxuDHzTXBT4=A-d|Y)m^K(-L~=;sl{s1 zEy@juAVs6Pj0YrdYb{cbf08akA@)7TlQSW{$S&P^F3M+0=y?uANCGbM``lOs8FJ&b zK)E`4%>Okdcmd93d9g^iKj`0?SRnN8mGhu63dvJN7U82M_^lMb*~X1gtGff54qeJZ zsz=bmZk~hR((aTGrYJlx`XvXfc|WpyQv4Q^%(#bamd2};1{gJwIOAROXhw zE|ZljtUf=H1nR>rUJ6B8|9LVCLxhO+19Wc@VID;blwHn~Ss0U^7HT_fRlT znV~=+Ar)y~!Y5Sy$PKKXIw9EUtIN99+I})9g(o8)Lw6!yK6_z^u}=s3yc<*J1vAA5 zi?T`1Dd(%KvWhI(6;YH+TN~9`lu_EARpa^k09^-fxwZ9ktdaW!xjej4X@}|z59dvVUrOh}bIFWvA3K@j`1BqH4qrs2 zG=$jAZ-C}J(Xyeejg;=KvK>{qw%Z&UO1oA-erifdIo(>u%POPBKh0ulv* z7tDYcfM1Tl0=sDN?D@0j3aqUpA0X(D$Rpx~9f&A@xOXAx{D7Sse&YOqpo<7NQ9A7N zB&gUT@|hR^Xl97!ou^gdi8(!DU>#i@BX_?os|o0rOTrAV?9-Gz=*~ zZhY=ySbpY~HXl*?>^Xm~f&;EtPSG7A08f5L4ohAY$TcHo8}y*>%Sw6!%Oqh;T7sW(S@AzEU-3o>guQpr!^59U*XBPKu184I;PXC!pAKl#c=frDlQDxEF) z&353dKN+oiq@Uze$MoOQ9%XVS9n*hJ#-g5usnl|6naN`N!^h*xqw~{C`DW8LB?1o=$2G%0u5u zc4MtQIBaPbne4vePDe&QRB%(Y9?&;WTF!oPd`=ie;3mE1ViG|*0De;`2TpEd;c}w zJLvHLHz5(iezKwMNd=?dN00= z|LTDiLXa5Ee+7(0fdlq;zLLO|MUJ2b4E<~jIdl)jMf#*3fJ#B zxI_rJ*r5Oi{9K1f27JzPW8(#Y!$O8Pv+bMLbX$V*ss9dUm{&Z-1S~K?z(ovtK6eHp zDD*uCW)amj*H`gX3|S>=Y1udykK}WJ0vZvNUYE&>4IBXK1v6hd*xtOR>UN2~0+Xdg zxFXp1u&1g9MTRcb(q=+|&rvL{?b)H*50)8JxK!WY&f(s-K8B`*u z4n$!#N94|<2*5zKy6|Rm24;u>$|C0mxDkgfWLf|xgQh)65?)N?1(+b6%>{`ZD3y6f z3c^U?atiN0kg{g(z?NJGrdN(?>ae(%qp@%f_zh*E_wZ<3dIYWf#AkwLjby~Fyx)HY zXja5D=v|135guv^l!s=(Xo;Rht)d0|d>N4$vM))asZ0$6RWwy_vZH9%Aw}o>V3w*jn$BQ*YPn;^Zw4Z<)ynUXEFH>LMI8d;m8nIuDe1L+b*>p?*+kbZ zXY*wuv%c7DdZw)$kZ%fnx3HHFg!y}{H%;5gQBXY^4Hgw*Jq#Jom`(CeLT_88)JHYCU}vBqU1=<^14G$ zhRrSekrz7W(Y@oH#XXke(?E_GIY^wQqmL(-=SRmIph-#H)KXU@Yuh|_vi9)!!*7>Y zCl?=2E`PZ`{&e>9>3g%AhhX~_l5O9cBnvEUe7kI#ji&gDs~*H8yz;MTX&K8Kv^)fS zEgK&)3?X?1sVB^%oI*c?7-<#eI#NkSc*h?+xVXSR0$aAe9{uz3`sn!hXjjIt(6x~)@ud2)Pla;4R>8LG>gtx5U2lb?@%`*3xAaq|B3 z)7fUoE^M=e=3*tX6u-r`!akh7J$id|ef;6mZ}0AJt!dO)$DtM#btbW>BC$9gLzi-t ztTfLzuc^E|{{>TKo8e!7dwX(mc5-zR?~sdsoF1QC|MKZFHxya*CR?qcJCxLX{^_E2 z&F7eyHP=|z86c&qd^DOQ@u*MVS=ayn``g9I_2qAG1<3d5r!&jS&uDB_HP`I}Ljep} z8C6#&XGdpO_2{wHR+kdxl*gSsipjY+x%~9o#qr7Y`-@M%o!6kLu-+2sE}A<2aC&0x z`6?SWrpdZf3!1c&w0=HPi!7trM#pJe@PJka>0%p=#Y&-D2voei?6{_dwC*Q|vs`f0 zLYqVl`ZR!VkU?YTXy9#I%AwKc!@L$`^}SmZAWF{(o@9b|%oz*cBUWt{E9{ZzQ#L$h zc)Od-ONzVJ8;uVJ!@+Pc>Ve+j;iy;BT*Z6JPZ-;C1bsIDGy6$|R2=W@W;cX0NAaxx za&@Ki4*vcQ`tOhrml9&-5bO{2K}E#lmI3i?q<>@H0^x`yA9s>(1VPv9hPZHuOGbFF z)&K)Ag`OV~bTuc4&av<6CG9~H`nF?LKklejEr*F0C)f%*gqY<|fxon$!IEPZb4qpK z_xr0s0u-L4>kdG}XN* z*p8Gg)_6M&!{(GcLIA6T%}HH`>|t{X{EKqoqMn1(XM@T+?Rqk+!L)2HkeG4R(ok!)6%%4MdtvjksEvrCqw`bY=4Q>10OAd9ZYjeA zX{5PeOXEE;dp`0#NV6fW)Vh?o1o5IQCR+KIWyeTX;f4Hpf_m-ig;ZbNUMPH?y^N(U zCW>Dl9)f>?Ui@7Zm%7N~vnDzer&H%n}sN0G+YqbuC_6s^=Cw(xA+kW?UL2t$XQ4xig+ zX5ddmFjycMPyy``M2iU`A}A=p4ud`*C?trYfJqJ9#hjZaF_pCzq_g5j?4m<3+#Tvx z4v(QlhI96Xoz;GvSN0b^rj>(koanPde)F1&VV8$s<;tm`Q;2lGyn_K#xF%O0E^QNG z9;3zIdZ)xMXZk!87z-YDHmnz3^%y~oqkY-IelCxm`ly9XPWi(1@@LOSpUAO-Dk!A3 z%_+F-3|vmW`f5Mxs;^he5ZU^q*O1TCcy_F3+Nu+6313Nz)FhyhS*3U z@Q$N{VX~~Vz$#Db)$gV3F*2<~+TJW^2+g#ftN=BIWr-0nB=~paFq8>C{;+J#Vw>n& zxHQPgN~_5e6nh3_JIiGohq;Q0MxR69b3EoRIVcNq!5k6v3Q#X_gw#O+BOa32u^yEa z<|=nl7_S9+8VpgaS*8yRUUAKvv9;zZ9d-6AAk=C-U_@gBz)R{csRL$H;9VkrXZU4Wtpp*N|*-V_Z7BIgJIRS_|{(;0BrfR_sWi zoC(SA_#O(FC9_tOlOwF8rflXf`B)9SM))=gT;FQG&NT19My>aA`Lk~x(%Lm?o*^ir zNcb`9_jF=Td!UzypC0(@FZ&!9Rh+k0uQbC&&D@}6zLmxUX5Kd{PI8x?XOO-_rkIG` zke*`uG(?U!^&Ai{?D!w4q!LvUG=s$T5#^lbZjRU-5%B9jF5@ab#;o6?KrIkVViH~@ z;W0c092kH}w#w{|=C(0;go!n3z3N%4YSU;!(H{{CVrD2?dX=d#7N0m`63{SaIWW+l zA#ql=Dg)rCpBvo&qP;b2|9nLo-2J)6yUky^`Rk5Xfe!!WiM?X4eE%I{*Yzlg_#{js zcZM221h2yX+8vHe{FnVM{`Y25*|Rwl|4V;l^e$NwZ^fE0QDPjt2yuYq6+n#|fB_Z8 zF+7*)L-X@|k?zI$svs}wFf9)%aMs*>X9u)i4br+XsP*@#i`3r--;+vm4;#Q7z7Eg5OAnAL^*^yb zSF!+A=zqh5@ve#gIvgH!`rjr}(bE;foL>eWVTDu|7lg%PM7`L5MAmuXHufome?%w& zw>EGMgp-;?^X&DEK@k^*r|-a5FxlTlfs^II7BGT3^(A+$`8rp>c&xGixr|wb0aV@p zjrYdp{vYji`+p;;-Thx4siHZQX1r1}ijK)hy34&a!AVPZn$z<`aE61J>k3wPtbTu{ z!HX!L(FJ0}bA-F0gi82vUmzFc5jq$oGbAxUGKeJE+q>q2(y65Poa*v_D9#wI#6T7Q z-`UC8Ouc1PTV1p^N^y60DXxX$?hb`QDehj}HAr!a7Ngx5&u&JbRQE{N#cR)O-@keuuzvqHiuVi#@oas**xCAXYYn<`%e?F%p%97*BAq zY57#x*V~>Wy=)1am5xH_Ft0X*_bx60!h=UZ9Kxz4kbeUfwX5`u4LC^nQYl$HTZZGz zY3>D zKPljW9!BTTK2lJO++_2eA0LXc8-=e)bRCPR?~nO3^P_Xbcc}h(ru~+^;h3>Z6Yc=T zF1HPC$UY%GS*#70;$^$;+Y_m$l+SFMy_=n^B%d|Y4PM&>?@1C5slCm+{!7$Zu*o}{ zc`KlPWB+J%;#0>z3)9{785%O8)~8MI;BC@M=;9T`o51pH$a%-VKY@5gp0fVFyR?CfMkh7cHg3jhmIJTPX;!KXzq0j%dJ}bVs}n;82}d9iWV2*g5mi&b=R<3ojeSg$?7N@-qA3#49=lkrCm~T3C;Aa1IH=;PNbNp>lZ=ge=BpjGZcfG5QG9U|7J$q znS-vfPoG6kPY;EaoSVBXebkf9BK}OHc)HGub@v_OjHuR@jPLXJ(XQ2us+hsQQJ3nyI|e=p?6S*YEG~` z2M@*_nQsg#sOC3KwJ;K|tcWa?R^&z|a0DesY-jtTt|Q$VZ&B`qPz{L8cdA$zhhu_+ z(gqM3V!@FFmvO%^raGj5dTEsJ`Tly6Pd??xG&CtDRT^#uNIHSDKHF{%+Q{?!x<3quykCR+L>l^u(4$wv@qfY2_@aWr~| zn|4=baZ6)O~Ah<$YDPao>m17|%7>0kS2k4LoR}wU6(%xZ_{zM+vTyBrJgfm2(-1QS2Z*|!`wZ;MCpE2qKr>F8;l>ZMBMWQ<6b z*$EROHFNKiuEfywP3pz7t^Wa>^!Mu2`I~SSOKU!gFB7PCRHou?MrEHE5TX%6;;CE& zQ>4TT&AG;iB7*EB!xy8&KAcGV|G<8!;b_*Wu)@k=f;P(|jKiznJIK2ltSwN(g|7TU zZDE?l?J!S_TuDi|rju-q#7g^VTOseC=1hZbaHV@LC)|m2GWsW>w_da(e(}MnG@7Z? zV>|q)xn_;mCY%)Sx!eY8jPxjz-%0(3TsLea?(}uH(754^{O!m`5BsE!JAB;t_UT{m z77azsbvQMlIt&cXRWlY~opFVP=eG>njE;~)+nc?V3)F52l+yHVUY}Q26Qk-9%MwRV zM-%_w=J=Ya%E+wAtnnm^{IoLn{zjDm%|>+aqoGd`v1@5hx&};Jr4E&ipxfT|e7ur* z+EK&G&Ro#Mh;Bi)u*9;cFb^P&>y;e+_7<8vuOuB(W4`-!K>&Ib-@tbGL`ODxp2g>B zs&>V!lYUD4P`kujOKxOjj3DrV<-?6h;Gw&N5;|i3gY1gx6&gJH_s5!eAIDhTpNhE#(6&U6Z3JQn_7U8i$ z4_|xLiaMH^fd~ zcOWS??iBmcBQlRo_iN8<-l>xCURsot@|^2fQYoHvt53BnqSKhSN`YplcM5fLJCPj>z{8lxc{IwWgss6pMq^@)e`ll@4P6RXTP{6IfK zot+4>spQ=g=nkq{^Rnw6UFkIz++#ohSkhRN9fD4=3iSh)W|)*Ddlj%uvVV{NJ?6jn z|ACuxfBZ;-$n0b2o-EsQH>)uYXdwht?f|`hH~^@=YqJAbk<+<++1#U!qZ z!?>#LsS9tYC7^-w)8^wpZ(-}jHPczmw)%ICJ|KxTkK&T2SqJJ$;SJVY( zut#hDBSQ)0#==3@9aUsqSHk#S2E%TLJkJaGD8%deDFL=d+x-3J#qg>DF84+Rwd2t@ zaOnPy^ofevwfod^-1F$q+702Wf^l1OhJpp**7- zusC)XNUB2d-(+)Ru`w&|s2%bs<$dFiH1E^|-*hx6OhFHVQpf$}zi9k&-#vkscO%ur zllk$E)n=WTi>&R2cPt{Ub1#8D=l^bd0VIiSG2a884vT#t;!D)Fm+W63m8UxA&;yUS zBtHhci+*YWe{#JE^uNWUP;+(l_a06e`S#aZf!XQzkN6j8^m;ObAIxnbE|wGUDa)^? zZO{pn-~*Vze`(f`F~QVrD;(dVjBHM~_E$a`m{VM6P+WUf(CI%QC+P+W>xN>qVGB4V zTC)~FDaD;Z7B86PJqC$=DgIqATm|iYDEhiz}z>DDe z6Nu(mHwmEa?om}n`%dFq-EevufY~@<*Y>hf)GOE(qLBzx-6B?J0+$yt_b!i@c}^bh zt8xjiApLex?ikoq`OCAcSc@s`dohRY@t=o3RO>>0;aucM2-O?0PX}?mMUJ>RdRVAO zx?w+%QU(->P2C}XH0yvSBKc)+Z2i(*BeqoU%H2+n^>y#Zcd^%z_Yn6_AFJSyko_hD zmr=xD_Dait=|3Y2d=uG_uiF{(8b&m;qI=sec`XAErF6Bj_)2I8kL6mXRgmLUUL)5n z3w$ZCVd=F929FDsLLBl4u%;8;-}pjY@DlC^3ClgAM4g3zW#P56DrI0{? zb|-b+r1*<3EGLXns=wan2n(LAWRatXr@ z>~p7@1}vin7@O#z>C(yF-yAXDkRM?rf*GeGbc~VC(p0yM{lrJc)DCOc`U{1`@29Ae2@+^y@}NieEGE z4PG#FL=ax_E&qra`a|GgG{%WW=V$yqrnplW&ZVLJ*5ajoOcke{ zV)^HPBh%*V$Me7OKDgQPttp5TOKp^r_IQ47CziC5kU&~8LYgzGDB8mxIa8hG+R^@` zSHVp}ZpF-|cW>XBMBYklV3KEU;&*80x4&-he0@atTyG}bu8*cthItaJUcwL%xFv(b z9$?1*7+PO7wv;;ZtxLftylzgzf^<{z!)_;~AEEV>2Uu2SXUBds@U7%9kN4Z!k{tp% z>A%{Bf%zm{W0CQs7V*(iYF^@;YZpf%tRNbD=)-?Ux3BI5VvDrl>NN;-umZebqJ8v4 z5~iQYviy$g%wiqg;s?~x47rqIKOBjk$zR~Uy2l{-w7|8C5k|ijVBWFdpVZLxWW?;d zuxJs)ncpdH=Apvb{HEEN<#id5@NVu9CZ<3Q4c<0#<=fzY1SdT|8L}I^UmssH;kzsr z9qIxHhForRV803ov~DQw7J}95hrli|l-GTaWWo>*+*xSkKdnCWLx0-g@Ar&ZSsE_4 zX1DuOz80PaFR7HeoIZ9{sI81bG~FKeMr2dJ<43PGco|bAqDS#3JDsVQ>gfE|8Ls}K zQhE?G1pq&3mt6&FWOxc7_cea|N)5L|=7q>PN*oYp6iRkR5mdH&<5nfD{3|Xk>R88) zHrXw|Wr9Zd{kl38t-2c}u3K=uRIjPkq2c6{?XEHdWGIJoa!mGi?r?0`*SrO5gBP!7 z1rsiuKglot<*pKc=9H}24Nk)DS#4z|=WG9Tj#{*~>>rK$k=F7O%Q%%%jb!Mft*08U zCu?2H%56r6?R(J?a*ZIO0v?cep^|6|S~1I{GltVMJasuu0ok!mn3Q2uTF(qq1}fVD z*{12zY~17_&tVlKk#>ZBR#tg6<*uM>_#=xk7v3IUuM0Z` zY-i2>>?Ns#WE^qnN?C6eWs4%DI%O4IOdfoGz<2#tv`Z(Q~EOuU!GpYpkM6j z>}(g|_wjf?zCAm6zV1B+4-BZ;*Jdn&$qBLw{4R8Kswny$v%pW6!kqsShmN30b8jZk z2r3gs8<^An{#Bp*4%T+=_7*41IBRXtaY0eh4+);X%`{D+JLNCIqnN>cIu$IcaFiEmeSRL5pAnM^SRWG_3wxI7HC?js%pdRg_b)ta_q|&-|tbE zL%SaU?m?e9?>SDtCQGGNLf*AYeszU@wei#yCoDI2 zf8kb1Z9HkgS)_FDTbe<|WwFMc;$0JoQhfcHDCIpTQm?2Ou?M0`yh|bZQQ;w6__^kgY z!D&Zu?p>4F+q5rk5`Wx(-k_j_yZMOkuW7KBLh7Z4aaS*qD zelIhl)5Oc--l#d=H9xRf8#lf(lC)&j9w}QxFxnzN(TmI~(dc`K)scd-=RF;yYFryG zk_$&SN`7?LJ*vRMyUpCk5y*8Lua&G(oy04-5p{gKI!E8O5*2;GA`C$XF_Zs zC5#8(wfcya97`xr{ri=E(@W};{_g>GLsyB(0uNaoNf#d55(3+Pcv3_vij*(ZANM42bZQu+ zN+X=USkW*xeVNM)$NR@p>Be6l-S9mHo@OLoC8WKTCAg)eq+$G%xojtbHBV%z&fn|v z8vLsXiO|Hh``OYfi5M2P8`So=lhZX6xNpq_C(N{TkuVda|Ch6o%@KdqMUrMah)n~Y z%l_kR`lLmDk10FIZA&A70`heEh&fc)a%AAi$Z7zzN_64=XhdK~*%-N3OmJF8_6FQ^T<- z|BZU92+N_ywV%BCtw$$q?FcRIc4SsB-VImlHocYmB?1v4sj5*|&8g*fFLb^o9BC6dbOb5)|<0)DP1XhGp4?SHozw?`t_AI8Bjs z`>d?P7f+G`G&>$i$Uem1tg6j08M_r@(hJq!N!U;*o zF+Z&4KV@wdPaX@Se9$A_ zpIbXHU@(1G)G1G2wzudq=@r!KzZ+9ge(k||7^u$m-qjsA$f3;+Yzh*lfxFy8^CsTw z;^)=@hpl%20%ja24%^+U*>efkx^!j7$2!w{_x#v03gUoZ-8FU!%Gzo^Wx6D#iA>#E zcN#g#PnB7Am4(Ko0X(8OFr_v1rKWq#R!@v4A1VZ!d2o#O`;{}6Od2w!>MI=6=!4iz z!5&>o)=DO_RVNbI_?~WZvqP-6s}A2agO4#q2S3m`H_!4CWBW_d8#jbVVs0iG#F>;@ ziO#oR>fb2fiyH0?kN8=M>$&h|PF!YmkV zzoJuA-`-tkK;5QR*7~O$G=E`A6>qK6rAQ=LK+|ns7ysq0{Nj6rWmPhL-a;-HE+zE- zS7U)V?ue8+aPh-$%M=o9L`(+TK`Y0#>zj;ER?QK&A>0-g^)WTFAA#R2-OQ<#w;3 z`!>C}88X`MUF>Uv{Rv%cp`iKsscE_cb;t^hJqNRmf|ISEcBYLbJ@$lnibhG>gt1?0 z;!6%y?*c|1V7=BssiOc+!OHEgGH04(c60y|aZ@VGPv<^Ko zct`t$^}yk?hjK%t$YM_CWtVV=Hz`Knulc4L_@F!)_?0@FqJ7>$A!a_2nL3Mq$H%q@ zkGH+Y4ir_fs%(`T*_rwjAc2>sTR*xYR4Wg}PGV7*&MjxYvlZ|1zp&{gS@3^oq`uOk z+_pvITNUybHpcuejw+bPJIZQcu9}e^uQyo#tXin4)qrKZtR%0+HBz?UreUG@+=#!w zup#($*>0{Sd_ZqOhxg7RkRUalG!PjL>dThrw$O#N(JGoBF z36@QsgTJ;la-n1i*0*=oWrU&?W;4Gy6$jk|+x{A;jeixvn~@OyGb#Cwkm5%DP*iP1ki9Ji!?=5%Y* z)tr4w^St1K@*hkr>${yKu+OWL^LB|RX}r|3sow&Ac>+M`&AaWSFCZwgbowgFn~=o^ zG(El(3i-jjzIL{hOT*tD3>l&x`TE3wl`k=Wrq*^6e1Z1^@BkWQOW4*UZAh`YKvE|= z1y69C6&OLBk?G0<=-1url)wS>OAh#mxg&s!r_lgtiRj$@u?q5BnE-F~wOo$&F02^c zR0MOj>CW5m^4!(|_xNM9fl*gy-BF?dwnh{=_R|PA*8i9lI6l$eiK{M!B5~%vm+^rm zy4R^Wr$r*XzE>#RX$KqcYBCcb>$cT>>;zFr{q=G9Ja-$;Fp}1m!#a}M1;YY)h08Qz zwZTeA!ysFcSiJegh7q7Q*z)cv&m#T;aXxWxf-Wz}UGKYo<*U3eCIAlo-hll))EUiF zv()ft!e1ZwfI@z5Vg_Pmh;TVCZ1TA~64*q&>v}}5neg?+2&#kwij++QHC6x!iUrzL zBLMtpj(rY+(-^K=;z4Ud^FEUSU4ll>-fyel?=RL}m!t_xRL~v)(x)hk?kY~r6X50G zrD}cKJ`AXjzy0_GYRC2&zL_Ri+`3Y}xJ!gVz^!383lzuxwikk9R@Zp{8|&G7@GFI`cgOCD6>2qds%lvh>E@Vg zp7lRu0sTXfCfYymB{;WUrCQl!?c>cofPI3~x&-jp+{f zBb*4tdW8PB2YZ`6Rxvx@x8Ddn;uGGP4wRhp-6g90f0wSk0T)WUBCg zSA=Msn{7LRRkBOBM+Xx5&AhiR>`TTH?QSf7K1WIzcE4CovOPH{;`w7oBt0w#SqTAi z;>d_`Q>DNZWZlV)h%U7lO7rb&U1g+ssywETCPH7@abmSTidf)os(f@m4pfJEqQLe& zZ5gsVgZbhGB>Dg?KGn0!08he&_ zXDE$pV0!5mAp7;-nK^g#4p=+iJ8a+k`jzWP%aCkTCjIDNGk?Ng6J}jLZGQ3oO7gyW zi(ZTdP|D z#$U2KrtKfzeP~MR0AI-mm{S~_6NU1Y)G38VY@Csd>XuYFMR${2_tIg2CKB;nkT2D{ z4i0rO8sMAg2a!PffmjlGMgRoAK>J}Q9a=*bc1+ZSAn<=d*~YA-^+)oX{Rga2JS7NO zDu9El_`Fr?YwSj(O!U@Z;X~OD`D#m?;3C=1nj#p3e>k7VV!aVyg@1 zx8F>gs7G+4Zo2Alp0?zG>YbY%=|?$&dmdDK#@eKo5F$9v9vnVAr|6z14aCI8t>(`q zhl|@{a7^Se-Tpu+9^w7?pOgyoiqO#riVhJr{*oey-;_#tZKDuXfY+()E5#-nY5P*> z>PwX&2F8F-RS(8$WMy1V$pe33$Xl1y&xr<~!4caxr@ zWtR>Aq6g=OS%hqwm(l=t+36jxxFrhj7!TKnU2N~87V5*p;p)CFX7tr?JX503r*0~4oDF38Fd)80Q;@Cc<@Bh>65#`Qnv zURh8C5YVD2bkGpRu=bj_=&`mVxN+bt-OxVw&Yh`is=Nt)L%f9q?fYaB8Z{V`?n&1z zAwO{vad6qfmDp(#>x{Zst}@w_2x@`oOJZIZQ-}P9fa&)s69|b2`73|(18f^jP5upb#XEXU&(wmY$t{{J79U$d%rN+gP{w&ZMOd< z5LV+q>UiXQFVkW&8?nc0QT{yps?{xr%Vz`%17>ygWM)eqh;E-}R;idn3^Xu)s6+X} zv8DeTvfPr+`{I&&5%eN89Y4eJAmE#$sjDmVW+;GAaoYwK-!rgpYv%7)B-PFANT;t(W)1)B?{KohYxG z@oI>R@aJJ-Kt297MKPbCApe9|p9>oACqkbjIYzs2hKeVa&m} zEE1X;b18Tg_~veJxm5z6B|e4Ok7lEUe(yK>t|)n3Sl?a}%XdXcSLpNQe<94rRJE{f zOtK(y8mtp-&Yw$HT-?OF9xeUQ-{J651{eNUj^zm-LUC5}{6+-jq{%5_+_Qu2#WXDM zGg2M=Ch6$rI{`9|8^aVlblYPp8j?lt?Tdb6#-IHfrz`y`Dm6pzt$ARG0vP|e2EZy0 z@3u6<|A1|l16s&*Qtq}`0u3J;eVUNg5Q@SUw^NGWBmFT)jXPHRXaD|<-|Abr-2U#x zwoJsy;d^SCm_sHslK!*~)4%7>EJUS0DwwY9+*V4%RN^JlOYeI94Dxq;p;_Lbgp zJ>frGddS?TdJG^t5=Fv)&4D;$NAIfRkb4xcA*|=+89A!UGthhktjXzU0}kb1PIe^O z*x6IjNR==PUn9G5nQ9XxmJuqQ;VLR+F*!WKc*GmwBQ)=v z#Y?+DgT_IDy?w1K!F%W|O^1*tjirm-0sL?)t@8c61bp9PZ*&-8Ct<$y|2;KLuq!4DoU&NS!wNzROia{?0N*^9iJ!5NkI|K7%_i60iZ6;)Z9lU%T#! zd@d~X{Xp#c8bePpvx<1975tWK37_;$vmiSCu1QaM!C3(xm`rR zt4GqBpS%Eznls&}(YFV;U;l|AbYBB+f|vIDKkZ*sSeIWX5+It6p%eN_*ev|#IM%QO zNy@)RmvGac1I*LxURA}%{LSwiM6 zhrRuA_uZq?8X|A}j%9 z(h{^Z43QiTTm%05o$(n}t(;oh^HS~>Y97#SMq;i`>o3MRmh-MYgat)x zG3j7{r>Kkgy{paQ_3+QzI@|lC%*@vvU?H}((6`GuS8yo_1G2M5j079_W0l{eT2}eZ zOB#&pw9#zfB$cQ#&L;uqAG_qn_aSt3Q5&!+=WTYqY4ULXHf3x7@+W?3`S*6yEJ|dk z2~6n5OTz1E^3$Js_LTe0nY3rdg4S!%Yfg*5GQ?s@Zyq44PJoQ!%M!|IcSUfkS1Mm_z$M&zzSv^3Ce+s*YEJSw9Sc82UF){svemwnHFq zeY^4);98@AsH|v?qe6kQ2^C~@8M)_8uoc4oQuDhP;FpRX# zoXg!dmz{NQk@UYfCt`*pi2VhLjaMcgV0d-SDQ+8S6dLIbsw=vgq?^2TIuEy4osFch zj9g-&W1(+}_^~9}0&vA3-%>lY2jb5-_3i`2wQHfdE#;i_9;*x5r4eREwjXbxbw2_hhs|z^7*J&E>2-+mxBVXzl&SFz zl14n)ZJ$?a3!3xgAGF@6v=hdHmcqJ&44;|-?X(yL7bQEtl!1it*}7crk(H=+w~>WX zbA)|48%vK`9P^dG#M6EACNK5J0<}-$rJoL&IDW_+c<6UcfVz?*!#>Lrdq)>=nJ)&x zMPu%H6wOx*D3x7K&tzzDC7o5s+^rp&ZT6bsv#gkK$=OOub_&jmz*%_b+M2L7-Fp#V zWa}=pr!^(sOL-kwG^&kT_9hdAEO*X%IU5(=S&Q0&f@^Ev>x?58Zm<5WWczHaHe`QW zia2%m7{2I45cKQJ!wdD`s`%=j9%{ukkDaNRB;=-CI@LMQv@BXp!8#>N3OBZ7*|d@( znI{qjXa9MOH{&mDRy0@N1xL#;aUe31*^+FZ6wZ{t&<`WLvG}|%YozoS#sdM#Z~2WO zxeCtC!G~s}A9t*in2h_bs3-i>7=1$=zD(s@=ck_;naK!!qPLmOkp6Zvs$i;uX&%R6 zUiXUoiYhEhorEH;TS9DY5=e)OH&tgSfsv`wLAhb130oY!+)8|#RpSZev^DepgE!jj z#0{?OI$bXlSm#@SM8FOWTJsqA#JLHeJ-EBKy>uz(hdlt4e2?rx!!Q0H`AfKD?e|;) z3w-RAAQ1%VU7^LrOIkD@tj(Mk<>!Sp&=}R9#A5boWZjwa+VyF8 zSm;}IdrydN! zqPdPTIZ-EdOPgQsTj@Sm&tp!xJ^Z#Acw$K1=S@Ai0tLM#adekG0zDH`b3oYo%jI~! zY#(r{okki&sTfZ_3@dQLyX)I6+7mwh3H-kI!^ZI@-NLt>z1nbRZCyS0M|V>jBLi!M z@4h~oxOxUqL%?l>}ot5s>7it~%3aP^>$; zz{{kJ`XI0wne?U~{s97dV9M2$CK!9e2hh=uB812!ZpQZ>ZP$qm88wY+&(Sb7c9fQK zovjo7H7@N1_NBx>sf|mE6}-_uyjuzppSwYHX2&JhKqQ}3xELrmvW7qzxzY(OL!z%0ScMbF53ya({3vytm^+_ zBBY%HDs}U?DTZX5a#-nO$RO`<8wq5gw0gq z{s<#7VAx{@B7lpzZM`lTDtV=k;#{X3A9lWC)(KK79FOWyc<(~KYEnwN&we8XC@wGl z)72gK+<}IV%M6wpuwnXRla(yS;ADg+h&|tXT^0s^>1}}L=@V_P0nIH@-A7Q&;GZ`T z*RlK+06!Uep{P<1V$+d4tnx&E`T6;M@YnB+;R3zVJ%HKCKfd%4*bs-vj9sUeA!p#X zdO%2~ZBiIYh9D|nJ`HHlauh_6q0H;kQ4VY+H$QfD<_ zX=y+TbU0e54?!G4vrU4k#xJ%2T1+&@HoxVVzGcXxJ%Ua%0u+F3-&bJTp8#;8IsW?u z(uYxL$^3p}yiL;DuP3iEC&cg_<#Rj^HwIE8^qVf%pi3iyY%Y;6IFs z_!6|b>UR+s0;(Eo`><+1R!vfU5+g4fuMq`Qc_QeQ0Ee5PUk`#1Os3xJ4YaP+#zReg zDKaLzIZ|PbQjKjPQ@RjIt0XWl(%ba5ozHgnYb=85MqDmKHgpZE}izfvgVDHO=iII z8#H%c@Ony}W2)!)Ko@#sD}PSY1m)z>oplmxM=t~saf|}egZ6kPH)lAK+}6uL zv(NBrqY_}cRy5Q@q9k5~xDi=-Wcm%qDL;c9-Y`>^M?7ozqKI^N73CkhIX-&Y@C<-h z>=|?itfE0<(O=}rI5Rq8t~Vl)BU^2u!g24jw%{OU&;0>iFPl`Z4MCQV3>D0Ci_zjS zr{Z0c?Qs9p|e+ zC%ZAXh%44%o3948qw9Q&(5C(yQH7SB>+1BS7;DJa8<8;L%XTZ9smNGSEcV%Y!pNN{ zL5PR}+ZU3#s?SNMpuFJii|>zgGA(Eg z?1{NKz-OC{40kAm_kZ}E?XBvE7_!(Uci>Rl35(+>gPY=VU4{-I>GLrccHnv0L&x?e z)w~ea>b(@-pB>jv$TdD@l161X;^oyXwQ)oEXqAr%2BRz(SQJiMpOnFcF^A4)odTL~5^q+S6}`1C`>AFa z!;$^{hTiTvPrN?ASIy!yrSfynhq%!HeY9*OMqtP1aI7OcC-}wkk;}$nKfQh0Pg$rL zVz-a{H*^pA1#xMh8IH+0vqnjC(ELq&%Be_m=`XhVPPfyZuAQ4B$Q?UI7jHeGpl(ZE z{L5P+h{(Mfgjg9f=|VR2i(Y){4qED%zXyk=&rT@EVoJ9M>@m3ObPz0bH-2&}_otz}jPJ5AH7? z{KC9q=#Ig`+JWN=j?QJ9ZpMt}Mep~m8g;{>-swld7*rey{E&m#kJ@Vr;zba;@K9nF zuNw;MGlW)xLVu;rp`x3*BRzTuV<^VSkTvms z$Jp^?_Q%}seR!%!&omMrZc?CX^KBwOWb}I8KH8+^SI}sgNmMcbvU~izc&ElijVrzJ zEGj{(<)mUgb|o>UnUuCA0{gBXowSq!&3>9{lkiLTj3rsATtv+DnOY1M4MLKf=xU_YL-h%9E273s5%NThNTw0zT8s(Wh^H&)&QbVPe<9a zXE2lb_BJ=qK;B;LFz&Qf@We_1=#xA>zzQ4*jjAI1{Tq!;BE_y-=+jT zaSHbn(lrPLZ}1I#Fo*R{o7YRFRm2bMK@B8Ox&E_RRy2RRJDpe$F^n6?;U}+BU3cm; z^K31|C|WDbDVm8Kk&c^K89%D|8SLaP zEs13&&DPD$9_niZB0LH#4D!-i9wj6pwbBiS9csyV3IR3*Rt*=i?TE_pli@Qg>lN$B z3HW~iONyE(^d3i#VrE@@H*sl4F1*!DLFo>yimftgN@iP5wy=&@H6dQhw6sJvzn012 zHFF0W6q_#|)wJAZb>B0+e>QEeGB`H073ew4&>|=vv$ZcOuP!=xXwLIr%F3%ssd1u= zXcp5KHX(Qq`$vX#Ik$4*ArNWnJ4{5Lw2CXP25=K$XDEC#v#-r#IG$_N2NsIRLrx7r z!!`GJRU4x9hQKj%SZ$9zgW_lNW`WcRe=lZtv~LR78PBbN)gQdp`g@HF;BVf6r^+hj zRMH~_|LdjN!@Gl|wKNb5Pk=YsBZ?Ky0FC(tR|1+yJxq&()-!%uxo3UN?iKB?^4+WSv&o6fO-&;Mwr0jgS8WoRs zjvpDg?0TbeU+*x3%|cU~-Dd2I1Q>6oN-}}4B{yM(Tn>Zr#GyY8K~NAUa+Bo%KmjR! z0LZ!KvkffyrIe>PnarnR$o!Az7<$zu{-2JLM{it)=GU-Zi063Xrn>XccxKSm9UkTJ zsR?RBV)C#lF0=d0!DNZ$t@_&`oUDxb3VVoV!>!X?afR>e^WOREHmX!3IrzXLKyiL>=IZUi2>|F6$U-&z$4LryyDZ53Wj|5QqleFb$x zN<;SiR&UW)P`gK56!3ZN9J1g?yQBA_^_HREq|Rjje-YCNj<)=6oh27`;=lA=iTLJe zRV}>BQ{>efE{r$goAdJevgLzWTo9{3Q>~E3;POIjP2#?5T2){b-GAz(GVwqXlYRM> z6ibyiPDttub<>nX-G=5esY&z|!D5EBAPx8d>AJ%xc`W??E1BT)3VztpgmhNUTG)V@ zFq>PE+`H_?y&K0(&?F3grP1?#iNyOQDhBFn?DYG8Y-~e3Zj}syat9?hiS&lpSbK0V z<3lq}BjZ&FyE)ijQ=qkj91;j~J7%vK)!y@aE8uUp<^n5w`#Nt264%|^>DAR{n03K3 ziJL($w4-nJ9<(WZqcP$8$K}i@ktl#-Z22ES7qaa9DI`5exaZrZ02Vtwc`8tIOzH?}PVia>!mo1(%9?1(x>p_PA6 zv6oc}S(%X!VAXF)z&(bbo~haJ587Qiia(Sq939%4%%2soU1fzry8+)PXamk2R<{2F z6iE+RD;X>I_wTpDDhRgv5tR^wct!W+MxK&N)zY}|4K|VQ(4yhH)#N%p15Gcgrf$YtZ5H{A(NJ|0k10z*qRy$?8~P!nV0J`FsTH>2$d+sVGh+@wDWS zORCaoD`yO|V6%G@0G!~ARHkl~A)o6F^d|iBwLd)y<@L5apV^JzGEu)}GB7GHBDNdU z_!a+}Vrv@rSF$L^B(8~>*LcrRcT~9NE6^DD%>Z~%Z8Qgy`o>}8?(Xx5KY=&!RTF{F z@c07gxwk8WTmeP1KQR8DG8Ohw(LpbC>4YM3+@{%uiQGo(%V0*yK@Id$JqmU{wW$<8 z0)vGeuGaG!xqv)|WY8NP8C2;R0HJBRrhDE!t7ZY7_*<0j_Q0HOUmP!(5u+^$3WQ>$ zsWxl07$G;=9OpPB*LokAh=0uAcHr*o(BXx+qL)Z$OJ5qPVf++5O5#rKjPU8FbO z_+2zNBNK;WRiFK!{s*fles#ub>)jhbv8WB6jOIO)$G^a)pW?O0N*jvh>U#v4>SqAE z=$w{W4n>QjTA6$fYVg`pUh{8r3vrKVE|NcAaoVm8soeWT_r{v?Q9{X|$G%{y3bK+y z|F6;cRbv;dZM$d%jhy08@*r9D?N6+s&cZH$!|u4-?;(0AD)VAG`!0I20s28F%>530 zEAMmALp7q%&#}(b2j?dkh`>;?)7|_jnBIfrZ?GAZKU{L#uhUfRC%x=i{@k=?+68Qp z-qpr|uosKh#pxc;wPlFioTnGiLao8_^0SXOcxo8%UiOyw59w<)Ccbw?yQVSD#TX=4 zya&9yH_O1A50LPCegHrR{J5-5Uqlg*9D0ATYb}t^Zm@f#9qxU59wj)ifkC72D%<&| z#n^|2watryNZ|o&Ol>8-Y9mnks?_Lk5&P~{kRW#=BkHs5KBIJQ35u+R$lkjFkNWjRxm;!ZApNfJ36q0{m zD0b31DM^*-jmB}OCkz*N0Rp1db295O4hAWOCk_$e7@V6)$&dINMp6a z87tpyE%AYF==UvuB0=u5>JvGBUTlh{fu`*rr@Gupof=E#P>1iIeBI}kJKu2o3sM&v zBSc5JoT#`Rn+j}CyBQ?&A{gScemIZUN-uc`t50F;{v2O4!V>wx+|o$Q@dK}eHjqN$ zpGbK<`ds(m&#Iw5GrY3vY9($s7M5N)4FZ*SCrB=km!Fq2I2Ph;g4kTORw&m8#z})V zPPb!w4MGgQBaEtWKi`C@0`2W00-_xv-VYc5W^yj-e45+ZW=A!h?zXR=PiF7>hr{w9 z@m9|6xA(onf{N11Q(WH0YU=kd5Qmd|r|U1R{MRrp7$1(!G4S*qc({N5ZnB6%h3y6{ zjdH5y{{ekHeJ+&I)%M#qGp@dy)%OG9z$IP9-xc ztymf@HH)>W7OOMcR4FlH>Z??OMz1!omb(Coeua(q_;n3M?gOYAWk$Bm;;u(t2fAKv zlquW=?GS1s?}6dg`HVjR*18o|Jp&rPn+ecTbcI#&RX>{b0a}D?`OYJU;LBe|x$N1h zEjO;kiwi|?{tBRoO_mkq2+eVGg5Q=*j-i!(n<(+|;%)#DZ%kQ-51$1?x1Lan9(sHi z@bG0LQYjAVvS*vorUs6AyT&X^Tidq2LocP0`+qN(5k%>#-aj?)ANI!P`_F@&&i`{G zX|w*H=@+%QOk7y?49o%xt349y8^91b|M(OX;#4%W7T1CeUip`@JT-VtFd-}tUv^SccAv${(>H~YvxNHa7R zZ^(m-PBdyM>&*Cx9cipxSouuzAR?s*^Ihv@Q^0plgLd<7afDR2_v*Gt!%o*6`@Yl# zL^p`Ft^OiF^Kvls(t>M0T2%A@6~Vvjy#F2_nE2o0gUT&!h_1|p{pDA(+#H2Xq46*p9PGmfAs>}aZL4c~_|9koP-+SXO{=-Jn zH^l$@{r%*wGf~*&2)X+_Tjj&>YQLt0y65WFrRbfTVZ?0`L11$fH$DG zO`fF>*H47=!vF+>?Apm+0{UWXL2o*w0bm$;6XTn1ZS9ThV}BY!La z2vJD!8npedX!#KaaE1uzEzkm!Wp&%RO8$w-jgO(5=}4VE&>JtjzKJHtN9_6*5pOC- zx&DM38pVwl{wnJGhhCj^2(&(k_oUYXHYg!c#6CgfTJgv?`t!y3%%yMTA&)tcJS*m( z)gIm*?qjugi3)c`DWaT!`C@mv&NA~JPL(Ep{h`i zPbdUdANf`p#0FEgJ^p>X5czbO^wAG@kDPG9@}JoY1^hayTmB?S@xp#$jKs>;KH3=HObyYD1A9Nt0ZeMpIu-aPv!F@Q!Z12HCcVfrP4of zJ^3EH1X|64EO*afHTp(fSVz0w)Wd>_!z%4w-q!LS*7 zFfUov;&fGy8h%a^D?(y1ozi-wNsnv}dA6x0wNHC-?StQHF(rWgJSFSVcRXb(dc`#9 zK!GtUpxslhdm|xx2=PoQ@|`UdGJ zEqONXwCuYdf*)>{ zCa2ENwE>^V&lcpJS(tfdE_11-iJY_8_Jl-DipFKv&Q+LHsE(mp(>WQ1niHXAX{Cu; znzkIn9vaG48WEeSxv-CDs+LZ;V5U1NaW=dkHnq5ASvXxS4`@05o4@~Z@%HSzp2Uv0 zd}-fFF>Qv@jW|ma`%cyR9m0jRs36dAa2Yh6o1Sb=wlAOW%XF-N0}i^o+k0D;FGiH7 zg=9Aq$<#!o$uZfkd=EvZXnMv{I=g1E$P#r-w)QG*awa(l1NiNJzw)i-Qzi=SzupPH zouAEY*$Tnfv)U3{m#}aJdmk?yeXhJwt3&h=X^vKbY;L}yCxY9W6sZNaN9GK)X-6uT znVIiG=qp1~H<=r{|J-flf7t!v|M_`-KI8dc57WJ_)X`?Y z^|84d^LdDG^YLQezZIvLbN*5Nex?A>W%}R2LBFp5?SAq9`8+>=Q2oz(nLI84=wBKE z+79s_AnwP7fCgx(2Yw>#VA&?rx!C$RP%!D_*#08_eU_j9N&Np05d8Uvy#-c@{;a&b{Ajy-%NIw#tC5Q^sOC71=LvkA4JB%jKEj zE~=rRZZ?l+4F2WGCy9g=Ig?Zixmqyn8P)V!3&}`F{Uco^fYf|2q>4E@H2t_{LFr%c z6Uta@ApQkiom-}x8X45G5Z*=?LJG5PqqF~lO8YM~=fB+1 zlqZg88?LcF4C5E;HS!Cn!@s0lnPc|8^HjF|`|rok_d-OpsyDkjyCZ8|5`LJJBQ~na&{G30F z<^PY({D?ox>_0pG{YL)xgD>*`XZiUv+J8!0&mYXj|EJeZ%6yR`Wn{gCO2r~)$p+Da z3>kq!9a@mQP!kd}sWA@*BX~V2u!0kBJXpz!?OQi2?m5-*q|-$!c9(R_welr*s*`4NAX@c-@o-G=|~&hD4`PoL%IGuwYkw*AMm&HwqicWLBb#dhzW0_MK~{K>8R zWkS$DAi?!=k^CDI-7{n#U5?&AIsJYP0cY|3|2!qYmim7+{67!&`d{+@f0my=!uyXa z3if$lf1aHAsjU*%zhMTbHZ(j#CaC8CDa=?DW=L8bq}`JJsZpuy3BbU^0ZicQlesY6 zvCJ_MOkOQR7S<*!+eo2BEYf3gb9&jxOcf-4dQ|A_O8oyL13;JY|9;c|tG~Pbh5vt! zpFgYp@3RDe{*TufznIYe-G7$we@!(T73s&NfL+Z0x3+focboAaU*3N{%g-Ms|Le6S zwM7|V|6q#1zqtbNc~ZdoJ^vU`KQaldWO>SCs*cIlBU8e9=&cNK(MHXS^TjSm@#;q? zasNCRTDX_1CV72^e5}g^FqmdSCA*X&W=dU@d?D~O`53(XPtVzE8Q@rx^^|2#!B92p z>jvpBPY4^r`LPqOxXxwS+RvAo_ERUOZAnYpmXx-ZlD3|Zc4<0V14e5S z+90E6ul+8s(BTvF%{t1MpK$hR>1M-m8(goJZT4AdrZVv-Ae(UfIh50q3J~(lu7xyH zO~Kb|CRN=G9NkNw#j;R;NEqJKRXehcBK`A{z%I!TxMK3xIf-8_g+2kxiS;@*xOT#D zCBb3{wQ0N*=31k9>0Vp2IW`YrGeu!I>%91Z73QCtjkPsX>AXy=-Z&a&4P2asb$Jff zMHyJ<=U<(leRa`_nweKym$WqN>YSXbA8Rw8o5XoR3g>wVocjwo$VX`#f1vcR7Nk#{ zANFHFYt02aH{t9`$zdDt)zY&rWWKGbROgBVt1zjyEUI}eXLZQ6n(0*MCsX|dtR9hd zno>T!ake&m3Tmql(zpyly^6+0)p;Y55cf~J|3h%gVZ%(mSBxeX6PYn;A8;Lpr6>bfVtS ze51X+msVHqfz=EQ-&nZ|&oq-G8n^ba%mSp+uwV46T}#))*e&Po`Z0ee_e`=DEbamp zCsdIkV_5^85&2F?k}yqqsz{zPsu+=Mgehl?OsbAIHxm)7AR9Z&Hlgz-#!j03+br_2 zqzT*nYQoZK&+dJv#`h}QhA6VxX3T%5Oix&$I%fTF@wFhz2pHlTwDS_ptz#Goc{!$` zYgBghcdo%&<4cGp`x7DBQDKq`p6P2SdC`W{I`xI^|By>EplEg>5;ibWCfBp&%WC!^ z_jEqv`6Djd8#Jzp%;=bwtBi5PAR$fsfYaJ(%Z9QkP z{Xfb=m)(uKZFl2dXyx(cBA}j_Q9b8ugM<{)%hLE z^!GGN#2u53i=?&DpNHzIt3Azfyb|B#3Djk8TWzD!pJ26XRT+L#42LPR7MBW737tH! zRaEfy^t|QQO4BT%smQ!RW253a(b7w@l+NmwZjb2i5x#c=Fc@&j)WZJXYFRkLL%TY@ z(&|=OSUVi%q}xMZSw+6Rk?p z+y*NRTiM$Mqt9zMZ<0EBtVgFbr$e4{&CN}IIj|mND*B!(qy4)ri((;}P*E(Vn|E8C zjfR&)NNw{WfkZ=Q_s<`=(sh>@OO4jmnAlDwLCymQ=qVzXb1s81*OOuhZA-{}cB9tM zhN&2CPAShe*#n*CDQj!hi)FIe3ezuk+4f5X20ugOiaj1n;el&B7R)_?^Z5205;Dwd z!B@1=&sR&CmkSw?WyZ-Yro zqR|toNuihoLq8w!CTCkX|1wrL57Ck(G}e(J!f3*1!la5Yeentn6PC?tLe$$Voe^_+ ziIzKH&Q#r%=Za}YhRmZV>C9i}df?H?9yF5~O@9*dHWf5!5>Y7P7mA%+4zA&<-Dqu5 zRRZQx-I6YkBf=$vtwfVH5!C&2WWD;cSwS8z9V{AihWjga+>wR~^I8SDR| z>6moj^+R_y=Jpv$k?A>|D*Gncm_1yvJf$%^NmJ7K&*+=~)maUSj(vi?lx)NwNaxOc zpxE?|$x|`Ssbr+{*M4*q^*gHw){ExCR4y<6_U#>v_oBHsk6%$6jy~CvS|(e*rWSq? zunL}yB~$8DWLkY0yY!?Z(WHd|I=Dm$+OWOp_m)%8Lo>{3YG+1O~vn=3=% zE|wcfMF^FB>Zy6?E`4Ip;>J#E86idum#MH+PF7qTl zR10WX2VYwI+UEP+7DEO{+p_a1%k&0hv&1($tK@+(XuZy$+BzG&V(S`D|J%1)(bjH< ztb1P){M$idZClV@bI|W*8yhXVWC?w94qG$eo|m|Eylml$Eni~~dCFt1({g9Qff4sL z&wB_ISC(r8oaxGB+$?v8|U?CJBmF~PW!CePrT>|&CcCR zWeRMtVU+-bY_nwrg#jb5H0;`*UY&shZv%yV=xv93zjmni zmmKO~xO$vP2>%Az*?!CdriIehHHA!tWJX!Z%oy>6N*ZgUH`?6tg=FMK8*DGkp@+ve zAqk^0mQYP*qL4(~XGB64;8x55PXx*N4b}04bpD_9 z_q6xVe(&h#_dWZ|Hyi)keHO^mRir!?BeBR7SK5dJskmp7#8fdt(|kg+V#=h^qdbCu zA-w3l=)y11UukMzzRY^S3Qe;){r_e7ZJPdDRBwAKEUB8SEn{TeocDS|o@|i*qfl*6 zPobFimAo@!BErgX$zQ%D{pJcR)WDe5ond)>ya2qd?ZXXocILO(-rL*QSYf1{!`&q# z?QLysY;1VoN{-xoPoXIv(jkpgQ6%kVkc>z1gz}7MV{k6*(0kR3Gj0TjUKQh(EgMQG zq-2@7nCWQ5V$BjVoDoWf#Tac8F)b8RM5B0yQJB~$WBOjmTap`*jbk#>AtOACg_JBd zVgk?k)nSqV)j4-YzOp^190ht<5*M~w+$jMLFKw&4i)Kjz64MWf7iytpf z$?3&_xFtfzuGwB4kk8=EENW}}u(!Rphg-CBxZB&?+Cmtn^x+hf$g{-Cmg-xx3o99n ztq-UD4_B&SYIA#QXQvPEJ2XkSx#4N*o+P6sPl$nKr{^s@t?mi)EN4|QrxeDx_1!s1 zQ-N{^(nKtihW5XnOP=XbHSPIQ7$DV@RxHDTg){@SazVdPw3Q91 zVuQ2FN&myY>A4+xz=p@;`r;pRdRz)rPsiC59A$ zGRd&uDL9nmG`^){rlPg4$c+)xE$dN4O<0aGaJ%{%IW>t*8Y(&Jr9_9!V$(bZ6&sek;on8L|g?rY%BBxMwi2QhZZEj|@7LB>y zgnuW+5P!jStVP3r%1!v!eJ~ksnt$A%>Mq+XM;y}lw#W&@wXb~>sr!8Gn`lUH*S?AL z)ch_aAFqA$|E_&Sex#C%LXpAQxmt^IDgK+qdd*w{bQ7CP@!xCFor*=mHvhm8L-zc? z9-N+ET%UiMIhMu$Y;D)i|JL5l{+ILrIey5ppOc)%6GmS1m}RP7>#6c%-Ezow)bEo2 zL$iX)8QJdlcjtC8y3FzB=KcMBL}8jp$nj>1Q>jg;Ep&5!_2!zKT%3{9w-;xFo598rQFL`PatGQ)Vv(WdgvFT)#SC}LrAP{>_T@^BnYIa6TJm85ybXLtM?6D?^crh} zDYE5Nfg^aVF}ySpaWQ3?uEQl{1F%@+Gs(vjO~ieM?|C*5!e!cq`CPDXOApYTxgujp z(c@2-I~xGN#xy18HZG$EM3I>V3RG`t45K(GvV_nywZjNdrQs{?90{z|z6(KR-h^*} z?wZw@Pm3%u`Uo)OAa#orDc%z>4o-+(wxEd>G8f7|fdN+ihPyrL*ikyL6h+qg26h$q zOga_M0Kqf-y=(46urio-b`<;yFdzv!OTcr27!g&(6AM_^T%fT0;S8atFm{NUdv2&n zNV3lP25yg<@Z5|v;v+pXcVBFVUf=6~4b}!QaU$Z~K!pa^PO}G)1xdLP`34!XjE#8A zX<8jUfV154zl)*+rXKT~>}-U)4l-xL?|4#B@DB^=!w$e6n2fmsX@SWpSKyzAbcTlj zwxvO8*APS8F~@6Kr?ebQ9+D;aEqJQZTWexL|5P(c*XJ6F(*jU2EYK{R@+mj7YeAG4 z>3c(77_>}cFw(fQ2LrjdVrbBH=O5I46j(qkC_#S4;9$*wM5+$#a$^{DBNvD(*K*ESxdOfnuawgAmoJQ-GPH9S3&xb|l1s1KenP2{FPk zgbzr5XQ*F_De@{a6>OI|3-=QqPXcDZ5^(XPDUs}s!&Y{~1cP0|QaTjU{UxN^=I}K6 z=>A+X#WD@pr{sPjQaBXEh``)gBC31dT8B-=%Ix~vGEg){lGc*Ll^NHPbA8KnZiEeO5dZadhGT32aRPH)F$J;x; zb;s*xz#rLQK94|q;M`s6;V^?$_JjmBT;)@FAa0SxFcI+ahFIZt8Lqp9w?!zZ7_JP{8+sqI2~9^tjOGh3tKIgb zLgIbz*|HcBNX{Zf#^WxO8KWuC(Y-YNz<4$-GK+m;PCL#IiQ$g4EpYqI&F4ZVt*jLyy&JHR z3Kay{Hh4?8iVFp|Jj^%+erd(XpMc|)R}*`1NT{yW(PJz!mGig|g-T~+O69F#eX^8} zoXEu#A7?K3%Ipy!wv`eMv+7(3O(+S^YSd{S;hKEym+emAQjsS_py7|x+N5E0Sj>Qv z4`-Em%0sBwKMH*9f>)|s;B{>T-r)ElBifG0DZAEKKtF6jRaLhuk z%xF9z0fLBOA;TF;aUd7`T@-{G2`^{5ps6Fjdm+>0o*P*s6Il;-UhzBlvlmaO92><+ z%xJ1-y^&VNa-9Qj!4298Ht3QXru#o&ch=cM%yMlG7~}-EA`(|b zp+dAla$IY$dsBMLx@1D{7)W^zAkd@5$i8PPmZn|%FQ4W@YTOvl2dtoMr9xmcZe<1q zxJPbQn&+ueH$|4t5X**lS6V?x2+ad$g2Wpr?@v;;qJPK3`S=Ey)L9Q*M_G3 zrf6>Mu5?Vz*TCeg;=W#9SYEc4La81g%q#=)?lOsg^NdiE(tA~KZI+s{Fp!EOPE-n+iW#Amzc9 z7_pHhyqI>8j*LXZ^+fm^xvDnf6+VZPbxTzU5K@xXlrh}pkw{ZYBuVz%}aT90qfu;ieR&jUL5qW!jiZ zLqbwad1mMYwMSLST1K@eX)~r#-N%3ohT~Lcj)R#c)0AgjCpQNwJZJ~mtg*N-C7qkF zB#5p#Vdd@Ewbb1;Y%yU*Zt4cY1dymMkDFZ^njl)>sk79o#6~Y@KMQU%#}GVZ@}jG zLUx;2rcTxitXx(%Xu;B`c2YoqxMLDGOHa5=dS;O`zc-nXQ=>2&!Gcj45%8gbxT6Xg zTC4>74Z;Je(%#5N(?EGO5=s>ud$xdM%__Eee{IktF~6g8WI&O@C=PUsp;b;o7qPE+ zQXw7Cr)Xv-Wm!^8o!DBT7{^#p|8v_}=V^f8z!9zWFnLcFkOO2lXhY&J&~ z39WPfD8YDU>cE;;0?>G^s@%d}>W;+}MQKAv6|;=jRBD<)WkdEr*|9GHsH>Jzpo+-5 zEM-c;riD=$X2jru#M?J!bty71W(8)fxu&a9(l;~K4LK<44MXemV^lLMW&xN1!Qc=m zx+d=R5&K^VZ94ha8@OMF0+ljzY{#J9nQH_M!a&#hqb7~a1gBKx# zc>AD+;){F6pi`y-iAHtHgdF&%ki^n3Hg-hoAnuk{C#$B~kPM^=$*2$8!Z-bUA05NqCku$D9bz;)A`iV+|mzh5V9@OCs z6!ix7;v$_EiZgc&ITNPwFaa*qUoq-P{c#;Ki?Va!m%;9rE(z zdT{NK`_tg&`?v3I$WJF%S0@)YgY#?h_9}dqfBPLdx%fNzVQ_KQb zk_2Yb@&tjMlsY~&Bln0ZP>5uc#flNR8Qi=+?~;qR7rnv7cUOapSLbieFK)Wz&H2^o z_a_%OCoc!D2RDBQ68dg%b8&u+?gJ-wjLVa&o5AV3*C$ux^4-d~@2p^r4If&Y$}G~}VMJbgl$uVjIc8FsMPy)ZUc&AS?HV|6j7*RejGcPIgq#JY z7Wa~+h18+RAY-tx1Q-q%r3P$Xd~AA&#!z>TF)%D-EPq^Ku&_v$i}l69Dwapf)QZQ zrpsyS26Y_TN6!!{@y*=a;{L8yMXKwHE?~3pJf^@;cxE><;H#mL{xUIf`UJ&W1$V%Y`F2NR0kXTHohIm7K;KWRAbb$^b z-mGSbvtxK;I!*34Jd%#A`-W0B)W|zG*x~U697W_LHg|%-g5$gpyis1%!J+tRVnp}q zajicVE_`ltQfNF808w1qgUyvtGGf3_x)3{%YnsIj7m=eG)N;QW&>fp*T>JCr zUvE1HvHp&G-Fk6XJFU;Qu z)EJ139u_3v^NTZcueQ0t!S^SZm**E}ga0}%35OT9( zhpu&_s9F=8SS@(UBqWfx+FZBP zOBkA2;z$BrsY*88&=HiZn+D$3a^pYD5Ka$q0hDml0m`iSju9+*%7G<%O7fyjpzVtx zSjlL`RObkoYDebFTurV2b8kE9Lzh(~^La9d^)*`yt&xC~|8i~ZFYkS2X2_Vn|MR+wHRjWCq$Y5WM_ggo33PlyUXGRBB zcCZ6Glx8vL{cF(2>Xai0UzgS8n8qR&sb3EH{^sVgOU751r!eaebaYFjwY3wHv3v5G zXT<}#Win$a(JY>1A{FBqi7EK{vzM3Oncbo88M(+3Z=OIDL~8?G2AL(Zj81t>cxHre z+Ym+>&NKtJIG%ZnZZJ31x{gRq0NYir+- zABv&Dr)KJyeE8e9WGmX_r#n&q1N48zG^jb(ni^GUZS6+RJZaW6b;`o&V1lTS{}mutTr_l_I?U;B-`|I2$f#bZ!Bp;D{ODOH+Dht{bb zCsKFgjdHMNs&_~>z&6K-ErrP1yggJsTcE6G(~4|v5P-0{a*x4}1^D(*!QMYBtmT$` zEO_)@JwGZ-BKcVG#xPP3v!6UWoE7^0XiWaIkk3q(Yin<17i}oy9|ezZiEUJ>Y%VuW zC6wlPZSBK{4{QI6OjtTKiobwITLi=>I5A%az`5pb3fsEE??|tgxeD;#nq$~QH)xn{ z?PR2xB&B>{M`B4AG}0JiLGo1C^d6S`+6QynZDQ1Kv4-A@{_XpHjp>nB1w5}9js5Z1 z-iUyaS|^HAkubxC|GF&k%ik*O_j{J+Yil3YJ}+r&?9a$y@0`N?d*wHX9{dx}u-D+HX4Q!!*+ zG@ODwkWPitgr@g&ric<#CbBRvNDF`*af8_u%-!$U|ri&JEN8d}X8KH$1 zy@YAZTJ+}ToqZj}^iYXlDtn==M;Qz{G3$22`8X9Am`5xkOi%h4?5X+@8MwZPM$^ii zKzpBOO#i3-)ePGu!5{xww(+0eJ){u7l92Qv{$;{wswY9LOk|Mf3+ zTqGx%<|p>s&hf7uXD>Me8TvY&u%t+NHXcB{z5VzcUY@zgI>+yS?WAvnUoo1@ZYa+*(j*q4HzRg3 zk+9(8Lyul2bPp;+cbar|%e*bi=nC2ezr(o9FdvC)Cm& zFc|KwFX7g*QbavNQ|NSFW80cm_`>m2F2*aT%MJzo9iF&LbX7C*nsPV(ZsHzEiKxD zg#@X^ApKZ46^w_0eC&rqb%7On8$+GgCgk=|0TGZQ2ca6Oit~R4*?^FGUMOFCI0`2L zt6t@({~nC*Vnm1F9EIjjI3^Fh@^r}g1;hKPA{zyVZn@$!^?{tyW4vpbzGWHEkDO|gt0Ez zslFu>nk8yNZ<#91QDbnhhYc~Cf#FYv7Ux2OiGOJ_<=HhreM$ACih{DLx+|x?3GE~E z7hng|6Wdio0w4q&yn^Aogr#f@jF#ryE{-b*c4Y9}uoObmv`C*M-b6fHYVD&UNZjj zYq6Q^dVR<=T@Jb{@{YUSF2DX>0*_nln^iA@N6xiyFDui9Egv-pX!JEE`+|sxaUr{W0ZKswUFk! z@g$Z;WK=mHjTn*}Uhkc5N7HfVxU&@$7LT>KH29oSjAB7&!D3)R0HG0&@ zhOIkQWy$!Jrd5KHY;=tOI1RcgX(u99o3Y4ZmTR@Cyb{%8cN|XGCRK`$vtBNeo|$L! zEAy}NfIVtWxk#?94f|AN;4IOyGWV_!+a@(egscZwo{L09q(0_Eq=_=$IcDWXbddne zd0Rs&HvdTCrL|}&$~0BCfH+j706l)3O*diBE@AU4#dJ?kSg-y~ZzN=on(mK8rFvF8 zmhVLW6R=r=@K~I}L)J+QA*eN`hbkMgj&28({^L%_ZW_nZxs_8l-s6C|<@pcAkfluj z$YkkT(-iP=PM6)5oM_FIIh(iGo7@H67YU?oP-v050+bo@#yUxmLo#PJFGgO`5f3DL`i1#W-e>w9? zB-hqE4$X7m!4-D2?pyYD(~6}dE6IubP>i(Nvkk2QeF73Zfs|!_1;~~kZjgN^S_SRu?MEB{|?(T56 zKV<#wj}iG2qe+3#m-3;ca<(}v)NCjo0*P;DKiZlj@|h(Dw-ic!VpNlt6ITIwRm^3< zZU>r+H8?@rPTR(VM;}^$!L?x3`D_WqrTT_l#z!g~ZnhOP4maTwR1P}}*1EUvECDX1 zqP^Yz(SCr((-W{?LoKqCpRU7VlHSea>iQ%i=cgo=vs{a@r1@m#tl7#Zw|+c~s&MPC z$O#|@U7&p69VGUiqYgJ$u)bM#YmkeSXlrnQdpB^WmnCpMB%U>1JR27{F!G}{GT-pY`x-0 zP3D_VxnMiQDL84jn3=D`@1Glxm6Z4++8-Wm4|k3Z4tDmo_Sp6w+drhc`|)l+*%=<~ zYz>d%t;7BPFy7tWqX$PZ8}9DW!~R}Ohlj)6XIJ78_D5*(7EzB=;~j=Fu1`y6xj@wY zxj8}Q0(mrdTVgV>lRVO9V8>p`Xkcgf6Ic!Gs-DqoVAuZdXg9FPd@jR*tLE}=ZaMH+ z#rmpGHm^7My|W%X5s}}IzlTWqc zxDw^<29)NlGGF8W7pUC`(@By*Tu+<0QW5oSyTXRtS*h2*Ns++_90lB_pIuysDR@9@ z&5G&J+^u$$bry{y@>h#|@U;6|^jEJn{Vh^=@p|;l#_?ZG?;iI4n+=)U=r%wAD}ZE5 zq<0K_MI6k-8A*izPwFBMy+sC>=yT_Qc28UpA?G%WGuFd7CR_c;{BP^{uz$GKB_TmY zq5fxphBiy_uVtb1nV3?ZRZkI0NOn}G2YvV5Ie{J|!7esL+nlQr!Qh5@0uxI7T|)Y0vXvl<~clLeLUgPkJcZJA@Dy)34C=duYSJmXJjmZWg6SrEk;#N%Ixgk1@^yym|qhK49e&MA`9 z!Pym&*wHiJqX#c};Td42QCF46EsO>b%a>QM&RAq)>s0Gfr&TTx0M7JO_>&ur{_=G+j=e2z`3of#lb&h}Slz~nl zBDn3&@z}WSZr%UVoAKqVOE*>I29K&EFIXe>#q?}qW*orR+_3jRSn!?&|z3-qF? zYGZ@R-N^FT&kneJTg-x8Z9bkUo;Z{Ke-JhuLUnlvlR^}|p*e2Jt4r6q;ywqWIDQ~P zcaq^ax5eQvQ<~N72-O|wb`*;x)5;sa&F(t6%QmrsSKu_?LGM{G*tdZ3n+DevV810B=R(F!%{iuDk`pV}B zp9_#+wXf;#fcz2 zK}Dho5#anSp_RSOafqtH95E2SUMa|-H0F1Z1Dz(WL@EH4kD&hi8=AkN52XmPx4pZ4 zc-VA^p3(<;Qx==c!2>l9L{rr37!jO&Y1Q3 z3?b?gu3so4IbqNNh!5Brvu|5)hWTiuLQriGrj7n*NeJzoNQz=v(qT_z>1?B% z))srU#bXDTZ^}f~Eo|k?5?0xsOv?`Ir$3(ew)@-t-r?TfZaE2_gqzdG?)4Qd8-DROhU`3n%t z(MD!5x6Y4DCOp<{4IsX^`Gpf^;F_^BHWJNRp67RnJ>y7dtjqWvwx2gB;AQz>(5x|rVQjBqQ;lwhB;UNu{E2fp=4>rC zY5TytmwjdjIm4>~M!o>%sWgpm$+`h*0~4&;g53t;P&ds7TcBVbpT_-m20g*6Ur}#giV468AX$o6phAsus<8x~VX`;F63gbLn2)jjendR(C*!eXp4|rG6{hKL z3Hw5myciK0bcMoqmVsoV%;=;d@*_=oVnbh0+R8-6x+E3%U0)2qZfR5p@4J+at9HZ= zP18>OOd6@qTyrC6t|cFjSq%`Z7pxe|f@E1zY48LmHanBMBpYzdiSSS@_|dbR$27%p zoxP?NH%LM;Y+5;YK%*JeA1MO3rCyxe)bIO@@$uxDhKI3+=8xJOB4PDIZ4JSG3bnHE zWQ4cs_;^3mXV}``Iee6SX3mDc{_)D0(wwa4P#hG$*oeq~s(8YtSZ(l_yoa}HG+8nE zv;Wq^CYiMi7!)k9`6-f1)(fl%E_H~30Zql$C5>lB-%?Cj#zG23#|-jj#?C>V7NM2EwEO?_}f>s*S5+2SkHbT{kiA%ncRyn6dz ze@DYAz$=-}p(ZlIbB!pGMQZonr!g_WyYw))OXU*pQkuQQJFWg$QUVg>@V4<>Q z<^am27`oe~DijdSAwUk4b(-QlF_U`Cvbuq+jCTIL+XOSl$>E3{9F2~)_WSYf-VtM?-Or{a`Aeu5g5y`i9kGeK?5t#s%9#%G6xH(lpj+#^KVedkx`r3y*mJrFtMM@wH>)DMjN|-8zLQbkr^s4M(Za;6q?S9zrTYINzMJQxU@s(yVDJL_k?|+LS ztjL{)gyH8f-B^mE@vz{GRShNEup1kxX!)hll&*v@{14(e9p`~@@3uR2)`0U#mV#E^ zbf(1yk2MOG_CV~v9bD4UVxwTQL)|E7L2@ho1K0*zU~XCm8(@E0BVl9X%9z&XIUZzO z6YnobPLV233N3IWU0)37SLFX!MxqCExuvy)71znIoeE7~j3r)71tPEttSg53gpWOn z%MCRxJezyZS>>O|PyAYS7 zI>-IryS~NNo^NsB8*KF}P0B&`e3QL?AH!~5n@PneNWkE=T;$W0yZW@b2KP3UG8vI` z3R%p7T%7sBOaS%T;1E=jTHB1($H<_M$*JQ5$grt)95GCtT|ye+e~!U&qzZ)v!W^+- zLmuQ&$ibn>FkCb?Ib8}G!fCZhExbn^gum?dBLcX@iQw1APlDa%&$89;TRKS@&0glC zYd!B#DuqheX^IM#w}xU#AMa*Taq9KnA>otf8D&$ET{9gK^3%!H#o*%A0Chl$zcG2I ztm{KeCHqSW(F))g-OoFod<--LTM(&&iem@|S_&$d<|3ZdvP-?o_&*9p24{Y5+mgI7 zvFa)G!Ik;kYC|ZP23~0_hi%)YY?pT8GqW_J#)*u_2%?AahVXJz@0!8ki5>03c?^>ZZMw5=KA`Uif zY-ZIeck#PjSa?uZ;Y@Sx5y!8Vj=zKh&7q_=zO;0zm91W_bGF#PmQD5KSr$%!X+SS4 znaPe04?$wRIlaW-0PmgBI*ZLDdU82fG|?(CkaW1+Kj<^Mdr0@T>E2#)w7)ex+!`JX z_eMh+A0)edx-;6{J{+>6;nr|}c+@}GN_G!MTSvRQd;9b`P2a7Tyf=hniGn%dkf(w0 zbaFYU1V%1G(d`|u82EIl+3JTMuT5hkQAgn)B&PAiUIak}1@E4ilp2vuVCPi}|J-ks zD0?j`C{?yH)TbUVs_SVR1bykHNSWs2Xi7nrI9YRCEDNS-jP z%`)K%a%)W85G%u4%}RQ7w7=8zJMf8Z0w8^)JfzB0#JT${p7JqwT(~(QoqzNmg+Mfx z-{SJ(m*0fqX;^4Xy1ii1`k8pzl-9TJ4SG;L&Q3%VP{gh)L(Gu|9}UU!mknPZ-1a~( zo=U2jT#Bht*$M^WCQgO)9)$4f(o$8S?B`={T0a}6Vu%%X7zUrVwd%z(*|b3NY#hZx zGNH^ozt|<%FI6<7({z#fYaXu9+R^4jx7C&FNX0W_Oo$aK4JQh8GZyPEe!f2f%&|I_WbaFYO;GC zh*wBd@k!bL=iTM!I53}jUmpeI{ES1j_XnXm$DNdC#RK}(fa8+g{1%_3Pn>-XrDqqMn2MM7X3k{HGHp{W0#?8gz`h4hX~IYm4)!=mcy%hj8KcLD!eaAj28?P;GNevLZF0ELzqhy#}93a0^pubw=uy6h+=S z-rw8X+3t2^k)5=CbRjZyo3n0p=CgN-N!#ip9#)pEKym@#r&K{;aaC6EF~qK&t&1L{ zM0xDR@kHBE`!tWRB1s0JU#fPpE_~dBZy<|h(Q$%UcI;z<*{)Z>5Z+tY3<|rSl%5sAx&8hGvSWY1%6jI6V5FB8?$%S- z-Dk43zIOHy*wC*f#BWZDg8Wb7J~IqEmQ1^caOXaB<(?%t_$UNj~?nI(aK1Cw4R=wioP<$qs@4%y0Ql zu~Cs?)R=#yQP!b0_-O!M&mCOiFJ%-{B&K{k(djH4CShYq4c7@D#FGLNTEMDDz*#Fq z;=>4+oG%-pdxOEsHt$JW`aOR=ws#3)aJ|=S@D2{~zpDV`c*G+BYVYz~0Hp!@o123- z2oNTX4iQF0E1McWjHGRhZT+p=xzX#Fen~t-&%Ho5!;FYrxq6C~bVKXj<`^)Ut!R7q z|G)8ABtYQzLJU|}{Fqd`wc}xK^G}6~Rn}{?zUq7C&i2m1ky+^5+`gKkl^caa8**>6 zG1=G7CbgCBw^Mp+n5ATOWRy((!ogtJjJdB|R6QR-fsP(VUK&EQ^J?8ZXb^xG2fRpx_K)pr#?4Tw*4?TZY#=crm}Ag%&T|rEO2v zLVRKrXETFN2eZbsgj$F0$~B+zDU`^)_7as7n0ht`%`pfECuthE*khiJD#gLr&L2kA zPG|L}Q069`GP2Gi7Ip1F)`PEue|0Lzb0&=HPnmrNE4f~LYeC|pw#d>RnQ|KxdrZ(+ zKV=Dr@eL}yN4{aVIS50@1Y9y6uZ`8=Q&q*iO1TIGPdU|}gyjsGuLwE58@owsy}!B|&ycxyBdQI! zlo01(qBd}C*mk;fAq+r-T#+fGM(Z)MKa>Hwcd>*mN9ptgi@eyPjdgUp&f~Y&&ktD7s&qzXNi*I;0^?Z+&um(F&M7im} zEXu7jG!3afL$FFS%hZsF%Xc$BuEr;Y^0jL{GXXi$f`W!RWR*;6XT%Ok#W{J=JeMMu za9>y%z{q3w%MfENE1y!@uv2_Gal;wNFv(_1ayv*{f|1l(IP@8axdc;IWi#JFJFw?4 zyl$%(52={>K=6RO#@Hh3P_ z13aZj&D!2U8w|sIxGvT2R2U=vATeL_!wBKyo$AYH(SmXqpTD za;MQch^wL=N3JB8xSnP4L<(0YaLJ^q)>@20vA!~}s+rcT-OH(nwkg=n0_iNB_3Yar zOUO7CLz?XSI zzXz%a#r3howh5~x$F#7-0*?2XGJJR>iVTw1yWw4i3H#hK zq>AH3jzGUqFhe!9)`+JJ^h#H#1{|@tHgXNbrqXi1T=CKn(GfXO?vaY7a36Hr`nWCQ zHvdZ5#BN8~CZH*ijlSb4#@n^fv7Y9Jj(e3QEud(8f%aL!U_X5YkKn~5$jTLh(gFd~ zF`T4~=_py@ELD-e>_|D-eqgmX-g40`bKbIfdrLbDS1C&QB4N z<>p1_xU<#oPdnWX=8%BD-*8)bXi)+a;6vt4s3h=m(-&ED%lx^$VpDN9k2)G#W!@~G1aVH9a zvQKtCIS0+Gpn%2%Ey!4qB121hncq7WSq8OAUY)jQU+>qmuTR9i_k~JlWK89dj?Gn& zrndHpm1jg87t(K$&jPokNJ;?E8~V@=Q(HD|D_2{{;+dbg1;8>f$vjBFGY;K2l(&xJ z8%->xd7-P$#O17pY9`;`GpmfsyCXo%vW5_(ivu&BSlV%4DURzCHNX! zKp{8cyZ?K3foGa~ORG$NqFmqbDHBC4pXFYo`hac4vuYtH5DwXcks3!BMd|eOT3i6$ z@ieVF%ePrSZYVe6UYgoMFQ9?I0GK<_m5GV!L$9LZKWgj*vin6gWjUhbt<4tF>W((2 zB{x!O>I^XOWzXmjw``^=*%fUnk_7X;Q4q$BR!e7H%+B(Bxp%|C zItwCW8!!Wy>Rq}MoE^kDt^m4-Ak`U(0#f70EMbry-pCZgRK&N9wMB%yoVlz;=s5)A z+9;9Af`m^%>tIk_q;1RcIz|oa5VI$sn7b1>;^S+(K$noCvO(>jwQbOxmdiINaJ3EB zG{~$}b1!6~V3W${6J>Es^PExXD_sO!-1_Iw*|aXcAcvYc;bKH&SKcy$ zV0$~{=7gowUOb^vt4%E#!@zp`fo=4a5z=ESp)h=iDuX#TELJfALpv|pmOby79169> z{Dh6To?)^MW!>e&5#i|CRBYZ&!$0I>+bL6uj;$n%i2^|Tav99Q%o$G%1=}i&K1&?d z;IP$SYjlBkaYSABEjlFxwR^zu zTh`eQ<(E57!J0#3);LHO?oL@F%>drnhzL2!A)p{M*YyzYo?9pBVSCamSK4zL3~V9j zbhgOY^)1ieWs=c&Vq*{I=8(AMd2S5|D;9$`q+R)pN?YNmbT6J)6xx;JvnKr0tFuK_ z_|RL&#zR&!TpK~m4wR7`VegG%Q4dYCN%tb%(uxR+xm52|(Aac?wm z;}}pBkXMSnTSM%MrphcyaPe#zwbHhFANWUAkc z@-@^mxCFQiM}_>v=A?td0NsH%gk>6Q^YSERr=A4_4$7@6qfs}Uj*6Ou=?}p;M&IS4 zP)5mWROUka1k`?K31B%D;9bj@i4mnFxw(1WbsWLSY>*Wcb>5qs*TD;d zec!qc?LT;-YeD|WqyW!NOX;D`I1!m8@0**~$^sSmAvOW8p)Qx~l*W^`P-4>uG8!U= zoKy$*0tdY&6%f@`8gDd}y2u+-h=*A<7zH-y6~3Q>d?T{p1&3!YE4=3dR*u8GOObyY z@1w@?_PU2o9_D@(vVOE+2*(iL81hZ&te$73KUJOj+b9TUZO#GS`Hr!U(umo^w(BUK=q(34=OSwc!|X2K*#~N6?Jjlfh+}RI;{2Wz;eKnrCAs zOH+hDi;iWOkJrm%u;F|~g(?G1yxqoK&Q%1sSQk%Pj;u=0$9}gy6jv~b(Xpo1F+QD< zby(lc=_R?kzS#&z=xr?+g1TpZWODW)Wk}=Ou@psSWqdX^SH>%<**%@DQtli(kTLeK z)!nd0s#)_j6d;_GWl`ZQri|X%6I}_9sm=~jjYh$f{ zwVdksD5>L$+iG?!t7Fny#)PaVESD^%5cHX+@aS$h=!XR*Q$D!5!!}0G!X!Dk=3a*B z4B9f$mBsF?Tn{y8QEsc|IEYAe8U(~gKNLguBa_Oh2bp3T{Q-DJJ{Tp)J4}86GnRrE zRdn_TYxM1*A}JqA3gs2=>{zhp-<$6DRDoQ=4OQ7R%~3@yp*~hHsM_biFD?D`r8pxq zYBi%GwIW%TKyf~Tx*B{qzO81ErO92HF&0raxV+naIe2Bx*aFfwt;oy4D>#e3vOa=Q z7HqUk5gNa`e!KbV>TQ?=!3yeGQl_yeVR+M4{k5KQ(ARvm$^y*fj+;4Y4r36p1}0fA z1lAV4ztW=jAG7HF$1ZyRsf*r!%A$WbR5vcbEgO3sra5u55_G9JgCl0bv|)N;R7T8Y zV^`8e$UyMs84)tU5gZ1`MshV|8-&}1aF$4cH(<1jE!w9Fg6zYdwfDJ%1n@Qt65QkQ zc7^t$GpGiC21{>OPJ+akOmYYv^);-^ml!{Q7{RS=NW;G=xhvk5p0~att`L1-tF6}d zfsBh*)-vOHk25PLz|GmQ!*qPquo)XH)p{ZnbJIX}dp9K%+CV4xd!8mSm1V8DQVnqS zY|Gdt)F|!%xVsUsG>#F)GSL%xh~NLZxdz$fJDR5Ej$VLSFxt9ihPhJBfu+xg5^3RI zg?!Y~gZ>2tA6HU*Dv~0_NGGn)yWao?M$wG1B&cTPvHSYKU-ZD@1y~-8w39QLLVgKG z5)l_ZTG-m7H07y6$(32_zqQl2bVw^y%r5Oxn`ndj#fBe1MuyF=AZ=I=m*>zL%Na)g zt?*7GX{K@t1`}s32?nSS$aBTw$bP_*w|7jUs621{!KGp`@f|?RfgWw+LY{*WjpyYp zD!&g>7L{Y$!#xM+TQ+OH>)*1OY7|SlW%%kGx+4T`q8Mlq`Xb(OYCpZ8Q#OD8D{DIr zBBPp*fleRxK~NSd#H4{u3LV5yI&~htD&xM9qhRGokV+Dx)XazNCv(y(Zi}^~_6{@m z*bsP%qvI~25SbDL{Go#8;K^9bl%nVo6RttcfoM9%|Agsy6IuYSF0G?U=?rE!C)hdX z?&my78dH@!SM$(9lRGr(wGv7zQE2OC1Z>*R1rI?D0L;~MUbdNkjDmr{y{V9{TDY+P zb+lhId;@>S$8dgULn;CQYFQ*Y_gu1ZL1ofuz>0B4);F|~iV710U5JDY{Ns3;i;5@G zM%=Xc;8$D;e&Uk7G60ZEk!)WIv|Pv9#){8{DVhsK8&=Vdn9MRdg{&Onw#YGS78xb2p0-7`R+cAqSp8M+Am9UQU^BBNTyplK2ZR2D20>PY&T;3j5NnHi z7)$HFWB{2ksTQt;;NtS-c8U z9vWrc&PH)I!bij+*kIs$mQGRaY4IeW_iB?)=|4r*W4HJ=>0s*Jv*ARD+Y$*jF5YbL zcRbY~e^$fk?8k;jocweR2WVh>;a08RetWlJ?J^kh!()x%qvoS}Cp55+g*6s9H+C)T z7K2h=A$e5{Go}Y;D?o@%$>2=Q2MF7&4#of+JgCvngl5N$0t-~*v!TN!sNXaLzGR=9 zycTca^VE&b@ji(bCNZ-1f|)364iJib%9e8W3ouj(5c=W#;lM1K3mj^#c~g<~YGlHO zyC0$&`d6cnRS4n>{vU^bv>~0JZl5GH*Gxi1@*g=h*H}~9z!+axylwBTq5i_OcOXu1 zCuZ{s${JZYtS0NE?^TP*;s`DS`wf#*1r=stJI)`Rp82sBYOmQK=bW4GU-WvWMUN*hocsrjz=ov) zqrj|6wxp+IV>Fe3XMDz3;uhlKYR&!ZT*6?EO8%-eg*4bANaX~2C{+8VGpxODxJ1fh z-4gkFO!}lpcDIjqkM<9?kM{igI)L$_{~}ns)idm4awF5;wtu`VUzx3y3fVJ-=a%4I zmuiD>4R7s=z`M1d41ES6;oSjfhQ(3gYA4!x~I6c9vr6W?&tZ-IP2dxfDqe zhf;SPzIt7${uU;2klMMvtPe$z0a>O(WxZ_-y`a2xqyaC6sv4z|$JLujbDxbtC{593OzpG|dv4ij0vn z@FBkTaUU2%Rb~}6SE%!oZkr>-;z|WI5pQKlzFJYuV5HL^ulj;wvSlmJ#7f=St~^*} zZIb0>z$?%$sL&Y<`n^zKs$E`(6UU#}_Gq?kj<;%6PaLgT&$BVq%A-^UXfOJCaQLCE z=x4pqCl0}rlvz&bZ2k?7xZnR^D-Yu4dB2(OZ*DHpYcGtIvQ9ddbw=c@kyfhV&F2yn zI!DdYJ!-$NLTFbh1H6Bfj98!T?GCqhhyBs$VE16>a4$X_F-BQ`e``A)4P(lV4u+#R zK4ffnn;snO42L_T{iFEs@Hxu>hjK+w{dcDy9rd;jb`JUnI|sWvz1=?TZ|x^rTX8=* z+}{}<9Bd!$Z?UAmwSUMM-5;_2;Z~pS&8ZlCUON*(sbGt?jB8HQ@0gA!^RB&}{<3=y z{n}t8GBQJf*IY&(&aR0i2(CicOG61s?LJKT9rM*Vq7mh3A=%ADGBputQg`^W-xKW{ zh!Jr^U=5DBW9N$!>H1P-K3Cp^#kW3Cef|_;9H9eS)hzP<>%aHDp7y>@$k#W~*X-x_ zU+bUWf1UpP{_E^#^8V}b@z-DdOuoMJ?}T;?J3{bQn9><|Q54z-={gVlDbp0(7{v&; zJgNbfHy)!ZPf(ONXM0OAE&|xaDpiH1&4k@;DxHX;!lcXWEWtBj1Ismz0gS>v`Aq4< zFC_!`#XlmJ{K9)iu3JmnMs@xx%Ox(EEJ3Nw4Oi=%d2LlCVAw$9IL|LRlL2^@BDT1e z5|odtp5-fAEm9jkrlwUX5LE|!WTm2c!z`{uMDzSaPKCUb!jPEJw`{7dcNu8=8B02~ zA}?;F|Bt;lZ*JR2_l3{j`V@WOd5^6blHyKLw{ojwNpW-|%Q}*roViI#ZURjbBM^;X z(GrjEXMd}oUeMS`kfJ0?hNq@tgFx@g@7aD&dO!rIn^$`yRbQ$#IaR~pr9P4|0rJZH zqQ^9S;f@%t9i|h^h(^s#Z6u)Ug4{+$t02x#S`aeJHOlP1H};I-u-`RmMl$gnn}LO~ zVe(jn@cKXogo&3kx%TAs=JfbbRfh3^#Arm=^b+xFr6yys zNlTl!S)6A$xwvyn7NVGTl*6R*?c(2*D_JIQh}}RG-&Ur=VEqYx7k2WYseFUilgl?1 zcB--J0+A8x1P!OjF}=dBb(LJU0H*m?NKkSv8v2zpx0Wk@^(>*wuA#@u2b$Pcem34! z8~Av;l^dL7LQo|HKJqd8Ww~6V5AJ_54c{E#-zmiXspNabg+B!O&#R<3y&mtmd_&q3 z{wvP5SDK(TVF}sc7R|bSGw{2kVx9`BIBTu{=N7-}`p(GB>h z0zu`($8Mb@mD-F1o5V20e7xeyMXZft8TG9g0x)XTFzww=M}{C-O{c0NqE3{ zN0w&ZPLOT4=aX&g!-5j5U{(V7=YcC}lDLOP9+k@?Zr$&c3Tpr#^;GiWI(i$5F>025d z>0iXBHF1oFoP*LLKqHU>WTLSmxyGp^>`7D5gU^su6bVN|vFpUAhSbI-%Ms|tjUULa z^4)rIcp;PChQ3X1h|2R3le^AqXhXO&<%Ie1A7pWH3u3EpYR+>eF;kse6h1Pp=xtBB#grxAF~g5l#{$qC$Eule42*vB)XWreU^(}^rC8G}y7 z77L5XW(^B~dlvi=6Vp*j8r zB|Q!F@5QHZeHVuos}1ra^-U6!q1F>*!?u%po)$N7e37%ztXsxD~&3EUN3@I>sN z9gF?B?|Ab9D)vv#PC3`Tk{trBIlRwT0F%Nye;BSY(H5+qhNsI zSLTeiooV^dK zJRLR0b8tmn=l)8#QK_$G1ig%KV3#nD>;;c};Xs$rk7pWQ_72d8#CGKtj*~GY^=nMp z;N=V5lsH9kfNqJCGl3K*1~rfwKn4n@Vah?SKb6sk0}Ms9Xygl{y$XBLeMc z6C+1JOCSj874?)Ps+F)2j1y{N$0*?1OjDeKPVx&odqq5vnxP+lfLw`R8VdzPp8(;W zNp>_hKM_e*x4mHJjU1u67&qP1Pn!_&SDsQ{;Z|)r*nAs$Oq-Ve?t}JmtSi=ePZgi9 z$gai_rbSF(aK)xceMup~ncp~pglJVG?p)(~7kYow+DX)EyGfp(Papt!$t)lYTo4LY`+}V52Z_u z&-s`xmE3t$;~9mZJuyUcn*eH+hUP4ugRm|Hj#31sO7|p`GAgft6u;-S5NVa|A%xQ2 zBYOy(M>4-iSM)t}ZM#-o&`qYgwg$nlW(4=Hsd=2OK}ftjxyTU|=d^o8IWck1FRG%H zY$)UF^|L=MA1l-PGOe9bN(pTKFXk0JP1kM7la1Cq9T*Ay(Vc|A=L zBL23j&l(fsQ&G36g9oj4?!f^A~EL=R$x;--mU=hcFo8(K*j zr`}<%Q?HOrG}m4kAOLZfV6^ZP9##BZQI$QJUYOV+$7iNsP+TBf^IrUv{*Gv&<1$N= z?eX7~E`!yE0`{4r{Nnr|@=cL0NTUd`A~VJ)GMZlyVgg@53hEKp#9k07a%2CSO;cPITYe=*L%y1?yqQL$kwexM`ov2s6ZFL2+}7J*`P4hQA#QjC9M)-f%{hK> z?HM?E(VeGdM(rvAYqH)dcfv5hI#VVJ;Y2UjLy_JtV(|)IxSBYGfNRB@7gjR$XDw zX7G6I(A%B!qy58=M>}s=D|yj++ZTHI{lr)1JN7uM?vth537@`1>;mU^fh0#fm4W)( zN9>&@z!+<(LnkulLCp}yCAeHdhA@lpKx2_-79xi};^12T(;;U#oEm$XTv5D2?nbfB*AdhFN)3c#e^ zTV-}(7Z4jdeqL*R&^pMz2P%J=DVAG*%Jl&tPQnr385V&Unxp(lW#HhS`yWq4m`a&` zC6`prSy`{6OLjimV+7>2B4&~JQXUrKoluE^N#Www4oy&#a#IS=p{D2t0^*EguKG}9 z%abrNuhq$eNaMp7Yn_a3ms6%{^2!L_-tk4wWXSe5r*M9xr6ZY=g>!}j`!86RGzz^a z#FIwH?7T=1;eL}#RXe(L5|Noh(2W;`fqk1Xz!M_l3itO`~YiAYC? zAQ=w(JrzH)pKjYa;!6HSN75mABcn`eM(6|w@@fZ42u>Q2?ir_!hwGm}@QqP3$ekTP zei=}v$Y-IC4_&T-5&IELhu*$SXwRQ=mJg$5%nE80G$6QP`k^6@bv{H zIC3SXcc~T-QB3kWVm%f9mOG-Poi}`q*Yyw-NKMG|olJ=fS!uid%Y9O09S{}{6l9>5 zl1x7ax?HI@x@#3OL_n9M9{W`D0E%MJGajEH~#zqlD`e}8|U|G=b2_LtD) z|AqWf>@T)C<4@S+AoNcf!Sn{YqCeGSrKbcyb8vDD4XM`y5*Cch560KLrqx)pYN6@X zSaWKuDK*xNZwAU6mk?c6+?mkf9U^{%!K<9rz%JW<7ZKN_?C?1|`lx6nMP(`Q zBThh8oIU0)Hc1NG&jAzlbp)4WoGWf(>Z@#Hb!P61Tsm&OSqwA)(fhoP@RUP>&cLcc z0qRRgImrDe7Pno)rx_QqBlausE&*7IPkOz*V1~29w>+v0O^8c)X2%jWYcRN=^4Na2 z=K9${t#4hi($=+}m;|uEP_c|KkOT>{%ae;mOKWlSvZE*3!f|Ej%LK?M)(xeW{K0;y ziZZ5oadzr^YEB(&vD{$ChR70b6xJ6{5GO!79`mu$D7QE@x*%S2wcTb5W2JQ9k(Nr< zrh|P!!yIcQiqD|rrRWjji!-tzOr?jEUGb@*7H&Ka_)|$kyLgH${)O-{3@HNCsZaMA zA$|%oC*h%ID%V{4!|C8qGZoAus=D=^9K~63;z0w-@GXU=w4Xh^hKW|Y+ zA@6}h*Rglg&jWA23hyA%OVKI*4czFiOC#4LJWmnzd6K61aIwcc*q)u98cx&d_lbqg zv@IvSH5sI1^1FzUQFe`3HOKz;t!6~N^WSW0)R$2}{9|`am16!HkFPNl;MqV)DWrQ{ zc;jsX8yu6mA(w)dY)_^NWwP7}z9&8!aDzuq#~Q5Z4bg6%wjgsT%aDRZ7!bGF(%U`t z#HC@(Evwn}u?cET9(Vc$KH~f;U)aEWfyy8kjo>ll-YqaIVZ6n7PE4N4-X$v5RwhN& z#6w;s$Y-&n2=+SmhxM2LhGfg-KauQF{HKmWADcv^q{%$zS*G;Ike#wFy)$z~OBLUl zCsHOPN-;%Bpag2yf|s)M$lh0Nuf^@HCE92#S`%!NN-qia@?N|OfqR7>Tyja@too^YW0Ir$361pwH(g;<|bF;plQ(iz-Pf zZOTuCtY7}T^e{ef9A*9T)uLe%NB5i*Veu%jm*I@AA<~#MYQPe33rTnxlpW!~HW`+c znG~&+2T!!>*#=M4Up#nl!!`KzR&I&?%)WRVfcND!xnE{zpfWi&yv}DB8NJK71LNRA z-ti4*@Xpd=3ymNs9qx0wxlR|cI+R)7QH;O8oo$t6V<*l;oTVYQv7^{dV^&is1aSB) zbQ_9#pZgRLhRzs@PC&GG>=Zyorx_UAjcjukyW{DGOV)3phD`b~0b58;Z_9+(B?fp- zd|kHSvJq^F8nS_GMFlTgW?pd<>Xo2$n}CP1zNB+kG!&Ln)b9>ENf{i<5;kh(r#rvg zG7PvT-nC4Odw92nJA*;DGir5)gAs1G+byfrw|Z8;L%O&-=r;$w-R`(G8uyyGGivVc zwnyX6XpIcGdS`S7+*ZHQFJQC%N$-Cyscm9F`0)KigFJ;BJWlxm$Ou?h*qCAt){*VT zhl-xeF>kf-BF{2Becg7q$7xr=0+XmaQ^>;@M1}Sm((O?T@%-^K?p5^w9uQs*P@uE+ z?Yx1I7S+mhA09$cnpo?KVl-X+p&j@UT*B`oYeMEaCW|=Cv+P%^(1@@m?O3WHLAtfD zeM#<<6tRrVp2;1Ic0A&n#0}Z5rei(^x-S|KNJM&Jr5a$n`&#u`gM)D5UC;)hI`Ii; zl4PL}j)0SI9^Vw?0!3Up4p0U^O z^*U{+EM06IMPSMzGSzrK9XD{E+Y*2Q29b>94FI3WBSQCKntu!Mnic~BUVPc|lGjzg z*>BHjY{~m-J}YjVIJcM-cTxF^sBwi-O?fu;pNkn6oJ+oaS;!8XM6vlCt~f+H%CW3? zOdid=zIZw+cGp~E`l4Z4k5zq9Ef2JNFF*T_Y6f4vIsW{HENi+hY5)^=Z4;-dgCh834Vh5eUl3gX1Pd#JrH)H?g#Q5V|&F~|4 zKLIbqqf_dhQyR+1Yk2lKAbueTiDjY1P$W>?ndx+OXK)X8TJ26(5%LLohT}6MTdOQ7 zd$Qo^~3kkcuf35AKUJn9rLT~uSfj4AO{w4N30Muf>LDo?igGjG51ll z17!Ik68i=Wx%N!3nMfTJT$Vh~;yW;dV92T-#5BYp|jrd?ud~Ngm0+`aTpO{t5gazVU>U95nr}gwy@P~<~8sU_&mi4 z#n=Afyu&x^oWVP=*cV*5cz-MK9#0|9b$XMFBB28!N-eS*`hJg{zeRA|<;g`lJ!g8` zac9P|MJ4;r(maY6yhfdL|eD?2R zqVJR@pSn*+^POrqxAk6lKEc)jb%W5yY1Y|4xS@kh7k?WC@H%7xlZ z&&il8HQ6t~9k`r5>BaQ>={$y%#50$vXbu^P1Hm!^!J@E`n&Nh z^WkF!PnJNg$-x02mUGuW)yO0T6s4))UC_Ohg;RT41XoxxuhBWmA@&;2EyHj!dyh-R zX^abH@=pAqeN$X_dRR0q0FP4ppuF-E{kjp~X%y%#vhaP-K#8Q3oqi7s&`@hj{yCs_&9zTSd_Rd&>&Wz}`+w+Yub>R9)4m<>ccImBq3%6O|4Gar zTZ#HmvaiAG4~NOb;ORf1JtNS>c+>4=nh3wvu zXDafcb^oFx!Jg(t7)^*>NHyOT}`!d$$FV;qvF^% zB+fFr*r7$fFY?gEfLpIIcGHf3Yc^W#e-tg%}A%=TU$Gdoe$0>yS=hJvcp}Z zMU_X?TT#+m&9W`7m$zz0-xhg5Xf2s*pD&o`vkJ+k5F_ z8~jzi?0iXBtpKk_K}ctyIjSLY*Fax(gdMy-!`{x9RM+RMC>dHC%ca^~^Z7VEh+`k9 z_?A6xS)9R4Xz{KNzchqe5Th`FRO)?%? z*y;_4Br360`p3BpYNqth%{MPH!3!8+ zfQhUwoFfJEz#mC(Sg;}f_J^*0XNQ<*N3kDKWWVruh{#??Fhpc$-0^t98yW^t(6}dx zfhel|1qMLyuB;aQP_Ss$<$RwJ>IyttlZigmOLQD#1kMP+4&3zSU(U5tb$J!P&d6Ti z)TshOTFquF>BFf#;qIF3fy|1NbjwW5RXhecbCvJ0nK`GEE}XpuE}W5F!hbWad_`G3 zrE~avtSZf~CErD~w?-FHt+Ms_hkia@-Z_*tBE+tR9qP`N)3{eCScfE`5GP#`x@f6h zi`zn7=f0*m)n)z|wy%h55!ZwaC&!`S4H!ob6E3i#2Eq=L3GdMq2Qp=yW<>I1Ba$Q8 zkM3!N{sp;qup1^fGdt&ma$TN_>eCw=gQcdXcZIDFMKoklm%wXZAgVs{CO)ANZJzIr{f)90%;AK>{#{4n5PK4-cA%F_z5u4$~$pCQp-Tu@DT zW;iijl(ues&LM4w384(~xA7z%ScnCJIyZ7drA*C|RHc{K8r=dvAFs_PFxjwmsFm|w zKc#j#?!gL5>8OKWdp>v$eZB>h8AM=72Ou z#2OH@Gw$^}tI_@BNyfw0plx=$yB1!f0zwdzlw%dxPbG)e zR1fgiv&eBoR>RDKjXiVt2(bpoxeM6_j2sy(rWem4o)CNwlp(hv*#dK*zy&+~_-&>M z%;H0F+lE@k(43dzU>C-HB8h*XJCrpkg{q5(3wWtmpQ=^@wAl(pnK&g2Pwltjlxjh|;fC|*Jj z%5$986v$Wi6T78yT^HbIev9#2f(9X);u|*KieK*X5@?#cfxx#1(bxxzgLtZ^>)Oay!3=!4&(% zIuR{O3Y@#m-Db+^GR{d4;%CmV^1V1w*;Xa`;}xmC7mdDna`HZwqtC!{l(Zxm&dB!g zObfB#Qh(;5lyFBR#Ery|X+Nf)n+}bv#st!uHcUF>Y4wh?P2;&1AjhbB#+B?qzV3;Z z%j~q+7eaGL(HPQBAzhh(U2BK>auIR)$HNnbvVc}it{*KOIa2w^b!|d+SRIP3k$5$W z$SlJ1=v#(<709!UeT(GBJegICX5tU(d!7Mc9j{DpkJ;)xz9WU5sVwpAVTX6DTHM6{mfqxLx#l32~6ZvfCO=1YZ z;KrIz*P5`P&ks^zi|NsNHNUppHJYn#HZWUo>s%>zw8z*4RZ7iHfU{WWpNp`deys`Z-!cN`xf}J&@wf$~D(L64b8#kqq&}fH#IJFEiKru{nwJ85aUB@(2pmh_S-R&M zoJj~HbIMWF!RavyNEmtI`u0BJ;7_ifoJV1o3KEpb`|CX2pzYOXIQX;9=aHAIt@OzlbOkshQs5PN4>++L{Hnblc@i6Iqn7UK zO?PoH94!5S>wV(yPBFV&&iT~=3nOb8-gh|X73-wu*rpvi_levA@vdS^#f2i}btI#M zV~e%8Uu3T77+s5N+75VGoZ3k931=7E6uVZS@Q6i%5N;d(CDv9nNE4uGm3qw}#O8IJ zqTlnJdwyNsql?r1%Z7;Bc6jG7Yf(*h6JAX9Otf<5IkrrhExKl6S9qyN#02i_ygseJ zyhUugzGr0L|F-!`lh+a9tEM zcJM?k*M12@4jggmb>3gsI`3EOyjtgvyUwq7(|+tis?~swMqed=>L-6n0cmXrl)RUu z4G~p0vX$j+ww$+!$X;`c`Q6CYs#dM$4h9+W#8-SFORqQWeNfCP44!RWzf|k`PuaQy zaEoHn2Y`&!EW-UQb*g`QftH9pDewsIs(>{nh8rm%maT0GZxA}>21^XSb=)giM2v_d zLTmc|*|7|(Aa^DY)k>0Y$CcKKY6K;H5Mo~tTlzE%9W790bk8s)z8A}Wh9Wn#0oeE; zP>r~ozUY!92@P#_Qqm}F6bxyDgCL15TTK`hwqrO>YBk#Z25QHTZ-^hT)!k~e2mcTd zkC!3q^o78qnjRxQU^~=CLmJSz-)b=BWTt)~>5(vy1`r=UDeS_ZUhGib%WZ#ae*1$c z5T=S!eGttM24k+gT?`k+4a)b)Zh#V>%QA)5rXVkge&z`M4H%m1a8`42Ca(5ZR6q`; z*O3<|p^`GS?T)FI`1IX%G$PoV3AKQDi>pkwQ;o|eFDYsq&kVpmCd4~bZjH)uAal(8 zOo%5OmE_Avh0F-0>TS}$R@`2Pn9QhqA-!#q6+*~s=?Xpm_JVqNHPMLhLClhL{l6Ws zqVMoyAU#CFwFPLoWJ%zsa~>zy{)u2Goc{IRcCCHO@^i*~%HNQu4`iaz(?PJFv6AcD zdq_*(4X}VnlOdl`T zF#nVAHH?dT8>rT+O4)7YKyEZ7$`q8qvPUvy?2K@S*3w$KlQv3yn=P*daxW-GM4Ods znaE)=#wIcL3=&S8Y2LAuHg_d1QB%1#mo81UEIEtN$S*SQ0sO8UZvx_4?7a^@4kJ6H zQGoW(&x)!XaM!uwB`8wjN=bp+tHkst!u|zM{pFM`6Y5x+J_|G@lE*$&FF?+e1QS5E zR4<>ox&uY3oDh=#3j#irFIHaRy0I{2i#JUoeYlN1L+)LpW;g?$qj*AnJDkppJ;SGw zYhC$tWV=SqklYH1k34R||4k(Qix-o;Iy4s>nATM58hU$Dt~;_8R&e4q;ddgG^&yRa zjq7D%A~@wTi26T=XK3GpXf!t4!UW1$%J}J`$}V;Z|Igvsc1;RD?^prv&wgY> zJn^wK0SKvzNi&fmgwmut%eQZc|29Ab`_6rw_z#EE?HWixBnazdJZ9JHJ65Zx4^-%^ z&P2BdgJ)u`?a>Z;1^>{8i7S|@7A zAvg%H1f;AnQOKgR+#!*46U7T#9zcVl9mCcycc+58Um$NO7Epu%4Xcdmd|0MflV-Ic zm^T77oe$8SHkgYCaYff;I-K~&*D;sJyQVLU=&Ry@<`JO>Vd=8V^Is-;Z(>OzfPCqLu$lA*g_bC|^fs`ZY}_7xzI(X5c(J zCxIUHW8?sF|m3N%T0JRt^3V#Oq zeqdX~#KMPoRyuc992c2iQjs5pkrx}o9RvAiVPl59ix7vwk%$zD!3m^?i<22~vy)f9 z24yOmVNbK~b9IZ4&1(kig$xF%=dDq;uH)P+SeHcN6zjL~O+3%u>EGb)hiK#pP@nE;z>eYouHF!O^ zffK79U1||9GL{M+06RU{rI$0GgT^&Boog}L2@Da_A_}j~D5PgT4Po>8;W>6E%B`45 zvEnxXoBS#71s91A7?3Ue;UwjuDN`NHo1)YDg?=BR*p*uDOxd#r*0}sKZb0(bJvcr* z=hu-rrToC;H*AvWFAyBVA(aqLA~oe0Ki1;Jbw^LIkZ3Zsirazgv{F&eX-0#{hFPnD z|2^^X$hntnlQnK^WFLSvMye0=wt+sLw=B*npU9l9`m~3BGg`xTqt)MSv>MHwcGsvO zqup$__V#xAUE>dw8qJ)AkVls6fl)*agtFGy-W6O0S3UtHh;_y9B6|$6Xj8%1>`lpx z_}CFQ6)?Mo;!;YqC%O3YnA~GlX0GD~I(M)Ni4-sa=wU<>GhG!Hr0UA7++snKD|$eH zi}#q&`Xp9}1iA}qI!Sxy6&*9e9xu-6+-3+%RkFwiIt4}eQKpX^eyBq!+ss85mbeAd zT?T_12=5w9co;K~Qa!>(=JzCMgR+Ge>oj@GcO>MCc-}f!F%Kc(i6uj8Wy!W{hoxCG zMdRdmsY2F6rKt)p!K9gL&vvpwBTB;{^s)D&oo0~U31^=CTD{h=GLPz^JtI8KK2eyE z@s-!NqFZ)|7I&t;;Rccr&*196_3bc>WkG(ou^uP2zN#PP)+*Y1z$GM;=0M77vj} zt@I*Vp7v@{ar4uCW6#_7XVgFQDLYS$y{|Ho=d%m)zDq2TL_~2q$&yls^b(C|q^eCd zDyIbkFeN_XO3&CT^d;RG&YhFz$sM=NCqYQPR0BL3gcp2Y00*GwllY&|7Wo}`p#-4c z2&r%iw*MmPcePOYHbd_`Ul60yfDKjvhs$=lQ4b|6lV`M?lGKb8@JSldGz@uYya+OU zH)lr8n2{MjL|V<}M_ViiRe^GG5T5k^72B9gZ^(ppYlr)h*Ht^~@%-&1yRuwHT5MUy z-fzamKQFF6emeYoa%9wu)1%9){lkyPr$!Ba(Eon$)A^@Qm*O91m*@KjN07;u2^zLT zCYnhpAmCy3$&+z=20#pU_BRa`9~XG$RhZsT1te+u1g7_tIK7u%xAME9bt`{gY26mk z^?Ws?M~Ja!9NpP$fqg6H@X@N4XS5uRCn54t-I>dEXLJ>)YG7~h1MD-W_O2cTq!*8|zpb(BZ$@{{dkyDS_`M-=s6&Xg^ z*fSpMfsprwr61I(!SubmXKJv z_{BCV_k2j~k%=8nqfx`8vmH_7&XhQ_IxyJ=JE2bq&-gDMh?m|7{b_wn{klF{iBWWe zypT_B<~qF)W@J4!Q+1CYZ|4t*nYG*iaGQ#qg@4rfW95t#d-^Z(!_bt-s+lI8dDMlx z%yLHxW206sxAJ^A$XDC;W@)`G8xKWAHNpOf$q*)W9ImWMm(L`XeSb1nOv$!eYO}FF ziMW?RPO%F_v~;O+B(-F@KhXfv{VkT&(p5~wmz)eRxA3xJV7ln6G?-WJ3Pq+%(^yz? zR@+b7uhRcF@wwEX6fLM${)$qHmxU`zzWEcuemEjH%uCsBbeggn`E#O{sr)$I`#g~t zJN5{VFtD#2W)f+1%)xkMj`|~u3~{?VFs-&_b*y%;-D#5cZf|IHn^v2Qdb@ap`|W-Y zw=o{ITm8YHhlfVZz_&rVRIAk@q-BnAr`zvYWK7!DctFgS*(bYrFc_J5+}>@Dx?QVn z_FEVay2HNN+a0xsyD9kHv7ZzXkd1F z!`=}0`pxFh>8fA!!bGhpj=s+qYW1?s%PQ0)-4}Mi7n1_8mu-Wr*g| zJ4Dk6s1?G%(sHMz;NSuc)WYSxHibjFFHfs?Q2lv`-XW(8L8kOgV^*Krw#Oz`pI`Y) z;#JpsvZQ{g)UuhYWGW@>mZ-+_=~$X7lP3k3%h*+$3aN%qWPV`XiFmG{!Yf*=2p*j)N;qy*U z-l++d@pr1>xz9!p9dSWZ#O&q0RX2TWN3_y*Ck>POga)jhE71qlg?l`6w#yEYoe!k* zphjT&*dub$G|JW>7jVvYR6mJ{XSJ+n=1b7?taYVj_G0cGDE3XfKgITnmG|q#Q`7{G>zO{F%=sqAixyg5QK#z!oB>KAxy!o(nvQW zopB`Pex_asDfni~zNR}DXM0LlP)wYYF5?+=|8YA@rb)@g7E2{dQB^L^e)Vvm6OfRt zF`*c^2yqzYEwu|0axulEF4s!P_&Xgah7`Sp5qT?g=ioAp1L~r;e5D-+pS`zA^S)?J zX2v5-><6{=0)|I0TLhUs1Aj^I=EmXL3IL-~6hZ`o+5>P;3E!lE4;yT%5rOb=^(;34LPE~GZKi#cs+dDWv1dg1f&s5A$ercLvu>v3q2Du)avOsdJRYb`8ze(h~TmV zmFf&$UOdhg@FWfwakY(tdLP%v0_rdJui!?PgX6>Vk8DF^$fyM_%X)Ie3<_q& zC?a$(`gW5y&g+NYX`Q^dikA^deeeJ{ddqWLo523U>9!wW(k426b*t7Xc+}v8Ahy zrMtNvYkFLKc{{rLW;@lU#mhnBNT1Y2wQ+6l5^TyM{aL1p5~j+yPjifvsUpi7663P7 zCdAw#mntn&&}+^^lwAv&fhZn=^(DepgZ;F|o46|v0V;{s2ibx*&2TfnagS}eSy1~+ zGTkJ)vLra=%3N*Xq&L@EOgG7;nW9P>Z#*A;*Y66lYzTD@+{_-C~QFihefXhg@r1qpq^sQTs%b2pp%Q6_;3x` z+@mfeY(Ah>S~9?_?4LxD+E-}8J`>~MrXY^hvTCYkST2Y)S$MSwze2NaoXrl>dkfo*vG)%qOhQz|V#c2Fx38^ltfO%J zNty3Afn@gwgTnrVgt9g4C*vD?>_~C(l{dI( zzo20Z()+BTCZ^azssltpRF*7P_m_Nd1ak52u*Gbzi9V3}l5WA6RckW#>;flZ~mz#wPi%P>&#*sz0 zIWFbJB8cm=2Cp!3UE=5@e4M(cE~)N;g^_J&cu_uSptFRnO)$_I2K+~`|FFOE&!7!~ z8K$FFb6AWn4ld6W4(1!|7mn*>86~c3C`2WBIQ?{aw1*BN-zRS9+}9F6Ba0G__2dJe zFi^~T%IS6384@|6J%!}id^}UCj)op>+<=@47x#e~I-hQc4-h+vx=RHcgAraUH@3t= z1@!=Gs8dzNmy|(vaZCBCl4-Z%_8t86=~vdapN~&YQgPDk@8|o!8a3ni?BMF;)4|W8 z*($0!-MRCli%*}=57vhtrYZi#s!~iL9o&Oq$E;T7G6A@a=$0BrC0){H8&%brZ@h z)u=e!J@l zK*o8#4Io9{J^C}k4&-Z*e1PbXjBS^sN)bZiSdSGA8hO>p%v>`POikj46XGt4J%)px zeJtJX^Yd26<<_xhb2hCyM44eG;S)a$1lM)81v14H@QkTn8>*Z}=mbD+#-Q$$T0fXR zb^i|?<@RtIjYbYxYv*{8xxM)*QJdBg{>@S?FHePJ0W}kudDO@Ly~tjXBikjcqjl4< zckCdDNU+oD4EvC1M$&w-n_6fS4o;5YR&w8lNT5)F)B7%w_Cc`9P+Bdi2|nI}om#|C-8r;d1i=$+8dYy&^8 zPd(mbhY)Xn91{N+$jq{9>X1r*SCthCu4JB3PGs4H^A)ZZfxtcoWE_E%%+4H#gv4Ph zQ7E$G{8GpcWj+?_$;X^SH^jcmgd|oPdospm7^u`oDzjri&1({#ykCkf6uv?1Bdvro z8kgjUoSbAa`+ToZUXuJKAkH*UtJy<$s2wP#(b&U~!kU=k8=FRckv#OS)ounni4FP( z>NL?E>IB=_#umt6?GhqbqQ$)A8}kCh31F67z_Q3iV2yiOh&aGD8K8G&42U6@EMzj* zXb%O&L2&PyQ=hu_U*zZA;FLR=WnI$aw{l_~D7wB0LMn-5xNQ|3oSvZ%2eK9hX2|5A5!HCtG@WOCagsXV@O_|Lh4nlQ3|z$xK?nPZwp^1u#vFa%602ns#*c6A1sQf8vd_O{vfZ(r z;Dg^3uUieS$nj)b32M~DgfT(Vmr)h@+a-Ot+o3x5Ro*+`wHM{T`2-L4kuP&l0Ge|tvgoY~?628jE=kBLD0p+mDyPWIniEsO%MAT{Rg_Am zCT=i%i}zIgazf?Ltp3 z8h|^Q#^lWJS8$LNIUk@z>dygrvoP43Azg-;FxA_*Nj z)CMDH)u+@HzvfDqv1%P^g1=2fC$|2LmW~Zego3r_hJhtkb)GAhaMwSeGY^M0H}mJ& zKXc!k632tB;Rl+p75F3w&nf|$O?5tKJaU?~4;b`K-v)b#1Y&VH>`<}Yh{zPB>Ao97~kD_nS01Y(8r^@)xo@!UIB^EO!W zCt_UQjYx>MMZgHXmD(8@pvb+TDGIT3%_U~rw~&YZ@Lumm0QabzDZaKFdV@s+Rvjf( z!0}oBV6~7Ks+KlRGiGfz6}Nl6hWvk%*L92S@JH(ZEYor3t&d-^NFMz2=l4hTv-3~?^UwOh z{^|btKauT)eC_xVk0-~TN3hS1GJ_Qjw2*N|P9rTA3*)+jBiDqaKuMp(k0Q4fcmfMK zXiOtFHsaFR%ns3v1c7kD9}%`9&(0U>T6et2adz6xR=dMTf;i64F1>Y+Eg4z39KK=& z)UR0ylKsx+lF)xtf4-kTVsV`x?H_(TYRs(0goZQES!*55X0zGvcHzIxW;6ZYUbor$ zk5+fk?KZp3cB}ax%~q$=Z2t#puC)~ff1)77{(m$dTvxhtFXYeHuPP`K8Nij>m=bIe z-#`uY?c3H?&I>XbvrXM!vCjmXIWG%O()ZY!5dK=uOAM{ee*=5oFPW0IwqiyM>Je8e zgabMbK_=l|HT1@|q)`Br;~wUEVqwK~CtET@tW0foV~Z?J$xKsHt*xaKLMnO;1(7*L zIM~vE)oiuAEvCq~oL~+n0m7k3SKHa?YT5a>LIxpa){Zy{DQlawLC0BzL_S0IUzz)s z7Zoz}j9qIBzEO7oL+yUDI}+uq?x3G8FVAXda(;FIwf>C9*SN8@wGW7rTduE}^y8qA zm{XTCVwgPifV@BZ5&WJx=!MHrsW(If!q%~JRSA%F@8X$lGNej;`684ux(|uTbF~Pw zMxt)a!(wR^Y}p$2)bKGTQk{7TxgN$@bRPn>XmpCmt$(B6{_{6k#WG{h`Nd~A2#GJpdnU?h1UHlIxUfQ( z@(CZUfDhKr^=?oXqFP;8V0X3=v@qFoHG&Uq@bzv0i(;*9ji=?MmFRE;V&D)Aw&X>2 z;b8L`G*~Wi;(iD0aoq9k2KtpP5B|{>L@lu!fcgQ#Y{7G7^TeSe?vmqkJ{@O9!I1_6 z9s@cf)YW5BLJBR zzh0oD(_;`NYMd#;v9Gzw2Z!h_z74<+?kzhsx89&Bwh)HXf#*7x2RY<;PUz=&Fl0dB zNcKq(IpNkDuu%Kd^LR>ShN^2Qpn5EH$hJLr_HjQwc6iJhtmy<<6xpH3UyjjRNHWEi z`dc)m^cn>=_vo_&pyY5xn4}-MTW>U4NuDcs1~JZ@UDELhw(il035T|8hi?VzBP%5c zZO4J0-ws zEqjc9`wba?dnaeC9$+v)O?={^`Vp#|=OUR$@9P6?shBv-;@0Lx`j~Ei5-1~?PE9)%a4A8eq+J$C>!;8~pc=Erh2ZfSWE{%( zb(5{aw=tyOmNCOnsX;8B-d*dRF>y(FY#E#!i9hkNH@&2k9mjHH4Lc;#Uk|nV&3>mj z>5Mk*f@YejAxfmh41r$eX~1kj+aB}xHg?9} zz8+p&ot=OBcy#&G(dUbA*(QQh6L*S8mM(rGjzKsn2or zeE|}{+(&X)V3_*UJ&2T8F3i8 zL0|pbArtUF)!Tt@Ch6~BY{0)d=a(72%X@0OIZuNrz0Gd)kcQaFd7F!D@`p@M9ovOp z_0r(st!?oYs6W+q3p{dx8_?#wscb>92Wp)e)Jq2!bN*vU@rJ zCZxo91>5;b9`3el**CToVJCZX%Xi>jj2mz#mS+NvfaxaH9d`bRUCnx&gA-D3=buaJ z{K?tt_xNt!E{his^=8Q_7ui-*O<+IJ$$K1-%u?b9Z02=-!e$<8UCFz|Ho29Vb$_3o zaO~KVyqg7{pYT{)s4fBP!~iUYMf~zJ-k9St9*-;7Qy%s-9~1T@9;}Lch86Cqa1Woi z%*Hf~bjX+p4+kI#25KDNgT$x^UC9;jI2(uWz zYBe7nU}c6o7g`lenj8jvGkM{|z)rFJVR&DD7-snHg%3k!ldEAk*btz7aP}E}22WtA zcdwB4!;rSPZk`{n_QMD}S2HrB{{7YHK7{pc zyy-P}``v^lcAtlQqF?wY=p%drFOP@Ta6Hss{NTw;*2;?vU)QrV$6 zJ}nxGLOavZh5+ev(Qd@sje-z|Q3a(dDE*y5X$*W?R;&QuCG83}Zw72`_1A!#hr9D} zbEVjR9pE?r$Wtoic%LVZrA>BbPQf7R5%+3rZ3OZu7|m4_#yNm*lX^O{-JOKrMY5wy zsD*RnqfP24zM0JH=)>fkP3q_j-_7gj;f4V8Pabi3#%vj6#;{9;&mV-(Hw5=q_`Fhe zqip!-`?DXVI39u1pCAP#ZfNu5A0e3`+cllY;@L-K-UA@>m2|Zu$)nXUUb1x1Pe}(0 z!yH?!OfwdX2d%*Z`JmFPfA^xr#^}{Y-uP;Uy^KG>4BwU3ilToYtu)W4lLr;IJY1XR zuIP2^@L{O*>!R4_eKlxX%|&T*L1r%2Z7F!|a}k6%?>1TMJRP6r%+to3F)jcT)o*Q! zpfr4{gr!x4rN8}cii7FT1RTA2R*k(@r(d+3H{0To-m>_MKiZCiVt|O~ zm>&q(3u-9D6CoMRum`D&U$rW9Vw5$t zEpjyT!uxy*&Hqb$n*C)}y>$9^S`D>nCrhQ3ei`@<-%Jv0dq4?%g*{u@{Dq>6^MC}= ztSk%2&Jx9%G;+fgIQ`fu$NuZ#aQf&D2VcMP>b`wLU%#>%H=f^b|Lhp@Ga(0u#5M2H zJ1k-)U22i-3c5bf3*q_ETLBdPj!|X>yfzDXWmQtQ_sTBdz;K2Z?(n?hTX@daLl3J+>AUy#XYWwyL zy~{&R+Zzs@7;xgl)cHe}DF4cECe&7p@i-n!qd-N`D|q-(7^#GwWof*o5xO zaFg3aBXDv!E0DelNKc#@kD}-tdE=|#J}|N4Tv?Hi+2}qPp;M3WlAZvOtGYHmaNv!k zhpn9BD%Aa~?ab_gXVYD7FetK!Y0oeq^7gAnN?Cp8YEY?HL8S^Ry&|ZT_Lr-m(z8LO zHMz;Pzf+i`U1iOJRaW7rx$V?RD>n*2RmR`vb)f66lJKt8ZB|&RGW|XmRC)scxw%+r z8NLftA+TpbU>g3?DaIz1wyGk!_)c|I71VlWKf4vN))V^KRj{doO^>Exs$kO#g-unE zX9b%o*u)=JuxZV(X*Y$M(h90xdnM4c6dQhK0X=~JBJa2L#wVnIs}i+-H+Ua4?bEdt|IrD?#gUeG`NigK&fJMs4!B6kt$>7v*V$(-l)G)w#eO=LFT`S zaHz6#ZlayD$WuNC9pwfp&Id~zvIx+$jdoV5hDRJ${ZG324X-al_#1&xl0nER>sN+ba!Z-fswyIf9Wz zJ~4@XL##@flxT3}i~lNN&3EIAKj>u&2MC}uAgtty&$C)0mxQ;}zecvpSCdWMb$aJl z@bY=#ErR(HYXqGLbKGnfy^?6>+*kVr z6`l-`MO>R$$R~eBBnS(NIvJp`I<)jx2V~UlTl%XU;23X6b3C*XBW&^Z@KGgJlQeUF zMSO)DZ@S$98JR`~3K?(u?IGzURtsPL>X*zxQX%SRLDXpl-|i~>aJ>Zqq!ph2F7UKG zJaPebd*Lmt%3<@Lgrc_NyeAI5y`=EF?WV41JsNk3de&5qek?w;@2aeH+j7dvpoC{h zdu6*)CSi-&!{PMGq%K>7=JnOYyb^b6u5=QFIE;cHerNr^A?(Q*&ur)Zhu;l$d4d4r z(~z3f`Qdlt^585h(r{^pe^%q=t*)v~*#FQ9B2H%7as``$MWFjUyqX~`IdJTb$RD`( z$m`ruibF)(bR{ET3a`B}_G_ zJYl~Je6%reCr9f9XOB+;5|-y~*0Ztt1WPbfL+pZlg6tHm8c1WtYKOnP5Quar#$Qt~ z9Y!+`ox}K7XsJR=-ve5z(91KUmyAMarA)2OifXq)FXD&CLoey9s{!$EY?A~s?dq!e zCZ0U&>V`v;3qG#svuEM3(%d{0i&V)~DRyJsrlus)$IhkJa)z-- z2UPk{L+c*yQ(4gW^t##DVVJ+-HZH6G0d-wshW3pe-lLzO?G@UrG+!0otnlWOYra~| zRqS@13U7WFc=P!onXdV=eCj1q$g32lKm0Bd$TCE-lq$?8ICv5*7_VsKwP1%nWfO;v zCO!#*Dy4j-4Euh-NnFgnhy*uH+)0L=f8>$e{$%v7)gRPQtJkcdR;#sLc_x)dIFIV= zozbYR262X+3gUbZ5GVgoR0^(-3qr0xt|?S{UFBfQVgP7O;nr&@DguMrT8d)5bEt`( zy35w%qT(_)$tKuuL1%Y`9Y-2)ePR;(hFB|Ga-kf+R5ipei*59Gl<=^9Bv1=AU*3D% z8I9Ln2yr(>{dP(%f{tM9D_r|fTswPyyjbDdO6m5}lx{2eo3$!P`dvWM4b-}g?n4r+ zUF(Maj&>`}TfXM)(#PYmZB_{7VF+bZY2ID{LWzsn7Y-$-lplu18-ho>oncA`r~PiW zdS+`-so%Grd*O75T; z_OC5^>l)bu;*2`Bd2eL)ZAk7yBX`gXrJ;P1`z(LT5+wJ$JVPVZ&kRE)Ipx&%3|it= zFVl|2f>WdlCj$LrA zsNhKz%=G%fNnFfFe1|;Z16c&}d}@*)u-(a}4eQ2uL)u-d+fF-Js4pTf2z`QQ>V3h) zTua=LMG}@csravrziSeehy8$D5#OhN@VoJC+sM!J39ljkS{bcn;_|B?*pn+Q+szda z*swbvftB(0C`_)gVa^_N6Ko{MrowK^IC~VGafw5E1;uCaY535R*K=+@v=xaHc2QxO z`)nbUP8`UO5>e&(^zQyj&dilhJm>x@ASQLK;3_EP=8z7l3QX#eZM1_j8pL^Z0h(XA zwv|D<@zBJaU%x^NN}>6J)s*8-w7Rl2y~Ll)2c>u`Y+MxE8C!_RCxJ)ZfFSk(dbfWw z*{;N!2fC&_ztB(#HTs$Ppi+LlwjRxxd5V%mh} zwF+vhg4(_xkdnT}j5q!6sA=k_g8>pF{5Hg9cNZI(%PcSdFz>$crcbOUsT?>_c~JDE zX@*naD|Ob|cL5~Z!6keuPWHI)>%Y0iD{ z9Efygy9q=Z`u1c({6H$rIA_+Ci5=(4ihRr#`(vpzm#X`jRGcM&ab_*As=}=1X6kOa&$JqNn}&(FS$-4AMst~ zB=Q~L=F0veiy2Y4Yx3wY2oDJX{M^H4GqJ2}SNpX&=(Q7{y3{2VN`7vX+)shzj1i`@ ziepM=0gJ}N=m%3L(yACaeK_YHcY$q@Uj?j9osSiIex<%HNq{BOqFxbqOnsBgh#Ou7 zAr7PR8C|tcm&aGrT#*jW*C50>J0j)V+JHcdwc?O#r1`I(u~$F;;2f^>0xu78ha^{LEbg2=-H6qEQ%SjX5N0( z(6U&S*thpp}dCQ!E;Q%LJ^ihoBt1H1w2x@6Bs1uBCD8obEn(XAWo&&>Z&5n zv`}&D?7cC%YG;94^gmh9#N?!g#j_^KwiWign`1`ikZRcNUGn6|ypqO*bS|I7jJtzeq=@TaN z?d@mA(TVyzLxjEXwkK z)CreWDVp|HcXCE^c2X%osHtaD{@Df0JT`IAP$K)TJ$1X4Bx8NqTYPQI;m;Qi?dtIH z!4ADQb#(({`*?D`;lSXIT3yYYS0j&XF3)zjN;Zv^3|booxLaIpK#*&e53S$&M=%~4 zAk0=V0y4>dIvF5u4>48iV>PD%z#j!Y!%nNhj_2xQ?XrX`_^te*KGo;uNd7g&EB!C!nattLhv7|xqDctnQC4!x_^2ws`)&&W?u11O!~h5 z@TxO(e($Ob*SI3DWpu(Jwt!)JJapT}pV+vxhl=-Zrt2GL&ZGbXGsk5&AWjWl+sc-3 z9|IS5yo@T~?)g3ai^lqz(3=^2nH{Ft&(c<8)b z=idMlrgRZFxT_4D@Xr;{WL9CLMg&?bajqOVdN00d3xFjvsEhW+bjPA3d(9tNw#Tvq zgl~dt3p)#+!Df&WiEQ;a06C(ias{NJ6$0}X*7GFgm1r8tGd0$Lw*?pir)sS80b*s_ zOtEZbzO(W?+4up0-O%Vn!Oz@5sYId5l)k~lWv28yH_s!n2~&(5)*CB-HDpx(J^!-t zILOE7m^Z;yV!?t=mL|Kq-M17sqw#8#!WxhpbSiNXQpiE+=ut8-NQJIq>{^ehE${ew z6K{e`MQvI5&is|Qe=L~`{)C+x6j}?mFJVd5RC`-r*U0-@Wma(oY3SQ@Awdf8R6WBd zy<@z0kkgxIpJxPxX-E{-#qfR0_nmI!&B+iu7eFibg0i%!0?ZmgNsx?p)&+kWJ)@|Y z2<|v@dZ&;n*FBe7u*@7WXwFDw{dPY^6nAILFt9;cXQqhHJrM?@rc% z7}x+SOz_dEl*)kK-ylyFb}3k;paE8lkRm1@W%f&tK0PK+6}?*M0Iw2^N=A(uWdAE# ze_hOftGOLWz#8M0SrNU;_3Cs`y^?Lt%FmZ65WTo$=cWDP_j|#*g>0nGnLu4I`I!CD zAoLWaw(~D>UB!Zu`ytR2Mi(BQg^5baRgg*V@WGNY`IYKeR8?XsN&+MtjWuADeOX=l z{F0GTo=BqI9!8`nM5j>r@yd5Ff|97R<-YR|%?-ekHOS;o=)a00CCys8prx@IQ~D$< zA_HAf3bccQkMr}MsYHdwJs!h15!hg>J^$wb9Ox0>nEI#Td$23rE_c{g>u9ca+?c1! z97k9J3BV7bW|3+jXXht$UA}^ToBZ~2*3ys1@BQL=cVpuhqvt!-oRk4ep=?#`D}tuw zq17BePf{NIyY=Q;1}t0(EL;PJAdo>RjKZgbi1??24V5(9LoN>3T9k`Q@sU$*?%rFX zql3yXA1r$hm0S>&-2EGuqy`>MX*{2WDuPm6Q6L{0DGZNNJhAEqvotf9$|%aF+Rvq_Q{$PbBr0bH z;x;^Xj>^7%`j*isR*nH5Sn+P)hHz$q0?MY$$4m6jMyJlDFO4q3JJ&f|4gyRaO;U_M zYcMMxS2{4T(&&kY?wk+nVFBJwhCrdsJ7@^#oyDvxlc{=v@G{H*;q>>+cE)p4oW(~! z;Q6jZR^{C~7oN2lGrrd5nFmxneFN$hNFt}3%WKm<$Ac>D8U{BjS7H*lO#d;D{`fKnkOMGk%rlqQUDq=ggR!~nS+xJdic z<2bWW&Z`XHjA#cN``zQRfDB~enYwS&yD6d(R`+L|PP^OTKJ2H-zKjr+?M>Jn#|^k0 zcF%{wyD4v!Q8!zl6H%ZMDLe;+5djkS7Szv6t-u z>}(A(X>b2MmstX8ht;lLcQj~SZ*iN-CyM-vC>I=7+R;&l`%t!y`m4~y-klpxY%Tkq zJ$;bikbS&27+rsuX8cu?gqm*y(x4JFr%T25Fhs)Fxt|i#M0xl^Nw7!)$zJm=jns=S zB=-`Y=`P6*7O0aCNx0-`RI2-nyg8iQpl6J`f9?dJMqJC1nC9;2oWjm`GX#|OA=BI#O=gy z`>nQR78YwC(m|vzT=G3U1k=yG8d4Mb=f6Lti9~HXY`#sOa<7l18M0l?;vc!%-a^Dm z(`FfeWJ^pD4oEpmPw~;f3UF1{Ql)FfD)j2rJ5^Ll6so@WA@Ht7>FG;|4DQTB0%!}< zre9bQnQ%)#3eml2MFN<@fS*>}hu^`+R`(|_8fnIQtiR{R4%1r90NZunqwYl8s*If$ zfNdz0Ki|ghG_}bhC=MERRNM7b%^GLct3Ij}LD>+_KL2#=6`slBY?uGG*0lNX+T(SK zW$NopMXyz!aK8mXj2+56J#wQ`W(_MHbXF7cFt`RCwg3@i zKR_A&_f*LelyHk>)wpr>ygVHI*|TMh{&i#Rync1Vi9N%{x{iHiT>{YR*wEU!^n_Sv z!32d*RL$gas53^F!IG9KOY)q4yGx9n13HLlam1iFYvdnBbm5CnKV3){6`m5FLOv(##i*{PxnEP(&fSi^=>6x4ZVjBir%R}Brs+H|0n z6nP5I26ztNiicaOQ|tjxb_8MC%u* zsI6fzu{Y@76|52~4d?HYy{G-}<Z^nsv(y&#ourlRNA8 zW3M_spirf2Ip)EX2)gL^JmS4lZZx22h9f9gAHv9n&kV=+deZmyuiE*ig^y<9Ns>=HF4a=OeBg=l>d}m!Js*zKFeOk_L zQsvMpwbGe)@ygE(?l6CMM${sW`UvJ7``>E*841NpiTDEt@&!l|!t{$C?d5iO5RVw5 z1^eY_81T_EZ1_I$NH?obtwI?g~-CCB`(iJ31xN4G9@{`dY%Gc6W zqV{f>DAjI~kKsj%+9+i%miHIhyLc)i)2`Z}-`}`Ge=hy}Ge8kq-AfC4DN_A12dJWOPpt zs<}=lQ5jt25_#OgqNC%%Myu%2Y|Wb{CC=Oc0<^mcoK!sL-jyOt&Pz8{^q$r`A=RB) zucV}mR$23i0f@sNR)e3rY=AD)R#_|_56t7SM=VogPRMslvZH(y0xclsoh@#@m{qoD zs3PY!SmnsKzK`YTcMkM|53C}qHJ7%uSqoVQ0W48E1%XvlCX(=|8wSCun{H0KZ(+tkiZmOpOyJmZ-30FJB1hp zD1N7G`6DCkA`S0+V@`yGaY-L6HU}zl_R2!Wo;ip4v)U&_;$tXezm6h}uPx=# z@tkbfE~uC|*t7F@XOn~YiYe0Bnxco}KZ@7Yb)#PO{TH$U!C505eLROMZz^&TVbra2 zuS6tywTj20(o)Hur&n=tH4aonumwozaA`#WX)cm2TfJ9sJ0I?dsFaALV}xPVB~L;P zCO`-SuM*Ysb8Ab+J>EczvwmgPT8rO{-Bu_1h7#FX?Xj}+F-?@0hdtW*1?-&=Qf|TN z@8ZZFvB*9|&hY*+RA2rlZY9Stmj>r6wZA+(3TVU4*kD@L>cvLPrCu6a+VS!$hssRl z=#>}ze4Z(+64QvNo&4C4!Ue{jHxhvy6s1>+5yj#@`4f$z1em?Gc!l`6e10j(X8rvo zU=*HHh{+kv>y7Kk15@}C!Pd-b8DQvLfxIIpr8-(OthcN`IAO!$Czj`)b)d(3($3oR zKL1s5ed5fg^tD4QiefLg0z5hl0&FJhz9gm7^fe^T+!3|*wUdE;Rb}1x?a{EM=JK@z zfA`S#%Xj7GfLyu~f1O#`$5~YcjanH|{}3$n4P}JXaFj|!S3T~pTB==Yq-|loas{Q* z4ZKr}I~d*yH5|Ms;vPL70}qj_b?j~|o^a4?=3AZ;st}N}=~8GvL8Pvea1x`H(vF0V zceoeQSRqutHYZl^(KF?#GiOC^M_U^-Cl#t+>9#a0%&v_tnQ&r5xo@5$TS=bH{WrBX z7+5O0`$ZJt;E`Y&KMkt9N4n_!xG!B2E`PBLHE5RyL!_*#fMZkI{6BvOANIfjJ&r~d zvRLxsKwC`WN@2v68T>l4re<02unfb|b8ADXB1*48p5u_#6dZ)MPcT|AVaYtR9tUAs zm(Ex1N{#3^XnU_HL3=yO1GpjRxB0CXp>uX|%AyBc2RcS3f zSyjH`L9xgu0Q(t}lhl$LHT~H|KV?eLTVZ0EKG-MZkA5mCB_GVy=5rGB(jM9R0OF-*7sMAu5%%vh3DSH{z&7NY zBb+qiS8L26x%YXUh~B>WbjcwM20^*|0{sV>^$ej9t*R3>;}Oi7w?lifYrFfUJwGr7 z_EH4f$|m@t(J%*KOT?M}18bdyeT|FX+#BOmTN8Y``1J*kS{CY$n(!*C6yG1C(4YIy)vVC9c1H+$Zvh5eTV-^100%!c5Rnt&lwEQPPd~w2Ogr? z3n`f1_~_E!tuteTs>!!j0Vps{zgcvnLoJ^i>aCeNS8wsNC#mIJz~;njjZs+1z3$>9 z+PNfvi|0>gstYGM(tvUq`U59d!3` z8-tRR6%SoL=!XIPA#6p(ipf%ib&gl=Id8;at)I7Fa$)CD?MI&vCgG}5gu(B;P`G@m zYdcr|S=+y2v-e~>%(-Umsb-OEg`%bR4ss*hzpeyR_EFX8pht^JLC-8Vf!f%=#UL~v zBR)WAlPF_DK}CRmmY@!V;6y=Y@w)TdrAmQ^0yBcXrP-0$p=#>#vf8bL`rTV{xbE!^0bUmR zB5Y8!9h5ZG!5sGyQz?KGek#F#R}yK=(nAt?R8I6RpPLNl+3k z7OnN{($NbKMh6TF0U3%3Y7cJ5Y=3*?d>fR{hvRDpB+BOkRE*Y}h$CwgsGIrgCr%64 z_a0cfNFpv<%DYi0yoCr$OR5ba0ZHqyQ4$;G1;UW~t)=C6G?Vl1L1pOmSPVi6Zc@r$ zizfjtirr{jp_kAsUokvbpKFc9brP=tYO$;j>|#?f=8bvrOz-yne%J&C>H?PXcH#ktC>UI$j+lgt-8mNtu zq|L15I{UfC_)%B4dz9w(Q=7y!ErqZ}S%)Y%h(=-y~A`p3u zDn8gTFs8C&S>3l$L$&l9tvQ99vUjYpPBM1)%z}y;PnRj~%@_yA7~Y^~Y=(dMS){W< zC^*}kan2>9FIn=_6i4SDYU@DL{H{H#^@b@GzfvyXG`+QU?;|*WZSjNF0H+XFlSj&w z+*t_~c1VA5zFi#Fcz?Yf-hATJ)GK7rWxYD>zeo?&paj|4OBBT8xa;(F zO=dbGZcLcfsm*~W%fsuxD^NHwuH8Li3JKLgxEhD{b$C(5a7jeV*XTmHxPWuvR@$_K zxBksGKbAv;rJKVCG&}qAySaD4voPWyD83Hh&*X?O2m8u6m&Ms9P zZk`TqII>78FnZzmL~|gcx=XVK=zW6c{swC*WNtQOZU$s-S%*tAcsYkOS%En!FxyXb zDXZc8bE!Fs;DkIRd6^c^aifPGb5jACQqfRHfZlmB6`0dhBxz41Uf)pGe?0C4TMK`{ z@KW*fG{($ILR{?E$IKRnM#xGg$PTdtw$dDg(Gq$Fq$>uzPQvcOPj>+llO?ElVJ_eB za`&_SIu8wv3qdL#p>J&buxL+wSiQI8GIuU980=v)$y`@Twd9Vk)Y(4Y=Lsv-^cN8> zXz8wy>;mLH?dl)lwnG$Q0wurQ1MN-DS~xEef`u?-(Df4UbyiAuw@{CC(jKd~Z_W07 z^%%%EEgPpbi{qB>j(2O@qp$a!aB?!Is|p5Da08V5KJ0uj`Vkjy4IqE?T=)CopT}u7 zO8ijr51>{d=7-n3Ve1>u0W}PqYS;KyUw~BV%b6+|7j`;tq`wC9r%Q}@aXO8c@I~Jyx zn8hFym#|#ytLb#-vP?QZ8`A|F*ivtmi`u$vg)_Vsp-y(yGX=?Gpip%>Vxa-qy*!sh z=lE0rTj6O8s7QCJ0T)#%kQY6)pkgeF8Ef;4sxdT3Ne7SZ{e_-`$;$~2JdsE2KqDXG z84P{~UEdwThnaUfH2ATv(?9KJEW@rNBz|^7tyDnU*%Ny1I6)bh@R1LJ9o+KN5?rw z7|ZG?PZEx>@5?eZR%eU_I8&QTw_fpr7Q?ADzRUyYK9FjXal`b8*}`6p&2Jwsr&f~* zGn5l^l$K0xm-|nNafcE)N35Yn-S-cJ1@dg0A~LF6(`DKl#fXt>x%#Gk`H;=Orq(0Hh->ou#u!34U!_vCl?x;&C!DYM^4paIL8{f9K^S-R zc9iesr-|c@dyk0cGoS?xtDDI41ylAZ ztztat687x)H;QI5NW)x*@2d_tevEjlx2%JdN@lY9T;DsCU)ze zbEk7g+s3&FJn;lytD7K>D0>jE`Ud{YwQM2+rh)vq^7$lPZxK@btQN`7*{B6-dyRB+ z#RM-mu;;d*NZZ>P;6Dr9?%?Cp^>=Gir!vx_M`h>jL9JqPO*X z!#E6TMc$OQ<>~_{g1Nl;YLT^RP5LHm>WJGzgxcYZLFR@a>?k|vM8NL+1fxOQkPmf! zBpKN#UO#V-nn{jNcHO1g^IMLzjK?&kj%rYq+iGDWw3D)1iK6Z7;c@#n!--uErHhcV z7f9PG0^1!*C+nl^ZyxtWf>+F0+;ljVO-L2c(Gg^F648?2O7iytID6M@5FBZT*lqyQ zd|M$$De4SemaFsrTl0R}5V3x#C@&M7TR*N~10D7|=b;0qi8Mc7r>`^TFS7ieR{I`D zUvD*M_I_^*L=)-&WlcSYCmgm!_I=+A&O%v`RibmttVmp-ORIC4d2yKm?u(FD zUZ|^ex*hk|!z{?Eb4pujv#VNI%?q1gFneIkvVJaO$P3LJ+)3uHv;0{!q$$!{SWjL5 zz)bS)EVxcBA?+C9FAfd>$>)Bvd332-`f{4Joks}SG?g{ntt*2GRCY^6O74Yjhyv0k zH!iO}fDdVH!aU*wPv3w$a7c^g@swa>doRo(w4=bFhpB>3UVM-E)@biW4QUl@k^{<< z3rh3W{X@zNDOV+@QwaDlNgFlKUYa%_Ueh_+b{g?IBWYD|sC^a$QEze8>$ZfW>GdN+j@M^Hx4TV1{ESJ{Z)l^1{DWPt3+`|<+C7Gy$@uGyd-HhnkI zS=2wSagaT5iBXzYxg%xCy%j=b4W*YL1?nmrpZY6!20yV+QK>1NXZPkdQIwoeEl75q`QFd`hUwZzdBJMO+LwnknQ#?D*9u{(f7&q+n8P6co+PP#H&3uywzIIlo^N_ckoNMN(iS~8Fk4RSTj5_n$5eM>U@yJ*~-Qf>LJ-PgRd#i%j(uG?AT``rRcf>I576D$2QK z;j4P4gbEN%!I^^$=3hKWs(TPCI-v6O3YPUO=hv6}E=cLm)&RpzKg5}H zh^&OX-m%An&UgLOUt0KC8#h~B34?RiZvQi#RDER>EBrS1jaS)!b=7Qbsjin*$gYYqO`ho!%7nShz}a|=kZDTA|}H;NS!JJR9+s-9m2f*%+P~Q`1$8! zm11?yE}z`Co=^|!Jo455oKbsT_+AE0VF!K7x1|)VyJ&?@!!vd8oE}mz>gGn`?krz} zlS*3)kz~B;Ss9hFM?5WSmLccnJFdyQrI0o1zjVnlTh(&AX-%VUv_P4T_4 zOB5D~KpK@7bzez)<3h<|J|X#TY6;1Wvod9Q=W~*ycZZ8GBmIVo(psPK6JA(AxzY5& z?C=P4qdk8nWFXuixiem{pVXkMS_%{8Nm->yCA6!0dNf@O-wlMJ`uwp*ih)j9C?W8peyq+$z# z`5)DQS|2~*Q<_vd?wlzQLzUX&54|>Az!nudGz=@?dI4lF>06X;#}szL-}&7592dWp zF`snXo({&zY>Y&}{o(@AE6iHfucnCNh)l(JQ)E_NG zaPOA1Q(%1|U!54-n^cVV;YA}v4lyzX71L0_@{Av{TX^ZEjZW0UPcCkixw$FcrdDU~ zG-vPJxq1Knru{MgK_KC8+`kJ6Nb+EY%0NanYnp$L@qA(;x-=eMT?&a&9$})zz>uBb zK;O#3V}s5u~1_A~Ymg7ke8Rt{&NM^SXwgA{LZ{5MY_r7S78 zf|oe8C(RxSEFKUOt54AXUEz4P@+5xeUZ^yU4`P0US3$&r5q#Ed21vE}rotb{&+X&^ z6md(}vDuoIR~tJv8LD5ddCgv2&T$EP2?wf&JzdADztq3mNYy8Kb;7r6=`wc9?=STn zfehI9Jp|plad~s2^XGhbA_()5MRp@_>pg9dzPY}5l6Rp>>2yw4 z66ShqSG*ZF-%lNCGzDBd>^;2!d2kJOn+MtZJ)`D5TgdxZ_fPiT>&4mr?m30u@W9*G zVVh3~M76JRkaR-(n_i5BWj;Fjt>Bf%snE8NeeY)_0#s5wN%dFv&es_FnFjzP3tk$ z^mbD8){^>~4E;QJ{Y)cxcIG<|M{Ph+3S6IoZKM-joB*31SwOFcQ5IoDKS)&wua45I zp5ZV{SQX?Tv$&UcVPzavfm5HZo%*41=55s}0m%^?kJ}3l${K(R%P$tRZ;oGtDq!PP z3}n-c*wai{%jvLGuz3zsK@E{XBno?#;N??+Pon*uVYe;o6qO8c>m9B0^sX#Eed-q;Tn@*j}Vs0{9fS;^Q&t|kPA2fBY-Lf|uK3h%P>@;{c-E^ky@lHH!Rz9qCI6Sqsdc6)VAe1{Co6!Q7 z=!^%>C1)-)`hF(2H7#DAKuUK{UX{y@Lcm4lA_X+4AWoZh|4;DSi2Iq59lwwu8pg$q_ZCmcV$}4JBE9EQk_*| z+SZMVEp@v~u3_A*N){T)$y)C8*ePKCM^&LC>#bnUR`yyLpInZYvvoovy?F*9(dg#8 z6;Fg2F>wy?#=eIf-i9k6W2m93vvPNGn18uqGvV&3K*!*4(Rc683A@c!0w-j8Q?BZ= zg}Wce4&q)H{8pZz-CXW%$>9I*1U-X+q?kp`hzIc zx2Hk1OAIPajsHgLR}%h1KRKqSg?7No29V5**{#Jokjt*3b4J!()Ek$!g9$f>i&zU2 z+79N+VcFZlnHfCF7K=$6Gv{x~AoD>p*?C6kfcU5pGuirfbGBvV#avB=F*){6-q7p< zLzOB8N5>861vz!}lC#lmI&(DuN|+cT6m{=HjKu7V#2`U5!j`np#fRN|ii%*Ns`5H$ ziyGsZf#3DHz?DD$v*hE67=Qi|EW|Les@zs0Yy)0CfzC94-dh!2u^Wj6xrn(w`-aIF ze^i8KD1#jFLiKigkJ>}i=!-(3Z#9_Z%{H?tXvK^5k1g^7!ULt)1W)1>W|wOT4k%u+ zpuv?fBuRD=5P@^&I<@L@nB5S#sTpv6Bliz)rzgSsb?#N`ap+z9by9=J&R6VOp#Hhp_V@FWFuGgf^D_3~ ziYU6;uTbl94g^O9cjoR8afoP2@wDS0j4QkKzr#~uBw@t!>vdo9YM?@xC@T2BZ%*%V4NoTyh<&_HPPd~Xva_xS=%jfiw4>@4g8<_!$fM-k1Yzd z%y<%ylSJyB#`vRj_gFE+RK*Gyw=+kAA}71{*vl>$DfiRfLbaE3ACNwYm4Te!T6w1O zqu_F;T~*IPVsjkJD+rQz|MTPY9htU(Aa_fCIiRz&_e7z;q;8tlz}6EgP8R5k@^=$jK}mzR-5sE+9D`LzwG_|{tXij zvBJ1u&Ex6Vv95x$15WOzu5=tLWDTu=)i&j8^<~?yhr*BL8HfU7_oY+sH;&oE#JG^( zt|8#&vpU6xm|ubXPlbgu9>)s&ze;`a7#D%Y#?|L+qx&Di>dR-LxJRqvhvmc_r$ir! zzfMG1r%RrERUK7*5Uxw73V+q#i%=*be3gU z4@3l=&uoIOI_vpPO01e%77&NEX^?t~6Sd za5h!$3taSiVhY&D*7kW*IGn4nyrSK`gO4i(XRQ7YRT$bYSGp@7%)8EA^2~m#OxMeo zT^CLRW9;lsL`cm~-z%X95hZWLR;e@qI)fxF&>WeH>1323=!Gznd?@fSAhQ?P&j38h<$6f5B3Aoo+8wsgHgT^U;m<_j=`Ew$~MRm?l3!? zFa=84sL0(otMtDtBnIYvaShI?{TA}KjEQH*N0-CYiuiv41v!A2xZ2wVQ{cWr&``Iavs5?K7#Iix39mjiI=ZTKs`@___``yBoRo^(LY*dkR!{)GPuZY@>z)~5Sm z+F@qwFsjeLHB0v1tOpwYgqU=|on(fbfd)T#aPI`UCL`O;Jv6t{S!jh-WQCb@AkVmv zXYa{t9w~N;o0qMP6;T>HY6~q9Y&ZCKMkHjq*wV_h+u4&kM=ulNRy{gQKRjXj-x*2J zuWwH^`PV7XVK{8hDOt}rMb9)}IK&M?Z;#!-Dv~qK@VFHR*h3wva8k@TD^xovHm3l} zK?lQ`wM}h@(`*n+j)pdqCkoj{oZLkHOY``-04P_Ar=w3dP3af=18e1v<7^H2tAm+U zz{#+MRQ6+P-G{wY;SxGp0Es?1m*5KPsD|o)@DV_wa~5s0sJ(Tk4Hvfs12J%X$J?Br zr?&>zBYPtd!!bc(4nT+R#UQb8{Ji==ZqcT`Ufn>BizGde`ioXR1-WzaxI68d?Qx@_ zcsb*G!HnKs7Tu%d;y?jm4~blTb7G$-GLSLDoqAywg5DkuB9q;% z*_RN$Z5E^`l)oo|9VSw`ee0hm4(2W_C-jvIE1ZAqgC-zLIV;78&?{vb8A}wONHd=K zOEYqe6_O-8^%0@SDBoC97vL_-6g|{ugB;ysPTeW4k;nylVp3rLRFk8(+saedGt2tY zuWH&pDqXi;j;S=qtruW%arV)EB(8(z zJ;TtI8zH+1iGJUqEWo91@z#7htg7^dnH(YBdo%wJgNOq;VOI2<+8yy2>s$CQY~ju- zD21)|{Bno^xrS@y+P(JmFcQPDt-0wh_;roy{-ieQe*U{cv-}MJBf&%bi<|c4HU!`0g*Fb4)Q|rGy z*&_E}JV^RJ=ajzc56gP8Ap$i~0@w}hH%}jy zc#4#=AlOugG<9(OfZE?7!SNq0m=n4mG=LDPUT5+DzYWQ;3&=b zyLtbDmtqQ6zHzUv-Ujp;*RJ7ot#ad81;B@p;kkDe94r8M2wtRPoMX11c5wc@z`IC) z+_*@8T?~BPO>cYKA^S3VFIf$t{H~j%>Ou|%(t0O9-dO4?h6ERrW2ZS#d{Q9xzt@hY z2tzz-HNH(kE=Lm{n^C5OF-9^~;(-Z;u8nO4N*gWB47%r@+`^42XLE5|Ew2Te;Tj%8kShQv1k)`yN}{<5kI1p3c&GC zu!lQquP8(lKh^dC8`%uCR5pGQ z=>R5SsEoJE(>3Ba_ebg0L3X5h4!p6FLfUE?GFG&hIdeuO)A}oHY}4FA&It<@QY+7~ zHPNsZS+4mhrcX5GJ8{jDY=p`>;ES>sbuo%Rik}R+0X4p`C!2qrK;AVD^J_&$9fytd zTtsUc#BLy;5E0J_Qx>*^)lat0J0QoVJ62z&Po4e7UM0g+zLR^l30yuD_rA)e*{}Y6 zftP=ttkEb&eUdK-34DznIjwV`r$2q>{Q1B*vpW7dKm69TIoXN6>S=H5;XD&T?=dpy zM_z0xHSSY>n6f;|7;9PI8vRb2$o}RYjUlf9)WWfMNvXgu#Mf@L4=ZOdemuRsm9PhmbQe#ThX2Gu4k2HN%Rdu;4GKZip!YRiRCt z2a0F6)%1P|NN*4y0GKGsQ0^OqDs<@X-rpj9vxb>N9zR%ED6kGOvI=}mVni7%Ak9d- zP~UQ(LD*FFnV?okSq0ch5H$_$8`GK^@N>W)P|#Y0JPgOd8$@$_BMeVcbu&1G5ZyQ6 z2Zr1KnW^8(>brqb2(y+EzZg-1`Y^JIa^K->L6?UO{g;1WVLsx1XjqU{5#Z`7^XqZ^ zW3S=$+q|Q^(&W4ykUFTpN6=f>$pTT!&<6C*%|;q&VnK!O2p=R~U8)2sF2@k z-wurOQ@>Qm;bZ%S>mO=c8~H_!eB_HT%^q}z&48TULKqq^4VtDnXWRX%vl|m=OU#RS zl}jE=T*W&B$tg4aTN=rSJ79Z3Iz_!?G^HC9N`#LLgn=c$%S6{D@{{Rq2uwX6a+Wy! zk=A$jYUTs2%U2I_2Fi+M`>S*3C)RlZI2Zsd0200~0tUXe`|hY}?4ORd-%2nNUj|Oj z>1vimO-4a_0N`KGY`Y0#prU@ch!tl&0HUqoGs+v>KiStfIM;YLPcAWVJ2;(QM`czi z-p$3qoBM`@s88Xv(Q_R6;q=4wSOjfLZy0M=X>IcBZ@YNn4MBnGwk%4kqrHwB(Tn#8bNtWZ^M#3O**JmiFDZ5KQA~ zjKM!EGOoj+kN_-RQ&SPCEynI)CF3oRl8KQBGAB|}YtChfczZIrYmY>mIJ>2oy6@xb zjNXA7AUIF>cmWj^_bcsDS8*j6Rmy?s0-;kD#~Pt}+NpLmdDJ!j6mm-ao#R&`T}jDc z9O>0j)3SfHqR@Sf1Q$-u*92}n4|ojs2Z2BmKgfHf5?2+pc*5{?)Z(yjb;&=RXDzyS zINs<464;UE!|a-&brF1jJ%ahf@A&zUBw)-E3-f2vAExN1?1$Q6ype0+gDyHbgzHES zAcIW7%pT(V<1p_O{Z>a}+)`KiNr!FEjh)@ig)9UNwB&NcaiNN6QWtzp4cIvVuiV|tx1HlR*#Ff5Xo4TB2Sz!QW_U|El_&16R`qk$ilZ_xp4dCbQ`;r#g zGwF6xe9Jzud>ip1B*yB@Vb1!~TkFAy@P`od^Ec?h3VoKG7s-&?g|gpgPX=|ld@4kJ zA1VLE1^;&G*s3D@+A(k8g8SwhoUyv?S$Deur5u=(RcB@hYwijjHZLAzkU%j(uaxX6 zCf*zE+<^iPH@r=9U)uzd{ptbJWuc;fOye_JBr1pE5bFSWPv6%ij?;w7gfs03lx%AR zjJfFI^I7?!R7?++fUMIxGaG4H6LS}~%;<03h2?e;;6Bqh^HA*Ce z9zdDrlgEDf|7A=HVV9y-8oMaE5R{a=-}*B1vH6~w4sj_V1F%8dJhQx0EqYmfzRmdf z<0UV-4f=qjPq`62c)x0{Wl`CHb-IIfMwhnogXrh`T&9$8&NdRPXya}tM~JBOhNKGf zS2py~S}Ij;A~meQ$>ahx!{&t{6zR9MR?1c;rIOKvLuCQ~vMH>bD$oOY8?{bWyL|8x)lX@>1wXJrsHteTJ#TKw~Hh|2(ykWWv${ z@=V|y!cn;;0%1GwGT_kU)-I%dAUF&}GM}cOl+3o=(ampNR=+iH2;jzGy(za*OW?T+ z1C&0fa*PC5o)4hL(Y=xDa?t;jk)Z~`>NXw-jWORS%@oBx~~bq*m(B|i?J zp#CoaZ$Oa00Q`^3g}%^z?vXeNYgV=e4g?K`p`kWesG8Q0t}jA1oehvrCb_!Rz zR8XD*7-Cas@2yHwA=#K=3axKKZ z%)Vwq?g!cDt;R5kR4!NDWPilb@(TFe`2YU-PvHHq98({BGgS@%kAmd=$2 z{sN3p4w@gqUto-20Gd%}#RIQQF(oo7;t7o!S7NY|zligcq#Jd%w&jxA9h!Fw5s;T8 z0YQqp$^-DKz!%hfw3O4hECKD8I~5b800}o+v82fX;v^!f!(#pdwH)xA-~c31gnYyp zq&S=!nJTqIX(cTKBK{N;HpEdN6+IS(UU@*BK!>0yS=G{y>In4X(uxQE`U`wA#P#{F6gFQfcmgH9`08_+Q#riAO z?L&srUN1>WhaxT~P|l1YgI0#zWm!3$lC+5o!mxTEbzZ4vDK{ZjDJ6%cCJ+G_O-0j9 zN$!#zi4(ISovto zZ0&!l^u*6Tb=F388@-|6mBp&%0V5MX&ul5>2~I6VmG}1yTnWCQS(v0`afFcD$H*M< z!Kyt5^Fr-d20E>+mZ=JhT_#bgDP?^a3Gf|_5Ct%lcTY`pwvz&(XmZ;|EKRlZ666qTt;Jv9Cz>S5E5u=gpt zS_1z7dGxZ@-fU`vjp&WsKaEDZZnoT!n2iV|uIU$Z#dNDSHH@YcLQzJn`%#WxmBd<} zUnvcCnO%v_Jp->Z@Ml9?QT3`^!?PC*{BKQK0*(kq3WGp;O2JUuHQ5+X8qcIQfGNGY z0?q2A7DC3w7UW|zRXjYV$VY)-^ea*Z-(p`#ZZj9Tc|Uyq&#TM`a*c1wmJmO}i<&sZ zB<~-+5lxzSPPK$T7mgh;qzDGn)IK02fRI93TQtO(-S;+l7l1tCtpc{9jeCH$qT0p5 zEdaIfR!rg@BU}Kk)VTLjYSy%zA`YVm0ZcMZu`9&X>SOcwx8>~1Z&8FO_E*48sAdh- z+vaEz2_zxd#e@VlS>3>{cMWP*C)mkZfGKFkpq}bQu^C1}W9GY^^-x)5YjkvK&&;tG z%FXr2OokfUVU}6^0fDjgK8rcFp4gTedyz1DOYBt{bjdO2x4E4STJBfc;?Mp;7I zL8Y2YFf-K|ReV)xD=v^RLP|YHoJ|R86!n+lJVHWLrEe6C?}Uu3ia}?SZjBRjFo9D6 zInZUSRQsw^LUbo#Al~TODPl2+1WFfuFsMiqcNsQmjij8#7C0f|#CwhdH6kY9v|)ZQ z#C+O#rcPHvhU&GLCTHZYJ?C_79djJ(II`GegX8c2L|DB&tw#AN+ubStP(n+TzX>{Cje@pDQ%s39i z6`NIvEv?K}6U_!P&jEql7+B&IMl6QnT)re$XdusF3^OiPU&3nJE})hxqvBTP+hBDx z!>)#9Xo0z!DJ-#!=|+CS)e(62r~TcZjbjiKWIlpow4$Tv9MLJ*)X;#*;a*V85m-{= zlU)}_ImX4pNIg)h=fF`mqQ)~YKw~Hl#8j%=K~Ee@Y}(3v7)tA+NGogv;~Z!Pqy&Lj!$@9n z2T;T*Rs-%JeMevN}moF!vgg^`lRDD*vF%!PJXM$%85^nH| z>Ky^YFkxeTqU6LV_A2H=sx;MZ@KvLNX3PuA^aaC^Nb|RBVlcHPPf#_oS9$Mn=TtAX zK0|{czC;0dzq8AfI2RlxoA3ciqCCsaYhs~)hBz}|fgv>`iXBs=L@d%yVkF_l^f!VW zUBIaz7o;%IR5lz?631$_BxhQBBc@9(vKc~5(OOIxr`o^5pff4oWtmnBEC-mG<%>pA zyz2!L{ri4pNi<+;G@#-Q=pFAHbYO&abvX-0n1YJ9ZQKcBL@^1lAlfOw(Z3|z=@UtxF@!P+0`UG zH~I`!7i1t|2@G4RW2Q?+PX!ZBCXnkfq--IC`h2TaDfJJ5%78b4kr=-m)^cV0G4w}IdSOU#q2mA#tAssQ$q~buF)e@l4u7s0xbr$wZ zIY*QqS9+%ueV|34OEiq;eYz7x+s^)n~_XH)#1_y612 z+;H(f5AyR2urUflr4`hN(N`zC-S&&EM%}y4`b+HyPz=ctY+4c#(6`y9wn?v{!ce{k zu{5j7QcZ^FIih*b<}{dOQwNhYPI?E&`~8#sW#9pQSaudAIZSmDd!{5SKczA3j@+TzO`4r6-`6-Q1iB5Sy@UTJ14PAv0ByB&}4m zcLn}dnCtZ5Q(8jPkXb=<#8GS0P)nn}J&{X(qRL|il6W;^;8|dgCJikCmD|wH+x^4S z{z>oP&71xH!QLy6#dtVG+iPo<+<=L|`a<*yVbGc+T!?8|TZ_|#@jWsT0PgB)EnBkK6ZCSJ*vF%$l3~K9Ta68wd z-Gg@rAKv%xdfv1xtg?}s(QoS^AEOXKhFXWU|n3JO;YnmMK82j^cMYSM#?mzY_Nryu*p}qxSTp*8vSj6 zLNt=z(KrwU!>59|i4*L~ji;}AI~Jcm^kmlbs;RV2mD63_;W_hdsJU{=Q>P}?ikWnb z*sZ6#%o-|QeW6LO=-UGGUhZ~b>h+=h#1^6QpqmO>6abd^0(A`&aYQAV6&goPHjVsD zey_Lxt|WK`{=a~jH;05a6$`!nNnj5C?`(Cu#rU6%%`J!jAL3^oj?X}#RAWJZtn7{{ ztS^~CW=LaInGW|6C;pwiz2jHjg0j5$wWrh!AwyYJw)||}6pvk9wb;48{i6M%5{L!AG+F8Jo1$*t0?V(<^}M6(1r4xOS$xljVqT<9!_eP4u>K5L3drCb@>Vf z4Y{DQyRUv&md3IF_+M;A!xK&uq|XbIb2LBWsWmS8H{gx}xY=R^nQ5c|IGKnF_0~`t%Jw{933YDp;*s7ijmETt3lW~WligY3c>kZ}kNQn2uA1kl?WYAPTRq`2t3@oG8s0Gf>r3qvepy-@T zS3v+dj3FNb=_njxf4ZW)8ipac0GOqQ9Aw;G3!Om|%th-lqH+NdRsX%T!*T%LimX(*x5sgaeK}#OYQ5NU zDEe1DeEpZKLjP{+)qkqFXaK!{XdC>yK?l$;5|BDCkqD3>I7TW{V)Hc=>$f9ey`Tjd zHJI~061`Su6Pp)1zSrTVSIJBv-iK8LWlO))Y^qhskq&biiZhWo) ziNfhhAdmG^6;6EB;ta^^bSkIh^b{ffAUae_X zadqW_Dchh##(ty?%T(hboGM=tK1LIeL>!0O>jX0}NP-dKK#xp+C~-0qh^-}NY*$Xl zG34l-=*{+QPMfYx1*=`g5nqvhHcX%b9HAg>Qw|qLS?6VFu=Hh8yvQc)&D7C8k-m?e zR(GofI_>na72xswKN+ zqw=7w;n}0^){k0NzTMD!EoFy?aFRynriOhPu9%bRyXqBqkJwn!ojTNk{83MJT^=!1 zmk&6YByy5ijC_SkhC~cmGegEH#1YpE@?-je@e+X?Oa#wa?+Ua+p)dHv-wTBYXe5e8 zfjm3v=?lq)blhPGz%U-eD48IN{j3_h7C22?oco>f#0nrABL)VDUmz4^E1agEmkeA8 zNr8|CNJkOrKudo>v#Q6+Q; zJ`6)6%eAjg%5P=T0p-*ouR9Pmb_6wbH#g@|fpW;vKE7?~2W-=bhK6F)(#t>C25rS( z_s|eW2;_D3a==U{MVQ1SLJ^n9(I)y`^Ja+wrFHuwfgv7Z6v*#eje-F{P?b7!ZL64U zTXM@Izno@OL_UzBOnhq&bdn4P%w&~x2uIn_?PxV! z<&kj$T+RHi@F*e_1+8?}@?La+sBk-^x&oxH-DPnmnr2zR-icxi(k*d-?82pkLY@fH zI<7$t5{)iEU`+#C7p>UH_C@MW;m6|D#}o}iJQ~-LajyQDqNXZ+jTXcSXZFmfTV|TF zV6ozCg~~N2c#&!oe2va(t~fz#yc1)E$Y#$>_CXVxFRsy`LYrA7KB9btvU4OJ+D)|5 zCR#j{I#$w}AU-C+E1#0+|B^v|47#@KkDxh1k)S2i6jiY64oKyUt{972lNp^K+g~oS zyf^G<5#=K~F_9M|3dQk2`cEohWSctoAexZq1o2kG`sl6rrYAo+QSSEJprabmsa%wl zR@_?EMD>Z2wHoq)?ydZ2N0!JAib}HOB$X_8Ou|rVQgb!Spy9mtr1TS3EP4WCN{@Ej<>uT3k_C%gVTk6O<2SHcsSj;_u;!rNQLrfUvgihsE1KKusyx%*L z5ojEa?B_ZjP`(n!VRes^EYbV$?p?ok^zLBySNZyxS+)R=5EBeb{jv?x+V&3K?w_1W zPQuV$`u4-gzJ7J0>11apFE*H#sG-$>`jwc9m;^f!$2-;|kYcAjX~sQCJ;x7}v{R4w zWk?4mn&JUy0#0HQlF{^MG~EVSh;0j#HE{qJq>w7b#!|ICYdovTPU|&WI8`K8Q z#Eg4qNMCI53|K-Kc>4lw{-r+23P_#Rwz+^ReT*cQ+6LYs(JPPs>QOXA)Ov78)bgX{ zPHRi4KeQI&ROXYzgbLb|*Aj~fOBkwH98+#r6gyOUBVEnj;fYp>DF9z&6#`-(FFfN4 zU9)Buo3|E3tQnJ_so$+VlYcbTW2uS@B9`t|-K(7}0zTy~48R~z+E9yKS}Ye`ate3T z;zm)Ht4(aa-9>6-X^jWDPbzDL1wIJrD4D1OimYOXju!h6Mdz3jc@Z->*H?O&O7^4k z8Fh$#FWnX=%Ckv*^OJu1UY&OGszdQfa}x1p06A<3l9b;nWuIguYmN` z`_d535RXhkjwwl`+C4@-9!~9y4Eb2oG^Sq1mdN-mc`HqNa5hIaRevgGGkP83k%k>$ zGS%gpa2t4??)rw8e>~U*e>LSa0*)A$IKuqiwSHeWlbAspZR|`*Ug8cCH>LR^k7LL{ zg@i1pFhnM_p=s75mh(_tX;dPp6XYk7JBgJ=m%NOZ6oZK+E|Nuc$yE8Y6URGrLTHZ? z89SwKOG&k>zuVRQuGJOT+EIdY!+a%5*eNjLeTd}5Mkw&+PgvULhLq+NLssH+_J>;J zrr+%30H0F+G4QukPk0@Xmm|(K6|~h%7coo7$<`X_Q$u^Ykw{u+eU}C;Gtz zO(YWAc#HMcR{hI;&?%SS>~!B_O^z8o{ml$CuP86Bp7{GX*HczQDj%4#=hH-|6(ma2 zrvi+UXoras+n{YeI4%?fC`9MdE)`RJjzcs;Odf*uA358hAn*Nm0@2)zCK-|+AH^x? z0LqTI4VGAY$>eX;X=Y{jSnT(?&X1vUO{VG(m0Z?zR0WReIzC7_}D0{RGD*#Lunn;fp^Lh*|K)jlI*4!fDdCR?X(!_0jhJA^|)@% z(y3BsJo!#;69ZJ`uq1;<94McC8I^?@!z4;^^6pPZzZ}}1C|R$MW=JV!fIr4zU_vC) zZ+|{`_b&gbntF{ACM3W^EOWJ5>6FAU|F8`#mbKn45htS`7EvK!8hN% zUTLLO{-V=tXq#F@E@bp-mbK1)`;S*(v(@P+xh>ANX*Gt@mPD_Q_fL*K9PjQgWz8H! z>f;y6+_l6~Z_U@eH*ypA_K)8m9p=b7Hx>C+-ZeQaU<6>u5se_n=UEE5^o#V#@wMwC>l0k41Ohi2jJKi7x81Bm-wcI)q{t96mT#IKW)EKX-lVndNV8UenNN{KR&)xWva6eH zyvi>iXJ3e+e+C5)v;0R^(#hJ}+3g>F+&?}(*xN5Oq*+D5a{o>o)WGu2?mM<3LqO>O zbE$_{`2?@sgEvRN9PS^N+cs$#VoF97PLv0ZNgVz5VDI`Zo$Q~!J=nWuOCN67(g)qr z3& zArVS5o2ID3Qp8EYZ|7o9Gbc#{Q}$SM8|_DxR+B`Uc;&H1YcW!^xoWa8Q#2$hQwwGw z#Ahg+n(XDO9_9LmG^Aq6LYs}Ob$^K9KE$z;oli=3s=aJ$=yb|e#wp}zG;OHAPxPN+ zrz%u_Or&?&2l=`I0JZ-%;5<{qTiSZ<>0&GFi)AP9@{^{+>}hX=~lCiAU(1`Y{NU09{$tIyeT zbcal4%xiVVxD2EnQjt)(LuOT{R2mk2ACx}h?v{q8MpgOZB7NR^&zded4kWO_p zj{VZez_${30g0?S#*ACe>}5r%rVE8cyo%_@WnJS^C|?6luXL?Hyv6uU{XpR1>*i0zQ|?8$>BNZ9WmF8g7znixZuwM9e}1 zfWd&~RwFa$GkIxxNFoFdcTP99)(&@0w>HuYM-k*3TbElKZ`I$$(H+0pebMb~He-rZ ziYl2wE}TyYKjG2%#D^g|==sneBVAN^Txrw=aID@5ZlRPUG7FLTtdTk9$<#_|SuuE( zThQe24iZ1JYY7SD3)C0ki9ilfAjFg=A@dE2d>FGN4LI;2rBh&(tyZ*ft1&K&qc}mL z&8*kiNO;!eN*S74qjz0Na!KS%>R^qqoBO-iXsLW16$vN?WZHl%Iwe9E`hfM{AV{;? z%VUkYU_pEksCYPELvs{>0Ks4tKpD}J6_EGQD?duv%$}}r8+10A>4jWAeLX#ZTxX{f zE$WKoy2zfHr+NtTw`1{Q55XYB5!wd%dQH@yk=GRIWPn3eAq>F~;*iaZbcDh?0)y8ugprRZIOwV4l#m?*{qUe?JILi)C{!u&l$cck z_GIZ;0l(bVR6$(`N(RmlPr27Qrqa_}%!)*c#ul1lpmPk<;Ih|mdp#r*QYxE2*gf83 zEwMIQDc1{ORAecNMT!OtpN~bU8a$*-j}&p1|Q71rid_s-~3)HhaToajE=2SA0iNBCTBa~NnHljR8~ zWV;R+Ys3?t>|y4Ub3{vGO@>~*Km8zeva*CynnnRQ*nQsvLkcBom!#Q@l%k)aI8=!O z(_8WsZmU?DK|B;~ii3zWz*mrd8YYy6#fa0n)Wxb1Xtl-|Md0_eqTj{p>c$k;&VMCd z%L-8dw5sPB_CXuY|7S#Y;6z^i(5RVsA(}jG6YCeGJ&y<`qQa! zaU`}7LrQ)rD>F!BRN^2}9_I=G$z+g`KW!;jW|NNeJ&f5xVW9Jad4Q9uJ~9nmT9p-gk8FPa>L z6){Fu&wYOCKXAX(Jn$rH<~y%i)dQ&Qe0Qtc+1OB>--)JH=M{lVIiqj4GLFP)-eJvz zvzXP(m}v9zZr&U#_NH=qmOSy?ASEDk%4W)RD|DHXSQj)J!Qv(a+Gp=0q5<}KRV~`7 z?_8CbTc4ONR8^0BG3WCeuJ^o^HdrT5sS36d7ytux#J-Y!3kPQRY;A6?Z#^_69`2kj zuvH?+ztBceS#RkgeH2A+)yrJPovrm3a;H`Go-Mr3h2Sz>7k}khJGT|Ua5SQ5lwERWcx6O59_mn0ruzI5hB!!ltW*y02(5yUT&#i#3gq3! zdZon|x~iS5g>7A|x=(x>W0UPCJuz4IN9M>7bwFQQqcVBJtcDNtp?({9o6IY>sW7~d zSLy~NPVs1jicQIg42Dp82b`=(-2)|iDwXT`1Pr!dvCrdl*{!WEUtGXvw$KLg7`Hm(3-Y6;bi;S)j; zK;k1TFYc59I{KiqEC;p%@Gw11X@jJm5(cnJoiCuRSjw@!xdu;UhK%$!Zgp|Cw1zWv zG#gCi?Q#*~LoBbr(y&kVI6Y-n2!(XAMag$)1pTQt*xc=$%j?Bo_ulA~52wB3qyPR@ zUBWKGJVhZE(q6E)^yVFzV_)qJLbDK9u~_fm6jAV8GvrwV<1r2tqQ#zFrsAy??fUvt#}RYtKHrzp8sI({6LQ9Y#`StDpg`{pRxt-+ z>;0Z;AWnDxJAZmjGk?4{s_7mHK!7CUOYw#;c4`Xnm(LL&uZsP1hPnB_8Fp7eIB2T0 zB&BYMPvk`DO5RHgpm>_I4~ebshr~Yv+66qOWMJfbo?ift{WGJaf?=yMh=;r4(;g** zyvcM|Y&}Us@)(^CL|?aJ60qa~Vp3-w+erV+jxcm{siIX*l%e7h}uaMhi|kfQG! z(oyJjJXBsn)qPQ^_4E=aiZ@$x1vK1!6l=fq`;73SkoY<6?^3R^u6XPjoshbeTrY4`q7P z^lt9S1oJcj6B&X55?rEp+;}!OVZd6PgyCAZ-RZ73M2FuH`v0@{?q6*rS)Tas`YYmy zsm1P*#KQ*L+U+@u0lTyXSOd6v`dgnkvDF4#^l631d0NW6FmJO8G9k5*2}rFZL`v8HVFo% zCyq1#uc2{yEVb&CzX7`DQij8gvSdK|TIPWJWXtrt5yqFElyNR^?VuFq-`RZ%NnaNb z-i^MeVV1QoEMj!KhUN2W+ZRFcteedg^;rg+$0#C zj+0`M5K>tyywZ;u17?e+iP??JT@2KE4`z#r@6;TA5iv$yWjRL?wtZIA0ViPZotMT{is?C#vtVjg^39(zuso zQ)@@NjTev?kn^F|?b@W5_dRpz2J_gLU@lkrB+KWhT6I@bG6fN!9wfLly;%}0!wv@d zhEAetUF9hT7aE|Rr4x#44QAzLmC$k3NS#vZ_66iKF~modXy5cN#h3borJw8(?msfZ z6UZHgkO9QaZP)daWkLs{xRQEJ;&w#*yOxNKQ~Rmd!GA9&7-My4^g;jX+NA<9>> zax1_kj^8$u$wG{7d-7!q($ab z4>b&vE!=Ve_!Ewi^$UhF>SbQfI%Yh(WMEu1gw2L^?nR~-^TkfJ^_$fF$iCq0#5AV$ zJejFxsyT`C^K!s^?FMp1wp&fOi5vrKZ<^-R^)EkweZoPn7AXXSZP5bbf^G3P2ixxL zp2w}wfPyt%Bc3~Z3kbsu3cs+_A2fAeU{ye z3GfU0t*OjMX8##yNkXx~1%DZyW0qQeTqcz&(r3`3a^9}UOZc{7Z&`C?f(bvN9UxZc(l>;W?gq6vb6;Mi=} zv}>C%)50^CK-552;JP0C1bzXPtB4WVy~h{OidDN*>_j=T&_!A2#b% zayyID&{2i*5*z7w0^UBP#V?$dqB5)pb2F@YWbWkGNY~YUE#m|b?@2KPVFj218&n;v zR_e7#Zjeb*aZW2XCQ@-^i9iR@uhZ?DU50~?FldC79PV0zi7FxO>yjjcc1aj(Kf^Up za%=GHoQMVA_)2F#Auf^bNt00FlFY|iv=P9nz0(902Jt*&*x$79&L=wXiQ3b3?0s;(G6;Oj4QdTTTB6j?k%I!2(6>b&&`CF-I^53M=Kc(=1hOL3TC5Cve!tG;CIkGAz`O!?%oVeRDS$S<_^~ZY_`Q zAf=1seU?WWMpvdQI!81Oj8h`Y(xN0ia4_1Q1rQ9N0Mfw91%k&6N7wv6d=B+DPGEpK zdg9=ki%o66nUM&tHh31Wy2Lu_;@QPu9f6A4Pk9bV)UptgGCB)wVQ?kLG6-d%bioLH ze3|j|e%wv1Eze>Yi5KQu_+YUtNc}Gc!@Jjz4n}GHvcthmVVQi1x!7AQ2EMqG& zQ7v$SwgR6!^{`NR3~rs)$mk_#E1dk3s(X-Atp3x9Yec@s_bF_Wx0*u(1mu)SG8h6w zkuA;+bP)V0+eD~sQDO~sXmv5mwBm|7U}B|80n-k2MW)lEvCJd9VQvu$ExGQafxyqr zVC1in0{+c@2*(IGe}W5m*3A~Z7D>f;u%AGcC)`H?+W&_vA1fyXc7vX@e5*T^V%{~F zF=N{b8JZByCZwX)#1O62iXsV_S&W1Z8!N7RoF1fM27{7P5LCPbRIx4Io(E*!_yDgj z&0BA-0FJwhIY#p=1_`<4M0kC6d0VJJQw~$?$lUm{rWa?6J#{AHZb?Mt57?4{voqrmjjPS6|x;H@`Y! zfpI8HZ4q#Sk8A`Wn?+yp9IUCqPK6pmW#O0FTgzdyQ~enohF+Ef_RB$ke;c{s)be2! z>oVb9^%m73l9z3WvNt6e(N?#nar#J$ge4DxlDn-I;3^TaUG3C6bp_0o5X#iPfd(^5H+s3i zp2!H|qZJ8SG!wriB*b1^5f^ihtJVke(MF!ss8!r!)UjWSZ>~ zGYUna<_oyA%q8Hrirh@rF_<)DY=*;YlkRbnlgQKd|Dy7&Lu#-BRc?`z6xfNkV)NW7 z9QF=$D^B46Ikvk;C07sa-&oGuW=fX6PzG~BObEQr*H5X%y-JdGQ>kdKlJ>&Fzd}F8 zSF)BMD)88MmHmKhL4bbM3Q&x(!V#R|%wd`+zxa7r z+RYS@pU_hR18XxpWWWx}a^v;c6~2g5F(9_+MmNspe(ZIT0WFlyQWvpVEb~;n+I@>} zRh|PlNTk@x2YF87J0)zSOPq=s1byO+13te8hmOFQ$bS8K_OLSlx5&m6@NDjB10Gyr zNB2Cn$&C@4Z^SVCv>XC#c85jbk}2R7l9qs54M1f`L@H67z{S~nA4ZdbfZT)t$r^yE z1w~P7_MI?=IY}CMD*6*irqPa(1uf3zf`DYnNrStA40LB^s@pJ0x@mxCN`=oMG13=5 zZ6~96A?I#BZg<2sT)fc?FD@Ou& zL}EL=ZKG%AU$02?Pk-s*Cy=#+bO5x0T-g`)TzfX=o`(XE`TS z;;C95xV6V~xEIi;dXU3FB=s2B%<3ru2<&DtG zpdh%AH;R!Cev7&^9=QU2tD=%8ZvSLf8@ z^J+6jzky}ywwo9Vt z5uRcTZjcu~umY$cJpjxSPfy7)QIqk&W;zHqDln26M!X6RTw zjKEK2`gPNY!+}gk!VT9+*`8LOLx3|sum-6%W)^E+Loj7)NTEPKwP$&0-)F|?*oB8@ z^b9wux4Un5sczrwkfQjCTq=>XihiN zRD#pw^u=wQMqPrmBVQV?3UlBRF4WO*P}V#67862?f^4mE>QV;&3i^ljH^GmV-T&mv zAcwce@vTZD!KKJF0CrC5zZV}*huyt*dwVY{T?v_^7YqK_)qLb0*4KMZPeth6e$GOXYYh8kEdoK03OV?7K0(%RSl~TlgY($er-Ni(f8Rxdzl%2<#WzM z2=H?dVHQp!PE#n~xltEx|Z=C?i(x zoPJ*Pk6uZ8;i4~mA0q42vTb(2ZMv#PY78z2ay-s*wjeHYrO_2-!i@A>BYU@Rp%?D< zc7nP&MvCHmhy_R_>KvcEf!sEoy$z$d4GU#ExIe2HzLd$mTxpTzB2{-P_mijkwRb$N z9$$F*>TlZH`i<%;{GD$DeTBa({}0qz{N^Re-=(>Yq`6iFS8gJcWF*J8f&qQwBraA$ zXUWpK>{_qdl_l_?R4*hzXz~Tdm0l;|av6bHV0+>i+elk; zywGZFcINvySJS1;K_*2t0lX_Q;wnWi_+`$3G8z3Z`;}OUog9PAztUtyp4kxKV*1@{ z1?b;gD(kjosm(w&t~qPtm;Myz>ZAEk46|r|m_-)4l`i1;^PG^&!xldJ@tO+3i)1??KLk!eN<FEds&D`C30n1xFkvr9k_ikul5I4?Ya_YG^xM%=_h>X=CR#@D%(Wq#G_l& zQmed65-4M+5E_VcSEHOSc{Ci9(IwEOaxg?UXw;Eff)M^H?==-WeK1?aV5sXgNQO7`PcVy@ z;KY!?DQUmV%M<&qOa}RP5}pO(Qv@dk+c(1aMM(K&WpG{Q+1!LY{b^-#Q<@% z<7Jxm)2lyP?iwWaS#s15`fbw6{ZQcmLh|S zEcc@o9c2{OF4HL-_E5;-D-?2S)hDHvkW){<+~3-9)(b<~G445P_uFQIbo>ISCg_?g zL%ZMpKc0`aivFejAK1!D=S~PBU~qIU@{Hp6S+R699z#}P(%fZ*C;&m3B1zH3m3^8? z69My*fne^ncd4Pn7rW>vf`ky(c?K@MSgXWZMLeM}Z|L9eRYJXM?hrNDF!EmS$hrI_ zOS|e8({nR#-Fr2fW!bHfhUEM*OO!v#AJ8KQM4RO(RfQOwX#W-;#bbzf!$(+#+z>O; z@VM+>ix&_ZZ($9B z6faoW;6!6@L!Rnt|HCSKar?)+m%sP_uhIWz`L7*-qd#524nN?I*knY?A7P(wK0e&* zCGM2PV5CN*HqZlXUY|)tsBd7C1_=R z_x8=Zat;cs%bUcxx|d1PYnh)P=vQsd19AuDR1NSsM`?4Hhrx5zZi4*$hO1Ji^Ye|S zMVLSbHDG7&TllZD_Z1n_mS~dy7|(`ECL>Bwn0E6lD@3YY$MbR^yV^h(ab_iC3T6_7 zzG?NqpVDMiFV=DXXY=(Ui+mtgkbqR8Ac&ZHWrX%is>3Fa9%j+8`>@VcSHmJ^iaK2PWZ5j!N^&ch2Y|$&<&dJ8KRCCiS8K*r-z!fKaTS({eRi0 zL!qdiRRq8vT)IbZH6G(P{=ODqCzY)h z%1jlg5aYR=u1`z&m~#>b9JBvSt`?b&3&6-||I^=&PcN@Oo?hHso(@0#2j031ISpR@ zczSs?_;f*Uqj;)x5fnHXd_29nruR#&PN;~3XkR7?o7dX^^ta*Xv$LDwr?bKFpZ}xn znS`>`h^tWTi7YV$Z z5BCBl-#~T3Ly^lL@%Khw0i;a%Q+ybFKbe@mM%WUfV0xJzLr>z^l(%NuF94s|h);Bl zdRbL|JU#;eBQjfb5!oB-qkukbb^*52Ls}J4sQDr>Tfh#MeA%k=ZiPDEFDDa~3myMr zwV2}>;14qYkmep^kW>t5R1mY9G8Hgzp2MH8k* z;$O|ftN0hZAK6)Joh%GrX(1*_cCYr&;b|Bc=xM@L6T{_8wj zct4A=|I185?`J%o`)|#V1@-?qTY^bZ`?lXw!>vwjrjofhJ{y#q(CwNPMKTBv47KZz z?Z4GEr(+t?DaMPAVYp-1>=@?gjNJ;2SbwkTw$+1?Um18uz(_d!zOK%mSHoc9Ltz+FmIh=8Mwck}g?flJqyeUk zuVfsu>>9!O@U>9sI5UUO@buiO(blmPG@@$FK=PlpD%J1n1Mtt#c|NX8H+%~>w`x^o z=F|ub;^g8A9dq0ykkG%>~w@ zT1pb)zA0mml(65-x3MlA7i+cyfp-vIJmpuF1@Dj-OKbl4$n?^kOkyz4V0**+o198B zAH((xCOAmWNtlfDbcIj7&480ru$+ZrS3nMu&>bH+ANNNWchJA+4D|PM51gh&zS^J* zd{VH|#d1kKnYN^dZ z2WdxptzfQsDqL_K=i-3KM{Qs{KKE&Gk@74Ej(|;bTKjC2WTTzAjME*3l1^Qz?l_Nj zNZ5|kX>Xk6D$}N(8|oH%se3B`WI&t0IZs}$KNo{buZ&<&N_~f8L|>F5_3B4>13Gna zdPMB!Yy>PZCC_kFONz^TTYS~Nr^mTm1TwE+_X@Pl?d|H|>q`Yqh3o?e(O|dKb#-Ok*NFZi+}pt zp6gOyN?#|>*Ol#d?cSb$U58+gUwYkQcN$+R^N*~MoLgQOSpiuCR7$o*^AUhIdv3@a z20-GtYjvWrwjM`KUfsrvA60H-b~8ape3!JTFST9R_R}z;^kXh3GL<5ddN#^r9uZQ4 zYyFm(QW-|HILQ8)-T%445;o$(+y^2wj1{h^o>a42N7ka z;jABLBxAf_5T>z}R>II6{DgeNN;vOyAu~nF+Ui_Iatya8ZLZqj>lmkaR{~smaLorx z4<}aw?FOQ@m~Z>7hVC}QU6WkWuk-WenScwSc=7S{`lUk)*KWB4w`JdtIAj2y)MdHk zHz7axx6G4&HT8$ZZK1kLF~eUqT!upqg9|aT>+Zv*%Jw6L4bwg5GW^8xUO5Y^J-yVB z{%ewTgz8Ou9kGyF-)A}bUyh`X$C$5{Y%l+)0njQRq?7D7J^;jJX#nbY$1o{fx_2{- zniol>Ntc@4_wAE3E1V&nmH)_yiZGt9FTTJ>Uto{@5~+0c1vUuq2q$P#jGoAfS&-kk zi07(9XtAkc2Xb2B{$;c@;&o%aZnW#h+l_cl2k{^FM$HS*Zsl^U(5-x334mU1C~z#t zGu0(mMOVfv{Z;eri8JkyxkmldgU&Et+GclL13N)e9A%nM@8EQ_~Db6(d=&0L>bP%@7d{bFW!-gw%eTsbVHEJBgDwu_8(|2G;;X8qnC_4e9mzTqif; zPZR=2lqg1Q0U@n$45n$eOc9Cs&|G7DL}sN8>$s=nC4~i&7%c7%pDRf%WIrN4DG@vv znwRNqn%$>jJk7J^Ld21-I;3(6N_`tIK4bagkEZzGgzk$ArShvrT%w`xlQdKg=*SCg zwAM_FHKXtwz!pFwIo=y_a9+PMU00`~%ZMVBd%KIMDFxR<)iQX5%0+-B=@P1h6_Yal8sWY+kw?QMgHw(bd6I4+niF1_jMDA~)icTw|GpVZ-#} z1S;s;cTmzgX`^P>_b#-Hu;p2Ke6zc!6b3Y7zZbv@+hy=Vb9Is7G{#g*hS)5g&XSdg zVm;25Ie7b0LOE(Ph4J5UST!NxEiyGo2%aB^f*Z#7=8*J^Jm42nKVV(w>r+ol*7=>8 zR`?EOrDNXxH9MM(RyNeh$fJac?u%ztYMkEBvaa$1{p|C8g>`sKQ3rX@8~+RA#_H!V z66ix0`-At94Q64Rz%mmTD-TsSwOIl~0mb((d3AUIrNoOpu~IUBDMkwOUY^VGrz;U< z)L)4EVrgiVOuBTG1bo0js2&zEyJ!82c)NcPaAL~;NbI^Ww^w8e=Q&C~gw~JA){raD z(eXvDrZSHbv!kK?CeFR;MI;OL(hU_PW1>uiJVcPl2Io^77Hk?PgzJ)m8__a{M2|mb zBYW$ipcO}WfQHx(Cn8Yh64TYQtbp%4hxT9wl&tAmNY};wOblmS?MTk!C+FL^pAdGdq2f$p{`198s@J%;Z(*ob(!~tDclMAi;#-Hq!1_a9WYj;l9xF$f{at<)q z=7I#Dg>D3={-7_|EhbXybd=(!}4jG%&A39?^n=3|>BgS*O zVKg{lVwgo$uPr3qg_d}U*uk}&BGH|f`8V2o%HHM`fAjA@zkh#rdUJVtcJyc8 z>D+TWADj(7pO?JOVVm~a(M30OddEKLCGzB#?iAA}w%q5dQ+j>H?;mke7d{G1a|TPF zN%ki9b65r&q3C0d9Wy`1C=ImyC15e_c^zi!8~eE6~`~Bhz^0$rgA;335W)xu}E0aeudO zbyqGHeFrZ{T`&as9@!9j0ofkLS*nX1oZ`ct;f~=CuAuy}D{eO%bP%1(hpSt4@A};T z81-xPBSbpi`31Ad^CxCCe@?Y5Vm*O`Zhdi*U1SBzfGuy1WRb-isyhBtxo`6FM`{rJ znr4yO*@8#{8WgC&JSdOSI%J}64O{r;CN%{v)P*jHN*F@0?!45`HvJw6_HbmJrgtSm zPYV*DYBmlP!18#iGy-Bj?v+*Apv+FtL~L!~OqI@A8Si_TMJKV&m!MF7zl^4kybi-p z`J-}yK-43JL=7EVYUA1B;Dp#>4R_n7ruSR z=~4LRS7_UY1@qu_e zZ+DK9%u+GU>wRMI;dxy!>ar)hrwsZ$KM{vhJHTgV3~*dZA#o!C)JG?qb54qr&Ps)SNru z83r>WOAGZ-@ZNJp&yVpjchy9H#H;qCUD=VP3@bKXY-qZx}-49#$-84~M($&!mBU+vfX zuMfOW))`)~b8-JkLJ-UA^0lJs)?QY?l)R^J)oLN*+;k?nDxf?NDiN$f0Ats#&#o*L zxh!TXHTPPZ0I7m}=V_bRKo@IR13oui(6(nlFhk6-)FQ41e|rSsgtxjA=!yR!p+Sv< z|DPBb6!7%CVL|nCe?#SCUDR0B+D1aES(i17kw|zHUc{=8s(uWlu>mr;m3aMv1dfIS zuM-+VU%oEF$LFv)zUS%DJzpQ&1CdLWwmsK+^hf_Uv>Ja~VR`{ize^u8 zvRsMrGS5|7Br7qKd4%I=&FzNtAA0^a=VH&jA`Nh80dCyRVwog6`)}XuzT1joJzi=Z zXX#D8_v+2wn|J#MuMhpbAO2Af2D&k5iew+DNtP=yQWTgGgiCI*;P7Iha!w@ul3KL4 zyG!jcv=H6lOr_`Y;W>ulPkh-}sJzQ5t+7p}I%*7qZcH#V3*C&&;g=KW{+-I%Dap)4 z0)M1A2@*2myuAQ}yM$v@yOh~+N*QM%ODwm%y=RBgHT}d?Ss7IC9>N+Ez#x9BLjWk) z>nhs6|7>SG*9eyrhyy$44(y<$bfsh&4yqdSqp@0j$A_(O1lLAWS)7AIPk5}xvxdxa z2ixe*@`x*u4lfYH8g+p+5v29yzapHr)Z2Mo{OGeW z_q8FeB_;{^Y+~<+@uzHHmPF=UOdWA zC1%bsgan8q-{Zc9o@9@AkUVCYE;gX~cqzlBI#}lFo9t&@(mz-gn72jc=K@0&sup*yPO?Sv^t&BZ;hsj z(g6Z;#Ls}QT@J0Hpb1Rdzx#T3xAdM}NJ@{p1d&}AsfFHtw4j!87nuf(LS0ilF4Vj> z60D0i_#)93{UaQ;bA>WKvILS@*JlHXN#89$%Xl{aa> zR;OIAlxJ0zo%Fv2-PPy8Z%mEs7MjKC55hHa{9+q2ly7QAgoV!PsGS?9Tl zXARO1qin~EWnb*=?(WWm$8$A@)w#F3|N7i}!~E70si$%H`aFgglibYx#tn&I|E6}N z50{@Vt~c6}Y{?DxBwPMh+>~sWKIg7vi9X-9WJ!MOzEmQ#`Ns5wWyy8?w^^4B5TiB| zSb&bIa_EV8apz1a=7;Z?(-2r1=CQW!{$@W_63kJ# z@wHB>B_h9jsQ&Ju+SK%4*+aE1Kx2tJM*vtaoJnTma0a6Ddo08M_*jOG{Ge&NJmLiH zcF>YfGA{G~Do)8)C7G>!P`E?W9uy6}yPo&$-tO+6|CF3>y~mTvvr?x%&9%1iysWA~ z=xh488Ig4}Z{5#>->9qZf$P*0H8D3$P!x$}o^ZlS(+nfZxK1}K(MoyNa9w~CDfjDm zCex{ktd~SQ5izB857v<3)UzL>Af&Uo5_&o6avmzM zk?5`*!;a!3I*ltkjQ~KE08|%HPP=4(4xEB2=3ej>^e!J(BTgBceM;RT(Y^p5)VsC5 za_>G!?j4wAp$8di7L-?v=Q&TuxqP6Edz8T?TNaCDfzQaLB`(BN6`H^?NP;p2&gH{Z z{EIptnH{8oxVr_6VQL*T>kcKbNwTT^LM73;)VJYf6TD=+pLi?r9z;qP6ounG zy;FG zNjB}`+cKNi`4YB!Fm|$OGw0h5C8ESn#hg>>(<-@-0piQzRUx<>=kU}jnFw=PDkLQ= zgiQcM5+Csq>#(T&6);C&^R#}<&4Kmjqw}*MvkP@a5!j;4Mu+x6nIzuR`elZHaXLIFPNIZQ*DPr>Qc?4YzpNUUQwv;Db$F65aJaHXbuf+4`TjT1)QFI`I; z)0ExFMZ9yj2kU3&^7QEB{B&o_Gz%*LT2vR$tu6$4-IQD_x|pwZbrY#HR#B_P{#gXc z#`ja39NZkq6eafJ%Q%m|n7hW8+dExkx%%?5*W!{#wjerBw8weWM)EK4+W1mjgHLTH z?qyuC+=ZD2Qg!d@%u<)6-af?3K(AHF1Fzd(EEC62$cyp?@s?Z$;S>>rML}?|%LH@w z>DzcAQdLYao3aa|acPCK6s|9eENVe&)X5|s_kze8^RF~nQD{wkr^>0N`FM;|5yiP0 z7p*OuHCCn(_#}%ThL>WVMXD#xvs`6&=FVMswKzCqcB%1dX>FD2kv0vx=i%rZIm|$t zV&fV5KObM1OKY)Q^jdW2US!8vnnFKt$6cC!<5d$c91;1~I**E)d79uH0~F`->RY@NeN(OGn8xIj`>&s+R}P=GR}M?P zGKqF~`~980-PfgFIegAuIdr|Ux4-MxBg}-=76FsQ**F#RWm3d2oq?)dFbGGAnU`=D z53^`6G!^+K6zjg$_f*62;_kKo+f45$*Wn{`FzoI2_Gq#M@8;FP6(TEUDlb)YxKTCG z`Fk(`7QeS#(YwQSv*;;7F4r?{g{>v;>)EOnFMVcyLDT@1pLA2_FpyXQtZsh9PY61`#l*V`y&gB*|o{ z#eZF@JXTsP5E~(5XVHe16S1)UH7%z$=V}rs2@vbhHtW)(JMku_Kz#aYx!{l}Z#>p^ z-|JpO|NZOm%JiIBie|agoI|kLn|EJv>KL+JK5;4;=(+zJyfyp+q;QpsxNG{V3mxyB z=r%h+k%2GAeICPhVD}m}gR)hgJEw|05Dx5ECb-SG(@z|MKH+FMXvsxv4mRjX(~V`k z70TsLIpX6*cP{l`yKuX2#fx*P{|dDFb-m*vdJNBLmYU*AmCHz(9aguttbzgnF)Ib& zX0nBw!e7tg^!6+p8xfl;D7`S-Ex$Bk5Q!>h+4z=XtW2kYx(+ZQSO#o&yG9mm^JtJO z^<8$WB1GNCZtK+(?F%D*ws?#6ovz@F)d*tTtK_og-Lmw>VeJ>(=3sa{iH>|u$KiZv z-M-bf3c3Y&5(1IeAd8#HVy2Hr)bqavD0Tb_mk}Ju>}Ku<&zRN5GF3~|)LKd*;0hf6 zr?7QcVhfo1L8%2TGxk~)Y`tW|>3!${PiyN$s(F@PsiN1qRcdi!zh`HMM`{80yhn+b zXoKUQr44Gr<-&RcLR}nMTFi2#XITP@MJkDvqEx=OdmtXfzBV_UR40T-`m|kINY`- zO|$v8wtR#GPBgpyQ@{V0n&OA9u&ML?${y$xtZSh->Kgn#Ht>C%M&^nQ3XWv|LdDbB zNvy|4fLN7}ma~14ngXLN$9xR{%uV`lJkhI8Zb0A@`Ifqp+uE{gX`0P70Vq;4+Gf7e zjO)R63s0;*=GCqyl?-!7>H9eO18S&xt>g`~ofrzffMEJdg^Cn0l06MAn@86e(quPs&UKJXW`?@v{wcF3F~!cnKt6kz8dNM8DKobCx5iqd*QLjh#r8yyN&D zkP2Y1S=Xq0EnEYn(ja-XI{#{GIsA%R4xdm99mJ%t;+;NQ76cRUw8~w@mxja@Pz~k| zGXWj=e(+IT9}ihP<=b95&Xv??1BVMI6#jViY3JkRC*RK)wIu-TlttR>=J$h-*T=)A ziVvSs@!?Y{J`5{vrLwS_rbC3rYyP5pE3Ph%uEiX)b2G>i!aK?Ft#?F14!me&_Px1W zTotm=r)gm>;e7#UfMNJLpWn0WUSyL(rLH3IX;}iY8w9xG8lG?r!&Sb)t`!V43S^YRi^Oec8B_3b4^9#5w#2brW) zJM>t&j%M4Ylk;>C~Gr^A;phEvW5GE#+nY13u`9h?wd*1)6RY9||KDx5P?z}h2Am%qdx9Jcw0ZWjRNF(}{wKt$@BN2WkDb-Yn!b)u zHURbFB8kUwk*tDTe@-b6`<4V75*dh$n24ZqF1(bc2mv*(ekN((YVB`Y*ztzauL>|(;81RE(0Umb2P z6fL!oc%9R58xE*}V`*eA4CQCA81V4JI=yxyUuJKEQxUDre1na1pyJ#Bj6j$HD3COp z`Z!mG;vw#cc;cDvyv{=tsUd~Cr*FY4sU5iYN$QMP-mYnQhg4>e4hnqo$HC1E8l-XC zmg5<0Ce{no;%sKQ7_C73Dr|&27#NUOaK`QusYxQIJy2X-06Uk+4Jt#vYVscbK6!ti zy#L_Id$?xulAjr2@(4=N-)qes29$rLbch+%Paf07@_onL#>7Y_G98;siuw5-ekoU- z!^g0yPSSGmAWLmt5F^!cjysd-nnLv$BJV!T-A)zXWzQvGrI~nhu_6g{M*S8 z2pugTV9!huf;vEpP2a^#y@)axq@`BovzHxSB`^N%WGLPbKLDXco=tPft{}7o&BR~Y zOiVuu z;bU~IiVFZ{@XNafa)(AbKZ*4MGN{@m4(e)H%7Yiec9JJ3Ypnj^G$UH$M@3;XcsUgk=C5Aj-L(sJP$Tke7^ z&PQ==FtZ`MFjsv5KsL3` zq-XhYVFR>CDATOdv?il&@}@)dIicVr%kO1Q;WrMYlGQq>BE?t?k8xMrDSE7W0V^1y z_nu9o9AyOkr)^qKFVca4ee&86oGmuMmlvXQE$Ct&_Pzdoq@U4s3!$X~v>B}mnm=7A zPyWe5YH_cUr03Pi%cCz&@Lx6cj#lCxb0Kn9xn}n$o&+&X{ME*ji*6{U!$>b_<8v_X zC}5p$^b|Ne%I@s?u}%!BOC$iCLZniH3?{Nw<_N*F zVL5ee#V;EtsQ@(Ln>-uEX-Al4QQXkd?(iWA4nE$|>AomS8W-KG01VVjHz#n|u&f%{ zh6F;6Sr!bCgh%@b>oYJw#8Wx#*Q^dBwbY<7}CAPye%@hO`jp-&>V#ai* zdKrU?yF*#ubMC^>20J`k>z;Ntggzqfbz zmXs{*j%e@i?(X&bJBR4qWUXtuAMmX;s(x`n_PkL8r|TI6Hg!{ZAO`V7c(_CH!u`y>iyzL(h(uy^vl(WD( z?t-0)RzpX~pzCN^V9*jF3kq8CD5$}HZiISvAkQJd zbEUZ!*m=KI59;jqVm+Yqe=jiSrku8qfjF0ju@cc+7wNtsrADBcpR*CL#k9QHPw(&e@5U!?Z?TVY5xhmw1^QgIj z(6=m7H$f8ELgrIdP&b>`_D$~l+5FSr_Wt8>FJ%u9y2eTQ=|IqxPG|&y_Jx`+=D0?5 zTnHhB3k@jJ;EculX3a|5x!c1XfP#~(7ar75CO(loda;e_>4ak0f~v=S1{U_J5TpOMDRB+DFbGpf?u zi;n2huaU%iJl%BN)0a;P=*dQA-C!oX74P{F(1ZWPp~Uz(+QeO^f6TfcBoc6A=u{!c zoQrr%=;a9gWOOg+A$P`CsKcVg(m;_!P*VM66VaAQ(uRrEPP0_C;WlBl8uX;M-zNs4S)ycGyS>oz zL8{<|&!mwa5%^E!YRao)OMFU`RXvEz1jDNqwpUz!L^vMuWy%v3Vtm1S z%1l-x`p@JY3a&{!E_6>^Sv@A}d3HrtyxxEJ`rYB1{dWgFR$poK9o=prbj;wSFLuoo z$)=vbYBmjY0Q3@BvW_K@U@L^INT8yfin-ESPGK9Cku0RyZJ z_q#}ZcisC={pF|ciog8yUGkTozDxfie)?|O|8DCq;=4OP3#Ke)sB-3WgF4GpYxWed zF3NNjHgG3WcRRX>vL&G%ApE!UzX0DLGXc>BS;$GDa?;yK@bL1&Ze>xZ`J#XTY;78j z`<{7Cfft_p_XZVU8qm0ijl`&m@eJa5Q}J+eW%^r5Hk6v31PrC|-0WIVF@5;Kd{?gh zm30<}6?QHkE(y_SsPbdiAhvwtZDTGUSOutW7An8-n$G4=Gw{&y;80Re7H5(bdvKh- zzrMD17*mm>ePh#9Q6JeM6uaUDo$N2MK3B;JePr8QQ|EF)y$F@Z5gyw%h?)X$xK{Z7 zge{w3fA`XT!DR!KKnsPsCkql~**)Af$C#;y2BgB|;cksHpOM~FBxc#d2%uA$N34~8 z=vqri7aX;B4^u{O0uH$4`Iu@r)VEICvkT z7(WU<>_Hp{WCC#GDzGu$n;nv`?G`Wdp_iNk`z|<&0o3={envKtGF?^bbeT_=bH=0q z5=Z%T(~lc*k4u=x^>mD63CG6Vf)m4c-_!E|nt01X$bdjPPm8WOz(}zT6bO#Wyx*Cp z#g6x$tLlrUH`g$+PQXr;K`yr~LPUt$NVQCdkWLz{^I!+6|#<%RrY_60vDbXpNKI`H7iL zYV(@ZY2}*HqJu%&05IAw|FY)NB1$lx1j<>RiJXhDDX%}9h>r4kmJjm`5V$ywj3?q3 z|DK7e93zT(J1S}$Loy-yY5Q{u{hO+&y+QXm?pqB&*CpNRx{m{ZYww`6m?IRW@V~5p z^R$H%xJvAxn|ap`Zr63wPOFO49-_2+;Oeglp*=)afBiAGJE_BEuVS)?Umfi2pT}ks zEeEBFr-Y@7Uo$YZIW$##Rd8xecxr8qkSBzwHU_C~#$gS;YA|Sh9+9U6t2TwJZpLIC zy*e&y;eern&l(+#%q1QyIvIi~G7N7aY0El}Q?Am>?t#=(DnrC+^ zA~P1V=^@7mOth4v<05K9L`;5VQ4BeL(*hBHr4~t+P6_9Ov>xmq!$+iN7h`n^W{?iH z-fdtO8#0y))cM9LOw)Mmy?C_eTyswE0IoP&PG?3YN()b}=F%PCqZeJdV6m!P+~;z! zP&wkzS)%6b^cf`$!h{?$!5?`Hx%N-lk!-BAmsjx1#p(6U`@zM@&DH7UkAvgWFV^cc zO;(QfD#)_DkXk$T1JYu&vcy?0@CBOED=mbc&v@@vsl|mV-p6S~#*{yzR_;X%M&X!! zV4?4*akeI(&r+Yhbd9~p3e^__m%dufM+%NYV32y>kpjPP)86uUd53&;sI3yzo5&F@ z0FtiIM{%i@wtX}h4lh4lf4UhAcLu|o>&v5ytKre*>BY4r)D>o|j!w=87as9HDMVe3 zN!gj*i_x+W^LRQdKv&_v{g*$AgWldATL}_BOPy1y*TLY2CbR6J)z@x<0(#Cl=nrK`Sc&5qqN-{ z03gGgK{d-oh5TZIb8z&LHK&r_6vRRR(IuM(GS{YoAWG9 zd0Imno6*>aQWM}^nnmil2EtE6ajo)s$$k!eojxpbrL})f^LtEzY0aEz*5&*wrJ=Bh zfo9trq@}mMH?vRP;_n-CJTUZbXA6FG@v*77B}9~J*2J2&L~67o+n9{zaaxy18(!e0Vj{=&T_v2a zmrM6-3PJ@_j(a1|ixR48oSGvgT8^EE`nvRo;Oj}z=r1fLf0AX35yAryqn-d|dU9MO zpnPLpei;mof%#^Y7Be08huht&Y7PUtvggobP0TFEkqb&T_u>GoS%%{D0y(XAV)}l2 z;b_%(0+t7?C2F&*)p_`d4Sf8>e={$?aHPN$orPU5LVjcfz(5gqBh%=n>Ov(T1gMzN z+@OAce420QROm*uCQ-3Alf#NHj1O49F5}{;( zi`5(GDl0V}h3k9@pW;=x&abe^vv7@8zT#b2SN2;x48O4_A9G90?>5>98+@c&ZnJN{ zdw2M{9iSfVZ~)p8@8YRV@kahvjAPaEWyBDaFu>QcI_keJvqDm)wLnNw1oTQ_;T9W) zzx-GDu-5Skn~T$GCUY-)?!hgIntu@c&YY#i3x*3wBfj=S6U`;yr9BBF4Ga6WiqRd` zvUgyS!|&rrwfUg@MBkvP1`=-_m53j-TU!vkK{1VBa%B&x)>h@OwWaayU~eozZ=Yb? zaK5yjel*blqu1B`jT0qPLWcj0q71l9m{X=u$*Q#_mMJ+~IdBCL7wFogi)_J?fLIofw3C>w&#tP8TrBWiuT>-tQ5D1sFA92=-TQHNl#eNs5ykgIGAvp& z*JoE<3Gv7TNsj_ENWn3MXTLC;X|f7FhKq=T0Nl~anRwHwVcfJl%j2ngM6S8RAR@(D zZo0zd)PmdWTa6laQ`LKpN)7UEy?-?Q|M8I+D>IMcF-)m6;y5^SSMxj#eMwiEj-1c4 z)O=oKVhZRRh4M*J&y7};Y7HkYuHw1UGgu2*${rAqN-owSCGpkfc}ue#DWUOI;U-bR3ya!>9wFPk%&y6YSzO-sAQozYnLtolN4$96^z{J|nr( zemu7y|0|c?U`FEj$aT+;M2lU=`eJYQ-9e521Q3c=0@gTxdkB$3hb4>wJ3ncwgonG=!~(Kyk7aM17u)2vYi((IK^aJVMmh*h#=eEe zN0{giK(EaSlbQqVKJ6CqT;VK#4~?x}=LqYC>nn9muhnpeK$Vk+JYsWev3{A&EVUF< zKY3W5WknZT@3U9rtFI%Dn!FOd&*mB|u3$q1UtuLNBby^m`B$~VQekrk6R}Lq#-`H3 z3@IM7y2Ku&eur;qBOv8$L};{5t^n6LrS zswhO=yvC5=SW_r)lqsEV7i@wDXS+-nmY`E-8q#r3^@J*BV1QGp-Zj(){59?(rkuM# z-lVGdzw}Q-xFftD|JRl2w6^#S%vl)sAd;o{H<7-~kBfDbL6jR%Km?GdaYJpTUZ;^S%e{ItLS>dm`S{nUi(v#Xt}t1}!Wa&W&R z)4Hk2@T0ZOYvEs88^Fo9h!c3l3zlwQRufIs`y@Il&*zp{ru+|g1OGHUy*#@9bb0gs z=<0NhJ9y{_zGZaBZ^g;F$fv%e3S#%Ut=C;6V+Y zqyO*oO!ouiJMKk@^F zHZim@wR%&V3ume~odv7vA8$Q#?3Meb>Y@ExaG$1WAI-oe`(XFGb>1G{ zVE;Gp_lPqJx!wVn39oTvqKqsXLNCnS03zOsT*j1=w5DJ9i!1ox*p;`z3g{^DaCNKh zIjjKYlHX$f(F>dYOO`78fjMoCnCwX+2}M;e`HbGsk;rmu>YDwFxzXsI%t|50&M<;FiP zf~x-O4P&XE8ZTpO**s*1KUotsWBTZVF{sgY-@@*vr=h@ZgKWkWWK-h2PL8jSxbb*c z(-Y0~o)iSrOr{hLGt8osSm#SnOTS-6Q&reBhxkGHwWo-r6iqcWsLWSHpBYYfB1;aU zW*YREk0&L4G{r5a7)PI>orTKBDlONaVtm!N>=@;c8s@lnHM8PiFo+&_x#i)1%0(kv@E)xSdZ`L ziQLV`lg(lN0&=xjf9pw>(f$H@7V>Yeq@`N#-r|ncCKNy*XORG_K802PKnn->>UVd+ zz+Ld0efWYAe-1ak62Uc|e4E1rmS|S60_(Phr@01xqchr*-T6u_%B${V&%o7JyU3Ne zYjAnn06Q#DPJ9D^Smi)}W(YAMLJ+aqg|!Aq?5j22B|xz{NU`Sl{M&eeI~G`+IW>5- z>-BBCIKDhNrcxz+{v||hv1${}WzZR*16kDv%Ls4~FDeN&V&y~eEwYF`+At@mA%2lX z>YCI^_VV*pXlFo$)g<7m>k{Btu^FQI$l(qc!A+lluQf|VTiC*p0;Dnfu~Wm8=o^YY zIOFlffSqabDnJ@Qi%DJS0T9IszeoP=11*8lXq=@Kyh(RPU)(P;u?+*b?c4}*B{cxA zZHaAUc%VNow!O@V&BJE8qvyDy8m|gjEZuM|Q_62Q%%VR*Dpenc_fmnMA*5C^697}X z*Dz*@FMq#^+aK+`ilUym!=zaC)tDEyrC!fzw%fw-a*2>6SUim5{M;*=uK0E&Y z^3$j5(!=5P<!<9y*@k}Vh0Tt)zD+mJb>Lh2tbnQ!i>MzhBASxWR+8&`tu4{>(O7#w zEJx}`%sT}kK&d%I;LA7_Um&R(AGu$|SSE??w6;WCm?o$+f@F`kZ?#C`Q7&`SHp*g! zJJR=%l?@~R7qRJ3IhCe&ZO|O$FoCFLI1H6*4mW~9kc!Ghg)%-HmICmvTp4KrOx|Dt zhKJKzi(sM!V5Hv3bgX(U?6%`PhS;c~f48w6ZKK`pvK4h0CUQk}5Jhj>v~#Bjj~e8o zM*dn`rq7O!^6~8OHR*lu7OF*rNFf$SL+Dw&5YqOk%m1_0#19{}UeLL{SmPS<j4#e10^Y?+;=Bi;%}?Q! zIaCy;*nve4*uDM$`htg==Fj{e|k}L|;W~Kte`@!B`FG#whB4S?mO`#g=qXRUt$}GbdO_`iX^S z`-py504b;d*U69*_;kWEw}!w);48pA~K6S2yc1o;p_xL~fv8M#4OQr^c6-R^J@ ztQ#jY)T`~PEgPdBGWRHbTu>CsY$=(6+K3P~-e%a#QuLHbEiuy{An@2ShIWW@`52G! z!j7I0U9tCOzqj|cw|{WZ+uscaX!Iyj5w~WX!~i|+pi(+MKn~)x_>e}+Vg^z`&(?Ph zh?xMwf|i{Z4p{W1nWWh~E()BbV>(F)2!|3G%5k1)ZD(jt3{nP(C!`NsSsS4A5SlRmHZ=0d2e3{PX}L1AL!|8EA-X&k16gS zQQfZ4GOEB0*3O}h(3unK;1N3X$x(I(hQM68V@CiOKi*jYUWsOmhykm^Cid-!ks8aT zZfb9=MWG;BAKhax>7Ho@%jf{JWh)&m;{*YJdM&4HKv3fhC4A`y zixNBo+K|ow?ryMr4&Erp3Z(n8Mq%>OOn47u+$_-j4pS#L2@3U4u+$+cX zo=>X&fcNRu9_!5qdv*Ew#DXaNrWL%7K%>3CvPwN#mu$@7T@dlFaL&}Vxx9~m#re9| zbT}Tq{?_AlZ+HLo`6j(xc2Tf>RO6;#Y54@o$M$df1YS3Q)q|MyTEVI}8yxu9d9cO~ zSk!5O#(v@x?qbK%$bo&vUF;5VIo0V0bz4_5~ajcU%axao*M1x+RL5&=tGu^k3XCS zP~NBqa(YhzAY;90mQ51{fq6+Lqwa``cgb%V;(Jn=v;*R&KU}RRPIIF-QiUv`3(0%k z4XcW&j=oE+B-VtYHVAZQrjt9Y##k%T!Ah2M)Pt+u4Wh3U{CDR}P?^|a&FMFif1qmPy<>`9K-vP>r ze3@ziGUa`027HSHrO>tG~J*VuFMzD>R zW84#y_*NzHEXxRfI@V$)7Ymif>9m8{6XlAND_LcVJ0ezOy33Z4jhXa<@@t^$@rCG$ zHcpkc!*~_uchmS&>L(sj^3a=SV3e1s*yAzj1(o1DY}0&vd~%s@V9vI2>NbACX&lV4 z?8I1#E)66-`l~dELQj1a74>&w?p#GCGq|02=g>{hYPYj1ju$C^z3I9HH<&QN2w7p|!3@8*>Wu9`Ry&<*Dd>%(Qx= zb$I8l-ncg-v*zYqxp!|y(eu2o^}T-@9sio!e98R=ZFETy-Eaxs>><%PjLlxdZ3F!-IoY`~KsPdA3}H&nMM~kAl!W%5&9YTC9bmdz9c= z2yFAH|MVIUYO#?IwRlc1>PDcSwWr61Xu8klPc5F?qq@}KzfEJY$fIWi_~yn+AA@lgbwJMI=}^w%31H5m zAuOyM4ECfA^J~Jv>H@;ng@lQ9L16*+g7kFdaIkM~Qd2-!7!nqUIrT?jH4d{r60;c` zz5yP-x~Gaq;oR%KaWL_F&rdnnnLR1`kyHVV%d9E*)$^pP+|J7GEOJ@QR9eIoryha_ zlpTpZnj%qCIbQYQ_mS@pP5yPEJ{am^Bcl#=>UHV@bW6(3rX*g(J=NTm+4X_iKk)dig`TcoNb5;HCIs#^_eAU1HjFYVf;6$ z^09+Fv9N(J@ZN(<>Yd89eChI|S@EmK)_U-sgKTYN{cfNMqs(Y*qyt>2yvsHP?puso zFt^VlP7<-yzCodeB&JQwQpM@+yt(tpz%-B@sizH$c)^Hf*uO9Rk)r0oq&)SFPdF!) zot1&jo2H|*z0$xcdTk*bMU9tZb*^^*5BuqxcmLwC8vZ>tp&NUsR6zsz+WE~2a-77m z7IP)jIGs+G3Fb^i{r37LTIW<6#6~Qna}0LQ(gJ1O2|4T(4h zg?K67A%2KBC!qaR9B1Z#Sjz3`&vPl}YCMx^tmi0V`>?F58U`q7X$j1g6*y6*P*ZAi z+RN1D3529wb~r5o%YX(O+dU>^G{cZ9bF@I_@6wtkE-a0^SYk9>k?1ZD_!k#4P*$bF z*TIdutjJ@RT})K#cObREy8VMv&(}O2=UMvyvXM)2I#XT}&(u@!`(%CZeYVp=_?j#@ z6b>qNmo3U3NpmVI`L@9=kW_xAot=^!vg621<&syk=Jcb23`Tf$G<{)%BpU-Y*oY?h zO6_)=o=0>rVfNrYjY$>_4>sPw>kTgjKL^dQmpM!t&tt5DT#VI0c57UV#VkX!usEkxY;xou%%DCM?w)W$er`VG`%-Q;SRRMmY-eA6_m zqvuaNYJkcMO6}XPG$73U>-Wn4!n~-}w7>X=B>K?V;oN?5&f1pW7mev?y&CxTgi@$q zNv_-mAYO!y1h`GAheb^9jhOz@?<+8yVFbX0`zY3;O;ex^ei&(-z-llz8j-8+at7lmOU&da<>$}#4+cZ6?&Bx@5KNR*cE%?4?C~LAKJdp@K2|Rn(p86%pjiQq8@0yRQaWvgwDxR)JglxpJS)E z$ZecP)?We11n(&9S8?V!e^@>FI%&y1gD87mHYAD?| z@{R=mG0cbRk!NfK65g;84bfy`SWT@F1pC6r^rpslU7Pbi%Sl>gqD(1H#ixZz&t1-r zxh(1(&ppba3Jbkj12Z|2=iLP}i)i6)C^mTqTMTRb?;Bg;`}c0tj%Fdexh>U7np@+w z=QVeo7uMF;5KL73%`Lc61%j5ELp@T3j1w0zMCAWBd33DAwh?kt9p9;Kv++@Q9XAd< zUZPwH2jXLp_ca~Ihx&+->&g$X|p#Qznx-{5( zD>m?kOe|}SQk4`h_4fL&AzklbEBv@8@&Wg* zJE?Jr?Xz^3hoY$IT}s+|wYyz%eNi6z*$d7Lxg?j8rNk+kAUfcHc*)`L|8V~NX05Rz zlCLFRe!EOE(F(MMTPsaHtWSiN#2P-ns;4gZuY%<%CJ7(V@uxiF*Bk|bSt*Y}B zGyh08R?Pkr_u|P`!6Us{yAu2hd6OmJUY}p%+^^#QE1UFH-H6V1AcaKqIR;e7e0p!b zghhp2FxcKGRP`LN6taJDQdZrklg$0AJvE-wVnggryNl201h@r+zOe zvX~s1MHcA-`il;XQm%BsRw%gIs5i}ctm%|(kS_!bP5{Cb<}}I`vsshbDK3VSR#iBm zYhgE6Ie@M9;flI&K?+gx_idfWOzjn`x~dcjZ|fHrEg)DmAbs;fR>yX(y7jP68I#d9 z^`L1>0{cC{q7O&m29ryfs-i>n$b?Lr{0;-@caKJ+zZ@kySZJDmh-G>TC?vR(?AF1c zZx4_GAs=A|^;a$ar$+Y*eFS_GS}C`-@mtoaya10AUNV88$=(stAv<;*v|)la@Ciq7B&sGu8s@+Qyv=D_xW4BG4)Ny~a?%ksXm5|Q*Op** zJG&es+~txrt$qp{bWh!yoVO~gelK@+SyoC69s3pPU6o_{Wjo{hREWZ*wZFW5yB`1wNq>S3uO=YKnqKvvVQwLe*qAulTB=6y zg+cOi46LI;NGg}mh~AtFa_K3SY!AkKa(c~f#*=-Q!tihn32s0Vt5z0E6A^sJ(|dhX zl$hGCO7Dewo0l`u*yO#3Ba4a&sBN;YN~s&k1R;q2ivsWPJ06(>Hx&yd_u-9TKm1)k zHqV;|YED}zvVZ)b+s;aP1C(y4g;P+dP?;Ui_rf1rh|*cq4LWsUWwcybBuAV0uHX$A zIOn`%&kv8-tEGNhlk9bVb~zpqw*&M_5t71VaI2q{@|2nao1~ke{#_5{lknUYdin9Z zWVLA6>tD_;(Gv%b9iGlDn{JZmcl^UeeJz^5k>l$zdp>fPB+J0pmmom$tPl|FRyX1y z#|h@C${*adKwd;(_#_CvqN?uO8Yc!FtWwHsqZdq(KVagItPrP`j1zHl%trFj-Zw%S zFvVEYm^?_gJ?wK#IG|_b_S`XX28KY1uGk|FYnA--Rj!4YAU9$waos?4CrI3to)2m9 zhV^c;c%XK#>MB+%uG&T~We390cxTCZ)#!t0GI=|2Q+?{I62M+i-!_@NHSUE5-Zf!k z3*2k|ruqFSnhN}$a0NSP%7m9(m9%sxledzXb6f`c9e@$d%ZVz`!PD+*z-+;Rftxs& zD9mC$P<`W|V1l9@<23V~_Z=;tNyb+Cu(4^s|ta&3!g&uC5oH9P0TnC^xi2u7IQ&or3Ae1Hn zDPfmalk;PCYLGAn-sjHe%>1DddQ0fxt^oj}Y7hr=UM(mkZttZSLE~B00%3Z6Ct>}K z_Q~FzP<(&KFA(-5sICQ6eZrSBxVPgu^LN>01)dpP>8rv->I{r9C-kfna0qNxJYesN z3ZSql!o_vx3*A+rkyouUxMzBJG)q3SmK13aq!T{_?NYEgb&6#42A4cirqWc9wn$&n zrZIcfPN!fL`Pc!)&Usmi!i;%K_irOop-=RHW6CaU!JqW@<^@}DI8XlFidKyCj{@Uq zKD|}fV(EDMxUFl(2zCXd?9tUJyRdlGzr?xmbY%VYVV0!s6BDIs2>E)JUUYi0=L=Mn z#TGVp34+5w9W+$c(CYGA)$$@7RPY!DKSFUQO&ODgo%CuS5)}^yT!DWG@X}5cY99i#u-iS9XZ!*2RFRrCX2QJ0%Zqx&b{@`8a!Am%B zG5|ZHg>jUfp*9t^67+Ku<3hJQ?$z=2#SLZxlv*r`A*?|Wf}tQ^HayNqeH)K;+y?j? zn%)HR8w#3+S0TjM{$b#}YN`U2n$!=+O$~fmdlq|U3dQZ3eL6kUCQL6n*YSHnDVeM@ zT5!VR+DV{q35_Nom%8TUbw+03r?bp~=@^*Plh%1rIrcS5#sp2SC&mkWjw6l}h=|5b zSa${7S4nt+R0!Z~IG}{ysK3h>*4!LWcflKTsVD8S<1u1N4KX$s6!wW^aX$jcvv26s z)h=B0;tf^mVYE`bQXkEj^%0GM!+(aUzVT)CAFdN4PxX(k;1_+V}@2dgT^=V8T6oV+=|j7&v18CWbf8lj$WtODu;py0d#9{N<{n| zylTc6#7>kFQdN!3^7J4Y6tKL5m@#>u*FR&K-<%@7uJ?FzqetV*A4K9VIZ>`}XToYR zW(JoCz0zo7C@G4mf9O;w?xI4{HC}+E0kTqNzyu9!6>yw*biPpVD*k#|4b6{I7=ljI zZ`5iuz4Bd#!Y&`-DuTnNDnhCYE>$mT`}0VWlrXFdUW!kqoB&{!!OIGoXiLg1mVb2I zRX1v)%k}8u<9W@STu3KbR+aLqEW#mo6H0oiBw2KO8_0lF%in?pJhIK*5bia52^Q`b zjwZ*S7gHW%{t^5ZlvJ%{QTiJ&z-}Rhk7kPGTtl5J40aHbL{JDA?IarmZL;0|R$Sh9 z>HGwYhNXF68Xr>E4tAwbLp2w}@XK&oHAAkeyRyTo6q}zsejQlPw>*j=%q@%H61f8z zdg*(+%FU?o|0`U@@^#?Pm#(PXhT@fIdM?Zrd&xo%KkH(Ds)WENl}WbloSrMjNqF1FJ`@=jgjN z?Z>2FGnJozFB7G&?_zej-%LXPpNYfa4)l1<3Tnv9CXi8=X>CLNxN-(rACK=(%p{+4>h# zNZ)Kkq_fL0dI_MpAN1_x`(hwP_CzvE*h^!=0|cDprB%WIl*os4FZtvC@k?EMh_!#B zAh-R2Z8_k~0GP|Fco5(jXS#B?1h<;JoUyiced8{!(cWp8H)fuGCJl-qH+UNCaFpo( z4v$}qUK|cQ89H%I8M-e}X2uXXflWWz16PpT^&cQXl52FKh z&L}ggtn=yAc=JW(84Pmh#e$~7?#jtTA!gvL!Ry?p2PHY7bkI8xD?A1|*IdoX#TGhz zfaK)?lu7izwqwEKuJl`XAZzNn$kD6~c|067<%zYq1WOj^J;t;yga2Q_y5mEr(2lud nwyODH?VEoZ$(=uZ|9$^`|9$^`%fJ5y00960$&aHU0N@n>P$Ot+ diff --git a/providers/docker/scs/cluster-addon/metrics-server/.helmignore b/providers/docker/scs/cluster-addon/metrics-server/.helmignore deleted file mode 100644 index 0e8a0eb3..00000000 --- a/providers/docker/scs/cluster-addon/metrics-server/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/providers/docker/scs/cluster-addon/metrics-server/Chart.lock b/providers/docker/scs/cluster-addon/metrics-server/Chart.lock deleted file mode 100644 index cb461d5a..00000000 --- a/providers/docker/scs/cluster-addon/metrics-server/Chart.lock +++ /dev/null @@ -1,6 +0,0 @@ -dependencies: -- name: metrics-server - repository: https://kubernetes-sigs.github.io/metrics-server/ - version: 3.12.2 -digest: sha256:b79715342d7c10e97664b5f4d79199044f5da6ef40cca906218cff05ca891122 -generated: "2025-01-13T15:40:51.780206883+01:00" diff --git a/providers/docker/scs/cluster-addon/metrics-server/charts/metrics-server-3.12.2.tgz b/providers/docker/scs/cluster-addon/metrics-server/charts/metrics-server-3.12.2.tgz deleted file mode 100644 index 4538e8a10e54c3dcb0d5047b4034cfb23683482e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10395 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 F0RZYxsxJTl diff --git a/providers/docker/scs/cluster-class/.helmignore b/providers/docker/scs/cluster-class/.helmignore deleted file mode 100644 index 0e8a0eb3..00000000 --- a/providers/docker/scs/cluster-class/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/providers/docker/scs/clusterstack.yaml b/providers/docker/scs/clusterstack.yaml deleted file mode 100644 index 488aa714..00000000 --- a/providers/docker/scs/clusterstack.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: clusterstack.x-k8s.io/v1alpha1 -kind: ClusterStack -metadata: - name: docker-131 - namespace: cluster -spec: - autoSubscribe: false - channel: custom - kubernetesVersion: "1.31" - name: scs - noProvider: true - provider: docker - versions: - - v0-sha.hdl6pjy diff --git a/providers/docker/scs/csctl.yaml b/providers/docker/scs/csctl.yaml deleted file mode 100644 index c9d2c1cb..00000000 --- a/providers/docker/scs/csctl.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: csctl.clusterstack.x-k8s.io/v1alpha1 -config: - kubernetesVersion: v1.30.10 - clusterStackName: scs - provider: - type: docker - apiVersion: docker.csctl.clusterstack.x-k8s.io/v1alpha1 diff --git a/providers/openstack/scs/cluster-addon/ccm/Chart.yaml b/providers/openstack/scs/1-32/cluster-addon/ccm/Chart.yaml similarity index 100% rename from providers/openstack/scs/cluster-addon/ccm/Chart.yaml rename to providers/openstack/scs/1-32/cluster-addon/ccm/Chart.yaml diff --git a/providers/openstack/scs/cluster-addon/ccm/overwrite.yaml b/providers/openstack/scs/1-32/cluster-addon/ccm/overwrite.yaml similarity index 100% rename from providers/openstack/scs/cluster-addon/ccm/overwrite.yaml rename to providers/openstack/scs/1-32/cluster-addon/ccm/overwrite.yaml diff --git a/providers/openstack/scs/cluster-addon/ccm/values.yaml b/providers/openstack/scs/1-32/cluster-addon/ccm/values.yaml similarity index 100% rename from providers/openstack/scs/cluster-addon/ccm/values.yaml rename to providers/openstack/scs/1-32/cluster-addon/ccm/values.yaml diff --git a/providers/openstack/scs/cluster-addon/cni/Chart.yaml b/providers/openstack/scs/1-32/cluster-addon/cni/Chart.yaml similarity index 54% rename from providers/openstack/scs/cluster-addon/cni/Chart.yaml rename to providers/openstack/scs/1-32/cluster-addon/cni/Chart.yaml index 8b8dd2f1..10b69016 100644 --- a/providers/openstack/scs/cluster-addon/cni/Chart.yaml +++ b/providers/openstack/scs/1-32/cluster-addon/cni/Chart.yaml @@ -1,9 +1,9 @@ apiVersion: v2 dependencies: -- alias: cilium - name: cilium - repository: https://helm.cilium.io/ - version: 1.17.4 + - alias: cilium + name: cilium + repository: https://helm.cilium.io/ + version: 1.19.1 description: CNI name: openstack-scs-1-32-cluster-addon type: application diff --git a/providers/openstack/scs/cluster-addon/cni/values.yaml b/providers/openstack/scs/1-32/cluster-addon/cni/values.yaml similarity index 100% rename from providers/openstack/scs/cluster-addon/cni/values.yaml rename to providers/openstack/scs/1-32/cluster-addon/cni/values.yaml diff --git a/providers/openstack/scs/1-32/cluster-addon/csi/Chart.yaml b/providers/openstack/scs/1-32/cluster-addon/csi/Chart.yaml new file mode 100644 index 00000000..c5ed4d75 --- /dev/null +++ b/providers/openstack/scs/1-32/cluster-addon/csi/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +dependencies: + - alias: openstack-cinder-csi + name: openstack-cinder-csi + repository: https://kubernetes.github.io/cloud-provider-openstack + version: 2.32.2 +description: CSI +name: openstack-scs-1-32-cluster-addon +type: application +version: v1 diff --git a/providers/openstack/scs/cluster-addon/csi/overwrite.yaml b/providers/openstack/scs/1-32/cluster-addon/csi/overwrite.yaml similarity index 100% rename from providers/openstack/scs/cluster-addon/csi/overwrite.yaml rename to providers/openstack/scs/1-32/cluster-addon/csi/overwrite.yaml diff --git a/providers/openstack/scs/cluster-addon/csi/values.yaml b/providers/openstack/scs/1-32/cluster-addon/csi/values.yaml similarity index 100% rename from providers/openstack/scs/cluster-addon/csi/values.yaml rename to providers/openstack/scs/1-32/cluster-addon/csi/values.yaml diff --git a/providers/openstack/scs/1-32/cluster-addon/metrics-server/Chart.yaml b/providers/openstack/scs/1-32/cluster-addon/metrics-server/Chart.yaml new file mode 100644 index 00000000..005860b5 --- /dev/null +++ b/providers/openstack/scs/1-32/cluster-addon/metrics-server/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +dependencies: + - alias: metrics-server + name: metrics-server + repository: https://kubernetes-sigs.github.io/metrics-server/ + version: 3.13.0 +description: Metrics Server +name: openstack-scs-1-32-cluster-addon +type: application +version: v1 diff --git a/providers/openstack/scs/1-32/cluster-addon/metrics-server/overwrite.yaml b/providers/openstack/scs/1-32/cluster-addon/metrics-server/overwrite.yaml new file mode 100644 index 00000000..7b1dcd5b --- /dev/null +++ b/providers/openstack/scs/1-32/cluster-addon/metrics-server/overwrite.yaml @@ -0,0 +1,4 @@ +values: | + metrics-server: + commonLabels: + domain: "{{ .Cluster.spec.controlPlaneEndpoint.host }}" diff --git a/providers/openstack/scs/1-32/cluster-addon/metrics-server/values.yaml b/providers/openstack/scs/1-32/cluster-addon/metrics-server/values.yaml new file mode 100644 index 00000000..a89bf027 --- /dev/null +++ b/providers/openstack/scs/1-32/cluster-addon/metrics-server/values.yaml @@ -0,0 +1,4 @@ +metrics-server: + fullnameOverride: metrics-server + args: + - --kubelet-insecure-tls diff --git a/providers/openstack/scs/cluster-class/Chart.yaml b/providers/openstack/scs/1-32/cluster-class/Chart.yaml similarity index 100% rename from providers/openstack/scs/cluster-class/Chart.yaml rename to providers/openstack/scs/1-32/cluster-class/Chart.yaml diff --git a/providers/openstack/scs/cluster-class/templates/_helpers.tpl b/providers/openstack/scs/1-32/cluster-class/templates/_helpers.tpl similarity index 100% rename from providers/openstack/scs/cluster-class/templates/_helpers.tpl rename to providers/openstack/scs/1-32/cluster-class/templates/_helpers.tpl diff --git a/providers/openstack/scs/cluster-class/templates/cluster-class.yaml b/providers/openstack/scs/1-32/cluster-class/templates/cluster-class.yaml similarity index 100% rename from providers/openstack/scs/cluster-class/templates/cluster-class.yaml rename to providers/openstack/scs/1-32/cluster-class/templates/cluster-class.yaml diff --git a/providers/openstack/scs/cluster-class/templates/image.yaml b/providers/openstack/scs/1-32/cluster-class/templates/image.yaml similarity index 100% rename from providers/openstack/scs/cluster-class/templates/image.yaml rename to providers/openstack/scs/1-32/cluster-class/templates/image.yaml diff --git a/providers/openstack/scs/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml b/providers/openstack/scs/1-32/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml similarity index 100% rename from providers/openstack/scs/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml rename to providers/openstack/scs/1-32/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml diff --git a/providers/openstack/scs/cluster-class/templates/kubeadm-control-plane-template.yaml b/providers/openstack/scs/1-32/cluster-class/templates/kubeadm-control-plane-template.yaml similarity index 100% rename from providers/openstack/scs/cluster-class/templates/kubeadm-control-plane-template.yaml rename to providers/openstack/scs/1-32/cluster-class/templates/kubeadm-control-plane-template.yaml diff --git a/providers/openstack/scs/cluster-class/templates/openstack-cluster-template.yaml b/providers/openstack/scs/1-32/cluster-class/templates/openstack-cluster-template.yaml similarity index 100% rename from providers/openstack/scs/cluster-class/templates/openstack-cluster-template.yaml rename to providers/openstack/scs/1-32/cluster-class/templates/openstack-cluster-template.yaml diff --git a/providers/openstack/scs/cluster-class/templates/openstack-machine-template-control-plane.yaml b/providers/openstack/scs/1-32/cluster-class/templates/openstack-machine-template-control-plane.yaml similarity index 100% rename from providers/openstack/scs/cluster-class/templates/openstack-machine-template-control-plane.yaml rename to providers/openstack/scs/1-32/cluster-class/templates/openstack-machine-template-control-plane.yaml diff --git a/providers/openstack/scs/cluster-class/templates/openstack-machine-template-worker.yaml b/providers/openstack/scs/1-32/cluster-class/templates/openstack-machine-template-worker.yaml similarity index 100% rename from providers/openstack/scs/cluster-class/templates/openstack-machine-template-worker.yaml rename to providers/openstack/scs/1-32/cluster-class/templates/openstack-machine-template-worker.yaml diff --git a/providers/openstack/scs/cluster-class/values.yaml b/providers/openstack/scs/1-32/cluster-class/values.yaml similarity index 100% rename from providers/openstack/scs/cluster-class/values.yaml rename to providers/openstack/scs/1-32/cluster-class/values.yaml diff --git a/providers/openstack/scs/clusteraddon.yaml b/providers/openstack/scs/1-32/clusteraddon.yaml similarity index 100% rename from providers/openstack/scs/clusteraddon.yaml rename to providers/openstack/scs/1-32/clusteraddon.yaml diff --git a/providers/openstack/scs/1-32/stack.yaml b/providers/openstack/scs/1-32/stack.yaml new file mode 100644 index 00000000..4c7690d4 --- /dev/null +++ b/providers/openstack/scs/1-32/stack.yaml @@ -0,0 +1,7 @@ +provider: openstack +clusterStackName: scs +kubernetesVersion: 1.32 + +addons: + ccm: 2.32.x + csi: 2.32.x diff --git a/providers/openstack/scs2/cluster-addon/ccm/Chart.yaml b/providers/openstack/scs/1-33/cluster-addon/ccm/Chart.yaml similarity index 92% rename from providers/openstack/scs2/cluster-addon/ccm/Chart.yaml rename to providers/openstack/scs/1-33/cluster-addon/ccm/Chart.yaml index 7fddcbd4..541cecd0 100644 --- a/providers/openstack/scs2/cluster-addon/ccm/Chart.yaml +++ b/providers/openstack/scs/1-33/cluster-addon/ccm/Chart.yaml @@ -7,4 +7,4 @@ dependencies: - alias: openstack-cloud-controller-manager name: openstack-cloud-controller-manager repository: https://kubernetes.github.io/cloud-provider-openstack - version: 2.34.1 + version: 2.33.1 diff --git a/providers/openstack/scs2/cluster-addon/ccm/overwrite.yaml b/providers/openstack/scs/1-33/cluster-addon/ccm/overwrite.yaml similarity index 100% rename from providers/openstack/scs2/cluster-addon/ccm/overwrite.yaml rename to providers/openstack/scs/1-33/cluster-addon/ccm/overwrite.yaml diff --git a/providers/openstack/scs2/cluster-addon/ccm/values.yaml b/providers/openstack/scs/1-33/cluster-addon/ccm/values.yaml similarity index 100% rename from providers/openstack/scs2/cluster-addon/ccm/values.yaml rename to providers/openstack/scs/1-33/cluster-addon/ccm/values.yaml diff --git a/providers/openstack/scs2/cluster-addon/cni/Chart.yaml b/providers/openstack/scs/1-33/cluster-addon/cni/Chart.yaml similarity index 88% rename from providers/openstack/scs2/cluster-addon/cni/Chart.yaml rename to providers/openstack/scs/1-33/cluster-addon/cni/Chart.yaml index 7bc28cb7..051b40b3 100644 --- a/providers/openstack/scs2/cluster-addon/cni/Chart.yaml +++ b/providers/openstack/scs/1-33/cluster-addon/cni/Chart.yaml @@ -7,4 +7,4 @@ dependencies: - alias: cilium name: cilium repository: https://helm.cilium.io/ - version: 1.18.5 + version: 1.19.1 diff --git a/providers/openstack/scs/1-33/cluster-addon/cni/values.yaml b/providers/openstack/scs/1-33/cluster-addon/cni/values.yaml new file mode 100644 index 00000000..8a312f0c --- /dev/null +++ b/providers/openstack/scs/1-33/cluster-addon/cni/values.yaml @@ -0,0 +1,14 @@ +cilium: + namespaceOverride: kube-system + tls: + secretsNamespace: + name: "kube-system" + sessionAffinity: true + sctp: + enabled: true + ipam: + mode: "kubernetes" + gatewayAPI: + enabled: true + secretsNamespace: + name: "kube-system" diff --git a/providers/openstack/scs2/cluster-addon/csi/Chart.yaml b/providers/openstack/scs/1-33/cluster-addon/csi/Chart.yaml similarity index 91% rename from providers/openstack/scs2/cluster-addon/csi/Chart.yaml rename to providers/openstack/scs/1-33/cluster-addon/csi/Chart.yaml index dd303380..e275b2ff 100644 --- a/providers/openstack/scs2/cluster-addon/csi/Chart.yaml +++ b/providers/openstack/scs/1-33/cluster-addon/csi/Chart.yaml @@ -7,4 +7,4 @@ dependencies: - alias: openstack-cinder-csi name: openstack-cinder-csi repository: https://kubernetes.github.io/cloud-provider-openstack - version: 2.34.1 + version: 2.33.1 diff --git a/providers/openstack/scs2/cluster-addon/csi/overwrite.yaml b/providers/openstack/scs/1-33/cluster-addon/csi/overwrite.yaml similarity index 100% rename from providers/openstack/scs2/cluster-addon/csi/overwrite.yaml rename to providers/openstack/scs/1-33/cluster-addon/csi/overwrite.yaml diff --git a/providers/openstack/scs2/cluster-addon/csi/values.yaml b/providers/openstack/scs/1-33/cluster-addon/csi/values.yaml similarity index 100% rename from providers/openstack/scs2/cluster-addon/csi/values.yaml rename to providers/openstack/scs/1-33/cluster-addon/csi/values.yaml diff --git a/providers/openstack/scs2/cluster-addon/metrics-server/Chart.yaml b/providers/openstack/scs/1-33/cluster-addon/metrics-server/Chart.yaml similarity index 100% rename from providers/openstack/scs2/cluster-addon/metrics-server/Chart.yaml rename to providers/openstack/scs/1-33/cluster-addon/metrics-server/Chart.yaml diff --git a/providers/openstack/scs/1-33/cluster-addon/metrics-server/overwrite.yaml b/providers/openstack/scs/1-33/cluster-addon/metrics-server/overwrite.yaml new file mode 100644 index 00000000..7b1dcd5b --- /dev/null +++ b/providers/openstack/scs/1-33/cluster-addon/metrics-server/overwrite.yaml @@ -0,0 +1,4 @@ +values: | + metrics-server: + commonLabels: + domain: "{{ .Cluster.spec.controlPlaneEndpoint.host }}" diff --git a/providers/openstack/scs/1-33/cluster-addon/metrics-server/values.yaml b/providers/openstack/scs/1-33/cluster-addon/metrics-server/values.yaml new file mode 100644 index 00000000..a89bf027 --- /dev/null +++ b/providers/openstack/scs/1-33/cluster-addon/metrics-server/values.yaml @@ -0,0 +1,4 @@ +metrics-server: + fullnameOverride: metrics-server + args: + - --kubelet-insecure-tls diff --git a/providers/openstack/scs2/cluster-class/Chart.yaml b/providers/openstack/scs/1-33/cluster-class/Chart.yaml similarity index 58% rename from providers/openstack/scs2/cluster-class/Chart.yaml rename to providers/openstack/scs/1-33/cluster-class/Chart.yaml index 2ea03e47..82b14c74 100644 --- a/providers/openstack/scs2/cluster-class/Chart.yaml +++ b/providers/openstack/scs/1-33/cluster-class/Chart.yaml @@ -1,9 +1,9 @@ apiVersion: v2 description: "This chart installs and configures: - * Openstack scs2 Cluster Class + * Openstack scs Cluster Class " -name: openstack-scs2-1-34-cluster-class +name: openstack-scs-1-33-cluster-class type: application version: v1 diff --git a/providers/openstack/scs2/cluster-class/templates/_helpers.tpl b/providers/openstack/scs/1-33/cluster-class/templates/_helpers.tpl similarity index 100% rename from providers/openstack/scs2/cluster-class/templates/_helpers.tpl rename to providers/openstack/scs/1-33/cluster-class/templates/_helpers.tpl diff --git a/providers/openstack/scs2/cluster-class/templates/cluster-class.yaml b/providers/openstack/scs/1-33/cluster-class/templates/cluster-class.yaml similarity index 100% rename from providers/openstack/scs2/cluster-class/templates/cluster-class.yaml rename to providers/openstack/scs/1-33/cluster-class/templates/cluster-class.yaml diff --git a/providers/openstack/scs2/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml b/providers/openstack/scs/1-33/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml similarity index 100% rename from providers/openstack/scs2/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml rename to providers/openstack/scs/1-33/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml diff --git a/providers/openstack/scs2/cluster-class/templates/kubeadm-control-plane-template.yaml b/providers/openstack/scs/1-33/cluster-class/templates/kubeadm-control-plane-template.yaml similarity index 100% rename from providers/openstack/scs2/cluster-class/templates/kubeadm-control-plane-template.yaml rename to providers/openstack/scs/1-33/cluster-class/templates/kubeadm-control-plane-template.yaml diff --git a/providers/openstack/scs2/cluster-class/templates/openstack-cluster-template.yaml b/providers/openstack/scs/1-33/cluster-class/templates/openstack-cluster-template.yaml similarity index 100% rename from providers/openstack/scs2/cluster-class/templates/openstack-cluster-template.yaml rename to providers/openstack/scs/1-33/cluster-class/templates/openstack-cluster-template.yaml diff --git a/providers/openstack/scs2/cluster-class/templates/openstack-machine-template-control-plane.yaml b/providers/openstack/scs/1-33/cluster-class/templates/openstack-machine-template-control-plane.yaml similarity index 100% rename from providers/openstack/scs2/cluster-class/templates/openstack-machine-template-control-plane.yaml rename to providers/openstack/scs/1-33/cluster-class/templates/openstack-machine-template-control-plane.yaml diff --git a/providers/openstack/scs2/cluster-class/templates/openstack-machine-template-worker.yaml b/providers/openstack/scs/1-33/cluster-class/templates/openstack-machine-template-worker.yaml similarity index 100% rename from providers/openstack/scs2/cluster-class/templates/openstack-machine-template-worker.yaml rename to providers/openstack/scs/1-33/cluster-class/templates/openstack-machine-template-worker.yaml diff --git a/providers/openstack/scs2/cluster-class/values.yaml b/providers/openstack/scs/1-33/cluster-class/values.yaml similarity index 100% rename from providers/openstack/scs2/cluster-class/values.yaml rename to providers/openstack/scs/1-33/cluster-class/values.yaml diff --git a/providers/openstack/scs2/clusteraddon.yaml b/providers/openstack/scs/1-33/clusteraddon.yaml similarity index 100% rename from providers/openstack/scs2/clusteraddon.yaml rename to providers/openstack/scs/1-33/clusteraddon.yaml diff --git a/providers/openstack/scs/1-33/stack.yaml b/providers/openstack/scs/1-33/stack.yaml new file mode 100644 index 00000000..f50583d1 --- /dev/null +++ b/providers/openstack/scs/1-33/stack.yaml @@ -0,0 +1,7 @@ +provider: openstack +clusterStackName: scs +kubernetesVersion: 1.33 + +addons: + ccm: 2.33.x + csi: 2.33.x diff --git a/providers/openstack/scs/1-34/cluster-addon/ccm/Chart.yaml b/providers/openstack/scs/1-34/cluster-addon/ccm/Chart.yaml new file mode 100644 index 00000000..bd086402 --- /dev/null +++ b/providers/openstack/scs/1-34/cluster-addon/ccm/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: CCM +name: CCM +version: v1 +dependencies: + - alias: openstack-cloud-controller-manager + name: openstack-cloud-controller-manager + repository: https://kubernetes.github.io/cloud-provider-openstack + version: 2.34.2 diff --git a/providers/openstack/scs/1-34/cluster-addon/ccm/overwrite.yaml b/providers/openstack/scs/1-34/cluster-addon/ccm/overwrite.yaml new file mode 100644 index 00000000..39076ecd --- /dev/null +++ b/providers/openstack/scs/1-34/cluster-addon/ccm/overwrite.yaml @@ -0,0 +1,4 @@ +values: | + openstack-cloud-controller-manager: + cluster: + name: {{ .Cluster.metadata.name }} diff --git a/providers/openstack/scs/1-34/cluster-addon/ccm/values.yaml b/providers/openstack/scs/1-34/cluster-addon/ccm/values.yaml new file mode 100644 index 00000000..3f290366 --- /dev/null +++ b/providers/openstack/scs/1-34/cluster-addon/ccm/values.yaml @@ -0,0 +1,21 @@ +openstack-cloud-controller-manager: + secret: + enabled: true + name: ccm-cloud-config + create: true + nodeSelector: + tolerations: + - key: node.cloudprovider.kubernetes.io/uninitialized + value: "true" + effect: NoSchedule + extraVolumes: + - name: clouds-yaml + secret: + secretName: clouds-yaml + extraVolumeMounts: + - name: clouds-yaml + readOnly: true + mountPath: /etc/openstack + cloudConfig: + global: + use-clouds: true diff --git a/providers/openstack/scs/1-34/cluster-addon/cni/Chart.yaml b/providers/openstack/scs/1-34/cluster-addon/cni/Chart.yaml new file mode 100644 index 00000000..051b40b3 --- /dev/null +++ b/providers/openstack/scs/1-34/cluster-addon/cni/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: CNI +name: CNI +version: v1 +dependencies: + - alias: cilium + name: cilium + repository: https://helm.cilium.io/ + version: 1.19.1 diff --git a/providers/openstack/scs/1-34/cluster-addon/cni/values.yaml b/providers/openstack/scs/1-34/cluster-addon/cni/values.yaml new file mode 100644 index 00000000..8a312f0c --- /dev/null +++ b/providers/openstack/scs/1-34/cluster-addon/cni/values.yaml @@ -0,0 +1,14 @@ +cilium: + namespaceOverride: kube-system + tls: + secretsNamespace: + name: "kube-system" + sessionAffinity: true + sctp: + enabled: true + ipam: + mode: "kubernetes" + gatewayAPI: + enabled: true + secretsNamespace: + name: "kube-system" diff --git a/providers/openstack/scs/1-34/cluster-addon/csi/Chart.yaml b/providers/openstack/scs/1-34/cluster-addon/csi/Chart.yaml new file mode 100644 index 00000000..cbe0cc17 --- /dev/null +++ b/providers/openstack/scs/1-34/cluster-addon/csi/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: CSI +name: CSI +version: v1 +dependencies: + - alias: openstack-cinder-csi + name: openstack-cinder-csi + repository: https://kubernetes.github.io/cloud-provider-openstack + version: 2.34.3 diff --git a/providers/openstack/scs/1-34/cluster-addon/csi/overwrite.yaml b/providers/openstack/scs/1-34/cluster-addon/csi/overwrite.yaml new file mode 100644 index 00000000..d191a115 --- /dev/null +++ b/providers/openstack/scs/1-34/cluster-addon/csi/overwrite.yaml @@ -0,0 +1,3 @@ +values: | + openstack-cinder-csi: + clusterID: "{{ .Cluster.metadata.name }}" diff --git a/providers/openstack/scs/1-34/cluster-addon/csi/values.yaml b/providers/openstack/scs/1-34/cluster-addon/csi/values.yaml new file mode 100644 index 00000000..4e648a4f --- /dev/null +++ b/providers/openstack/scs/1-34/cluster-addon/csi/values.yaml @@ -0,0 +1,41 @@ +openstack-cinder-csi: + secret: + enabled: true + name: csi-cloud-config + create: true + filename: cloud.conf + data: + cloud.conf: |- + [Global] + use-clouds = "true" + clouds-file = /etc/openstack/clouds.yaml + storageClass: + delete: + isDefault: true + csi: + plugin: + volumes: + - name: clouds-yaml + secret: + secretName: clouds-yaml + - name: cloud-conf + secret: + secretName: csi-cloud-config + volumeMounts: + - name: clouds-yaml + readOnly: true + mountPath: /etc/openstack + - name: cloud-conf + readOnly: true + mountPath: /etc/kubernetes + - name: cloud-conf + readOnly: true + mountPath: /etc/config + nodeSelector: + node-role.kubernetes.io/control-plane: "" + tolerations: + - key: node.cloudprovider.kubernetes.io/uninitialized + value: "true" + effect: NoSchedule + - key: node-role.kubernetes.io/control-plane + effect: NoSchedule diff --git a/providers/openstack/scs/1-34/cluster-addon/metrics-server/Chart.yaml b/providers/openstack/scs/1-34/cluster-addon/metrics-server/Chart.yaml new file mode 100644 index 00000000..2ac06b1a --- /dev/null +++ b/providers/openstack/scs/1-34/cluster-addon/metrics-server/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: Metrics Server +name: metrics-server +version: v1 +dependencies: + - name: "metrics-server" + version: "3.13.0" + repository: "https://kubernetes-sigs.github.io/metrics-server/" + alias: "metrics-server" diff --git a/providers/openstack/scs/1-34/cluster-addon/metrics-server/overwrite.yaml b/providers/openstack/scs/1-34/cluster-addon/metrics-server/overwrite.yaml new file mode 100644 index 00000000..7b1dcd5b --- /dev/null +++ b/providers/openstack/scs/1-34/cluster-addon/metrics-server/overwrite.yaml @@ -0,0 +1,4 @@ +values: | + metrics-server: + commonLabels: + domain: "{{ .Cluster.spec.controlPlaneEndpoint.host }}" diff --git a/providers/openstack/scs/1-34/cluster-addon/metrics-server/values.yaml b/providers/openstack/scs/1-34/cluster-addon/metrics-server/values.yaml new file mode 100644 index 00000000..a89bf027 --- /dev/null +++ b/providers/openstack/scs/1-34/cluster-addon/metrics-server/values.yaml @@ -0,0 +1,4 @@ +metrics-server: + fullnameOverride: metrics-server + args: + - --kubelet-insecure-tls diff --git a/providers/openstack/scs/1-34/cluster-class/Chart.yaml b/providers/openstack/scs/1-34/cluster-class/Chart.yaml new file mode 100644 index 00000000..4fa495ee --- /dev/null +++ b/providers/openstack/scs/1-34/cluster-class/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +description: "This chart installs and configures: + + * Openstack scs Cluster Class + + " +name: openstack-scs-1-34-cluster-class +type: application +version: v1 diff --git a/providers/openstack/scs/1-34/cluster-class/templates/_helpers.tpl b/providers/openstack/scs/1-34/cluster-class/templates/_helpers.tpl new file mode 100644 index 00000000..2339c125 --- /dev/null +++ b/providers/openstack/scs/1-34/cluster-class/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "cluster-class.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "cluster-class.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "cluster-class.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "cluster-class.labels" -}} +helm.sh/chart: {{ include "cluster-class.chart" . }} +{{ include "cluster-class.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "cluster-class.selectorLabels" -}} +app.kubernetes.io/name: {{ include "cluster-class.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "cluster-class.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "cluster-class.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/providers/openstack/scs/1-34/cluster-class/templates/cluster-class.yaml b/providers/openstack/scs/1-34/cluster-class/templates/cluster-class.yaml new file mode 100644 index 00000000..d7fbd338 --- /dev/null +++ b/providers/openstack/scs/1-34/cluster-class/templates/cluster-class.yaml @@ -0,0 +1,844 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: ClusterClass +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }} +spec: + controlPlane: + ref: + apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + kind: KubeadmControlPlaneTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane + machineInfrastructure: + ref: + kind: OpenStackMachineTemplate + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane + infrastructure: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-cluster + workers: + machineDeployments: + - class: default-worker + template: + bootstrap: + ref: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 + kind: KubeadmConfigTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker + infrastructure: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker + variables: + # Image variables + - name: imageName + required: false + schema: + openAPIV3Schema: + type: string + description: | + The base name of the OpenStack image used for provisioning servers. + If `imageIsOrc` is enabled, this name refers to an ORC image resource. + If `imageIsOrc` is disabled, the name is used to filter images available in the OpenStack project. In this case, the specified image must already exist within the project. + If `imageAddVersion` is enabled, the Kubernetes version will be appended to form the complete image name (e.g., imageName-v1.32.5) + default: "ubuntu-capi-image" + - name: imageIsOrc + required: false + schema: + openAPIV3Schema: + type: boolean + description: | + Indicates whether the image name refers to an ORC image resource. + If set to true (default), the `imageName` is interpreted as a reference to an ORC image. + If set to false, the `imageName` is used to filter images in the OpenStack project instead. + default: false + - name: imageAddVersion + required: false + schema: + openAPIV3Schema: + type: boolean + description: | + Add a suffix with the Kubernetes version to the imageName. E.g. imageName-v1.32.5. + default: true + - name: disableAPIServerFloatingIP + required: false + schema: + openAPIV3Schema: + type: boolean + default: false + example: false + description: "DisableAPIServerFloatingIP controls whether a floating IP should be attached to the API server." + # Network variables + - name: networkExternalID + required: false + schema: + openAPIV3Schema: + type: string + example: "ebfe5546-f09f-4f42-ab54-094e457d42ec" + format: "uuid4" + description: "networkExternalID is the ID of an external OpenStack Network. This is necessary to get public internet to the VMs in case there are several external networks." + - name: networkMTU + required: false + schema: + openAPIV3Schema: + type: integer + example: 1500 + description: "networkMTU sets the maximum transmission unit (MTU) value to address fragmentation for the private network ID." + - name: dnsNameservers + required: false + schema: + openAPIV3Schema: + type: array + description: "dnsNameservers is the list of nameservers for the OpenStack Subnet being created. Set this value when you need to create a new network/subnet which requires access to DNS." + default: ["9.9.9.9", "149.112.112.112"] + example: ["9.9.9.9", "149.112.112.112"] + items: + type: string + - name: nodeCIDR + required: false + schema: + openAPIV3Schema: + type: string + format: "cidr" + default: "10.8.0.0/20" + example: "10.8.0.0/20" + description: |- + nodeCIDR is the OpenStack Subnet to be created. + Cluster actuator will create a network, a subnet with nodeCIDR, + and a router connected to this subnet. + If you leave this empty, no network will be created. + # Control plane + - name: controlPlaneFlavor + required: false + schema: + openAPIV3Schema: + type: string + default: "SCS-2V-4" + example: "SCS-2V-4-20s" + description: |- + OpenStack instance flavor for control plane nodes. + (Default: SCS-2V-4, replace by SCS-2V-4-20s or specify a controlPlaneRootDisk.) + - name: controlPlaneRootDisk + required: false + schema: + openAPIV3Schema: + type: integer + minimum: 0 + example: 25 + default: 50 + description: |- + Root disk size in GiB for control-plane nodes. + OpenStack volume will be created and used instead of an ephemeral disk defined in flavor. + Should only be used for the diskless flavors (>= 20), otherwise set to 0. + - name: controlPlaneServerGroupID + required: false + schema: + openAPIV3Schema: + type: string + default: "" + example: "3adf4e92-bb33-4e44-8ad3-afda9dfe8ec3" + description: "The server group to assign the control plane nodes to (can be used for anti-affinity)." + - name: controlPlaneAvailabilityZones + required: false + schema: + openAPIV3Schema: + type: array + example: ["nova"] + description: "controlPlaneAvailabilityZones is the set of availability zones which control plane machines may be deployed to." + items: + type: string + - name: controlPlaneOmitAvailabilityZone + required: false + schema: + openAPIV3Schema: + type: boolean + example: true + description: |- + controlPlaneOmitAvailabilityZone causes availability zone to be omitted when creating control plane nodes, + allowing the Nova scheduler to make a decision on which availability zone to use based on other scheduling constraints. + # Workers + - name: workerFlavor + required: false + schema: + openAPIV3Schema: + type: string + default: "SCS-4V-8" + example: "SCS-4V-8" + description: "OpenStack instance flavor for worker nodes (default: SCS-4V-8, which requires workerRootDisk)." + - name: workerRootDisk + required: false + schema: + openAPIV3Schema: + type: integer + minimum: 0 + example: 25 + default: 50 + description: |- + Root disk size in GiB for worker nodes. + OpenStack volume will be created and used instead of an ephemeral disk defined in flavor. + Should be used for the diskless flavors (>= 20), otherwise set to 0. + - name: workerServerGroupID + required: false + schema: + openAPIV3Schema: + type: string + default: "" + example: "869fe071-1e56-46a9-9166-47c9f228e297" + description: "The server group to assign the worker nodes to." + - name: workerAdditionalBlockDevices + required: false + schema: + openAPIV3Schema: + type: array + default: [] + items: + type: object + properties: + name: + type: string + sizeGiB: + type: integer + default: 20 + type: + type: string + default: "__DEFAULT__" + required: ["name"] + # Access management + - name: sshKeyName + required: false + schema: + openAPIV3Schema: + type: string + default: "" + example: "capi-keypair" + description: "The ssh key name to inject in the nodes (for debugging)." + - name: securityGroups + required: false + schema: + openAPIV3Schema: + type: array + default: [] + example: ["security-group-1"] + description: |- + The names of extra security groups to assign to worker and control plane nodes. + Will be ignored if `securityGroupIDs` is used. + items: + type: string + - name: securityGroupIDs + required: false + schema: + openAPIV3Schema: + format: "uuid4" + type: array + default: [] + example: ["9ae2f488-30a3-4629-bd51-07acb8eb4278"] + description: "The UUIDs of extra security groups to assign to worker and control plane nodes" + items: + type: string + - name: workerSecurityGroups + required: false + schema: + openAPIV3Schema: + type: array + default: [] + example: ["security-group-1"] + description: |- + The names of extra security groups to assign to the worker nodes. + Will be ignored if `workerSecurityGroupIDs` is used. + items: + type: string + - name: workerSecurityGroupIDs + required: false + schema: + openAPIV3Schema: + format: "uuid4" + type: array + default: [] + example: ["9ae2f488-30a3-4629-bd51-07acb8eb4278"] + description: "The UUIDs of extra security groups to assign to the worker nodes" + items: + type: string + - name: identityRef + required: false + schema: + openAPIV3Schema: + type: object + default: {} + properties: + name: + type: string + example: "openstack" + default: "openstack" + description: "The name of the secret that carries the OpenStack clouds.yaml" + cloudName: + type: string + example: "openstack" + default: "openstack" + description: "The name of the cloud to use from the clouds.yaml" + # Kubernetes API server + - name: certSANs + required: false + schema: + openAPIV3Schema: + type: array + default: [] + example: ["mydomain.example"] + description: "certSANs sets extra Subject Alternative Names for the API Server signing cert." + items: + type: string + - name: apiServerLoadBalancer + required: false + schema: + openAPIV3Schema: + type: string + default: "octavia-ovn" + example: "none, octavia-amphora, octavia-ovn" + description: | + Cluster-API by default places a LoadBalancer in front of the kubernetes API server. + (There are also LBs that the CCM creates for a service type LoadBalancer which are configured independently.) + This setting here is to configure the LoadBalancer that is placed in front of the apiServer. + You can choose from 3 options: + + none: + No LoadBalancer solution will be deployed + + octavia-amphora: + Uses OpenStack's LoadBalancer service Octavia (provider:amphora) + + octavia-ovn: + (default) Uses OpenStack's LoadBalancer service Octavia (provider:ovn) + - name: apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs + required: false + schema: + openAPIV3Schema: + type: array + example: ["192.168.10.0/24"] + description: |- + apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs restrict access to the Kubernetes API server on a network level. + Ensure that at least the outgoing IP of your Management Cluster is added to the list of allowed CIDRs. + Otherwise CAPO can’t reconcile the target Cluster correctly. + This requires amphora as load balancer provider in version >= v2.12. + items: + type: string + - name: oidcConfig + required: false + schema: + openAPIV3Schema: + type: object + properties: + clientID: + type: string + example: "kubectl" + description: "A client id that all tokens must be issued for." + issuerURL: + type: string + example: "https://dex.k8s.scs.community" + description: >- + URL of the provider that allows the API server to + discover public signing keys. Only URLs that use the https:// scheme are + accepted. This is typically the provider's discovery URL, changed to have an + empty path. + usernameClaim: + type: string + example: "preferred_username" + default: "preferred_username" + description: >- + JWT claim to use as the user name. By default sub, + which is expected to be a unique identifier of the end user. Admins can choose + other claims, such as email or name, depending on their provider. However, + claims other than email will be prefixed with the issuer URL to prevent naming + clashes with other plugins. + groupsClaim: + type: string + example: "groups" + default: "groups" + description: "JWT claim to use as the user's group. If the claim is present it must be an array of strings." + usernamePrefix: + type: string + example: "oidc:" + default: "oidc:" + description: >- + Prefix prepended to username claims to prevent + clashes with existing names (such as system: users). For example, the value + oidc: will create usernames like oidc:jane.doe. If this flag isn't provided and + --oidc-username-claim is a value other than email the prefix defaults to ( + Issuer URL )# where ( Issuer URL ) is the value of --oidc-issuer-url. The value + - can be used to disable all prefixing. + groupsPrefix: + type: string + example: "oidc:" + default: "oidc:" + description: >- + Prefix prepended to group claims to prevent clashes + with existing names (such as system: groups). For example, the value oidc: will + create group names like oidc:engineering and oidc:infra. + # + # Patches + # + patches: + # + # Patches for OpenStackClusterTemplate resource. + # + - name: apiServerLoadBalancerOctaviaAmphora + description: "Takes care of the patches that should be applied when variable apiServerLoadBalancer is set to octavia-amphora." + enabledIf: {{ `'{{ eq .apiServerLoadBalancer "octavia-amphora" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: replace + path: "/spec/template/spec/apiServerLoadBalancer/enabled" + value: true + - op: add + path: "/spec/template/spec/apiServerLoadBalancer/provider" + value: "amphora" + - name: apiServerLoadBalancerOctaviaOVN + description: "Takes care of the patches that should be applied when variable apiServerLoadBalancer is set to octavia-ovn." + enabledIf: {{ `'{{ eq .apiServerLoadBalancer "octavia-ovn" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: replace + path: "/spec/template/spec/apiServerLoadBalancer/enabled" + value: true + - op: add + path: "/spec/template/spec/apiServerLoadBalancer/provider" + value: "ovn" + - name: apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs + description: "Takes care of the patches that should be applied when variable allowedCIDRs is set." + enabledIf: {{ `'{{ and .apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs (eq .apiServerLoadBalancer "octavia-amphora") }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/apiServerLoadBalancer/allowedCIDRs" + valueFrom: + variable: apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs + - name: networkExternalID + description: "Sets the ID of an external OpenStack Network. This is necessary to get public internet to the VMs." + enabledIf: {{ `'{{ if .networkExternalID }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/externalNetwork" + value: {} + - op: add + path: "/spec/template/spec/externalNetwork/id" + valueFrom: + variable: networkExternalID + - name: networkMTU + description: "Sets the network MTU when variable networkMTU exist in cluster resource." + enabledIf: {{ `'{{ if .networkMTU }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/networkMTU" + valueFrom: + variable: networkMTU + - name: controlPlaneAvailabilityZones + description: "Sets the availability zones which control plane machines may be deployed to." + enabledIf: {{ `'{{ if .controlPlaneAvailabilityZones }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/controlPlaneAvailabilityZones" + valueFrom: + variable: controlPlaneAvailabilityZones + - name: controlPlaneOmitAvailabilityZone + description: "Causes availability zone to be omitted when creating control plane nodes." + enabledIf: {{ `'{{ if .controlPlaneOmitAvailabilityZone }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/controlPlaneOmitAvailabilityZone" + valueFrom: + variable: controlPlaneOmitAvailabilityZone + - name: identityRef + description: "Sets the OpenStack identity reference." + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: /spec/template/spec/identityRef + valueFrom: + variable: identityRef + - name: nodeCIDRSubnet + description: |- + Sets the NodeCIDR for the OpenStack Subnet to be created. + Cluster actuator will create a network, a subnet with NodeCIDR, + and a router connected to this subnet. + enabledIf: {{ `'{{ if .nodeCIDR }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/managedSubnets" + valueFrom: + template: | + - cidr: '{{ `{{ .nodeCIDR }}` }}' + dnsNameservers: + {{ `{{- range .dnsNameservers }}` }} + - {{ `{{ . }}` }} + {{ `{{- end }}` }} + - name: disableAPIServerFloatingIP + description: "DisableAPIServerFloatingIP controls whether a floating IP should be attached to the API server." + enabledIf: {{ `"{{ if .disableAPIServerFloatingIP }}true{{end}}"` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/disableAPIServerFloatingIP" + valueFrom: + variable: disableAPIServerFloatingIP + # + # Patches for control plane's OpenStackMachineTemplate resources. + # Note: Control plane patches are only applied when the control plane is managed by Kubeadm. + - name: controlPlaneImage + description: "Sets the OpenStack image name that is used for creating the control plane servers." + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: /spec/template/spec/image + valueFrom: + template: | + {{ `{{ if .imageIsOrc }}imageRef{{ else }}filter{{ end }}` }}: + name: {{ `{{ .imageName }}{{ if .imageAddVersion }}-{{ .builtin.controlPlane.version }}{{ end }}` }} + - name: controlPlaneFlavor + description: "Sets the openstack instance flavor for the KubeadmControlPlane." + enabledIf: {{ `'{{ ne .controlPlaneFlavor "" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: replace + path: "/spec/template/spec/flavor" + valueFrom: + variable: controlPlaneFlavor + - name: controlPlaneRootDisk + description: "Sets the root disk size in GiB for control-plane nodes." + enabledIf: {{ `'{{ if .controlPlaneRootDisk }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/rootVolume" + valueFrom: + template: | + sizeGiB: {{ `{{ .controlPlaneRootDisk }}` }} + - name: controlPlaneServerGroupID + description: "Sets the server group to assign the control plane nodes to." + enabledIf: {{ `'{{ ne .controlPlaneServerGroupID "" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/serverGroup" + valueFrom: + template: | + id: {{ `{{ .controlPlaneServerGroupID }}` }} + # + # Patches for control plane's as well as worker's OpenStackMachineTemplate resources. + # Note: Control plane patches are only applied when the control plane is managed by Kubeadm. + # + # Note: The securityGroups patch must be placed before securityGroupIDs, workerSecurityGroups, and workerSecurityGroupIDs. + # The patch order ensures the last applied patch overwrites previous ones. + - name: securityGroups + description: "Sets the list of the openstack security groups for the worker and the control plane instances." + enabledIf: {{ `'{{ if .securityGroups }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: true + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .securityGroups }} { filter: { name: {{ . }}}}, {{ end }} ]'` }} + # Note: The securityGroupIDs patch must be placed before workerSecurityGroups, workerSecurityGroupIDs and after securityGroupIDs. + # The patch order ensures the last applied patch overwrites previous ones. + - name: securityGroupIDs + description: "Sets the list of the openstack security groups for the worker and the control plane instances by UUID." + enabledIf: {{ `'{{ if .securityGroupIDs }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: true + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .securityGroupIDs }} { id: {{ . }} }, {{ end }} ]'` }} + - name: sshKeyName + description: "Sets the ssh key to inject in the nodes." + enabledIf: {{ `'{{ ne .sshKeyName "" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: true + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/sshKeyName" + valueFrom: + variable: sshKeyName + # + # Patches for worker's OpenStackMachineTemplate resources. + # + - name: workerImage + description: "Sets the OpenStack image name that is used for creating the worker servers." + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: /spec/template/spec/image + valueFrom: + template: | + {{ `{{ if .imageIsOrc }}imageRef{{ else }}filter{{ end }}` }}: + name: {{ `{{ .imageName }}{{ if .imageAddVersion }}-{{ .builtin.machineDeployment.version }}{{ end }}` }} + - name: workerFlavor + description: "Sets the openstack instance flavor for the worker nodes." + enabledIf: {{ `'{{ ne .workerFlavor "" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: replace + path: "/spec/template/spec/flavor" + valueFrom: + variable: workerFlavor + - name: workerRootDisk + description: "Sets the root disk size in GiB for worker nodes." + enabledIf: {{ `'{{ if .workerRootDisk }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/rootVolume" + valueFrom: + template: | + sizeGiB: {{ `{{ .workerRootDisk }}` }} + - name: workerServerGroupID + description: "Sets the server group to assign the worker nodes to." + enabledIf: {{ `'{{ ne .workerServerGroupID "" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/serverGroup" + valueFrom: + template: | + id: {{ `{{ .workerServerGroupID }}` }} + - name: workerAdditionalBlockDevices + enabledIf: {{ `'{{ if .workerAdditionalBlockDevices }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: /spec/template/spec/additionalBlockDevices + valueFrom: + template: | + {{ `{{- range .workerAdditionalBlockDevices }}` }} + - name: {{ `{{ .name }}` }} + sizeGiB: {{ `{{ .sizeGiB }}` }} + storage: + type: Volume + volume: + type: {{ `{{ .type }}` }} + {{ `{{- end }}` }} + # Note: The workerSecurityGroups patch must be placed before workerSecurityGroupIDs and after securityGroups and securityGroupIDs. + # The patch order ensures the last applied patch overwrites previous ones. + - name: workerSecurityGroups + description: "Sets the list of the openstack security groups for the worker instances." + enabledIf: {{ `'{{ if .workerSecurityGroups }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .workerSecurityGroups }} { filter: { name: {{ . }}}}, {{ end }} ]'` }} + # Note: The workerSecurityGroupIDs patch must be placed after securityGroups, securityGroupIDs and workerSecurityGroupIDs. + # The patch order ensures the last applied patch overwrites previous ones. + - name: workerSecurityGroupIDs + description: "Sets the list of the openstack security groups for the worker instances by UUID." + enabledIf: {{ `'{{ if .workerSecurityGroupIDs }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .workerSecurityGroupIDs }} { id: {{ . }} }, {{ end }} ]'` }} + # + - name: certSANs + description: "certSANs sets extra Subject Alternative Names for the API Server signing cert." + enabledIf: {{ `'{{ if .certSANs }}true{{end}}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + kind: KubeadmControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/certSANs" + valueFrom: + variable: certSANs + - name: oidcConfig + description: "Configure API Server to use external authentication service." + enabledIf: {{ `'{{ if and .oidcConfig .oidcConfig.clientID .oidcConfig.issuerURL }}true{{end}}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + kind: KubeadmControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-client-id" + valueFrom: + variable: oidcConfig.clientID + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-issuer-url" + valueFrom: + variable: oidcConfig.issuerURL + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-username-claim" + valueFrom: + variable: oidcConfig.usernameClaim + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-groups-claim" + valueFrom: + variable: oidcConfig.groupsClaim + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-username-prefix" + valueFrom: + variable: oidcConfig.usernamePrefix + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-groups-prefix" + valueFrom: + variable: oidcConfig.groupsPrefix diff --git a/providers/openstack/scs/1-34/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml b/providers/openstack/scs/1-34/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml new file mode 100644 index 00000000..4c1494ed --- /dev/null +++ b/providers/openstack/scs/1-34/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml @@ -0,0 +1,13 @@ +apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +kind: KubeadmConfigTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker +spec: + template: + spec: + joinConfiguration: + nodeRegistration: + kubeletExtraArgs: + cloud-provider: external + provider-id: 'openstack:///{{ `{{ instance_id }}` }}' + name: '{{ `{{ local_hostname }}` }}' diff --git a/providers/openstack/scs/1-34/cluster-class/templates/kubeadm-control-plane-template.yaml b/providers/openstack/scs/1-34/cluster-class/templates/kubeadm-control-plane-template.yaml new file mode 100644 index 00000000..767eac68 --- /dev/null +++ b/providers/openstack/scs/1-34/cluster-class/templates/kubeadm-control-plane-template.yaml @@ -0,0 +1,89 @@ +apiVersion: controlplane.cluster.x-k8s.io/v1beta1 +kind: KubeadmControlPlaneTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane +spec: + template: + spec: + kubeadmConfigSpec: + clusterConfiguration: + apiServer: {} + controllerManager: + extraArgs: + cloud-provider: external + bind-address: 0.0.0.0 + secure-port: "10257" + scheduler: + extraArgs: + bind-address: 0.0.0.0 + secure-port: "10259" + etcd: + local: + dataDir: /var/lib/etcd + extraArgs: + listen-metrics-urls: http://0.0.0.0:2381 + auto-compaction-mode: periodic + auto-compaction-retention: 8h + election-timeout: "2500" + heartbeat-interval: "250" + snapshot-count: "6400" + files: + - content: | + --- + apiVersion: kubeproxy.config.k8s.io/v1alpha1 + kind: KubeProxyConfiguration + metricsBindAddress: "0.0.0.0:10249" + path: /etc/kube-proxy-config.yaml + - content: | + #!/usr/bin/env bash + + # + # (PK) I couldn't find a better/simpler way to conifgure it. See: + # https://github.com/kubernetes-sigs/cluster-api/issues/4512 + # + + set -o errexit + set -o nounset + set -o pipefail + + dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + readonly dir + + # Exit fast if already appended. + if [[ ! -f ${dir}/kube-proxy-config.yaml ]]; then + exit 0 + fi + + # kubeadm config is in different directory in Flatcar (/etc) and Ubuntu (/run/kubeadm). + kubeadm_file="/etc/kubeadm.yml" + if [[ ! -f ${kubeadm_file} ]]; then + kubeadm_file="/run/kubeadm/kubeadm.yaml" + fi + + # Run this script only if this is the init node. + if [[ ! -f ${kubeadm_file} ]]; then + exit 0 + fi + + # Append kube-proxy-config.yaml to kubeadm config and delete it + cat "${dir}/kube-proxy-config.yaml" >> "${kubeadm_file}" + rm "${dir}/kube-proxy-config.yaml" + + echo success > /tmp/kube-proxy-patch + owner: root:root + path: /etc/kube-proxy-patch.sh + permissions: "0755" + preKubeadmCommands: + - bash /etc/kube-proxy-patch.sh + initConfiguration: + nodeRegistration: + kubeletExtraArgs: + cloud-provider: external + provider-id: 'openstack:///{{ `{{ instance_id }}` }}' + name: '{{ `{{ local_hostname }}` }}' + joinConfiguration: + nodeRegistration: + kubeletExtraArgs: + cloud-provider: external + provider-id: 'openstack:///{{ `{{ instance_id }}` }}' + name: '{{ `{{ local_hostname }}` }}' diff --git a/providers/openstack/scs/1-34/cluster-class/templates/openstack-cluster-template.yaml b/providers/openstack/scs/1-34/cluster-class/templates/openstack-cluster-template.yaml new file mode 100644 index 00000000..9d03326f --- /dev/null +++ b/providers/openstack/scs/1-34/cluster-class/templates/openstack-cluster-template.yaml @@ -0,0 +1,45 @@ +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: OpenStackClusterTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-cluster +spec: + template: + spec: + identityRef: + cloudName: overridden-by-patch + name: overridden-by-patch + apiServerLoadBalancer: + enabled: false + managedSecurityGroups: + allNodesSecurityGroupRules: + - remoteManagedGroups: + - controlplane + - worker + direction: ingress + etherType: IPv4 + name: VXLAN (Cilium) + portRangeMin: 8472 + portRangeMax: 8472 + protocol: udp + description: "Allow VXLAN traffic for Cilium" + - remoteManagedGroups: + - controlplane + - worker + direction: ingress + etherType: IPv4 + name: HealthCheck (Cilium) + portRangeMin: 4240 + portRangeMax: 4240 + protocol: tcp + description: "Allow HealthCheck traffic for Cilium" + - remoteManagedGroups: + - controlplane + - worker + direction: ingress + etherType: IPv4 + name: Hubble (Cilium) + portRangeMin: 4244 + portRangeMax: 4244 + protocol: tcp + description: "Allow Hubble traffic for Cilium" diff --git a/providers/openstack/scs/1-34/cluster-class/templates/openstack-machine-template-control-plane.yaml b/providers/openstack/scs/1-34/cluster-class/templates/openstack-machine-template-control-plane.yaml new file mode 100644 index 00000000..703c1b1c --- /dev/null +++ b/providers/openstack/scs/1-34/cluster-class/templates/openstack-machine-template-control-plane.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: OpenStackMachineTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane +spec: + template: + spec: + flavor: overridden-by-patch + image: + imageRef: + name: overridden-by-patch diff --git a/providers/openstack/scs/1-34/cluster-class/templates/openstack-machine-template-worker.yaml b/providers/openstack/scs/1-34/cluster-class/templates/openstack-machine-template-worker.yaml new file mode 100644 index 00000000..920dbb0a --- /dev/null +++ b/providers/openstack/scs/1-34/cluster-class/templates/openstack-machine-template-worker.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: OpenStackMachineTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker +spec: + template: + spec: + flavor: overridden-by-patch + image: + imageRef: + name: overridden-by-patch diff --git a/providers/openstack/scs/1-34/cluster-class/values.yaml b/providers/openstack/scs/1-34/cluster-class/values.yaml new file mode 100644 index 00000000..e69de29b diff --git a/providers/openstack/scs/1-34/clusteraddon.yaml b/providers/openstack/scs/1-34/clusteraddon.yaml new file mode 100644 index 00000000..d346ba22 --- /dev/null +++ b/providers/openstack/scs/1-34/clusteraddon.yaml @@ -0,0 +1,21 @@ +apiVersion: clusteraddonconfig.x-k8s.io/v1alpha1 +clusterAddonVersion: clusteraddons.clusterstack.x-k8s.io/v1alpha1 +addonStages: + AfterControlPlaneInitialized: + - name: cni + action: apply + - name: metrics-server + action: apply + - name: csi + action: apply + - name: ccm + action: apply + BeforeClusterUpgrade: + - name: cni + action: apply + - name: metrics-server + action: apply + - name: csi + action: apply + - name: ccm + action: apply diff --git a/providers/openstack/scs/1-34/stack.yaml b/providers/openstack/scs/1-34/stack.yaml new file mode 100644 index 00000000..3d219ba0 --- /dev/null +++ b/providers/openstack/scs/1-34/stack.yaml @@ -0,0 +1,7 @@ +provider: openstack +clusterStackName: scs +kubernetesVersion: 1.34 + +addons: + ccm: 2.34.x + csi: 2.34.x diff --git a/providers/openstack/scs/1-35/cluster-addon/ccm/Chart.yaml b/providers/openstack/scs/1-35/cluster-addon/ccm/Chart.yaml new file mode 100644 index 00000000..1a120416 --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-addon/ccm/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: CCM +name: CCM +version: v1 +dependencies: + - alias: openstack-cloud-controller-manager + name: openstack-cloud-controller-manager + repository: https://kubernetes.github.io/cloud-provider-openstack + version: 2.35.0 diff --git a/providers/openstack/scs/1-35/cluster-addon/ccm/overwrite.yaml b/providers/openstack/scs/1-35/cluster-addon/ccm/overwrite.yaml new file mode 100644 index 00000000..39076ecd --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-addon/ccm/overwrite.yaml @@ -0,0 +1,4 @@ +values: | + openstack-cloud-controller-manager: + cluster: + name: {{ .Cluster.metadata.name }} diff --git a/providers/openstack/scs/1-35/cluster-addon/ccm/values.yaml b/providers/openstack/scs/1-35/cluster-addon/ccm/values.yaml new file mode 100644 index 00000000..3f290366 --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-addon/ccm/values.yaml @@ -0,0 +1,21 @@ +openstack-cloud-controller-manager: + secret: + enabled: true + name: ccm-cloud-config + create: true + nodeSelector: + tolerations: + - key: node.cloudprovider.kubernetes.io/uninitialized + value: "true" + effect: NoSchedule + extraVolumes: + - name: clouds-yaml + secret: + secretName: clouds-yaml + extraVolumeMounts: + - name: clouds-yaml + readOnly: true + mountPath: /etc/openstack + cloudConfig: + global: + use-clouds: true diff --git a/providers/openstack/scs/1-35/cluster-addon/cni/Chart.yaml b/providers/openstack/scs/1-35/cluster-addon/cni/Chart.yaml new file mode 100644 index 00000000..051b40b3 --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-addon/cni/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: CNI +name: CNI +version: v1 +dependencies: + - alias: cilium + name: cilium + repository: https://helm.cilium.io/ + version: 1.19.1 diff --git a/providers/openstack/scs/1-35/cluster-addon/cni/values.yaml b/providers/openstack/scs/1-35/cluster-addon/cni/values.yaml new file mode 100644 index 00000000..8a312f0c --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-addon/cni/values.yaml @@ -0,0 +1,14 @@ +cilium: + namespaceOverride: kube-system + tls: + secretsNamespace: + name: "kube-system" + sessionAffinity: true + sctp: + enabled: true + ipam: + mode: "kubernetes" + gatewayAPI: + enabled: true + secretsNamespace: + name: "kube-system" diff --git a/providers/openstack/scs/1-35/cluster-addon/csi/Chart.yaml b/providers/openstack/scs/1-35/cluster-addon/csi/Chart.yaml new file mode 100644 index 00000000..b7d9ee53 --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-addon/csi/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: CSI +name: CSI +version: v1 +dependencies: + - alias: openstack-cinder-csi + name: openstack-cinder-csi + repository: https://kubernetes.github.io/cloud-provider-openstack + version: 2.35.0 diff --git a/providers/openstack/scs/1-35/cluster-addon/csi/overwrite.yaml b/providers/openstack/scs/1-35/cluster-addon/csi/overwrite.yaml new file mode 100644 index 00000000..d191a115 --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-addon/csi/overwrite.yaml @@ -0,0 +1,3 @@ +values: | + openstack-cinder-csi: + clusterID: "{{ .Cluster.metadata.name }}" diff --git a/providers/openstack/scs/1-35/cluster-addon/csi/values.yaml b/providers/openstack/scs/1-35/cluster-addon/csi/values.yaml new file mode 100644 index 00000000..4e648a4f --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-addon/csi/values.yaml @@ -0,0 +1,41 @@ +openstack-cinder-csi: + secret: + enabled: true + name: csi-cloud-config + create: true + filename: cloud.conf + data: + cloud.conf: |- + [Global] + use-clouds = "true" + clouds-file = /etc/openstack/clouds.yaml + storageClass: + delete: + isDefault: true + csi: + plugin: + volumes: + - name: clouds-yaml + secret: + secretName: clouds-yaml + - name: cloud-conf + secret: + secretName: csi-cloud-config + volumeMounts: + - name: clouds-yaml + readOnly: true + mountPath: /etc/openstack + - name: cloud-conf + readOnly: true + mountPath: /etc/kubernetes + - name: cloud-conf + readOnly: true + mountPath: /etc/config + nodeSelector: + node-role.kubernetes.io/control-plane: "" + tolerations: + - key: node.cloudprovider.kubernetes.io/uninitialized + value: "true" + effect: NoSchedule + - key: node-role.kubernetes.io/control-plane + effect: NoSchedule diff --git a/providers/openstack/scs/1-35/cluster-addon/metrics-server/Chart.yaml b/providers/openstack/scs/1-35/cluster-addon/metrics-server/Chart.yaml new file mode 100644 index 00000000..2ac06b1a --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-addon/metrics-server/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: Metrics Server +name: metrics-server +version: v1 +dependencies: + - name: "metrics-server" + version: "3.13.0" + repository: "https://kubernetes-sigs.github.io/metrics-server/" + alias: "metrics-server" diff --git a/providers/openstack/scs/1-35/cluster-addon/metrics-server/overwrite.yaml b/providers/openstack/scs/1-35/cluster-addon/metrics-server/overwrite.yaml new file mode 100644 index 00000000..7b1dcd5b --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-addon/metrics-server/overwrite.yaml @@ -0,0 +1,4 @@ +values: | + metrics-server: + commonLabels: + domain: "{{ .Cluster.spec.controlPlaneEndpoint.host }}" diff --git a/providers/openstack/scs/1-35/cluster-addon/metrics-server/values.yaml b/providers/openstack/scs/1-35/cluster-addon/metrics-server/values.yaml new file mode 100644 index 00000000..a89bf027 --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-addon/metrics-server/values.yaml @@ -0,0 +1,4 @@ +metrics-server: + fullnameOverride: metrics-server + args: + - --kubelet-insecure-tls diff --git a/providers/openstack/scs/1-35/cluster-class/Chart.yaml b/providers/openstack/scs/1-35/cluster-class/Chart.yaml new file mode 100644 index 00000000..4b02c544 --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-class/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +description: "This chart installs and configures: + + * Openstack scs Cluster Class + + " +name: openstack-scs-1-35-cluster-class +type: application +version: v1 diff --git a/providers/openstack/scs/1-35/cluster-class/templates/_helpers.tpl b/providers/openstack/scs/1-35/cluster-class/templates/_helpers.tpl new file mode 100644 index 00000000..2339c125 --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-class/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "cluster-class.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "cluster-class.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "cluster-class.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "cluster-class.labels" -}} +helm.sh/chart: {{ include "cluster-class.chart" . }} +{{ include "cluster-class.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "cluster-class.selectorLabels" -}} +app.kubernetes.io/name: {{ include "cluster-class.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "cluster-class.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "cluster-class.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/providers/openstack/scs/1-35/cluster-class/templates/cluster-class.yaml b/providers/openstack/scs/1-35/cluster-class/templates/cluster-class.yaml new file mode 100644 index 00000000..07f42c60 --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-class/templates/cluster-class.yaml @@ -0,0 +1,837 @@ +apiVersion: cluster.x-k8s.io/v1beta2 +kind: ClusterClass +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }} +spec: + controlPlane: + templateRef: + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 + kind: KubeadmControlPlaneTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane + machineInfrastructure: + templateRef: + kind: OpenStackMachineTemplate + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane + infrastructure: + templateRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-cluster + workers: + machineDeployments: + - class: default-worker + bootstrap: + templateRef: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 + kind: KubeadmConfigTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker + infrastructure: + templateRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker + variables: + # Image variables + - name: imageName + required: false + schema: + openAPIV3Schema: + type: string + description: | + The base name of the OpenStack image used for provisioning servers. + If `imageIsOrc` is enabled, this name refers to an ORC image resource. + If `imageIsOrc` is disabled, the name is used to filter images available in the OpenStack project. In this case, the specified image must already exist within the project. + If `imageAddVersion` is enabled, the Kubernetes version will be appended to form the complete image name (e.g., imageName-v1.32.5) + default: {{ .Values.variables.imageName | quote }} + - name: imageIsOrc + required: false + schema: + openAPIV3Schema: + type: boolean + description: | + Indicates whether the image name refers to an ORC image resource. + If set to true (default), the `imageName` is interpreted as a reference to an ORC image. + If set to false, the `imageName` is used to filter images in the OpenStack project instead. + default: {{ .Values.variables.imageIsOrc }} + - name: imageAddVersion + required: false + schema: + openAPIV3Schema: + type: boolean + description: | + Add a suffix with the Kubernetes version to the imageName. E.g. imageName-v1.32.5. + default: {{ .Values.variables.imageAddVersion }} + # Network variables + - name: networkExternalID + required: false + schema: + openAPIV3Schema: + type: string + example: "ebfe5546-f09f-4f42-ab54-094e457d42ec" + format: "uuid4" + description: |- + ID of an external OpenStack network. Required when multiple + external networks exist and VMs need public internet access. + - name: networkMTU + required: false + schema: + openAPIV3Schema: + type: integer + example: 1500 + description: "MTU for the private cluster network. Set this to avoid fragmentation issues." + - name: dnsNameservers + required: false + schema: + openAPIV3Schema: + type: array + description: "DNS nameservers for the cluster subnet. Only used when a new network/subnet is created." + default: {{ .Values.variables.dnsNameservers | toJson }} + example: ["9.9.9.9", "149.112.112.112"] + items: + type: string + - name: nodeCIDR + required: false + schema: + openAPIV3Schema: + type: string + format: "cidr" + default: {{ .Values.variables.nodeCIDR | quote }} + example: "10.8.0.0/20" + description: |- + CIDR for the cluster subnet. CAPO will create a network, subnet, + and router. Leave empty to skip network creation. + # Machine configuration + # These apply to all nodes by default. Use topology.controlPlane.variables.overrides + # or topology.workers.machineDeployments[].variables.overrides to differentiate. + - name: flavor + required: false + schema: + openAPIV3Schema: + type: string + default: {{ .Values.variables.flavor | quote }} + example: "SCS-2V-4" + description: |- + OpenStack instance flavor. Applies to all nodes by default. + Override per control plane or worker via topology variables overrides. + - name: rootDisk + required: false + schema: + openAPIV3Schema: + type: integer + minimum: 0 + example: 50 + default: {{ .Values.variables.rootDisk }} + description: |- + Root disk size in GiB. OpenStack volume will be created and used + instead of an ephemeral disk defined in the flavor. + Set to 0 to use the flavor's ephemeral disk. + - name: serverGroupID + required: false + schema: + openAPIV3Schema: + type: string + default: {{ .Values.variables.serverGroupID | quote }} + example: "3adf4e92-bb33-4e44-8ad3-afda9dfe8ec3" + description: "Server group for anti-affinity placement. Override per CP/worker via topology." + - name: additionalBlockDevices + required: false + schema: + openAPIV3Schema: + type: array + default: {{ .Values.variables.additionalBlockDevices | toJson }} + description: |- + Additional block devices (Cinder volumes) to attach to nodes. + Override per CP/worker via topology. + items: + type: object + properties: + name: + type: string + sizeGiB: + type: integer + default: 20 + type: + type: string + default: "__DEFAULT__" + required: ["name"] + # Cluster-level (control plane only, managed by OpenStackClusterTemplate) + - name: controlPlaneAvailabilityZones + required: false + schema: + openAPIV3Schema: + type: array + default: {{ .Values.variables.controlPlaneAvailabilityZones | toJson }} + example: ["nova"] + description: "Availability zones for control plane nodes (OpenStack cluster-level setting)." + items: + type: string + - name: controlPlaneOmitAvailabilityZone + required: false + schema: + openAPIV3Schema: + type: boolean + default: {{ .Values.variables.controlPlaneOmitAvailabilityZone }} + description: |- + Omit availability zone when creating control plane nodes, letting the + Nova scheduler decide based on other scheduling constraints. + # Access management + - name: sshKeyName + required: false + schema: + openAPIV3Schema: + type: string + default: {{ .Values.variables.sshKeyName | quote }} + example: "capi-keypair" + description: "SSH key to inject into all nodes (for debugging)." + - name: securityGroups + required: false + schema: + openAPIV3Schema: + type: array + default: {{ .Values.variables.securityGroups | toJson }} + example: ["security-group-1"] + description: |- + Extra security groups by name for all nodes. + Ignored if securityGroupIDs is set. Override per CP/worker via topology. + items: + type: string + - name: securityGroupIDs + required: false + schema: + openAPIV3Schema: + format: "uuid4" + type: array + default: {{ .Values.variables.securityGroupIDs | toJson }} + example: ["9ae2f488-30a3-4629-bd51-07acb8eb4278"] + description: |- + Extra security groups by UUID for all nodes. + Takes precedence over securityGroups. Override per CP/worker via topology. + items: + type: string + - name: identityRef + required: false + schema: + openAPIV3Schema: + type: object + default: {{ .Values.variables.identityRef | toJson }} + properties: + name: + type: string + example: "openstack" + default: {{ .Values.variables.identityRef.name | quote }} + description: "The name of the secret that carries the OpenStack clouds.yaml" + cloudName: + type: string + example: "openstack" + default: {{ .Values.variables.identityRef.cloudName | quote }} + description: "The name of the cloud to use from the clouds.yaml" + # API server + - name: disableAPIServerFloatingIP + required: false + schema: + openAPIV3Schema: + type: boolean + default: {{ .Values.variables.disableAPIServerFloatingIP }} + description: "Disable the floating IP on the API server load balancer." + - name: certSANs + required: false + schema: + openAPIV3Schema: + type: array + default: {{ .Values.variables.certSANs | toJson }} + example: ["mydomain.example"] + description: "Extra Subject Alternative Names for the API server TLS certificate." + items: + type: string + - name: apiServerLoadBalancer + required: false + schema: + openAPIV3Schema: + type: string + default: {{ .Values.variables.apiServerLoadBalancer | quote }} + example: "octavia-ovn" + description: |- + Load balancer in front of the API server. + Options: none, octavia-amphora, octavia-ovn (default). + - name: apiServerAllowedCIDRs + required: false + schema: + openAPIV3Schema: + type: array + example: ["192.168.10.0/24"] + description: |- + Restrict access to the API server to these CIDRs (network-level ACL). + Requires amphora as load balancer provider (CAPO >= v2.12). + Ensure the management cluster's outgoing IP is included, + otherwise CAPO cannot reconcile the workload cluster. + items: + type: string + - name: oidcConfig + required: false + schema: + openAPIV3Schema: + type: object + properties: + clientID: + type: string + example: "kubectl" + description: "A client id that all tokens must be issued for." + issuerURL: + type: string + example: "https://dex.k8s.scs.community" + description: >- + URL of the provider that allows the API server to + discover public signing keys. Only URLs that use the https:// scheme are + accepted. This is typically the provider's discovery URL, changed to have an + empty path. + usernameClaim: + type: string + example: "preferred_username" + default: {{ .Values.variables.oidcConfig.usernameClaim | quote }} + description: >- + JWT claim to use as the user name. By default sub, + which is expected to be a unique identifier of the end user. Admins can choose + other claims, such as email or name, depending on their provider. However, + claims other than email will be prefixed with the issuer URL to prevent naming + clashes with other plugins. + groupsClaim: + type: string + example: "groups" + default: {{ .Values.variables.oidcConfig.groupsClaim | quote }} + description: "JWT claim to use as the user's group. If the claim is present it must be an array of strings." + usernamePrefix: + type: string + example: "oidc:" + default: {{ .Values.variables.oidcConfig.usernamePrefix | quote }} + description: >- + Prefix prepended to username claims to prevent + clashes with existing names (such as system: users). For example, the value + oidc: will create usernames like oidc:jane.doe. If this flag isn't provided and + --oidc-username-claim is a value other than email the prefix defaults to ( + Issuer URL )# where ( Issuer URL ) is the value of --oidc-issuer-url. The value + - can be used to disable all prefixing. + groupsPrefix: + type: string + example: "oidc:" + default: {{ .Values.variables.oidcConfig.groupsPrefix | quote }} + description: >- + Prefix prepended to group claims to prevent clashes + with existing names (such as system: groups). For example, the value oidc: will + create group names like oidc:engineering and oidc:infra. + # Container runtime + - name: registryMirrors + required: false + schema: + openAPIV3Schema: + type: array + default: {{ .Values.variables.registryMirrors | toJson }} + description: "Registry mirrors for upstream container registries. Configures both containerd and CRI-O to pull through a mirror." + items: + type: object + properties: + hostnameUpstream: + type: string + example: "docker.io" + description: "The hostname of the upstream registry." + urlUpstream: + type: string + example: "https://registry-1.docker.io" + description: "The server URL of the upstream registry." + urlMirror: + type: string + example: "https://registry.example.com/v2/dockerhub" + description: "The URL of the mirror registry." + certMirror: + type: string + example: "" + description: "TLS certificate of the mirror in PEM format (optional)." + # + # Patches + # + patches: + # + # Patches for OpenStackClusterTemplate resource. + # + - name: apiServerLoadBalancerOctaviaAmphora + description: "Takes care of the patches that should be applied when variable apiServerLoadBalancer is set to octavia-amphora." + enabledIf: {{ `'{{ eq .apiServerLoadBalancer "octavia-amphora" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: replace + path: "/spec/template/spec/apiServerLoadBalancer/enabled" + value: true + - op: add + path: "/spec/template/spec/apiServerLoadBalancer/provider" + value: "amphora" + - name: apiServerLoadBalancerOctaviaOVN + description: "Takes care of the patches that should be applied when variable apiServerLoadBalancer is set to octavia-ovn." + enabledIf: {{ `'{{ eq .apiServerLoadBalancer "octavia-ovn" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: replace + path: "/spec/template/spec/apiServerLoadBalancer/enabled" + value: true + - op: add + path: "/spec/template/spec/apiServerLoadBalancer/provider" + value: "ovn" + - name: apiServerAllowedCIDRs + description: "Restricts API server access to the given CIDRs (requires amphora LB)." + enabledIf: {{ `'{{ and .apiServerAllowedCIDRs (eq .apiServerLoadBalancer "octavia-amphora") }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/apiServerLoadBalancer/allowedCIDRs" + valueFrom: + variable: apiServerAllowedCIDRs + - name: networkExternalID + description: "Sets the ID of an external OpenStack Network. This is necessary to get public internet to the VMs." + enabledIf: {{ `'{{ if .networkExternalID }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/externalNetwork" + value: {} + - op: add + path: "/spec/template/spec/externalNetwork/id" + valueFrom: + variable: networkExternalID + - name: networkMTU + description: "Sets the network MTU when variable networkMTU exist in cluster resource." + enabledIf: {{ `'{{ if .networkMTU }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/networkMTU" + valueFrom: + variable: networkMTU + - name: controlPlaneAvailabilityZones + description: "Sets the availability zones which control plane machines may be deployed to." + enabledIf: {{ `'{{ if .controlPlaneAvailabilityZones }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/controlPlaneAvailabilityZones" + valueFrom: + variable: controlPlaneAvailabilityZones + - name: controlPlaneOmitAvailabilityZone + description: "Causes availability zone to be omitted when creating control plane nodes." + enabledIf: {{ `'{{ if .controlPlaneOmitAvailabilityZone }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/controlPlaneOmitAvailabilityZone" + valueFrom: + variable: controlPlaneOmitAvailabilityZone + - name: identityRef + description: "Sets the OpenStack identity reference." + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: /spec/template/spec/identityRef + valueFrom: + variable: identityRef + - name: nodeCIDRSubnet + description: |- + Sets the NodeCIDR for the OpenStack Subnet to be created. + Cluster actuator will create a network, a subnet with NodeCIDR, + and a router connected to this subnet. + enabledIf: {{ `'{{ if .nodeCIDR }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/managedSubnets" + valueFrom: + template: | + - cidr: '{{ `{{ .nodeCIDR }}` }}' + dnsNameservers: + {{ `{{- range .dnsNameservers }}` }} + - {{ `{{ . }}` }} + {{ `{{- end }}` }} + - name: disableAPIServerFloatingIP + description: "DisableAPIServerFloatingIP controls whether a floating IP should be attached to the API server." + enabledIf: {{ `"{{ if .disableAPIServerFloatingIP }}true{{end}}"` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/disableAPIServerFloatingIP" + valueFrom: + variable: disableAPIServerFloatingIP + # + # Patches for OpenStackMachineTemplate resources (image). + # Image patches must stay separate because they use different builtin variables: + # - .builtin.controlPlane.version for CP + # - .builtin.machineDeployment.version for workers + # + - name: controlPlaneImage + description: "Sets the OpenStack image for control plane nodes." + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: /spec/template/spec/image + valueFrom: + template: | + {{ `{{ if .imageIsOrc }}imageRef{{ else }}filter{{ end }}` }}: + name: {{ `{{ .imageName }}{{ if .imageAddVersion }}-{{ .builtin.controlPlane.version }}{{ end }}` }} + - name: workerImage + description: "Sets the OpenStack image for worker nodes." + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: /spec/template/spec/image + valueFrom: + template: | + {{ `{{ if .imageIsOrc }}imageRef{{ else }}filter{{ end }}` }}: + name: {{ `{{ .imageName }}{{ if .imageAddVersion }}-{{ .builtin.machineDeployment.version }}{{ end }}` }} + # + # Unified machine patches -- target both CP and workers. + # Users can override per CP/worker via topology.controlPlane.variables.overrides + # and topology.workers.machineDeployments[].variables.overrides. + # + - name: flavor + description: "Sets the OpenStack instance flavor for all nodes." + enabledIf: {{ `'{{ ne .flavor "" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: true + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: replace + path: "/spec/template/spec/flavor" + valueFrom: + variable: flavor + - name: rootDisk + description: "Sets the root disk size in GiB. 0 means use ephemeral disk from flavor." + enabledIf: {{ `'{{ if .rootDisk }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: true + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/rootVolume" + valueFrom: + template: | + sizeGiB: {{ `{{ .rootDisk }}` }} + - name: serverGroupID + description: "Sets the server group for anti-affinity." + enabledIf: {{ `'{{ ne .serverGroupID "" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: true + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/serverGroup" + valueFrom: + template: | + id: {{ `{{ .serverGroupID }}` }} + - name: additionalBlockDevices + description: "Attaches additional Cinder volumes to nodes." + enabledIf: {{ `'{{ if .additionalBlockDevices }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: true + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: /spec/template/spec/additionalBlockDevices + valueFrom: + template: | + {{ `{{- range .additionalBlockDevices }}` }} + - name: {{ `{{ .name }}` }} + sizeGiB: {{ `{{ .sizeGiB }}` }} + storage: + type: Volume + volume: + type: {{ `{{ .type }}` }} + {{ `{{- end }}` }} + # + # Access patches -- target both CP and workers. + # securityGroupIDs takes precedence over securityGroups (last patch wins). + # + - name: securityGroups + description: "Sets security groups by name for all nodes." + enabledIf: {{ `'{{ if .securityGroups }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: true + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .securityGroups }} { filter: { name: {{ . }}}}, {{ end }} ]'` }} + - name: securityGroupIDs + description: "Sets security groups by UUID for all nodes. Takes precedence over securityGroups." + enabledIf: {{ `'{{ if .securityGroupIDs }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: true + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .securityGroupIDs }} { id: {{ . }} }, {{ end }} ]'` }} + - name: sshKeyName + description: "Sets the SSH key to inject into all nodes." + enabledIf: {{ `'{{ ne .sshKeyName "" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: true + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/sshKeyName" + valueFrom: + variable: sshKeyName + # + - name: certSANs + description: "certSANs sets extra Subject Alternative Names for the API Server signing cert." + enabledIf: {{ `'{{ if .certSANs }}true{{end}}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 + kind: KubeadmControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/certSANs" + valueFrom: + variable: certSANs + - name: oidcConfig + description: "Configure API Server to use external authentication service." + enabledIf: {{ `'{{ if and .oidcConfig .oidcConfig.clientID .oidcConfig.issuerURL }}true{{end}}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 + kind: KubeadmControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" + valueFrom: + template: | + name: oidc-client-id + value: {{ `'{{ .oidcConfig.clientID }}'` }} + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" + valueFrom: + template: | + name: oidc-issuer-url + value: {{ `'{{ .oidcConfig.issuerURL }}'` }} + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" + valueFrom: + template: | + name: oidc-username-claim + value: {{ `'{{ .oidcConfig.usernameClaim }}'` }} + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" + valueFrom: + template: | + name: oidc-groups-claim + value: {{ `'{{ .oidcConfig.groupsClaim }}'` }} + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" + valueFrom: + template: | + name: oidc-username-prefix + value: {{ `'{{ .oidcConfig.usernamePrefix }}'` }} + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" + valueFrom: + template: | + name: oidc-groups-prefix + value: {{ `'{{ .oidcConfig.groupsPrefix }}'` }} + # + # Registry mirror patches + # + - name: registryMirrorsControlPlane + description: "Configure registry mirrors on control plane nodes (containerd + CRI-O)." + enabledIf: {{ `'{{ if .registryMirrors }}true{{end}}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 + kind: KubeadmControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/files" + valueFrom: + template: | + {{ `{{- range $r := .registryMirrors }}` }} + - content: | + server = "{{ `{{ $r.urlUpstream }}` }}" + [host."{{ `{{ $r.urlMirror }}` }}"] + capabilities = ["pull","resolve"] + override_path = true + owner: root:root + path: /etc/containerd/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/hosts.toml + permissions: "0644" + - content: | + [[registry]] + prefix = "{{ `{{ $r.hostnameUpstream }}` }}" + location = "{{ `{{ $r.hostnameUpstream }}` }}" + [[registry.mirror]] + location = "{{ `{{ $r.urlMirror }}` }}" + owner: root:root + path: /etc/containers/registries.conf.d/50-mirror-{{ `{{ $r.hostnameUpstream }}` }}.conf + permissions: "0644" + {{ `{{- if $r.certMirror }}` }} + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containerd/certs/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containers/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + {{ `{{- end }}` }} + {{ `{{- end }}` }} + - name: registryMirrorsWorker + description: "Configure registry mirrors on worker nodes (containerd + CRI-O)." + enabledIf: {{ `'{{ if .registryMirrors }}true{{end}}'` }} + definitions: + - selector: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 + kind: KubeadmConfigTemplate + matchResources: + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/files" + valueFrom: + template: | + {{ `{{- range $r := .registryMirrors }}` }} + - content: | + server = "{{ `{{ $r.urlUpstream }}` }}" + [host."{{ `{{ $r.urlMirror }}` }}"] + capabilities = ["pull","resolve"] + override_path = true + owner: root:root + path: /etc/containerd/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/hosts.toml + permissions: "0644" + - content: | + [[registry]] + prefix = "{{ `{{ $r.hostnameUpstream }}` }}" + location = "{{ `{{ $r.hostnameUpstream }}` }}" + [[registry.mirror]] + location = "{{ `{{ $r.urlMirror }}` }}" + owner: root:root + path: /etc/containers/registries.conf.d/50-mirror-{{ `{{ $r.hostnameUpstream }}` }}.conf + permissions: "0644" + {{ `{{- if $r.certMirror }}` }} + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containerd/certs/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containers/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + {{ `{{- end }}` }} + {{ `{{- end }}` }} diff --git a/providers/openstack/scs/1-35/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml b/providers/openstack/scs/1-35/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml new file mode 100644 index 00000000..732d78b3 --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml @@ -0,0 +1,15 @@ +apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 +kind: KubeadmConfigTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker +spec: + template: + spec: + joinConfiguration: + nodeRegistration: + kubeletExtraArgs: + - name: cloud-provider + value: external + - name: provider-id + value: 'openstack:///{{ `{{ instance_id }}` }}' + name: '{{ `{{ local_hostname }}` }}' diff --git a/providers/openstack/scs/1-35/cluster-class/templates/kubeadm-control-plane-template.yaml b/providers/openstack/scs/1-35/cluster-class/templates/kubeadm-control-plane-template.yaml new file mode 100644 index 00000000..b199f36c --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-class/templates/kubeadm-control-plane-template.yaml @@ -0,0 +1,109 @@ +apiVersion: controlplane.cluster.x-k8s.io/v1beta2 +kind: KubeadmControlPlaneTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane +spec: + template: + spec: + kubeadmConfigSpec: + clusterConfiguration: + controllerManager: + extraArgs: + - name: cloud-provider + value: external + - name: bind-address + value: 0.0.0.0 + - name: secure-port + value: "10257" + - name: profiling + value: "false" + - name: terminated-pod-gc-threshold + value: "100" + scheduler: + extraArgs: + - name: bind-address + value: 0.0.0.0 + - name: secure-port + value: "10259" + - name: profiling + value: "false" + etcd: + local: + dataDir: /var/lib/etcd + extraArgs: + - name: listen-metrics-urls + value: http://0.0.0.0:2381 + - name: auto-compaction-mode + value: periodic + - name: auto-compaction-retention + value: 8h + - name: election-timeout + value: "2500" + - name: heartbeat-interval + value: "250" + - name: snapshot-count + value: "6400" + files: + - content: | + --- + apiVersion: kubeproxy.config.k8s.io/v1alpha1 + kind: KubeProxyConfiguration + metricsBindAddress: "0.0.0.0:10249" + path: /etc/kube-proxy-config.yaml + - content: | + #!/usr/bin/env bash + + # + # (PK) I couldn't find a better/simpler way to conifgure it. See: + # https://github.com/kubernetes-sigs/cluster-api/issues/4512 + # + + set -o errexit + set -o nounset + set -o pipefail + + dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + readonly dir + + # Exit fast if already appended. + if [[ ! -f ${dir}/kube-proxy-config.yaml ]]; then + exit 0 + fi + + # kubeadm config is in different directory in Flatcar (/etc) and Ubuntu (/run/kubeadm). + kubeadm_file="/etc/kubeadm.yml" + if [[ ! -f ${kubeadm_file} ]]; then + kubeadm_file="/run/kubeadm/kubeadm.yaml" + fi + + # Run this script only if this is the init node. + if [[ ! -f ${kubeadm_file} ]]; then + exit 0 + fi + + # Append kube-proxy-config.yaml to kubeadm config and delete it + cat "${dir}/kube-proxy-config.yaml" >> "${kubeadm_file}" + rm "${dir}/kube-proxy-config.yaml" + + echo success > /tmp/kube-proxy-patch + owner: root:root + path: /etc/kube-proxy-patch.sh + permissions: "0755" + preKubeadmCommands: + - bash /etc/kube-proxy-patch.sh + initConfiguration: + nodeRegistration: + kubeletExtraArgs: + - name: cloud-provider + value: external + - name: provider-id + value: 'openstack:///{{ `{{ instance_id }}` }}' + name: '{{ `{{ local_hostname }}` }}' + joinConfiguration: + nodeRegistration: + kubeletExtraArgs: + - name: cloud-provider + value: external + - name: provider-id + value: 'openstack:///{{ `{{ instance_id }}` }}' + name: '{{ `{{ local_hostname }}` }}' diff --git a/providers/openstack/scs/1-35/cluster-class/templates/openstack-cluster-template.yaml b/providers/openstack/scs/1-35/cluster-class/templates/openstack-cluster-template.yaml new file mode 100644 index 00000000..6d96089f --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-class/templates/openstack-cluster-template.yaml @@ -0,0 +1,91 @@ +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: OpenStackClusterTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-cluster +spec: + template: + spec: + identityRef: + cloudName: overridden-by-patch + name: overridden-by-patch + apiServerLoadBalancer: + enabled: false + managedSecurityGroups: + allNodesSecurityGroupRules: + - remoteManagedGroups: + - controlplane + - worker + direction: ingress + etherType: IPv4 + name: VXLAN (Cilium) + portRangeMin: 8472 + portRangeMax: 8472 + protocol: udp + description: "Allow VXLAN traffic for Cilium" + - remoteManagedGroups: + - controlplane + - worker + direction: ingress + etherType: IPv4 + name: HealthCheck (Cilium) + portRangeMin: 4240 + portRangeMax: 4240 + protocol: tcp + description: "Allow HealthCheck traffic for Cilium" + - remoteManagedGroups: + - controlplane + - worker + direction: ingress + etherType: IPv4 + name: Hubble (Cilium) + portRangeMin: 4244 + portRangeMax: 4244 + protocol: tcp + description: "Allow Hubble traffic for Cilium" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus kube-proxy exporter + portRangeMin: 10249 + portRangeMax: 10249 + protocol: tcp + description: "Allow Prometheus traffic for kube-proxy exporter" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus kube-controller-manager exporter + portRangeMin: 10257 + portRangeMax: 10257 + protocol: tcp + description: "Allow Prometheus traffic for kube-controller-manager exporter" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus kube-scheduler exporter + portRangeMin: 10259 + portRangeMax: 10259 + protocol: tcp + description: "Allow Prometheus traffic for kube-scheduler exporter" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus node exporter + portRangeMin: 9100 + portRangeMax: 9100 + protocol: tcp + description: "Allow Prometheus traffic for scraping node exporter" + controlPlaneNodesSecurityGroupRules: + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus etcd exporter + portRangeMin: 2381 + portRangeMax: 2381 + protocol: tcp + description: "Allow Prometheus traffic for scraping etcd exporter" diff --git a/providers/openstack/scs/1-35/cluster-class/templates/openstack-machine-template-control-plane.yaml b/providers/openstack/scs/1-35/cluster-class/templates/openstack-machine-template-control-plane.yaml new file mode 100644 index 00000000..703c1b1c --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-class/templates/openstack-machine-template-control-plane.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: OpenStackMachineTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane +spec: + template: + spec: + flavor: overridden-by-patch + image: + imageRef: + name: overridden-by-patch diff --git a/providers/openstack/scs/1-35/cluster-class/templates/openstack-machine-template-worker.yaml b/providers/openstack/scs/1-35/cluster-class/templates/openstack-machine-template-worker.yaml new file mode 100644 index 00000000..920dbb0a --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-class/templates/openstack-machine-template-worker.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: OpenStackMachineTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker +spec: + template: + spec: + flavor: overridden-by-patch + image: + imageRef: + name: overridden-by-patch diff --git a/providers/openstack/scs/1-35/cluster-class/values.yaml b/providers/openstack/scs/1-35/cluster-class/values.yaml new file mode 100644 index 00000000..1344cf85 --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-class/values.yaml @@ -0,0 +1,50 @@ +# ClusterClass variable defaults +# These are referenced by the ClusterClass template and can be overridden per deployment. +# Variables apply to all nodes by default. Use topology.controlPlane.variables.overrides +# or topology.workers.machineDeployments[].variables.overrides to differentiate. +variables: + # Image configuration + imageName: "ubuntu-capi-image" + imageIsOrc: false + imageAddVersion: true + + # API server + disableAPIServerFloatingIP: false + apiServerLoadBalancer: "octavia-ovn" + certSANs: [] + + # Network + dnsNameservers: ["9.9.9.9", "149.112.112.112"] + nodeCIDR: "10.8.0.0/20" + + # Machine configuration (override per CP/worker via topology) + flavor: "SCS-2V-4" + rootDisk: 50 + serverGroupID: "" + additionalBlockDevices: [] + + # Access management + sshKeyName: "" + securityGroups: [] + securityGroupIDs: [] + + # Cluster-level (control plane only) + controlPlaneAvailabilityZones: [] + controlPlaneOmitAvailabilityZone: false + + # Identity + identityRef: + name: "openstack" + cloudName: "openstack" + + # Container runtime + registryMirrors: [] + + # OIDC + oidcConfig: + clientID: "" + issuerURL: "" + usernameClaim: "preferred_username" + groupsClaim: "groups" + usernamePrefix: "oidc:" + groupsPrefix: "oidc:" diff --git a/providers/openstack/scs/1-35/clusteraddon.yaml b/providers/openstack/scs/1-35/clusteraddon.yaml new file mode 100644 index 00000000..bea5fc78 --- /dev/null +++ b/providers/openstack/scs/1-35/clusteraddon.yaml @@ -0,0 +1,30 @@ +apiVersion: clusteraddonconfig.x-k8s.io/v1alpha1 +clusterAddonVersion: clusteraddons.clusterstack.x-k8s.io/v1alpha1 +addonStages: + AfterControlPlaneInitialized: + - name: cni + action: apply + - name: metrics-server + action: apply + - name: csi + action: apply + - name: ccm + action: apply + BeforeClusterUpgrade: + - name: cni + action: apply + - name: metrics-server + action: apply + - name: csi + action: apply + - name: ccm + action: apply + AfterClusterUpgrade: + - name: cni + action: apply + - name: metrics-server + action: apply + - name: csi + action: apply + - name: ccm + action: apply diff --git a/providers/openstack/scs/1-35/stack.yaml b/providers/openstack/scs/1-35/stack.yaml new file mode 100644 index 00000000..e302c418 --- /dev/null +++ b/providers/openstack/scs/1-35/stack.yaml @@ -0,0 +1,7 @@ +provider: openstack +clusterStackName: scs +kubernetesVersion: 1.35 + +addons: + ccm: 2.35.x + csi: 2.35.x diff --git a/providers/openstack/scs/README.md b/providers/openstack/scs/README.md deleted file mode 100644 index 6533d842..00000000 --- a/providers/openstack/scs/README.md +++ /dev/null @@ -1,120 +0,0 @@ -# Cluster Stacks - -## Getting started - -```sh -# Create bootstrap cluster -kind create cluster - -# Init Cluster API -export CLUSTER_TOPOLOGY=true -export EXP_CLUSTER_RESOURCE_SET=true -export EXP_RUNTIME_SDK=true -kubectl apply -f https://github.com/k-orc/openstack-resource-controller/releases/latest/download/install.yaml -clusterctl init --infrastructure openstack - -kubectl -n capi-system rollout status deployment -kubectl -n capo-system rollout status deployment - -``` - -values.yaml - -``` -clusterStackVariables: - ociRepository: registry.scs.community/kaas/cluster-stacks -controllerManager: - rbac: - additionalRules: - - apiGroups: - - "openstack.k-orc.cloud" - resources: - - "images" - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -``` - -``` -# Install CSO and CSPO -helm upgrade -i cso \ --n cso-system \ ---create-namespace \ -oci://registry.scs.community/cluster-stacks/cso \ ---values values.yaml - -kubectl create namespace cluster -``` - -``` -# Add secret using csp-helper chart -helm upgrade -i openstack-secrets -n cluster --create-namespace https://github.com/SovereignCloudStack/openstack-csp-helper/releases/latest/download/openstack-csp-helper.tgz -f -``` - -```sh -cat < /tmp/kubeconfig -kubectl get nodes --kubeconfig /tmp/kubeconfig -``` diff --git a/providers/openstack/scs/cluster-addon/ccm/Chart.lock b/providers/openstack/scs/cluster-addon/ccm/Chart.lock deleted file mode 100644 index ed7ad098..00000000 --- a/providers/openstack/scs/cluster-addon/ccm/Chart.lock +++ /dev/null @@ -1,6 +0,0 @@ -dependencies: -- name: openstack-cloud-controller-manager - repository: https://kubernetes.github.io/cloud-provider-openstack - version: 2.32.0 -digest: sha256:251e0efadca5de3ef39707c196a64a5d5292427faf4163ce8295d416d6e8c355 -generated: "2025-05-28T21:50:59.574735035+02:00" diff --git a/providers/openstack/scs/cluster-addon/ccm/charts/openstack-cloud-controller-manager-2.32.0.tgz b/providers/openstack/scs/cluster-addon/ccm/charts/openstack-cloud-controller-manager-2.32.0.tgz deleted file mode 100644 index 9e97d80575b00ddf493c9e13f4123aa9e66b9e63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18995 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 z z_S$QP=9h(eGiLwx8g@^i6s2m%MQ*P(C;YN3Rf);GL2GS6U~4S7YJQdH&Q(3*+tXHIE$Zg9NZwJaD9{8xe)OF-5Sut?YE*k8lCF26&#e^%^AiWt zMliE0KcG0MP;SPbJlH;a_VUNazn;B(@%_`k?z9wkPBqBFKVH0k^6J+Yr_Z0X6?WsB zPs(H;^@2Iaff>!f7~p|(!r(*)_&30pTazb>yBL|BCJFevK5SPdSlg~=2s>)~(gnM= z1E!9Pw7>!ru6&|HkNXYCIN^9@<-y*uj$Ce5Pa91)B0(BwQrc?N*5$S8h)mAg2PU-2 znZSd+w&ESQHeO$Y;2CA2GF^glwx2sE5Z$&_1aQYUEfl6gWR=$$qk={>8N%zcb8o$F zPzKw@{&_tVc&GfPUF5cVJ5^ZoTKMp0^m73>d95A=Y>sLGGzsF0g+Sv}E7yoT-UQPI zd0LsS8g109)6ItK;7~nITPayP|GEOPDxjMoRXZ_TDOx)ly9%uusGH(tk2lv)ayEZf zU}FJnhzL&S_0>_csJm3vc+(tfb(mbAwvpm)+ySgy%G!Z7od(L%S|O9|vGRJcZVs1) zH`s&87fW7SC7)%$MNF&(eknoDHyYWDct zewDA~*~tHA26)p&$=SFI`&(teXD^jr`vS{iA#R?@pd& z)7r)!tWf0Iw?zJxrugR`q)+%4$=_HIO z!&J?(UR`_`AlFt7nycXk;$wNg>M6?FWWt&?TU#z1USE(kL9Ux_wZWIp{f#49Ebs(Mrn?cA?)|Lqxckh5Ray~xUWG6w!Eb6fUSvwrKaPBmnK;tE4E?yIeZGSdrGwNohv} z$H_QU+0p(+ui&-HEhtcuc;4S>uFjy#wk%MU7V)jqpGf;D-r9y#20|rQkx(Z!#nAHt zc-zd~pyGRjoF#9|tb0G0+->mVt7l#Sr@5HoRFFs~@m43yWYG+9ennY4Y=xV1%;ci* zR=HetiKtS-)_+@qL|K-YcHA_WL)dOby)5m7v;-u63zW5Yw!MbJbH_n+ty+53c*J@54tr@-=b=y`A7 zzU5PIij$ej#ja8cM;UEP185RG654&!Q3=PK)Ko9bT!PAkk*%x|1O`E1=2UaKN#xa> z{ZVBuMJeESCc&b~VDCLmfwEXj3Y847QKra%{POJVxh9B$-JKq^B_C*+GgOCjDa1@d z7$^CuoMQ#eC1oR#*j%rsNRX=Ra)CZ01uB6uiKXTR6#}bp~<5fp9AAvUq3L%|Yu%W18gWI=fRz z#Jf%9U9dyS^GvFQIIgY(TngdWMfR?kVy==$n&PSpDvZdaEs1h2=*&zoZbZB!I}aro zrY;cJ)mKLp87<8q8Ll;id218=yp;p3 z(A_t0zZO+-4gF8w_tVoR$aQW5TxI|F_Zs`ZM~CR7!X(7CdzcKe@unN>`$Qn_C9UP337 z+i$@Pu}gJ9-z8p11x+2+GB;%fpi^UdL2)$03t*h5N~m~&?Lz%>KfP$Zd47eY#Cn=W zVf_+6Cy%p%VJ4(kxvHxOi3`@KskfmMJrrL3%C67NlK0OYJ{!ybYupC5YX4{NsP6yW zAMD+q|GkrE3(jb^U@GTZMGS4h#rfm^D|n`*e(+S8cVmLt5Khle&;IsA$$zQW6~aYJ zqOC1>s-$v}F?_Uuv!}hY#~>&*mp5dLMk>vED9ishza$ypcW9O+Sb03Xtu4>}+se5> zG2^|6y0vHH`R`g~B<4zuvJ+@T@tRnza%14!8 z66Rz!RGtrM3`rH^WG0ClP4N)QV%{q(a+U%DPRIHpty9fM3!OGnZLd@^>Y8mIhuxHo z97+H_Y{w)L>ShHW{O@1B^ugC_ln>hp;R5{cPG9-*-~aJ<^!cl1;@71u$SRq-J&ED? zan49OF~%)PCr>9SEnYu)hofAnx;i;mapjWTjZixcPu^t=b6!0fX^imq?;+GVKAm`L zYHsC`TDf~{M|HD#3c_U7G^TFjM2Er;YS9hYd^#zAR7__7uF2-?+SbU>#oT^Q{ zPdSddg+Qgw){D~m!{{E^eT~`75Ip#r#&XH%-C*E~F_V+s2Vc*YPcnkO30pPWy7+ep zWuvxDXR~unsS!E=Rak5Vs^*tXpia$3bWqs?ZqI06&|c7tCUmm+0dGKhr&2ip?mI>0 zwaHtlY7(HkFWuYtb0Q9_{xr2bhVs!rbf@L=tsae8Yh{GjMB3{T2|@gXcf zlw}=CxWyIeb|9>LFKRW2&{DHerdqqvdl!V11QtZ2;Psc8@iRYHsY3eSO9=7wHs`Djs(Sv^l zHfqX?uaAx7vZ#MME8<|Y^^h^yYv0g|O7&|6LZs&25#ST~vL(|NU;B&*uMEp=yXt zuz(*5ac-}5>=X?ZmXphX2{&oIrLMe&4VrmJOc1LB!!5X^RN$VhaMG*2uUv!a;`u{a zJFF+@>xAse5Yx)-w6NAfp>8A2GDb5-)TRAJoR13E$!HuC&T%z1kCe-DGb*7-4}1ewD0UWt&dT z>(a`^Gnc85vVM`Hi-rYcg|8K3-v9MNks4ag<*CdcF-9elpO|LGt7Pd9G3ZPmGz#p<4F zcfG{F`tJ0s%;>r1tkO4dtSxVDt`;r36=e+)E~|cfq&$1|cwGWUEZ%^AH)lPRk&^8< zB3(z#+oRg)`BRgxU`^VgjMxh6ldyjqBxHC(xL}*}UuWIhBjevBYtvMq1kPy;r@5d! zLJ4MTQgxc44HWnNP049|(L0qDR#I-SK`VnfNhiJeo+`3}xQz;3WL;VL_J-pPW`ac5 z9gO+?U~DoNTpdA`y*Mwg;4le@35Vj;cTObV-}iZ1jQ18l5D!3ih5mU zTnml$R=JMKwkIR6dd^jxIY&Um<;5m862vIFE1)c`8U!6xa{Xifi>UTV05Yi0g9l=0th z&wuU*fBMJHqo4ox$A3Kf`0;@c+ZFP_2N3i^Fi8n#SAOL}^EvnbZnFQYhHAyxuRpAj z|7q`_p8xl7u)lx5|9cnDUGD#?wq8#e%_mbm&-JvzwhJymb1}bKj z>p{W=c;3Xs?N$-?(=sAW66{pNLM2PRvZse_gODMHW+q`xy)_8H2;Kl}%@CeR1nAf) zEf+(zvC*wZ4m7lu!AO8V^(3G^}tmG##FR|4Vv) zdU5g7%U6#t;K{33FJBGe?=;u*(c}v$b`HY>%N z4EX`Zh$If{erYPwzw~nx{ZCykdUO5n__(S6^^fn*|KG`T=lY-HXUQ1NqyUs`q%tA3 zsUL>P)PKqtboJ*eN`-T<#ppj`igju5b@ZNJ4+vBWM%UV0ZLiSU%>KzjCAc4+Kv-Z9 z+D~p_dr@y|@!}TWq5{WKxe=79rFQ7FD=?dpRId(II08*vgVXh;w+B9&SVj%O;%wv9 zo1HxAs1bBUDLTJpb0wxRnP4tlbes;KfdAjMwZ`r9NeiHsUe_A0_N2Ka`QPx~H(dVD zua&Kjor7rwuZ9b%*@6bYImC5blxFHYrqLy4@KmvTF+%$OEl^3uVLQed!x0iV9;#fM zRX%4pjWKg?M>j5@oh6eoD~s2rWTEzV&dR}V_qV;Bt~tBTl&xUpsL;-PIfWE@`Yy)q z`C`&8xD&voQBc3l@RFqQQx5(NW#w6bvP1=k6F^xeN4D}-Uxii$Tw$(FVAbK2r%+t3 zs{U0uRAgvc;cW9I`?~4SCcat;LB_D{o=kE8?!|%*8E;#n z`v8F%+17G*A|@KUEFw$h_O@e5nyPEaH}lD%vfX{(xur?AIYw(?v&mHb+%oHaP`+yU zsjUUOgS1W331yg@*9^~zeBa9$Hog(%vVs4AF-npViY~d{^g~QQnE-d0{xr8+O`DlB zstF%Loi=wJX*sX&X+2OMUZcr6#zgOt-T=#B%;>Bwuk3TgwF(>ap57PkjS@QQ?d^{b z2Ql9N7WKb9I*JbQxIY?42YaLa@$vp)bi6<4ACHdv{hmQEoY1Xj2ZP|*!2uLqs_wtN z{omVa`|XcA+rNb7*MGS`1NHqh_#YJf^OxWse>C697dwx3WQ{+rRqnxChgKEq#PZ4T z#B7Ck6f6vZI8^`li>l#s^qbWFi$6)~{&}wYyWGHlP%%SzTJYO)`=QhRzmfmP#GS8l z0Il%<92_1u{6G8s`}3c7^4$IT&%Y|awZI@nr({r^N_AB^@CjF!J0U?v#Z4_q;vNt^ zJ%4H?HFvlEnxfV)r)i~F@67d8W+%*iHGi3J=TwSW`_24S-=8{ncB34a#YR-9G;g3| zeYpkUO8GV=ZQtG6vYn!K?`bMRP_LA#uuU1H`D}#Qjs&SMuh2or@KnGRNfH=g5DcYU z-3cL=q?n9V`bhx`G)o}HQcsXr6x&_M?1uqm5>1i{Ji(2vuE&ya?mpLRa~XyFY(qY* zTqsP9WkFGm!70`!C*Ep!vLdFa?Ql(Na4BW5jeoBIZ##yu(=63}Wk{64t6sBN186Q)a~*qfs!9D-52fE|Ll$kob3eM#U6NE5AW20P+D`dR z-JNrEQ2%b5Q>@x-@PlNr7L{$e-|JjQcBxB7Y#f%915!nLy~r@A{p@N0>T1DYV}H&K z6m$RJDwb_la~N&ACG`K>jf3MF(Q)ebn&n|{nmE^sX)|3rOAzNu+T4hqPixRNq-(SK z(s;;gkoS{Qw;*nWnJ8{dD$=1lS8gcj`TE%fa5718?8ypbOd_dYscX^Xa%%3XF@$le zd#~`gyeYzMO-ibmM!c8HaqAf(_qK`w4O}p*2Uo&2x4X$@0CQ0qFAk&*^To<(r{l}5 zmnE&*C>1TLrilN57f=E1*9l>+q0?L8Op5uns;wwqO+q9R9-wH3{hi9qoL_S2pL%Yh z|C=OHH{Jg|7#!5^|2ga*-tYh3$#eJmzoX3C8+^@`zik4jao6v5TN1MBHMz}PWXtnS zHQI1i9F)f8{I4~;Q~6>Vb0Yn0u9o46m6VO#WqO5cEMMx{V0$Zaq0#zNoXkQ#?HNZ< zM>XpvD(f}dRl!kd6Ol#Vb^N+5b=Kvpc<`%sqll7NEiD&b ztguCvmWD^u)j7jV1r0c!&T_w^lDEz_ztfV@aHpZDw~u3MR=3JXNEDN=uPP3OqSI4yTOx_G{Qi{LzVt&0SURpiFJrr+n!-`>0~nDyVm~ayMRR$dVix= zc#hew21I63%`OTgf||>) zossDB8oL`Sl9ukk2j%1bJDMe}@={OWWrB3aHhEJazqzp9QLcM+2mEbHs!gTN7pvpm zKi$tw^ncAUE7kv%`u}micK`d{!SUeeUjM&~=g#9lpS;WDA|RKX)HukCD~h|96<0U~ zWf7AKQPovq$gbP6oGEINO~+ zWK@>XqC{P|_tEI|Sxa)U$BT^(AL$Qx{z(_9v8I1(+N*pCIT*fO03)}p)X zLVS>#7~r+})xonMhlWyJa|>K6Z#PD;&V?0dy4_8fZMn+7k_q)L6;7kYSILXru2jv9 zW%nG$v?hpb#%x8F<;=Mpqk*bF-WW^Pa!PGT|36G4KqkYiQ?Ng<-M zkve^5uiIX|C@VNEE3?~g-Igox)SH{H(x`JGQ%Mobi9`1|;6q-LXx-(bt)I2w?y&I6 zITh|KvjPq9aAzY@+))3!iTqfCSq{5m?s zo{iuBE$6~G9}D8|s462AU0xv;mjPHLAz~3K4E{*cF&(xAfEIUuYZ)OJ#=RLLX|H-I z_?GRxZdcK?F&yq0{*sRRE^*@`P8M=yzrrv@bJ>YGW+`)U8xpcO&j-r?&l*x*C?nxor=pRrTvE z>LOZFz;?e~Rj5myOT1W1=v+EC@LWgc=<9GUu{!$a-wT$z5GX%S;5n9@rS^johEcqLNU8GiT63~Ym!dUPC$zVc z?8chR)j0^h$yO4-sa{W2&!01B$*HsF?|`4+bRwoc4EkyesT@NULsqrmE5DZp51Xmq z;8+MMEc;N8s>-Ypz`Vbrv~9k_6N~- zw3L<-XZ^+WbYgfj28Pm@&I-b5FnTy4fbS?xFiNYSewt#Hu|-g6SOQ<`T}2db^Ebd z3}io9jzf4lR%*VWn(cRiUy@8`N#fSzPVtpwD@C393T0@0NwV*b4saT+(XeXBZCPTn z3n_IM9p#uaxxq$=X~Y(pu+u@d`M*@0!cCe~kq*4@1EN@^5Mnep`x~3h@u*~Opj01^fY3sNl`O=MyB!%N7}7s zy6{kccvun48lkdb*|!qCmAX}PO)s>L2L)!s6YzZ~a;6x;S9@K`+4{Y}#Rpr|0yay8 zQ-08g&~(2jYPFTKTEnARz%}E&6UGvxldr{T z$kmrkJdpc2)OW9z=P?WRu$ph9^amIn9mF_-#jb|>C>Hva{{BnqTOwH8>eY8@lr(esYd&XL~s^lED~cCDrg;MGJc zwY&*9MWY1A)yzP4TA#Rcx$BVm1AD6C+z}G>)R^sjZ>nnUrXB{QVXxY>?~V~E~XhR<$XTBS-gIY4NX?b_g92v+oDJE2#Y z!M0qfPLphkV6d~J|9p|pFe4ERc3?ZtvSz6dCQatAe%G8)<#|L%VGC3< zwPVv^^gB+A(9zcG%cqhFC7Pp3sd`p0M+0wcqPav|vAyb7n*6;2aM|&!oMY~&St_qV z^?a_B1-n3{p?5{NI)TN32$O|a>vL93Fb3!vJ`25aZtAHp?YuETtVU|i7FyzdrCtTM zQyow~w>M!!D|)4NYe&Cu9u_yv`_9Wxt8CSRu-(?SJJc-qwiJVMr;b(ED8lM6H{nzZ zRHU1cAZ_zm>MHXBWJ4xYKdn9mdu2R22r`D}gyy_yJ0aHy=j%*%U2@fA|7^2&L+)P1 z+_twhKE7QZZrTc8$^8mn`g5cB4?!84;G5=u8SEYJHR3-GkMH9@?&A4e`G4K~TNj#- zK%Mz-6|9EQB3Yn3cwVGuk8t~LtW%&&EV?`*ves1Tg)$!$j%wr20UlO9y6UdUAXwkg zGYJD5)hb!uDql~QS6aGBu7qZ@mie@ee^-G3l_K@bZ+4Y+%jl#hzxcoSe%*huW?gOR zv}4xUn|rB#o6k+;e|hR|`3kT?{_h}M#H)WqkeuW@V@Gj4-Zh5El-Tn zY_MrkHo{Bh*22OpMQmYTY?mLXKKI%ZJMX`L@58pB|0k!Zskl?|(1&_BDgs|*32aA5 z$l4|r>_PYnCrFSvzEI_Zj~@$j%`e?g{tj%LgT9)Mm0G@OZKrxPzuf~SkuVLo$$p#8 zuDa)JZe8Eqp)kEi-gAW|(tb*P#ZYlaRg;_ToiuXH>^bnzz{nVmaIAC`mHsZZSI#KH zO@+vYz22-$v>BQxDMEWZV`S2kl;Yu3%#tldyFgAm^=qw}VUr|4YnFsA1|D>OswQ<_ zm!Sq^n4mwls*w*`w7bJ4O|c`zJ4m_z8Q!In%E>V4xV_Lr@$ z++_XNTaGtA|9jYP-2Wo~xL^P8;<@wnUnlb;slJ_5rP2}`jrwlKS+)0?K1t_q5VHww@VksM0aAik03Sb| zJlMX>M>rCRB98S4I>)w_=5VNa(wzJ(CMl!wp+P|5H%?P2#ZUbA%2yo95;_OqN4ZXa zu+db3x)r3p6n>YJBnF($VUCy%`qA;mNFT#oIgmA?=uxyIGg}0OH-Gie)Id3YXZ1zJ z#AgXnOAD7j)D?l!{52hRYN&0+GsN-tjjC#G4C)E!>utEkc_*|oe|uMnZO|K>A?&o+ z$Lh~=ud@?MN2snSHF32WW>$4Qf-6UEX}C!dJ*)rPXo@uem)>RB8xwrvj9}(6#TeLpN*9-G%S8S_ho5K#Z(*t2gq);2fIOhP`IOJ@Kqf4I8 z06CVB86MwSNzxob@JI*1JSz9Gw)F-1&r!B(&qa(WQ#V+V6f#=Lqq+}#`D z!8XSjwg%tsA0O?k)+i!_@N0u@L065j-_y6q&rqiJoHrF}P4ET7P%V-yTH7|szY$Qj zM))1#_*_a6Wp?ej>w96Q7;A*ih|qw2H$k?4Nm8}08WQ=X#qt686zv@z$szkXte5-e zrq506zbnM#^j&EJuH63{H1_`v_V4q*-pO<4_TNvMX#kCAF3g4RlHru-Mw4Yiy5Kic z{xzB`xi9AxrDB;(ZiVLVnWeKb^jM@$|)Chw!TGJ;YexNZ=T@r48m>75eexP`;66K7QQU1*2o8#ogEX zLI)jFnJj=|*-f09QyyqeN!+BiQa=%7jKk8>kI!Fyr&jwx?OC?d~Q;w~vzRRsFd2-}um`jQt5fL+c(d+|>Z?QakAR`vSzV)Lpe z%6^~bT)?UBKunEeuk*fvPg};jj23y^(J%-SBbTKRrrAw(bt#i%4edF{ zq3&_M!!(;MpEijjO5$Z}c zNcKpHd)TBjhRdkqTAS5#4@#||Oe+o#>SzDe4Mg=U5-P@K$9^;f(U+1`vjsGce(<`t za+6~-jqY}ynQ38Oe-&b-NALqm1qam5P||lhhyqz#Y&iZ zZq@T_xTio`?oMq3xi+BN8hA5v%dTuT_Ex&J5BmCpVd1PX3a)Rsdqix9`B84?k)vZgD7+bb1l>tKl|KHtQhn9ENdIi>X-X?`>*=BE&FeV z7(wx<)fKqH{yRS2uiyW4e7Jva|J}*+`Ru>vl4Bl!ck9-l<-{+>_Ol)BwEWbW#hY1u zwvA6?^RBbQ$rX5_%g&Fhu*SDUZP>%YQTLL$W?UEOJxXhQu$P2&0} zb(+PMg59RE;gLWN@hw}&6%4AqQAoOsa-DoJ4TkHu9L+h}q%*2Wxn09yr+L8Xww(}` z(oG`YsvDwy-pcL1zChZQKE5*-{3S%_m{i@|*HV5BCM;`x0y4GXw47r9E{>daR=`{H z<=mn&unykWaOPOBE%2`6$XOH2ExU1SCpH-OYr1f{pxn@bW5Ism?i*K}ZZgLzO5G=P z-h9I8R_Ve`=9#-9Z8EVs>fN$qPR`#AMz&P5zsgZ9(O)s5u9R#WG57s--Eg<&|DoxG zUd#V8=pP=`&;K4D9vjQ$_VF^|9d4~8dyzI`W2E6`o#tZtq~sL`PQe!)ZYcHoNjIBq;xBQrSYeBP0`HghLAWS~ z>i!MfV!DDm$!^kW0cW){P9^%QcDaVjr}xR#R8%W=H`A;J7k~UFZ#J1JS(1C z^Z%*5+wBKISH*uF9W>&<_WSqeKkwxEjQ+pH#s7MC`;Na#{`b%C_EVkwq>*2$g-_(> zs}plGC!eZ)D=t14(lsN#D#%R1V@i_K^BrZkJSQnuU7Zx-*i zYg^{VEv1-gVo`Ok7z#N9xe*t-qgak!)sN89M14G#5PMUH!cyj3YXm;6dtrIcz9dJ( z_0;DWMNe$L^39AY5&P8Q;531^P&4}r^Q?Ip@(~iTQnp0x@ZjD+dC%fxxsE+z) z^EItrK{xY6tcK?-bed55E~~YqVJCyu8(AGN+ioe_%=O##F?Dch_aW_6yR*ce?rdXI zz_;wGa=N%7$6U{8wFJ-&Jy-5vecA3S>+EQ#gc}Y)hg$gwoguet9<1G?Pa-|aqCXZJ zqN^o{(v_Q2a|z#qJIAty%2=)nNhPX!S;@4MwHvEYdyAhylDNw1^+ZWk-TjKWxV1mG zC*52^Zz$>NP*;GuWqIeemR(=uwc55{F}rm3X$8x4utZjy<}N=hE4Qit__qAN49BGF zB8V07e@6%Xqk8<`-of6z|MxDQ+wuQi%K`jKvdpJB{;Rq1i}C#`bh_QX>hsM!zPg1^ zwlmhJ4&f@kcux5~8Ot|ZO+=x_# z3vo4(+@fofr-J2=9G~W5y7_n-7G8~G&hhz1XnqmieCMjll0r9$B&<-o+k;y4UE*(> zHhN#xW#`17)wpd*?3`P&+qYd6!JQ3n94F_t{`EVqSGl3IfL*p$w=W8tIiK2L+|V-9 zK)*)R=dT^L1gg};LM zkXwp!tcowY}zRqeO||F^<(oL%n9V}*Kk`P$w<=i1S0q2Hl< z$p*|lu-9BW>ewz+pxZTzUMQiJ*d|jHJzUR7?Hf>#K-g#Xc2FRSn-uANH$??lBkCY9b2jbYeh} z)X`AXJ;{~Rj-|2J;ZG^micWt@d%GMaIK>PJ9QW2gW@)bdU)8=YX8LB3iIG6J}TdL{Z64@8b3Ss5T^5{x!XN&>~lYuqvFJQBypV*eVsHQ5trccuO8^qio$qqD18wOsN=5nNqXx1M7 zlKzs~XvA;$FKLUVqZ`+l%aJn_2zNRHy{aAn4gTRb{9#k96&Ez4IEIHoAk`viZk;@k z#f=n@SfDfxr4?4PMmhSQ=H7mB&7_mt7ERv|NDn~_5Hv7{&D|a z|G$f83(k>{GKNd8)Z;1_j1)#WNn$B~GZbB-3Fe`<1+S-s1JARJGQojQagwNL6__Cr zO-VZ01%?wO$lNS=<$ILI-WH^IqT`#lwNt^wk;4D66T-_hSpZE{O^Fg@m_b5P9D3p7 zi(fC~^5kv7nPNnEb9MnSVcZKRMD*0Z8htMu{gd_7zxKs+(v$z#KlwcEm4HSly38|W zwQ%oI$geW*Q8+@E-lI^=GVjsG-Z(}?#3%}B(1i7xs9;|U!0tKpk1S!PFh zt|yGa=%8EcCF4gdi(KuJT z4F3hM*#dsn?jCrJ(tqY~T9$@0Bv3*p&1wSc(PaFbMbd|L6r8z<&5` z*#D!hBkfQuVW>YZG8_?<07wqwnq>(dLNU8M&zXfQntD}~RMgY(v z8;0zq6Oz93Jb@;wz-l-GOdW(1Jgd=2C%&q}fB|OBWA!^{$n)6J+tH;}=w%Gt6-+9%_8_em^e} zLX4q>2S5bYUWnzGS|d7H0Jlh|8i?e|0)5jCY?mi}5HBVm-w8c0#-rRJ_egIEHb|rU zqPAbi7#1{#E1D-UB;*pyFC1gQ?*v0Y(lKQ-Eh6-~z}-R8n}w3MPg5XLUr8fufQ|6b z+bY=7<+!bi;IHz_mZX2j=x>2D{NRp+w*|WE1|ubR<-9tVQlAT)M!1|=W+Qkdo$T(Z zE-#f{3oCSqAnE9NTu_E4_$)!3uj0+WPAS(8ml8n%Z)lRwFkEOrpdjtS)3N0}Kk#?K zZRUkqs(jzb+?a5QCx>{UmP;+I<)TN@Nd;p+@I4Pj5$60kjqxgUPgO~n(YOpa?!>#& z&?}7MpBNGNr5XTE{<*TT3-V9AK?~phm3W>sXjZ{JH(tI`IgoJ$l3hucva@9!J?LAN z;m~y50*A0Q0&IbzfI@mpnPE<|1z{BHXg}1_$r;9Ee!Naym*c> zsrwDI}kQ9(Xd>1&KVGv}7 zxdd>9QZ*?jvbNUjm<~wDDCcBE5+WA5S*80;F=4z;4XIk#(cLN~dC$;Fy$^z*U41oF zxxVfSkf0Gx%nF6FZ2hWw2w!IC_Z$~Ga5)<9-$QY*!n%7u!06}zr%`ca^W#Ua(&;!$ z${vSdXxxlAoezt{%hs$oKfSp4>E)}(#Rt&7#qSxN4V{-@2(1Fn#?>!N$Yn=I_t-d! z8(9$APZ-wsCaO(d;>FSyFCEYHx(%B1R%Kx0$B&AB-j=&bz@89hfaer`)P2zQP-mVG^yzzFi8k?tMDUrsMMRMh&yx77w z2gB7Kgp^q%NtAAWr3G^ZP;v=YTh0#f^h*~0AkC`Wm1piX>$w?@ko-&Y*}uvT-Vnz}2N1+NUK$?M zIA?gSyy@m8*YqIV?QqU9ysc^^Z(-X=4Mm610!CC!b)`MkzkN%52ap_E?W|M%ufp7G z!qnasplPz$3E}6zVTPCcJFTBn@&L++=~SS^`i?StBa}*}ieW|-jY?w|jcBgtm4nw? zy>T9T0sJVbpw%GbR(;#RX}%v;&>ExPdt0V-ppXj;o3Fq|Hu$>AO%e|dnKpl|vi(RA zRxS6B)V2vs5r>gf-YJW*WN4IRQ#8VYL~2GU4R=asbb?bXNB#;ev>q&1fGI9~g^q8` z@YtDT))+J6#Z+5Uxt;BLqL6}$13B62^?q0LiXtUZg3uYm)W#nVGYz z;PI(68PbOW#!_^lbOD6(9P{;?)H=GEM}|>(gj2)_l>$P_Y%@~E>EhOoDdw_yE3)i- zMCn2fWQj)E3uPzNCY#$eJ!vG`d>P(VVwkmr;W1`bQ2k8DW1^gns#r{;Tv|42%$84{ z`QESd{ayaUjv#Q~&M<8Ra4X z+p>UfKKVO;_9XAj&_c1Hl)j1w)bz&hBAlt|AJJKs5N$%+rNe*-y}ug(bR>|P@e)FcL2UtGY;@8R^t-{FU+FCOm#CfcR< zPKr)05hOFUw1?gW#+Amct4lj&NdyT>Cpns6n9w<9soH|aY(}_VD^VJI37HXLBx0jG e;a9Z%eE-}(_s?g1{{H{~0RR8^FZkjB(g6VYqpmFg diff --git a/providers/openstack/scs/cluster-addon/cni/Chart.lock b/providers/openstack/scs/cluster-addon/cni/Chart.lock deleted file mode 100644 index c6919af3..00000000 --- a/providers/openstack/scs/cluster-addon/cni/Chart.lock +++ /dev/null @@ -1,6 +0,0 @@ -dependencies: -- name: cilium - repository: https://helm.cilium.io/ - version: 1.17.4 -digest: sha256:bbf6df8f80e3eef4e48b2f767734b19ec0c69aa23afc12f5a2f04587383ef089 -generated: "2025-05-28T21:50:53.229284267+02:00" \ No newline at end of file diff --git a/providers/openstack/scs/cluster-addon/cni/charts/cilium-1.17.4.tgz b/providers/openstack/scs/cluster-addon/cni/charts/cilium-1.17.4.tgz deleted file mode 100644 index 06abb163c5ababce8c966d0f9077411a26a77299..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 219592 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μd;g8c&Xg-L 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 E0DNNO0ssI2 diff --git a/providers/openstack/scs/cluster-addon/csi/Chart.lock b/providers/openstack/scs/cluster-addon/csi/Chart.lock deleted file mode 100644 index 033ad7f2..00000000 --- a/providers/openstack/scs/cluster-addon/csi/Chart.lock +++ /dev/null @@ -1,6 +0,0 @@ -dependencies: -- name: openstack-cinder-csi - repository: https://kubernetes.github.io/cloud-provider-openstack - version: 2.32.0 -digest: sha256:86fda0773b17ffa6ae496715f005d132d987a79cbf9a7c16ca5eb96942fb630b -generated: "2025-05-28T21:50:56.213810258+02:00" diff --git a/providers/openstack/scs/cluster-addon/csi/Chart.yaml b/providers/openstack/scs/cluster-addon/csi/Chart.yaml deleted file mode 100644 index 980ef00b..00000000 --- a/providers/openstack/scs/cluster-addon/csi/Chart.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v2 -dependencies: -- alias: openstack-cinder-csi - name: openstack-cinder-csi - repository: https://kubernetes.github.io/cloud-provider-openstack - version: 2.32.0 -description: CSI -name: openstack-scs-1-32-cluster-addon -type: application -version: v1 diff --git a/providers/openstack/scs/cluster-addon/csi/charts/openstack-cinder-csi-2.32.0.tgz b/providers/openstack/scs/cluster-addon/csi/charts/openstack-cinder-csi-2.32.0.tgz deleted file mode 100644 index 23e67eced69d286af4bce1e9235fe51db1af5abd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7100 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 diff --git a/providers/openstack/scs/cluster-addon/metrics-server/.helmignore b/providers/openstack/scs/cluster-addon/metrics-server/.helmignore deleted file mode 100644 index 0e8a0eb3..00000000 --- a/providers/openstack/scs/cluster-addon/metrics-server/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/providers/openstack/scs/cluster-addon/metrics-server/Chart.lock b/providers/openstack/scs/cluster-addon/metrics-server/Chart.lock deleted file mode 100644 index 56929d7e..00000000 --- a/providers/openstack/scs/cluster-addon/metrics-server/Chart.lock +++ /dev/null @@ -1,6 +0,0 @@ -dependencies: -- name: metrics-server - repository: https://kubernetes-sigs.github.io/metrics-server/ - version: 3.12.2 -digest: sha256:b79715342d7c10e97664b5f4d79199044f5da6ef40cca906218cff05ca891122 -generated: "2025-05-28T21:51:03.060237224+02:00" diff --git a/providers/openstack/scs/cluster-addon/metrics-server/Chart.yaml b/providers/openstack/scs/cluster-addon/metrics-server/Chart.yaml deleted file mode 100644 index ad407d13..00000000 --- a/providers/openstack/scs/cluster-addon/metrics-server/Chart.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v2 -dependencies: -- alias: metrics-server - name: metrics-server - repository: https://kubernetes-sigs.github.io/metrics-server/ - version: 3.12.2 -description: Metrics Server -name: openstack-scs-1-32-cluster-addon -type: application -version: v1 diff --git a/providers/openstack/scs/cluster-addon/metrics-server/charts/metrics-server-3.12.2.tgz b/providers/openstack/scs/cluster-addon/metrics-server/charts/metrics-server-3.12.2.tgz deleted file mode 100644 index 4538e8a10e54c3dcb0d5047b4034cfb23683482e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10395 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 F0RZYxsxJTl diff --git a/providers/openstack/scs/cluster-class/.helmignore b/providers/openstack/scs/cluster-class/.helmignore deleted file mode 100644 index 0e8a0eb3..00000000 --- a/providers/openstack/scs/cluster-class/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/providers/openstack/scs/csctl.yaml b/providers/openstack/scs/csctl.yaml deleted file mode 100644 index 7a65ae35..00000000 --- a/providers/openstack/scs/csctl.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: csctl.clusterstack.x-k8s.io/v1alpha1 -config: - clusterStackName: scs - kubernetesVersion: v1.32.5 - provider: - apiVersion: openstack.csctl.clusterstack.x-k8s.io/v1alpha1 - type: openstack diff --git a/providers/openstack/scs/image-manager.yaml b/providers/openstack/scs/image-manager.yaml new file mode 100644 index 00000000..7a28a5b5 --- /dev/null +++ b/providers/openstack/scs/image-manager.yaml @@ -0,0 +1,39 @@ +--- +images: + - name: ubuntu-capi-image + enable: true + format: raw + login: ubuntu + min_disk: 20 + min_ram: 1024 + status: active + visibility: private + multi: false + separator: "-" + meta: + architecture: x86_64 + hw_disk_bus: virtio + hw_rng_model: virtio + hw_scsi_model: virtio-scsi + hw_watchdog_action: reset + hypervisor_type: qemu + os_distro: ubuntu + os_purpose: k8snode + replace_frequency: never + uuid_validity: none + provided_until: none + tags: + - clusterstacks + versions: + - version: 'v1.32.12' + url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2204-kube-v1.32/ubuntu-2204-kube-v1.32.12.qcow2 + checksum: "sha256:cb39992db553b7106c4b127cb3e9cd6418ce830d26aad2b5ab364d1dcc222fa6" + - version: 'v1.33.8' + url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2404-kube-v1.33/ubuntu-2404-kube-v1.33.8.qcow2 + checksum: "sha256:203f5635447f4a59e220bfb649c40eb86f065c051650e4ea1cd11706c0d1f5be" + - version: 'v1.34.4' + url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2404-kube-v1.34/ubuntu-2404-kube-v1.34.4.qcow2 + checksum: "sha256:df3f26b0026a1a9ca3b681df2d8675a7341e138dca6f2326592975bb7c0fe792" + - version: 'v1.35.1' + url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2404-kube-v1.35/ubuntu-2404-kube-v1.35.1.qcow2 + checksum: "sha256:5d424380e2fa85b7a51ad1e955e8efb09ca460b0e80d47cf2f36c02d94dc3f03" diff --git a/providers/openstack/scs/versions.yaml b/providers/openstack/scs/versions.yaml deleted file mode 100644 index 99fc8c73..00000000 --- a/providers/openstack/scs/versions.yaml +++ /dev/null @@ -1,9 +0,0 @@ -- kubernetes: 1.30.13 - cinder_csi: 2.30.3 - occm: 2.30.5 -- kubernetes: 1.31.9 - cinder_csi: 2.31.7 - occm: 2.31.3 -- kubernetes: 1.32.5 - cinder_csi: 2.32.0 - occm: 2.32.0 diff --git a/providers/openstack/scs2/README.md b/providers/openstack/scs2/README.md deleted file mode 100644 index 69b00f9e..00000000 --- a/providers/openstack/scs2/README.md +++ /dev/null @@ -1,149 +0,0 @@ -# Cluster Stacks - -## Prerequisites - -- kind -- kubectl -- helm -- clusterctl (v1.10x) - -## Getting started - - -### Prepare a management Cluster using kind - -```sh -# Create bootstrap cluster -kind create cluster - -# Init Cluster API -export CLUSTER_TOPOLOGY=true -export EXP_CLUSTER_RESOURCE_SET=true -export EXP_RUNTIME_SDK=true -kubectl apply -f https://github.com/k-orc/openstack-resource-controller/releases/latest/download/install.yaml -clusterctl init --infrastructure openstack:v0.12.6 - -kubectl -n capi-system rollout status deployment -kubectl -n capo-system rollout status deployment -``` - -### Install the Cluster Stack Operator - -``` -helm upgrade -i cso \ --n cso-system \ ---create-namespace \ -oci://registry.scs.community/cluster-stacks/cso -``` - -### Prepare some environment variables for reuse - -```sh -export CLUSTER_NAMESPACE=cluster -export CLUSTER_NAME=my-cluster -export CLUSTERSTACK_NAMESPACE=cluster -export CLUSTERSTACK_VERSION=v1 -export OS_CLIENT_CONFIG_FILE=${PWD}/clouds.yaml -kubectl create namespace $CLUSTER_NAMESPACE --dry-run=client -o yaml | kubectl apply -f - -``` - -### Add clouds.yaml as Secret - -```sh -# Create secret for CAPO -kubectl create secret -n $CLUSTER_NAMESPACE generic openstack --from-file=clouds.yaml=$OS_CLIENT_CONFIG_FILE --dry-run=client -oyaml | kubectl apply -f - - -# Prepare the Secret as it will be deployed in the Workload Cluster -kubectl create secret -n kube-system generic clouds-yaml --from-file=clouds.yaml=$OS_CLIENT_CONFIG_FILE --dry-run=client -oyaml > clouds-yaml-secret - -# Add the Secret to the ClusterResourceSet Secret in the Management Cluster -kubectl create -n $CLUSTER_NAMESPACE secret generic clouds-yaml --from-file=clouds-yaml-secret --type=addons.cluster.x-k8s.io/resource-set --dry-run=client -oyaml | kubectl apply -f - -``` - -```yaml -cat < /tmp/kubeconfig -kubectl get nodes --kubeconfig /tmp/kubeconfig -``` diff --git a/providers/openstack/scs2/cluster-class/.helmignore b/providers/openstack/scs2/cluster-class/.helmignore deleted file mode 100644 index 0e8a0eb3..00000000 --- a/providers/openstack/scs2/cluster-class/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/providers/openstack/scs2/csctl.yaml b/providers/openstack/scs2/csctl.yaml deleted file mode 100644 index d56bbbe7..00000000 --- a/providers/openstack/scs2/csctl.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: csctl.clusterstack.x-k8s.io/v1alpha1 -config: - clusterStackName: scs2 - kubernetesVersion: v1.34.3 - provider: - apiVersion: openstack.csctl.clusterstack.x-k8s.io/v1alpha1 - type: openstack diff --git a/providers/openstack/scs2/image.yaml b/providers/openstack/scs2/image.yaml deleted file mode 100644 index 080ae774..00000000 --- a/providers/openstack/scs2/image.yaml +++ /dev/null @@ -1,32 +0,0 @@ ---- -apiVersion: openstack.k-orc.cloud/v1alpha1 -kind: Image -metadata: - name: "ubuntu-capi-image-v1.33.6" -spec: - cloudCredentialsRef: - cloudName: "openstack" - secretName: "openstack" - managementPolicy: managed - resource: - visibility: private - properties: - hardware: - diskBus: scsi - scsiModel: virtio-scsi - vifModel: virtio - qemuGuestAgent: true - rngModel: virtio - architecture: x86_64 - minDiskGB: 20 - minMemoryMB: 2048 - operatingSystem: - distro: ubuntu - version: "24.04" - content: - diskFormat: qcow2 - download: - url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2404-kube-v1.33/ubuntu-2404-kube-v1.33.6.qcow2 - hash: - algorithm: sha256 - value: ff458b22c33fc08eca9ba6635783e9a409b6f0613f577c4acdec554db7e2f6a7 diff --git a/providers/openstack/scs2/kubernetes.yaml b/providers/openstack/scs2/kubernetes.yaml deleted file mode 100644 index 86b47884..00000000 --- a/providers/openstack/scs2/kubernetes.yaml +++ /dev/null @@ -1,55 +0,0 @@ ---- -images: - - name: ubuntu-capi-image - enable: true - format: raw - login: ubuntu - min_disk: 20 - min_ram: 1024 - status: active - visibility: public - multi: false - separator: "-" - meta: - architecture: x86_64 - hw_disk_bus: virtio - hw_rng_model: virtio - hw_scsi_model: virtio-scsi - hw_watchdog_action: reset - hypervisor_type: qemu - os_distro: ubuntu - os_purpose: k8snode - replace_frequency: never - uuid_validity: none - provided_until: none - tags: - - clusterstacks - versions: - - version: 'v1.33.4' - url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2404-kube-v1.33/ubuntu-2404-kube-v1.33.4.qcow2 - checksum: "sha256:1f55111551d5c9948d4e02215be56a712ed818d9000592d4f11d4b6cc4407ade" - build_date: 2025-12-17 - - version: 'v1.33.5' - url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2404-kube-v1.33/ubuntu-2404-kube-v1.33.5.qcow2 - checksum: "sha256:bd9efa9cad5d7028306eb26ecdc42a2f84337542a050381766714c4c0c1f7a98" - build_date: 2025-12-17 - - version: 'v1.33.6' - url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2404-kube-v1.33/ubuntu-2404-kube-v1.33.6.qcow2 - checksum: "sha256:ff458b22c33fc08eca9ba6635783e9a409b6f0613f577c4acdec554db7e2f6a7" - build_date: 2025-12-17 - - version: 'v1.34.0' - url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2404-kube-v1.34/ubuntu-2404-kube-v1.34.0.qcow2 - checksum: "sha256:1321c0978818752619ab994acccf4e2d9b241aa738fc56ed0a46b0ebe21fedfb" - build_date: 2025-12-17 - - version: 'v1.34.1' - url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2404-kube-v1.34/ubuntu-2404-kube-v1.34.1.qcow2 - checksum: "sha256:0f2153d01e13693a680010045b1c7f4c511495eff4f8672ea29b475b02b43dc4" - build_date: 2025-12-17 - - version: 'v1.34.2' - url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2404-kube-v1.34/ubuntu-2404-kube-v1.34.2.qcow2 - checksum: "sha256:8d37637dc86cd307e50ba2dab2d96aa3c46933552f5607f4663b40c246adb0a8" - build_date: 2025-12-17 - - version: 'v1.34.3' - url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2404-kube-v1.34/ubuntu-2404-kube-v1.34.3.qcow2 - checksum: "sha256:b3c487345dd8ff2eea6ddd3e526d068abc3a59d40a994581f6dfc7be10df427b" - build_date: 2025-12-17 diff --git a/providers/openstack/scs2/versions.yaml b/providers/openstack/scs2/versions.yaml deleted file mode 100644 index 85cff539..00000000 --- a/providers/openstack/scs2/versions.yaml +++ /dev/null @@ -1,9 +0,0 @@ -- kubernetes: 1.32.8 - cinder_csi: 2.32.2 - occm: 2.32.0 -- kubernetes: 1.33.7 - cinder_csi: 2.33.1 - occm: 2.33.1 -- kubernetes: 1.34.3 - cinder_csi: 2.34.1 - occm: 2.34.1 From c2fcaed5a3126a0f896c2b241a57bfe27b2b5346 Mon Sep 17 00:00:00 2001 From: Jan Schoone Date: Mon, 23 Feb 2026 17:13:30 +0000 Subject: [PATCH 03/11] feat(hcp): Add Hosted Control Plane cluster stack for OpenStack Add providers/openstack/hcp/ with versions 1-33, 1-34, and 1-35. The HCP stack runs the Kubernetes control plane as pods in the management cluster using the teutonet Hosted Control Plane provider (HostedControlPlaneTemplate, controlplane.cluster.x-k8s.io/v1alpha1). Only worker nodes are created as OpenStack VMs. Key differences from the scs stack: - No machineInfrastructure for control plane (no CP VMs) - Worker-prefixed variables (workerFlavor, workerRootDisk, etc.) - Gateway API integration (gatewayName/gatewayNamespace variables) - disableAPIServerFloatingIP: true, apiServerLoadBalancer: none by default - Same addons as scs (Cilium, CCM, CSI, metrics-server) Based on origin/hcp branch, adapted to per-minor-version structure. Assisted-by: Claude Code Signed-off-by: Jan Schoone --- .../hcp/1-33/cluster-addon/ccm/Chart.yaml | 10 + .../hcp/1-33/cluster-addon/ccm/overwrite.yaml | 4 + .../hcp/1-33/cluster-addon/ccm/values.yaml | 21 + .../hcp/1-33/cluster-addon/cni/Chart.yaml | 10 + .../hcp/1-33/cluster-addon/cni/values.yaml | 14 + .../hcp/1-33/cluster-addon/csi/Chart.yaml | 10 + .../hcp/1-33/cluster-addon/csi/overwrite.yaml | 3 + .../hcp/1-33/cluster-addon/csi/values.yaml | 41 ++ .../cluster-addon/metrics-server/Chart.yaml | 10 + .../metrics-server/overwrite.yaml | 4 + .../cluster-addon/metrics-server/values.yaml | 4 + .../hcp/1-33/cluster-class/Chart.yaml | 5 + .../1-33/cluster-class/templates/_helpers.tpl | 62 ++ .../templates/cluster-class.yaml | 664 ++++++++++++++++++ .../hosted-control-plane-template.yaml | 10 + ...ubeadm-config-template-default-worker.yaml | 13 + .../templates/openstack-cluster-template.yaml | 43 ++ ...stack-machine-template-default-worker.yaml | 12 + .../hcp/1-33/cluster-class/values.yaml | 24 + .../openstack/hcp/1-33/clusteraddon.yaml | 21 + providers/openstack/hcp/1-33/stack.yaml | 7 + .../hcp/1-34/cluster-addon/ccm/Chart.yaml | 10 + .../hcp/1-34/cluster-addon/ccm/overwrite.yaml | 4 + .../hcp/1-34/cluster-addon/ccm/values.yaml | 21 + .../hcp/1-34/cluster-addon/cni/Chart.yaml | 10 + .../hcp/1-34/cluster-addon/cni/values.yaml | 14 + .../hcp/1-34/cluster-addon/csi/Chart.yaml | 10 + .../hcp/1-34/cluster-addon/csi/overwrite.yaml | 3 + .../hcp/1-34/cluster-addon/csi/values.yaml | 41 ++ .../cluster-addon/metrics-server/Chart.yaml | 10 + .../metrics-server/overwrite.yaml | 4 + .../cluster-addon/metrics-server/values.yaml | 4 + .../hcp/1-34/cluster-class/Chart.yaml | 5 + .../1-34/cluster-class/templates/_helpers.tpl | 62 ++ .../templates/cluster-class.yaml | 664 ++++++++++++++++++ .../hosted-control-plane-template.yaml | 10 + ...ubeadm-config-template-default-worker.yaml | 13 + .../templates/openstack-cluster-template.yaml | 43 ++ ...stack-machine-template-default-worker.yaml | 12 + .../hcp/1-34/cluster-class/values.yaml | 24 + .../openstack/hcp/1-34/clusteraddon.yaml | 21 + providers/openstack/hcp/1-34/stack.yaml | 7 + .../hcp/1-35/cluster-addon/ccm/Chart.yaml | 10 + .../hcp/1-35/cluster-addon/ccm/overwrite.yaml | 4 + .../hcp/1-35/cluster-addon/ccm/values.yaml | 21 + .../hcp/1-35/cluster-addon/cni/Chart.yaml | 10 + .../hcp/1-35/cluster-addon/cni/values.yaml | 14 + .../hcp/1-35/cluster-addon/csi/Chart.yaml | 10 + .../hcp/1-35/cluster-addon/csi/overwrite.yaml | 3 + .../hcp/1-35/cluster-addon/csi/values.yaml | 41 ++ .../cluster-addon/metrics-server/Chart.yaml | 10 + .../metrics-server/overwrite.yaml | 4 + .../cluster-addon/metrics-server/values.yaml | 4 + .../hcp/1-35/cluster-class/Chart.yaml | 5 + .../1-35/cluster-class/templates/_helpers.tpl | 62 ++ .../templates/cluster-class.yaml | 664 ++++++++++++++++++ .../hosted-control-plane-template.yaml | 10 + ...ubeadm-config-template-default-worker.yaml | 13 + .../templates/openstack-cluster-template.yaml | 43 ++ ...stack-machine-template-default-worker.yaml | 12 + .../hcp/1-35/cluster-class/values.yaml | 24 + .../openstack/hcp/1-35/clusteraddon.yaml | 21 + providers/openstack/hcp/1-35/stack.yaml | 7 + 63 files changed, 2976 insertions(+) create mode 100644 providers/openstack/hcp/1-33/cluster-addon/ccm/Chart.yaml create mode 100644 providers/openstack/hcp/1-33/cluster-addon/ccm/overwrite.yaml create mode 100644 providers/openstack/hcp/1-33/cluster-addon/ccm/values.yaml create mode 100644 providers/openstack/hcp/1-33/cluster-addon/cni/Chart.yaml create mode 100644 providers/openstack/hcp/1-33/cluster-addon/cni/values.yaml create mode 100644 providers/openstack/hcp/1-33/cluster-addon/csi/Chart.yaml create mode 100644 providers/openstack/hcp/1-33/cluster-addon/csi/overwrite.yaml create mode 100644 providers/openstack/hcp/1-33/cluster-addon/csi/values.yaml create mode 100644 providers/openstack/hcp/1-33/cluster-addon/metrics-server/Chart.yaml create mode 100644 providers/openstack/hcp/1-33/cluster-addon/metrics-server/overwrite.yaml create mode 100644 providers/openstack/hcp/1-33/cluster-addon/metrics-server/values.yaml create mode 100644 providers/openstack/hcp/1-33/cluster-class/Chart.yaml create mode 100644 providers/openstack/hcp/1-33/cluster-class/templates/_helpers.tpl create mode 100644 providers/openstack/hcp/1-33/cluster-class/templates/cluster-class.yaml create mode 100644 providers/openstack/hcp/1-33/cluster-class/templates/hosted-control-plane-template.yaml create mode 100644 providers/openstack/hcp/1-33/cluster-class/templates/kubeadm-config-template-default-worker.yaml create mode 100644 providers/openstack/hcp/1-33/cluster-class/templates/openstack-cluster-template.yaml create mode 100644 providers/openstack/hcp/1-33/cluster-class/templates/openstack-machine-template-default-worker.yaml create mode 100644 providers/openstack/hcp/1-33/cluster-class/values.yaml create mode 100644 providers/openstack/hcp/1-33/clusteraddon.yaml create mode 100644 providers/openstack/hcp/1-33/stack.yaml create mode 100644 providers/openstack/hcp/1-34/cluster-addon/ccm/Chart.yaml create mode 100644 providers/openstack/hcp/1-34/cluster-addon/ccm/overwrite.yaml create mode 100644 providers/openstack/hcp/1-34/cluster-addon/ccm/values.yaml create mode 100644 providers/openstack/hcp/1-34/cluster-addon/cni/Chart.yaml create mode 100644 providers/openstack/hcp/1-34/cluster-addon/cni/values.yaml create mode 100644 providers/openstack/hcp/1-34/cluster-addon/csi/Chart.yaml create mode 100644 providers/openstack/hcp/1-34/cluster-addon/csi/overwrite.yaml create mode 100644 providers/openstack/hcp/1-34/cluster-addon/csi/values.yaml create mode 100644 providers/openstack/hcp/1-34/cluster-addon/metrics-server/Chart.yaml create mode 100644 providers/openstack/hcp/1-34/cluster-addon/metrics-server/overwrite.yaml create mode 100644 providers/openstack/hcp/1-34/cluster-addon/metrics-server/values.yaml create mode 100644 providers/openstack/hcp/1-34/cluster-class/Chart.yaml create mode 100644 providers/openstack/hcp/1-34/cluster-class/templates/_helpers.tpl create mode 100644 providers/openstack/hcp/1-34/cluster-class/templates/cluster-class.yaml create mode 100644 providers/openstack/hcp/1-34/cluster-class/templates/hosted-control-plane-template.yaml create mode 100644 providers/openstack/hcp/1-34/cluster-class/templates/kubeadm-config-template-default-worker.yaml create mode 100644 providers/openstack/hcp/1-34/cluster-class/templates/openstack-cluster-template.yaml create mode 100644 providers/openstack/hcp/1-34/cluster-class/templates/openstack-machine-template-default-worker.yaml create mode 100644 providers/openstack/hcp/1-34/cluster-class/values.yaml create mode 100644 providers/openstack/hcp/1-34/clusteraddon.yaml create mode 100644 providers/openstack/hcp/1-34/stack.yaml create mode 100644 providers/openstack/hcp/1-35/cluster-addon/ccm/Chart.yaml create mode 100644 providers/openstack/hcp/1-35/cluster-addon/ccm/overwrite.yaml create mode 100644 providers/openstack/hcp/1-35/cluster-addon/ccm/values.yaml create mode 100644 providers/openstack/hcp/1-35/cluster-addon/cni/Chart.yaml create mode 100644 providers/openstack/hcp/1-35/cluster-addon/cni/values.yaml create mode 100644 providers/openstack/hcp/1-35/cluster-addon/csi/Chart.yaml create mode 100644 providers/openstack/hcp/1-35/cluster-addon/csi/overwrite.yaml create mode 100644 providers/openstack/hcp/1-35/cluster-addon/csi/values.yaml create mode 100644 providers/openstack/hcp/1-35/cluster-addon/metrics-server/Chart.yaml create mode 100644 providers/openstack/hcp/1-35/cluster-addon/metrics-server/overwrite.yaml create mode 100644 providers/openstack/hcp/1-35/cluster-addon/metrics-server/values.yaml create mode 100644 providers/openstack/hcp/1-35/cluster-class/Chart.yaml create mode 100644 providers/openstack/hcp/1-35/cluster-class/templates/_helpers.tpl create mode 100644 providers/openstack/hcp/1-35/cluster-class/templates/cluster-class.yaml create mode 100644 providers/openstack/hcp/1-35/cluster-class/templates/hosted-control-plane-template.yaml create mode 100644 providers/openstack/hcp/1-35/cluster-class/templates/kubeadm-config-template-default-worker.yaml create mode 100644 providers/openstack/hcp/1-35/cluster-class/templates/openstack-cluster-template.yaml create mode 100644 providers/openstack/hcp/1-35/cluster-class/templates/openstack-machine-template-default-worker.yaml create mode 100644 providers/openstack/hcp/1-35/cluster-class/values.yaml create mode 100644 providers/openstack/hcp/1-35/clusteraddon.yaml create mode 100644 providers/openstack/hcp/1-35/stack.yaml diff --git a/providers/openstack/hcp/1-33/cluster-addon/ccm/Chart.yaml b/providers/openstack/hcp/1-33/cluster-addon/ccm/Chart.yaml new file mode 100644 index 00000000..541cecd0 --- /dev/null +++ b/providers/openstack/hcp/1-33/cluster-addon/ccm/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: CCM +name: CCM +version: v1 +dependencies: + - alias: openstack-cloud-controller-manager + name: openstack-cloud-controller-manager + repository: https://kubernetes.github.io/cloud-provider-openstack + version: 2.33.1 diff --git a/providers/openstack/hcp/1-33/cluster-addon/ccm/overwrite.yaml b/providers/openstack/hcp/1-33/cluster-addon/ccm/overwrite.yaml new file mode 100644 index 00000000..39076ecd --- /dev/null +++ b/providers/openstack/hcp/1-33/cluster-addon/ccm/overwrite.yaml @@ -0,0 +1,4 @@ +values: | + openstack-cloud-controller-manager: + cluster: + name: {{ .Cluster.metadata.name }} diff --git a/providers/openstack/hcp/1-33/cluster-addon/ccm/values.yaml b/providers/openstack/hcp/1-33/cluster-addon/ccm/values.yaml new file mode 100644 index 00000000..3f290366 --- /dev/null +++ b/providers/openstack/hcp/1-33/cluster-addon/ccm/values.yaml @@ -0,0 +1,21 @@ +openstack-cloud-controller-manager: + secret: + enabled: true + name: ccm-cloud-config + create: true + nodeSelector: + tolerations: + - key: node.cloudprovider.kubernetes.io/uninitialized + value: "true" + effect: NoSchedule + extraVolumes: + - name: clouds-yaml + secret: + secretName: clouds-yaml + extraVolumeMounts: + - name: clouds-yaml + readOnly: true + mountPath: /etc/openstack + cloudConfig: + global: + use-clouds: true diff --git a/providers/openstack/hcp/1-33/cluster-addon/cni/Chart.yaml b/providers/openstack/hcp/1-33/cluster-addon/cni/Chart.yaml new file mode 100644 index 00000000..051b40b3 --- /dev/null +++ b/providers/openstack/hcp/1-33/cluster-addon/cni/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: CNI +name: CNI +version: v1 +dependencies: + - alias: cilium + name: cilium + repository: https://helm.cilium.io/ + version: 1.19.1 diff --git a/providers/openstack/hcp/1-33/cluster-addon/cni/values.yaml b/providers/openstack/hcp/1-33/cluster-addon/cni/values.yaml new file mode 100644 index 00000000..8a312f0c --- /dev/null +++ b/providers/openstack/hcp/1-33/cluster-addon/cni/values.yaml @@ -0,0 +1,14 @@ +cilium: + namespaceOverride: kube-system + tls: + secretsNamespace: + name: "kube-system" + sessionAffinity: true + sctp: + enabled: true + ipam: + mode: "kubernetes" + gatewayAPI: + enabled: true + secretsNamespace: + name: "kube-system" diff --git a/providers/openstack/hcp/1-33/cluster-addon/csi/Chart.yaml b/providers/openstack/hcp/1-33/cluster-addon/csi/Chart.yaml new file mode 100644 index 00000000..e275b2ff --- /dev/null +++ b/providers/openstack/hcp/1-33/cluster-addon/csi/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: CSI +name: CSI +version: v1 +dependencies: + - alias: openstack-cinder-csi + name: openstack-cinder-csi + repository: https://kubernetes.github.io/cloud-provider-openstack + version: 2.33.1 diff --git a/providers/openstack/hcp/1-33/cluster-addon/csi/overwrite.yaml b/providers/openstack/hcp/1-33/cluster-addon/csi/overwrite.yaml new file mode 100644 index 00000000..d191a115 --- /dev/null +++ b/providers/openstack/hcp/1-33/cluster-addon/csi/overwrite.yaml @@ -0,0 +1,3 @@ +values: | + openstack-cinder-csi: + clusterID: "{{ .Cluster.metadata.name }}" diff --git a/providers/openstack/hcp/1-33/cluster-addon/csi/values.yaml b/providers/openstack/hcp/1-33/cluster-addon/csi/values.yaml new file mode 100644 index 00000000..4e648a4f --- /dev/null +++ b/providers/openstack/hcp/1-33/cluster-addon/csi/values.yaml @@ -0,0 +1,41 @@ +openstack-cinder-csi: + secret: + enabled: true + name: csi-cloud-config + create: true + filename: cloud.conf + data: + cloud.conf: |- + [Global] + use-clouds = "true" + clouds-file = /etc/openstack/clouds.yaml + storageClass: + delete: + isDefault: true + csi: + plugin: + volumes: + - name: clouds-yaml + secret: + secretName: clouds-yaml + - name: cloud-conf + secret: + secretName: csi-cloud-config + volumeMounts: + - name: clouds-yaml + readOnly: true + mountPath: /etc/openstack + - name: cloud-conf + readOnly: true + mountPath: /etc/kubernetes + - name: cloud-conf + readOnly: true + mountPath: /etc/config + nodeSelector: + node-role.kubernetes.io/control-plane: "" + tolerations: + - key: node.cloudprovider.kubernetes.io/uninitialized + value: "true" + effect: NoSchedule + - key: node-role.kubernetes.io/control-plane + effect: NoSchedule diff --git a/providers/openstack/hcp/1-33/cluster-addon/metrics-server/Chart.yaml b/providers/openstack/hcp/1-33/cluster-addon/metrics-server/Chart.yaml new file mode 100644 index 00000000..2ac06b1a --- /dev/null +++ b/providers/openstack/hcp/1-33/cluster-addon/metrics-server/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: Metrics Server +name: metrics-server +version: v1 +dependencies: + - name: "metrics-server" + version: "3.13.0" + repository: "https://kubernetes-sigs.github.io/metrics-server/" + alias: "metrics-server" diff --git a/providers/openstack/hcp/1-33/cluster-addon/metrics-server/overwrite.yaml b/providers/openstack/hcp/1-33/cluster-addon/metrics-server/overwrite.yaml new file mode 100644 index 00000000..7b1dcd5b --- /dev/null +++ b/providers/openstack/hcp/1-33/cluster-addon/metrics-server/overwrite.yaml @@ -0,0 +1,4 @@ +values: | + metrics-server: + commonLabels: + domain: "{{ .Cluster.spec.controlPlaneEndpoint.host }}" diff --git a/providers/openstack/hcp/1-33/cluster-addon/metrics-server/values.yaml b/providers/openstack/hcp/1-33/cluster-addon/metrics-server/values.yaml new file mode 100644 index 00000000..a89bf027 --- /dev/null +++ b/providers/openstack/hcp/1-33/cluster-addon/metrics-server/values.yaml @@ -0,0 +1,4 @@ +metrics-server: + fullnameOverride: metrics-server + args: + - --kubelet-insecure-tls diff --git a/providers/openstack/hcp/1-33/cluster-class/Chart.yaml b/providers/openstack/hcp/1-33/cluster-class/Chart.yaml new file mode 100644 index 00000000..e6aa48aa --- /dev/null +++ b/providers/openstack/hcp/1-33/cluster-class/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +description: "OpenStack HCP (Hosted Control Plane) Cluster Class for K8s 1.33" +name: openstack-hcp-1-33-cluster-class +type: application +version: v1 diff --git a/providers/openstack/hcp/1-33/cluster-class/templates/_helpers.tpl b/providers/openstack/hcp/1-33/cluster-class/templates/_helpers.tpl new file mode 100644 index 00000000..2339c125 --- /dev/null +++ b/providers/openstack/hcp/1-33/cluster-class/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "cluster-class.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "cluster-class.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "cluster-class.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "cluster-class.labels" -}} +helm.sh/chart: {{ include "cluster-class.chart" . }} +{{ include "cluster-class.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "cluster-class.selectorLabels" -}} +app.kubernetes.io/name: {{ include "cluster-class.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "cluster-class.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "cluster-class.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/providers/openstack/hcp/1-33/cluster-class/templates/cluster-class.yaml b/providers/openstack/hcp/1-33/cluster-class/templates/cluster-class.yaml new file mode 100644 index 00000000..0cd5c804 --- /dev/null +++ b/providers/openstack/hcp/1-33/cluster-class/templates/cluster-class.yaml @@ -0,0 +1,664 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: ClusterClass +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }} +spec: + controlPlane: + ref: + apiVersion: controlplane.cluster.x-k8s.io/v1alpha1 + kind: HostedControlPlaneTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane + # No machineInfrastructure section - control plane is hosted in management cluster + infrastructure: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-cluster + workers: + machineDeployments: + - class: default-worker + template: + bootstrap: + ref: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 + kind: KubeadmConfigTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker + infrastructure: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker + variables: + # Control plane variables (hosted in management cluster) + - name: controlPlaneReplicas + required: false + schema: + openAPIV3Schema: + type: integer + minimum: 1 + default: 3 + description: "Number of hosted control plane replicas for high availability." + - name: gatewayName + required: false + schema: + openAPIV3Schema: + type: string + default: "" + description: "Name of the Gateway API resource for control plane ingress. Optional - control plane accessible via standard kubeconfig if not set." + - name: gatewayNamespace + required: false + schema: + openAPIV3Schema: + type: string + default: "default" + description: "Namespace of the Gateway API resource." + # Image variables + - name: imageName + required: false + schema: + openAPIV3Schema: + type: string + description: | + The base name of the OpenStack image used for provisioning servers. + If `imageIsOrc` is enabled, this name refers to an ORC image resource. + If `imageIsOrc` is disabled, the name is used to filter images available in the OpenStack project. In this case, the specified image must already exist within the project. + If `imageAddVersion` is enabled, the Kubernetes version will be appended to form the complete image name (e.g., imageName-v1.33.6) + default: "ubuntu-capi-image" + - name: imageIsOrc + required: false + schema: + openAPIV3Schema: + type: boolean + description: | + Indicates whether the image name refers to an ORC image resource. + If set to true (default), the `imageName` is interpreted as a reference to an ORC image. + If set to false, the `imageName` is used to filter images in the OpenStack project instead. + default: false + - name: imageAddVersion + required: false + schema: + openAPIV3Schema: + type: boolean + description: | + Add a suffix with the Kubernetes version to the imageName. E.g. imageName-v1.33.6. + default: true + - name: disableAPIServerFloatingIP + required: false + schema: + openAPIV3Schema: + type: boolean + default: true + example: true + description: "DisableAPIServerFloatingIP controls whether a floating IP should be attached to the API server. Set to true for hosted control plane since API server runs in management cluster." + # Network variables + - name: networkExternalID + required: false + schema: + openAPIV3Schema: + type: string + example: "ebfe5546-f09f-4f42-ab54-094e457d42ec" + format: "uuid4" + description: "networkExternalID is the ID of an external OpenStack Network. This is necessary to get public internet to the VMs in case there are several external networks." + - name: networkMTU + required: false + schema: + openAPIV3Schema: + type: integer + example: 1500 + description: "networkMTU sets the maximum transmission unit (MTU) value to address fragmentation for the private network ID." + - name: dnsNameservers + required: false + schema: + openAPIV3Schema: + type: array + description: "dnsNameservers is the list of nameservers for the OpenStack Subnet being created. Set this value when you need to create a new network/subnet which requires access to DNS." + default: ["9.9.9.9", "149.112.112.112"] + example: ["9.9.9.9", "149.112.112.112"] + items: + type: string + - name: nodeCIDR + required: false + schema: + openAPIV3Schema: + type: string + format: "cidr" + default: "10.8.0.0/20" + example: "10.8.0.0/20" + description: |- + nodeCIDR is the OpenStack Subnet to be created. + Cluster actuator will create a network, a subnet with nodeCIDR, + and a router connected to this subnet. + If you leave this empty, no network will be created. + # Workers + - name: workerFlavor + required: false + schema: + openAPIV3Schema: + type: string + default: "SCS-4V-8" + example: "SCS-4V-8" + description: "OpenStack instance flavor for worker nodes (default: SCS-4V-8, which requires workerRootDisk)." + - name: workerRootDisk + required: false + schema: + openAPIV3Schema: + type: integer + minimum: 0 + example: 25 + default: 50 + description: |- + Root disk size in GiB for worker nodes. + OpenStack volume will be created and used instead of an ephemeral disk defined in flavor. + Should be used for the diskless flavors (>= 20), otherwise set to 0. + - name: workerServerGroupID + required: false + schema: + openAPIV3Schema: + type: string + default: "" + example: "869fe071-1e56-46a9-9166-47c9f228e297" + description: "The server group to assign the worker nodes to." + - name: workerAdditionalBlockDevices + required: false + schema: + openAPIV3Schema: + type: array + default: [] + items: + type: object + properties: + name: + type: string + sizeGiB: + type: integer + default: 20 + type: + type: string + default: "__DEFAULT__" + required: ["name"] + # Access management + - name: sshKeyName + required: false + schema: + openAPIV3Schema: + type: string + default: "" + example: "capi-keypair" + description: "The ssh key name to inject in the nodes (for debugging)." + - name: securityGroups + required: false + schema: + openAPIV3Schema: + type: array + default: [] + example: ["security-group-1"] + description: |- + The names of extra security groups to assign to worker nodes. + Will be ignored if `securityGroupIDs` is used. + items: + type: string + - name: securityGroupIDs + required: false + schema: + openAPIV3Schema: + format: "uuid4" + type: array + default: [] + example: ["9ae2f488-30a3-4629-bd51-07acb8eb4278"] + description: "The UUIDs of extra security groups to assign to worker nodes" + items: + type: string + - name: workerSecurityGroups + required: false + schema: + openAPIV3Schema: + type: array + default: [] + example: ["security-group-1"] + description: |- + The names of extra security groups to assign to the worker nodes. + Will be ignored if `workerSecurityGroupIDs` is used. + items: + type: string + - name: workerSecurityGroupIDs + required: false + schema: + openAPIV3Schema: + format: "uuid4" + type: array + default: [] + example: ["9ae2f488-30a3-4629-bd51-07acb8eb4278"] + description: "The UUIDs of extra security groups to assign to the worker nodes" + items: + type: string + - name: identityRef + required: false + schema: + openAPIV3Schema: + type: object + default: {} + properties: + name: + type: string + example: "openstack" + default: "openstack" + description: "The name of the secret that carries the OpenStack clouds.yaml" + cloudName: + type: string + example: "openstack" + default: "openstack" + description: "The name of the cloud to use from the clouds.yaml" + # Kubernetes API server + - name: certSANs + required: false + schema: + openAPIV3Schema: + type: array + default: [] + example: ["mydomain.example"] + description: "certSANs sets extra Subject Alternative Names for the API Server signing cert." + items: + type: string + - name: apiServerLoadBalancer + required: false + schema: + openAPIV3Schema: + type: string + default: "none" + example: "none, octavia-amphora, octavia-ovn" + description: | + For hosted control plane, typically set to "none" since the API server runs in the management cluster. + You can choose from 3 options: + + none: + (default) No LoadBalancer solution will be deployed + + octavia-amphora: + Uses OpenStack's LoadBalancer service Octavia (provider:amphora) + + octavia-ovn: + Uses OpenStack's LoadBalancer service Octavia (provider:ovn) + - name: apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs + required: false + schema: + openAPIV3Schema: + type: array + example: ["192.168.10.0/24"] + description: |- + apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs restrict access to the Kubernetes API server on a network level. + Ensure that at least the outgoing IP of your Management Cluster is added to the list of allowed CIDRs. + Otherwise CAPO can't reconcile the target Cluster correctly. + This requires amphora as load balancer provider in version >= v2.12. + items: + type: string + # + # Patches + # + patches: + # + # Patches for control plane variables (apply to HostedControlPlaneTemplate) + # + - name: controlPlaneReplicas + description: "Sets the number of hosted control plane replicas." + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1alpha1 + kind: HostedControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/replicas" + valueFrom: + variable: controlPlaneReplicas + - name: gatewayName + description: "Sets the Gateway API resource for control plane ingress." + enabledIf: {{ `'{{ ne .gatewayName "" }}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1alpha1 + kind: HostedControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/gateway" + valueFrom: + template: | + name: {{ `{{ .gatewayName }}` }} + namespace: {{ `{{ .gatewayNamespace }}` }} + - name: certSANs + description: "certSANs sets extra Subject Alternative Names for the API Server signing cert." + enabledIf: {{ `'{{ if .certSANs }}true{{end}}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1alpha1 + kind: HostedControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/apiServer/certSANs" + valueFrom: + variable: certSANs + # + # Patches for OpenStackClusterTemplate resource. + # + - name: apiServerLoadBalancerOctaviaAmphora + description: "Takes care of the patches that should be applied when variable apiServerLoadBalancer is set to octavia-amphora." + enabledIf: {{ `'{{ eq .apiServerLoadBalancer "octavia-amphora" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: replace + path: "/spec/template/spec/apiServerLoadBalancer/enabled" + value: true + - op: add + path: "/spec/template/spec/apiServerLoadBalancer/provider" + value: "amphora" + - name: apiServerLoadBalancerOctaviaOVN + description: "Takes care of the patches that should be applied when variable apiServerLoadBalancer is set to octavia-ovn." + enabledIf: {{ `'{{ eq .apiServerLoadBalancer "octavia-ovn" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: replace + path: "/spec/template/spec/apiServerLoadBalancer/enabled" + value: true + - op: add + path: "/spec/template/spec/apiServerLoadBalancer/provider" + value: "ovn" + - name: apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs + description: "Takes care of the patches that should be applied when variable allowedCIDRs is set." + enabledIf: {{ `'{{ and .apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs (eq .apiServerLoadBalancer "octavia-amphora") }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/apiServerLoadBalancer/allowedCIDRs" + valueFrom: + variable: apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs + - name: networkExternalID + description: "Sets the ID of an external OpenStack Network. This is necessary to get public internet to the VMs." + enabledIf: {{ `'{{ if .networkExternalID }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/externalNetwork" + value: {} + - op: add + path: "/spec/template/spec/externalNetwork/id" + valueFrom: + variable: networkExternalID + - name: networkMTU + description: "Sets the network MTU when variable networkMTU exist in cluster resource." + enabledIf: {{ `'{{ if .networkMTU }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/networkMTU" + valueFrom: + variable: networkMTU + - name: identityRef + description: "Sets the OpenStack identity reference." + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: /spec/template/spec/identityRef + valueFrom: + variable: identityRef + - name: nodeCIDRSubnet + description: |- + Sets the NodeCIDR for the OpenStack Subnet to be created. + Cluster actuator will create a network, a subnet with NodeCIDR, + and a router connected to this subnet. + enabledIf: {{ `'{{ if .nodeCIDR }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/managedSubnets" + valueFrom: + template: | + - cidr: '{{ `{{ .nodeCIDR }}` }}' + dnsNameservers: + {{ `{{- range .dnsNameservers }}` }} + - {{ `{{ . }}` }} + {{ `{{- end }}` }} + - name: disableAPIServerFloatingIP + description: "DisableAPIServerFloatingIP controls whether a floating IP should be attached to the API server." + enabledIf: {{ `"{{ if .disableAPIServerFloatingIP }}true{{end}}"` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/disableAPIServerFloatingIP" + valueFrom: + variable: disableAPIServerFloatingIP + # + # Patches for worker's OpenStackMachineTemplate resources. + # Note: No control plane patches since control plane runs in management cluster + # + - name: workerImage + description: "Sets the OpenStack image name that is used for creating the worker servers." + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: /spec/template/spec/image + valueFrom: + template: | + {{ `{{ if .imageIsOrc }}imageRef{{ else }}filter{{ end }}` }}: + name: {{ `{{ .imageName }}{{ if .imageAddVersion }}-{{ .builtin.machineDeployment.version }}{{ end }}` }} + - name: workerFlavor + description: "Sets the openstack instance flavor for the worker nodes." + enabledIf: {{ `'{{ ne .workerFlavor "" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: replace + path: "/spec/template/spec/flavor" + valueFrom: + variable: workerFlavor + - name: workerRootDisk + description: "Sets the root disk size in GiB for worker nodes." + enabledIf: {{ `'{{ if .workerRootDisk }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/rootVolume" + valueFrom: + template: | + sizeGiB: {{ `{{ .workerRootDisk }}` }} + - name: workerServerGroupID + description: "Sets the server group to assign the worker nodes to." + enabledIf: {{ `'{{ ne .workerServerGroupID "" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/serverGroup" + valueFrom: + template: | + id: {{ `{{ .workerServerGroupID }}` }} + - name: workerAdditionalBlockDevices + enabledIf: {{ `'{{ if .workerAdditionalBlockDevices }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: /spec/template/spec/additionalBlockDevices + valueFrom: + template: | + {{ `{{- range .workerAdditionalBlockDevices }}` }} + - name: {{ `{{ .name }}` }} + sizeGiB: {{ `{{ .sizeGiB }}` }} + storage: + type: Volume + volume: + type: {{ `{{ .type }}` }} + {{ `{{- end }}` }} + # Note: The securityGroups patch must be placed before securityGroupIDs, workerSecurityGroups, and workerSecurityGroupIDs. + # The patch order ensures the last applied patch overwrites previous ones. + - name: securityGroups + description: "Sets the list of the openstack security groups for the worker instances." + enabledIf: {{ `'{{ if .securityGroups }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .securityGroups }} { filter: { name: {{ . }}}}, {{ end }} ]'` }} + # Note: The securityGroupIDs patch must be placed before workerSecurityGroups, workerSecurityGroupIDs and after securityGroupIDs. + # The patch order ensures the last applied patch overwrites previous ones. + - name: securityGroupIDs + description: "Sets the list of the openstack security groups for the worker instances by UUID." + enabledIf: {{ `'{{ if .securityGroupIDs }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .securityGroupIDs }} { id: {{ . }} }, {{ end }} ]'` }} + - name: sshKeyName + description: "Sets the ssh key to inject in the nodes." + enabledIf: {{ `'{{ ne .sshKeyName "" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/sshKeyName" + valueFrom: + variable: sshKeyName + # Note: The workerSecurityGroups patch must be placed before workerSecurityGroupIDs and after securityGroups and securityGroupIDs. + # The patch order ensures the last applied patch overwrites previous ones. + - name: workerSecurityGroups + description: "Sets the list of the openstack security groups for the worker instances." + enabledIf: {{ `'{{ if .workerSecurityGroups }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .workerSecurityGroups }} { filter: { name: {{ . }}}}, {{ end }} ]'` }} + # Note: The workerSecurityGroupIDs patch must be placed after securityGroups, securityGroupIDs and workerSecurityGroupIDs. + # The patch order ensures the last applied patch overwrites previous ones. + - name: workerSecurityGroupIDs + description: "Sets the list of the openstack security groups for the worker instances by UUID." + enabledIf: {{ `'{{ if .workerSecurityGroupIDs }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .workerSecurityGroupIDs }} { id: {{ . }} }, {{ end }} ]'` }} diff --git a/providers/openstack/hcp/1-33/cluster-class/templates/hosted-control-plane-template.yaml b/providers/openstack/hcp/1-33/cluster-class/templates/hosted-control-plane-template.yaml new file mode 100644 index 00000000..b1caeea8 --- /dev/null +++ b/providers/openstack/hcp/1-33/cluster-class/templates/hosted-control-plane-template.yaml @@ -0,0 +1,10 @@ +apiVersion: controlplane.cluster.x-k8s.io/v1alpha1 +kind: HostedControlPlaneTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane +spec: + template: + spec: + gateway: + namespace: overridden-by-patch + name: overridden-by-patch diff --git a/providers/openstack/hcp/1-33/cluster-class/templates/kubeadm-config-template-default-worker.yaml b/providers/openstack/hcp/1-33/cluster-class/templates/kubeadm-config-template-default-worker.yaml new file mode 100644 index 00000000..4c1494ed --- /dev/null +++ b/providers/openstack/hcp/1-33/cluster-class/templates/kubeadm-config-template-default-worker.yaml @@ -0,0 +1,13 @@ +apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +kind: KubeadmConfigTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker +spec: + template: + spec: + joinConfiguration: + nodeRegistration: + kubeletExtraArgs: + cloud-provider: external + provider-id: 'openstack:///{{ `{{ instance_id }}` }}' + name: '{{ `{{ local_hostname }}` }}' diff --git a/providers/openstack/hcp/1-33/cluster-class/templates/openstack-cluster-template.yaml b/providers/openstack/hcp/1-33/cluster-class/templates/openstack-cluster-template.yaml new file mode 100644 index 00000000..3b8ae609 --- /dev/null +++ b/providers/openstack/hcp/1-33/cluster-class/templates/openstack-cluster-template.yaml @@ -0,0 +1,43 @@ +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: OpenStackClusterTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-cluster +spec: + template: + spec: + identityRef: + cloudName: overridden-by-patch + name: overridden-by-patch + apiServerLoadBalancer: + enabled: false + disableAPIServerFloatingIP: true + managedSecurityGroups: + allNodesSecurityGroupRules: + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: VXLAN (Cilium) + portRangeMin: 8472 + portRangeMax: 8472 + protocol: udp + description: "Allow VXLAN traffic for Cilium" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: HealthCheck (Cilium) + portRangeMin: 4240 + portRangeMax: 4240 + protocol: tcp + description: "Allow HealthCheck traffic for Cilium" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Hubble (Cilium) + portRangeMin: 4244 + portRangeMax: 4244 + protocol: tcp + description: "Allow Hubble traffic for Cilium" diff --git a/providers/openstack/hcp/1-33/cluster-class/templates/openstack-machine-template-default-worker.yaml b/providers/openstack/hcp/1-33/cluster-class/templates/openstack-machine-template-default-worker.yaml new file mode 100644 index 00000000..920dbb0a --- /dev/null +++ b/providers/openstack/hcp/1-33/cluster-class/templates/openstack-machine-template-default-worker.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: OpenStackMachineTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker +spec: + template: + spec: + flavor: overridden-by-patch + image: + imageRef: + name: overridden-by-patch diff --git a/providers/openstack/hcp/1-33/cluster-class/values.yaml b/providers/openstack/hcp/1-33/cluster-class/values.yaml new file mode 100644 index 00000000..5ebb9d61 --- /dev/null +++ b/providers/openstack/hcp/1-33/cluster-class/values.yaml @@ -0,0 +1,24 @@ +# Default values for hosted-control-plane cluster class + +# OpenStack credentials +identityRef: + name: "openstack" + cloudName: "openstack" + +# Network configuration +networkExternalID: "" +dnsNameservers: + - "9.9.9.9" + - "149.112.112.112" +nodeCIDR: "10.8.0.0/20" + +# Worker node configuration +workerFlavor: "SCS-2V-4" +workerRootDisk: 25 + +# Control plane configuration (hosted in management cluster) +controlPlaneReplicas: 3 + +# Gateway configuration for hosted control plane (optional) +gatewayName: "" +gatewayNamespace: "" diff --git a/providers/openstack/hcp/1-33/clusteraddon.yaml b/providers/openstack/hcp/1-33/clusteraddon.yaml new file mode 100644 index 00000000..d346ba22 --- /dev/null +++ b/providers/openstack/hcp/1-33/clusteraddon.yaml @@ -0,0 +1,21 @@ +apiVersion: clusteraddonconfig.x-k8s.io/v1alpha1 +clusterAddonVersion: clusteraddons.clusterstack.x-k8s.io/v1alpha1 +addonStages: + AfterControlPlaneInitialized: + - name: cni + action: apply + - name: metrics-server + action: apply + - name: csi + action: apply + - name: ccm + action: apply + BeforeClusterUpgrade: + - name: cni + action: apply + - name: metrics-server + action: apply + - name: csi + action: apply + - name: ccm + action: apply diff --git a/providers/openstack/hcp/1-33/stack.yaml b/providers/openstack/hcp/1-33/stack.yaml new file mode 100644 index 00000000..43c10ac5 --- /dev/null +++ b/providers/openstack/hcp/1-33/stack.yaml @@ -0,0 +1,7 @@ +provider: openstack +clusterStackName: hcp +kubernetesVersion: 1.33 + +addons: + ccm: 2.33.x + csi: 2.33.x diff --git a/providers/openstack/hcp/1-34/cluster-addon/ccm/Chart.yaml b/providers/openstack/hcp/1-34/cluster-addon/ccm/Chart.yaml new file mode 100644 index 00000000..bd086402 --- /dev/null +++ b/providers/openstack/hcp/1-34/cluster-addon/ccm/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: CCM +name: CCM +version: v1 +dependencies: + - alias: openstack-cloud-controller-manager + name: openstack-cloud-controller-manager + repository: https://kubernetes.github.io/cloud-provider-openstack + version: 2.34.2 diff --git a/providers/openstack/hcp/1-34/cluster-addon/ccm/overwrite.yaml b/providers/openstack/hcp/1-34/cluster-addon/ccm/overwrite.yaml new file mode 100644 index 00000000..39076ecd --- /dev/null +++ b/providers/openstack/hcp/1-34/cluster-addon/ccm/overwrite.yaml @@ -0,0 +1,4 @@ +values: | + openstack-cloud-controller-manager: + cluster: + name: {{ .Cluster.metadata.name }} diff --git a/providers/openstack/hcp/1-34/cluster-addon/ccm/values.yaml b/providers/openstack/hcp/1-34/cluster-addon/ccm/values.yaml new file mode 100644 index 00000000..3f290366 --- /dev/null +++ b/providers/openstack/hcp/1-34/cluster-addon/ccm/values.yaml @@ -0,0 +1,21 @@ +openstack-cloud-controller-manager: + secret: + enabled: true + name: ccm-cloud-config + create: true + nodeSelector: + tolerations: + - key: node.cloudprovider.kubernetes.io/uninitialized + value: "true" + effect: NoSchedule + extraVolumes: + - name: clouds-yaml + secret: + secretName: clouds-yaml + extraVolumeMounts: + - name: clouds-yaml + readOnly: true + mountPath: /etc/openstack + cloudConfig: + global: + use-clouds: true diff --git a/providers/openstack/hcp/1-34/cluster-addon/cni/Chart.yaml b/providers/openstack/hcp/1-34/cluster-addon/cni/Chart.yaml new file mode 100644 index 00000000..051b40b3 --- /dev/null +++ b/providers/openstack/hcp/1-34/cluster-addon/cni/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: CNI +name: CNI +version: v1 +dependencies: + - alias: cilium + name: cilium + repository: https://helm.cilium.io/ + version: 1.19.1 diff --git a/providers/openstack/hcp/1-34/cluster-addon/cni/values.yaml b/providers/openstack/hcp/1-34/cluster-addon/cni/values.yaml new file mode 100644 index 00000000..8a312f0c --- /dev/null +++ b/providers/openstack/hcp/1-34/cluster-addon/cni/values.yaml @@ -0,0 +1,14 @@ +cilium: + namespaceOverride: kube-system + tls: + secretsNamespace: + name: "kube-system" + sessionAffinity: true + sctp: + enabled: true + ipam: + mode: "kubernetes" + gatewayAPI: + enabled: true + secretsNamespace: + name: "kube-system" diff --git a/providers/openstack/hcp/1-34/cluster-addon/csi/Chart.yaml b/providers/openstack/hcp/1-34/cluster-addon/csi/Chart.yaml new file mode 100644 index 00000000..cbe0cc17 --- /dev/null +++ b/providers/openstack/hcp/1-34/cluster-addon/csi/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: CSI +name: CSI +version: v1 +dependencies: + - alias: openstack-cinder-csi + name: openstack-cinder-csi + repository: https://kubernetes.github.io/cloud-provider-openstack + version: 2.34.3 diff --git a/providers/openstack/hcp/1-34/cluster-addon/csi/overwrite.yaml b/providers/openstack/hcp/1-34/cluster-addon/csi/overwrite.yaml new file mode 100644 index 00000000..d191a115 --- /dev/null +++ b/providers/openstack/hcp/1-34/cluster-addon/csi/overwrite.yaml @@ -0,0 +1,3 @@ +values: | + openstack-cinder-csi: + clusterID: "{{ .Cluster.metadata.name }}" diff --git a/providers/openstack/hcp/1-34/cluster-addon/csi/values.yaml b/providers/openstack/hcp/1-34/cluster-addon/csi/values.yaml new file mode 100644 index 00000000..4e648a4f --- /dev/null +++ b/providers/openstack/hcp/1-34/cluster-addon/csi/values.yaml @@ -0,0 +1,41 @@ +openstack-cinder-csi: + secret: + enabled: true + name: csi-cloud-config + create: true + filename: cloud.conf + data: + cloud.conf: |- + [Global] + use-clouds = "true" + clouds-file = /etc/openstack/clouds.yaml + storageClass: + delete: + isDefault: true + csi: + plugin: + volumes: + - name: clouds-yaml + secret: + secretName: clouds-yaml + - name: cloud-conf + secret: + secretName: csi-cloud-config + volumeMounts: + - name: clouds-yaml + readOnly: true + mountPath: /etc/openstack + - name: cloud-conf + readOnly: true + mountPath: /etc/kubernetes + - name: cloud-conf + readOnly: true + mountPath: /etc/config + nodeSelector: + node-role.kubernetes.io/control-plane: "" + tolerations: + - key: node.cloudprovider.kubernetes.io/uninitialized + value: "true" + effect: NoSchedule + - key: node-role.kubernetes.io/control-plane + effect: NoSchedule diff --git a/providers/openstack/hcp/1-34/cluster-addon/metrics-server/Chart.yaml b/providers/openstack/hcp/1-34/cluster-addon/metrics-server/Chart.yaml new file mode 100644 index 00000000..2ac06b1a --- /dev/null +++ b/providers/openstack/hcp/1-34/cluster-addon/metrics-server/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: Metrics Server +name: metrics-server +version: v1 +dependencies: + - name: "metrics-server" + version: "3.13.0" + repository: "https://kubernetes-sigs.github.io/metrics-server/" + alias: "metrics-server" diff --git a/providers/openstack/hcp/1-34/cluster-addon/metrics-server/overwrite.yaml b/providers/openstack/hcp/1-34/cluster-addon/metrics-server/overwrite.yaml new file mode 100644 index 00000000..7b1dcd5b --- /dev/null +++ b/providers/openstack/hcp/1-34/cluster-addon/metrics-server/overwrite.yaml @@ -0,0 +1,4 @@ +values: | + metrics-server: + commonLabels: + domain: "{{ .Cluster.spec.controlPlaneEndpoint.host }}" diff --git a/providers/openstack/hcp/1-34/cluster-addon/metrics-server/values.yaml b/providers/openstack/hcp/1-34/cluster-addon/metrics-server/values.yaml new file mode 100644 index 00000000..a89bf027 --- /dev/null +++ b/providers/openstack/hcp/1-34/cluster-addon/metrics-server/values.yaml @@ -0,0 +1,4 @@ +metrics-server: + fullnameOverride: metrics-server + args: + - --kubelet-insecure-tls diff --git a/providers/openstack/hcp/1-34/cluster-class/Chart.yaml b/providers/openstack/hcp/1-34/cluster-class/Chart.yaml new file mode 100644 index 00000000..4874c66d --- /dev/null +++ b/providers/openstack/hcp/1-34/cluster-class/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +description: "OpenStack HCP (Hosted Control Plane) Cluster Class for K8s 1.34" +name: openstack-hcp-1-34-cluster-class +type: application +version: v1 diff --git a/providers/openstack/hcp/1-34/cluster-class/templates/_helpers.tpl b/providers/openstack/hcp/1-34/cluster-class/templates/_helpers.tpl new file mode 100644 index 00000000..2339c125 --- /dev/null +++ b/providers/openstack/hcp/1-34/cluster-class/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "cluster-class.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "cluster-class.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "cluster-class.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "cluster-class.labels" -}} +helm.sh/chart: {{ include "cluster-class.chart" . }} +{{ include "cluster-class.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "cluster-class.selectorLabels" -}} +app.kubernetes.io/name: {{ include "cluster-class.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "cluster-class.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "cluster-class.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/providers/openstack/hcp/1-34/cluster-class/templates/cluster-class.yaml b/providers/openstack/hcp/1-34/cluster-class/templates/cluster-class.yaml new file mode 100644 index 00000000..0cd5c804 --- /dev/null +++ b/providers/openstack/hcp/1-34/cluster-class/templates/cluster-class.yaml @@ -0,0 +1,664 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: ClusterClass +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }} +spec: + controlPlane: + ref: + apiVersion: controlplane.cluster.x-k8s.io/v1alpha1 + kind: HostedControlPlaneTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane + # No machineInfrastructure section - control plane is hosted in management cluster + infrastructure: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-cluster + workers: + machineDeployments: + - class: default-worker + template: + bootstrap: + ref: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 + kind: KubeadmConfigTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker + infrastructure: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker + variables: + # Control plane variables (hosted in management cluster) + - name: controlPlaneReplicas + required: false + schema: + openAPIV3Schema: + type: integer + minimum: 1 + default: 3 + description: "Number of hosted control plane replicas for high availability." + - name: gatewayName + required: false + schema: + openAPIV3Schema: + type: string + default: "" + description: "Name of the Gateway API resource for control plane ingress. Optional - control plane accessible via standard kubeconfig if not set." + - name: gatewayNamespace + required: false + schema: + openAPIV3Schema: + type: string + default: "default" + description: "Namespace of the Gateway API resource." + # Image variables + - name: imageName + required: false + schema: + openAPIV3Schema: + type: string + description: | + The base name of the OpenStack image used for provisioning servers. + If `imageIsOrc` is enabled, this name refers to an ORC image resource. + If `imageIsOrc` is disabled, the name is used to filter images available in the OpenStack project. In this case, the specified image must already exist within the project. + If `imageAddVersion` is enabled, the Kubernetes version will be appended to form the complete image name (e.g., imageName-v1.33.6) + default: "ubuntu-capi-image" + - name: imageIsOrc + required: false + schema: + openAPIV3Schema: + type: boolean + description: | + Indicates whether the image name refers to an ORC image resource. + If set to true (default), the `imageName` is interpreted as a reference to an ORC image. + If set to false, the `imageName` is used to filter images in the OpenStack project instead. + default: false + - name: imageAddVersion + required: false + schema: + openAPIV3Schema: + type: boolean + description: | + Add a suffix with the Kubernetes version to the imageName. E.g. imageName-v1.33.6. + default: true + - name: disableAPIServerFloatingIP + required: false + schema: + openAPIV3Schema: + type: boolean + default: true + example: true + description: "DisableAPIServerFloatingIP controls whether a floating IP should be attached to the API server. Set to true for hosted control plane since API server runs in management cluster." + # Network variables + - name: networkExternalID + required: false + schema: + openAPIV3Schema: + type: string + example: "ebfe5546-f09f-4f42-ab54-094e457d42ec" + format: "uuid4" + description: "networkExternalID is the ID of an external OpenStack Network. This is necessary to get public internet to the VMs in case there are several external networks." + - name: networkMTU + required: false + schema: + openAPIV3Schema: + type: integer + example: 1500 + description: "networkMTU sets the maximum transmission unit (MTU) value to address fragmentation for the private network ID." + - name: dnsNameservers + required: false + schema: + openAPIV3Schema: + type: array + description: "dnsNameservers is the list of nameservers for the OpenStack Subnet being created. Set this value when you need to create a new network/subnet which requires access to DNS." + default: ["9.9.9.9", "149.112.112.112"] + example: ["9.9.9.9", "149.112.112.112"] + items: + type: string + - name: nodeCIDR + required: false + schema: + openAPIV3Schema: + type: string + format: "cidr" + default: "10.8.0.0/20" + example: "10.8.0.0/20" + description: |- + nodeCIDR is the OpenStack Subnet to be created. + Cluster actuator will create a network, a subnet with nodeCIDR, + and a router connected to this subnet. + If you leave this empty, no network will be created. + # Workers + - name: workerFlavor + required: false + schema: + openAPIV3Schema: + type: string + default: "SCS-4V-8" + example: "SCS-4V-8" + description: "OpenStack instance flavor for worker nodes (default: SCS-4V-8, which requires workerRootDisk)." + - name: workerRootDisk + required: false + schema: + openAPIV3Schema: + type: integer + minimum: 0 + example: 25 + default: 50 + description: |- + Root disk size in GiB for worker nodes. + OpenStack volume will be created and used instead of an ephemeral disk defined in flavor. + Should be used for the diskless flavors (>= 20), otherwise set to 0. + - name: workerServerGroupID + required: false + schema: + openAPIV3Schema: + type: string + default: "" + example: "869fe071-1e56-46a9-9166-47c9f228e297" + description: "The server group to assign the worker nodes to." + - name: workerAdditionalBlockDevices + required: false + schema: + openAPIV3Schema: + type: array + default: [] + items: + type: object + properties: + name: + type: string + sizeGiB: + type: integer + default: 20 + type: + type: string + default: "__DEFAULT__" + required: ["name"] + # Access management + - name: sshKeyName + required: false + schema: + openAPIV3Schema: + type: string + default: "" + example: "capi-keypair" + description: "The ssh key name to inject in the nodes (for debugging)." + - name: securityGroups + required: false + schema: + openAPIV3Schema: + type: array + default: [] + example: ["security-group-1"] + description: |- + The names of extra security groups to assign to worker nodes. + Will be ignored if `securityGroupIDs` is used. + items: + type: string + - name: securityGroupIDs + required: false + schema: + openAPIV3Schema: + format: "uuid4" + type: array + default: [] + example: ["9ae2f488-30a3-4629-bd51-07acb8eb4278"] + description: "The UUIDs of extra security groups to assign to worker nodes" + items: + type: string + - name: workerSecurityGroups + required: false + schema: + openAPIV3Schema: + type: array + default: [] + example: ["security-group-1"] + description: |- + The names of extra security groups to assign to the worker nodes. + Will be ignored if `workerSecurityGroupIDs` is used. + items: + type: string + - name: workerSecurityGroupIDs + required: false + schema: + openAPIV3Schema: + format: "uuid4" + type: array + default: [] + example: ["9ae2f488-30a3-4629-bd51-07acb8eb4278"] + description: "The UUIDs of extra security groups to assign to the worker nodes" + items: + type: string + - name: identityRef + required: false + schema: + openAPIV3Schema: + type: object + default: {} + properties: + name: + type: string + example: "openstack" + default: "openstack" + description: "The name of the secret that carries the OpenStack clouds.yaml" + cloudName: + type: string + example: "openstack" + default: "openstack" + description: "The name of the cloud to use from the clouds.yaml" + # Kubernetes API server + - name: certSANs + required: false + schema: + openAPIV3Schema: + type: array + default: [] + example: ["mydomain.example"] + description: "certSANs sets extra Subject Alternative Names for the API Server signing cert." + items: + type: string + - name: apiServerLoadBalancer + required: false + schema: + openAPIV3Schema: + type: string + default: "none" + example: "none, octavia-amphora, octavia-ovn" + description: | + For hosted control plane, typically set to "none" since the API server runs in the management cluster. + You can choose from 3 options: + + none: + (default) No LoadBalancer solution will be deployed + + octavia-amphora: + Uses OpenStack's LoadBalancer service Octavia (provider:amphora) + + octavia-ovn: + Uses OpenStack's LoadBalancer service Octavia (provider:ovn) + - name: apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs + required: false + schema: + openAPIV3Schema: + type: array + example: ["192.168.10.0/24"] + description: |- + apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs restrict access to the Kubernetes API server on a network level. + Ensure that at least the outgoing IP of your Management Cluster is added to the list of allowed CIDRs. + Otherwise CAPO can't reconcile the target Cluster correctly. + This requires amphora as load balancer provider in version >= v2.12. + items: + type: string + # + # Patches + # + patches: + # + # Patches for control plane variables (apply to HostedControlPlaneTemplate) + # + - name: controlPlaneReplicas + description: "Sets the number of hosted control plane replicas." + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1alpha1 + kind: HostedControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/replicas" + valueFrom: + variable: controlPlaneReplicas + - name: gatewayName + description: "Sets the Gateway API resource for control plane ingress." + enabledIf: {{ `'{{ ne .gatewayName "" }}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1alpha1 + kind: HostedControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/gateway" + valueFrom: + template: | + name: {{ `{{ .gatewayName }}` }} + namespace: {{ `{{ .gatewayNamespace }}` }} + - name: certSANs + description: "certSANs sets extra Subject Alternative Names for the API Server signing cert." + enabledIf: {{ `'{{ if .certSANs }}true{{end}}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1alpha1 + kind: HostedControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/apiServer/certSANs" + valueFrom: + variable: certSANs + # + # Patches for OpenStackClusterTemplate resource. + # + - name: apiServerLoadBalancerOctaviaAmphora + description: "Takes care of the patches that should be applied when variable apiServerLoadBalancer is set to octavia-amphora." + enabledIf: {{ `'{{ eq .apiServerLoadBalancer "octavia-amphora" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: replace + path: "/spec/template/spec/apiServerLoadBalancer/enabled" + value: true + - op: add + path: "/spec/template/spec/apiServerLoadBalancer/provider" + value: "amphora" + - name: apiServerLoadBalancerOctaviaOVN + description: "Takes care of the patches that should be applied when variable apiServerLoadBalancer is set to octavia-ovn." + enabledIf: {{ `'{{ eq .apiServerLoadBalancer "octavia-ovn" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: replace + path: "/spec/template/spec/apiServerLoadBalancer/enabled" + value: true + - op: add + path: "/spec/template/spec/apiServerLoadBalancer/provider" + value: "ovn" + - name: apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs + description: "Takes care of the patches that should be applied when variable allowedCIDRs is set." + enabledIf: {{ `'{{ and .apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs (eq .apiServerLoadBalancer "octavia-amphora") }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/apiServerLoadBalancer/allowedCIDRs" + valueFrom: + variable: apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs + - name: networkExternalID + description: "Sets the ID of an external OpenStack Network. This is necessary to get public internet to the VMs." + enabledIf: {{ `'{{ if .networkExternalID }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/externalNetwork" + value: {} + - op: add + path: "/spec/template/spec/externalNetwork/id" + valueFrom: + variable: networkExternalID + - name: networkMTU + description: "Sets the network MTU when variable networkMTU exist in cluster resource." + enabledIf: {{ `'{{ if .networkMTU }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/networkMTU" + valueFrom: + variable: networkMTU + - name: identityRef + description: "Sets the OpenStack identity reference." + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: /spec/template/spec/identityRef + valueFrom: + variable: identityRef + - name: nodeCIDRSubnet + description: |- + Sets the NodeCIDR for the OpenStack Subnet to be created. + Cluster actuator will create a network, a subnet with NodeCIDR, + and a router connected to this subnet. + enabledIf: {{ `'{{ if .nodeCIDR }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/managedSubnets" + valueFrom: + template: | + - cidr: '{{ `{{ .nodeCIDR }}` }}' + dnsNameservers: + {{ `{{- range .dnsNameservers }}` }} + - {{ `{{ . }}` }} + {{ `{{- end }}` }} + - name: disableAPIServerFloatingIP + description: "DisableAPIServerFloatingIP controls whether a floating IP should be attached to the API server." + enabledIf: {{ `"{{ if .disableAPIServerFloatingIP }}true{{end}}"` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/disableAPIServerFloatingIP" + valueFrom: + variable: disableAPIServerFloatingIP + # + # Patches for worker's OpenStackMachineTemplate resources. + # Note: No control plane patches since control plane runs in management cluster + # + - name: workerImage + description: "Sets the OpenStack image name that is used for creating the worker servers." + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: /spec/template/spec/image + valueFrom: + template: | + {{ `{{ if .imageIsOrc }}imageRef{{ else }}filter{{ end }}` }}: + name: {{ `{{ .imageName }}{{ if .imageAddVersion }}-{{ .builtin.machineDeployment.version }}{{ end }}` }} + - name: workerFlavor + description: "Sets the openstack instance flavor for the worker nodes." + enabledIf: {{ `'{{ ne .workerFlavor "" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: replace + path: "/spec/template/spec/flavor" + valueFrom: + variable: workerFlavor + - name: workerRootDisk + description: "Sets the root disk size in GiB for worker nodes." + enabledIf: {{ `'{{ if .workerRootDisk }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/rootVolume" + valueFrom: + template: | + sizeGiB: {{ `{{ .workerRootDisk }}` }} + - name: workerServerGroupID + description: "Sets the server group to assign the worker nodes to." + enabledIf: {{ `'{{ ne .workerServerGroupID "" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/serverGroup" + valueFrom: + template: | + id: {{ `{{ .workerServerGroupID }}` }} + - name: workerAdditionalBlockDevices + enabledIf: {{ `'{{ if .workerAdditionalBlockDevices }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: /spec/template/spec/additionalBlockDevices + valueFrom: + template: | + {{ `{{- range .workerAdditionalBlockDevices }}` }} + - name: {{ `{{ .name }}` }} + sizeGiB: {{ `{{ .sizeGiB }}` }} + storage: + type: Volume + volume: + type: {{ `{{ .type }}` }} + {{ `{{- end }}` }} + # Note: The securityGroups patch must be placed before securityGroupIDs, workerSecurityGroups, and workerSecurityGroupIDs. + # The patch order ensures the last applied patch overwrites previous ones. + - name: securityGroups + description: "Sets the list of the openstack security groups for the worker instances." + enabledIf: {{ `'{{ if .securityGroups }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .securityGroups }} { filter: { name: {{ . }}}}, {{ end }} ]'` }} + # Note: The securityGroupIDs patch must be placed before workerSecurityGroups, workerSecurityGroupIDs and after securityGroupIDs. + # The patch order ensures the last applied patch overwrites previous ones. + - name: securityGroupIDs + description: "Sets the list of the openstack security groups for the worker instances by UUID." + enabledIf: {{ `'{{ if .securityGroupIDs }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .securityGroupIDs }} { id: {{ . }} }, {{ end }} ]'` }} + - name: sshKeyName + description: "Sets the ssh key to inject in the nodes." + enabledIf: {{ `'{{ ne .sshKeyName "" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/sshKeyName" + valueFrom: + variable: sshKeyName + # Note: The workerSecurityGroups patch must be placed before workerSecurityGroupIDs and after securityGroups and securityGroupIDs. + # The patch order ensures the last applied patch overwrites previous ones. + - name: workerSecurityGroups + description: "Sets the list of the openstack security groups for the worker instances." + enabledIf: {{ `'{{ if .workerSecurityGroups }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .workerSecurityGroups }} { filter: { name: {{ . }}}}, {{ end }} ]'` }} + # Note: The workerSecurityGroupIDs patch must be placed after securityGroups, securityGroupIDs and workerSecurityGroupIDs. + # The patch order ensures the last applied patch overwrites previous ones. + - name: workerSecurityGroupIDs + description: "Sets the list of the openstack security groups for the worker instances by UUID." + enabledIf: {{ `'{{ if .workerSecurityGroupIDs }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .workerSecurityGroupIDs }} { id: {{ . }} }, {{ end }} ]'` }} diff --git a/providers/openstack/hcp/1-34/cluster-class/templates/hosted-control-plane-template.yaml b/providers/openstack/hcp/1-34/cluster-class/templates/hosted-control-plane-template.yaml new file mode 100644 index 00000000..b1caeea8 --- /dev/null +++ b/providers/openstack/hcp/1-34/cluster-class/templates/hosted-control-plane-template.yaml @@ -0,0 +1,10 @@ +apiVersion: controlplane.cluster.x-k8s.io/v1alpha1 +kind: HostedControlPlaneTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane +spec: + template: + spec: + gateway: + namespace: overridden-by-patch + name: overridden-by-patch diff --git a/providers/openstack/hcp/1-34/cluster-class/templates/kubeadm-config-template-default-worker.yaml b/providers/openstack/hcp/1-34/cluster-class/templates/kubeadm-config-template-default-worker.yaml new file mode 100644 index 00000000..4c1494ed --- /dev/null +++ b/providers/openstack/hcp/1-34/cluster-class/templates/kubeadm-config-template-default-worker.yaml @@ -0,0 +1,13 @@ +apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +kind: KubeadmConfigTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker +spec: + template: + spec: + joinConfiguration: + nodeRegistration: + kubeletExtraArgs: + cloud-provider: external + provider-id: 'openstack:///{{ `{{ instance_id }}` }}' + name: '{{ `{{ local_hostname }}` }}' diff --git a/providers/openstack/hcp/1-34/cluster-class/templates/openstack-cluster-template.yaml b/providers/openstack/hcp/1-34/cluster-class/templates/openstack-cluster-template.yaml new file mode 100644 index 00000000..3b8ae609 --- /dev/null +++ b/providers/openstack/hcp/1-34/cluster-class/templates/openstack-cluster-template.yaml @@ -0,0 +1,43 @@ +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: OpenStackClusterTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-cluster +spec: + template: + spec: + identityRef: + cloudName: overridden-by-patch + name: overridden-by-patch + apiServerLoadBalancer: + enabled: false + disableAPIServerFloatingIP: true + managedSecurityGroups: + allNodesSecurityGroupRules: + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: VXLAN (Cilium) + portRangeMin: 8472 + portRangeMax: 8472 + protocol: udp + description: "Allow VXLAN traffic for Cilium" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: HealthCheck (Cilium) + portRangeMin: 4240 + portRangeMax: 4240 + protocol: tcp + description: "Allow HealthCheck traffic for Cilium" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Hubble (Cilium) + portRangeMin: 4244 + portRangeMax: 4244 + protocol: tcp + description: "Allow Hubble traffic for Cilium" diff --git a/providers/openstack/hcp/1-34/cluster-class/templates/openstack-machine-template-default-worker.yaml b/providers/openstack/hcp/1-34/cluster-class/templates/openstack-machine-template-default-worker.yaml new file mode 100644 index 00000000..920dbb0a --- /dev/null +++ b/providers/openstack/hcp/1-34/cluster-class/templates/openstack-machine-template-default-worker.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: OpenStackMachineTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker +spec: + template: + spec: + flavor: overridden-by-patch + image: + imageRef: + name: overridden-by-patch diff --git a/providers/openstack/hcp/1-34/cluster-class/values.yaml b/providers/openstack/hcp/1-34/cluster-class/values.yaml new file mode 100644 index 00000000..5ebb9d61 --- /dev/null +++ b/providers/openstack/hcp/1-34/cluster-class/values.yaml @@ -0,0 +1,24 @@ +# Default values for hosted-control-plane cluster class + +# OpenStack credentials +identityRef: + name: "openstack" + cloudName: "openstack" + +# Network configuration +networkExternalID: "" +dnsNameservers: + - "9.9.9.9" + - "149.112.112.112" +nodeCIDR: "10.8.0.0/20" + +# Worker node configuration +workerFlavor: "SCS-2V-4" +workerRootDisk: 25 + +# Control plane configuration (hosted in management cluster) +controlPlaneReplicas: 3 + +# Gateway configuration for hosted control plane (optional) +gatewayName: "" +gatewayNamespace: "" diff --git a/providers/openstack/hcp/1-34/clusteraddon.yaml b/providers/openstack/hcp/1-34/clusteraddon.yaml new file mode 100644 index 00000000..d346ba22 --- /dev/null +++ b/providers/openstack/hcp/1-34/clusteraddon.yaml @@ -0,0 +1,21 @@ +apiVersion: clusteraddonconfig.x-k8s.io/v1alpha1 +clusterAddonVersion: clusteraddons.clusterstack.x-k8s.io/v1alpha1 +addonStages: + AfterControlPlaneInitialized: + - name: cni + action: apply + - name: metrics-server + action: apply + - name: csi + action: apply + - name: ccm + action: apply + BeforeClusterUpgrade: + - name: cni + action: apply + - name: metrics-server + action: apply + - name: csi + action: apply + - name: ccm + action: apply diff --git a/providers/openstack/hcp/1-34/stack.yaml b/providers/openstack/hcp/1-34/stack.yaml new file mode 100644 index 00000000..377cc46a --- /dev/null +++ b/providers/openstack/hcp/1-34/stack.yaml @@ -0,0 +1,7 @@ +provider: openstack +clusterStackName: hcp +kubernetesVersion: 1.34 + +addons: + ccm: 2.34.x + csi: 2.34.x diff --git a/providers/openstack/hcp/1-35/cluster-addon/ccm/Chart.yaml b/providers/openstack/hcp/1-35/cluster-addon/ccm/Chart.yaml new file mode 100644 index 00000000..1a120416 --- /dev/null +++ b/providers/openstack/hcp/1-35/cluster-addon/ccm/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: CCM +name: CCM +version: v1 +dependencies: + - alias: openstack-cloud-controller-manager + name: openstack-cloud-controller-manager + repository: https://kubernetes.github.io/cloud-provider-openstack + version: 2.35.0 diff --git a/providers/openstack/hcp/1-35/cluster-addon/ccm/overwrite.yaml b/providers/openstack/hcp/1-35/cluster-addon/ccm/overwrite.yaml new file mode 100644 index 00000000..39076ecd --- /dev/null +++ b/providers/openstack/hcp/1-35/cluster-addon/ccm/overwrite.yaml @@ -0,0 +1,4 @@ +values: | + openstack-cloud-controller-manager: + cluster: + name: {{ .Cluster.metadata.name }} diff --git a/providers/openstack/hcp/1-35/cluster-addon/ccm/values.yaml b/providers/openstack/hcp/1-35/cluster-addon/ccm/values.yaml new file mode 100644 index 00000000..3f290366 --- /dev/null +++ b/providers/openstack/hcp/1-35/cluster-addon/ccm/values.yaml @@ -0,0 +1,21 @@ +openstack-cloud-controller-manager: + secret: + enabled: true + name: ccm-cloud-config + create: true + nodeSelector: + tolerations: + - key: node.cloudprovider.kubernetes.io/uninitialized + value: "true" + effect: NoSchedule + extraVolumes: + - name: clouds-yaml + secret: + secretName: clouds-yaml + extraVolumeMounts: + - name: clouds-yaml + readOnly: true + mountPath: /etc/openstack + cloudConfig: + global: + use-clouds: true diff --git a/providers/openstack/hcp/1-35/cluster-addon/cni/Chart.yaml b/providers/openstack/hcp/1-35/cluster-addon/cni/Chart.yaml new file mode 100644 index 00000000..051b40b3 --- /dev/null +++ b/providers/openstack/hcp/1-35/cluster-addon/cni/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: CNI +name: CNI +version: v1 +dependencies: + - alias: cilium + name: cilium + repository: https://helm.cilium.io/ + version: 1.19.1 diff --git a/providers/openstack/hcp/1-35/cluster-addon/cni/values.yaml b/providers/openstack/hcp/1-35/cluster-addon/cni/values.yaml new file mode 100644 index 00000000..8a312f0c --- /dev/null +++ b/providers/openstack/hcp/1-35/cluster-addon/cni/values.yaml @@ -0,0 +1,14 @@ +cilium: + namespaceOverride: kube-system + tls: + secretsNamespace: + name: "kube-system" + sessionAffinity: true + sctp: + enabled: true + ipam: + mode: "kubernetes" + gatewayAPI: + enabled: true + secretsNamespace: + name: "kube-system" diff --git a/providers/openstack/hcp/1-35/cluster-addon/csi/Chart.yaml b/providers/openstack/hcp/1-35/cluster-addon/csi/Chart.yaml new file mode 100644 index 00000000..b7d9ee53 --- /dev/null +++ b/providers/openstack/hcp/1-35/cluster-addon/csi/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: CSI +name: CSI +version: v1 +dependencies: + - alias: openstack-cinder-csi + name: openstack-cinder-csi + repository: https://kubernetes.github.io/cloud-provider-openstack + version: 2.35.0 diff --git a/providers/openstack/hcp/1-35/cluster-addon/csi/overwrite.yaml b/providers/openstack/hcp/1-35/cluster-addon/csi/overwrite.yaml new file mode 100644 index 00000000..d191a115 --- /dev/null +++ b/providers/openstack/hcp/1-35/cluster-addon/csi/overwrite.yaml @@ -0,0 +1,3 @@ +values: | + openstack-cinder-csi: + clusterID: "{{ .Cluster.metadata.name }}" diff --git a/providers/openstack/hcp/1-35/cluster-addon/csi/values.yaml b/providers/openstack/hcp/1-35/cluster-addon/csi/values.yaml new file mode 100644 index 00000000..4e648a4f --- /dev/null +++ b/providers/openstack/hcp/1-35/cluster-addon/csi/values.yaml @@ -0,0 +1,41 @@ +openstack-cinder-csi: + secret: + enabled: true + name: csi-cloud-config + create: true + filename: cloud.conf + data: + cloud.conf: |- + [Global] + use-clouds = "true" + clouds-file = /etc/openstack/clouds.yaml + storageClass: + delete: + isDefault: true + csi: + plugin: + volumes: + - name: clouds-yaml + secret: + secretName: clouds-yaml + - name: cloud-conf + secret: + secretName: csi-cloud-config + volumeMounts: + - name: clouds-yaml + readOnly: true + mountPath: /etc/openstack + - name: cloud-conf + readOnly: true + mountPath: /etc/kubernetes + - name: cloud-conf + readOnly: true + mountPath: /etc/config + nodeSelector: + node-role.kubernetes.io/control-plane: "" + tolerations: + - key: node.cloudprovider.kubernetes.io/uninitialized + value: "true" + effect: NoSchedule + - key: node-role.kubernetes.io/control-plane + effect: NoSchedule diff --git a/providers/openstack/hcp/1-35/cluster-addon/metrics-server/Chart.yaml b/providers/openstack/hcp/1-35/cluster-addon/metrics-server/Chart.yaml new file mode 100644 index 00000000..2ac06b1a --- /dev/null +++ b/providers/openstack/hcp/1-35/cluster-addon/metrics-server/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +type: application +description: Metrics Server +name: metrics-server +version: v1 +dependencies: + - name: "metrics-server" + version: "3.13.0" + repository: "https://kubernetes-sigs.github.io/metrics-server/" + alias: "metrics-server" diff --git a/providers/openstack/hcp/1-35/cluster-addon/metrics-server/overwrite.yaml b/providers/openstack/hcp/1-35/cluster-addon/metrics-server/overwrite.yaml new file mode 100644 index 00000000..7b1dcd5b --- /dev/null +++ b/providers/openstack/hcp/1-35/cluster-addon/metrics-server/overwrite.yaml @@ -0,0 +1,4 @@ +values: | + metrics-server: + commonLabels: + domain: "{{ .Cluster.spec.controlPlaneEndpoint.host }}" diff --git a/providers/openstack/hcp/1-35/cluster-addon/metrics-server/values.yaml b/providers/openstack/hcp/1-35/cluster-addon/metrics-server/values.yaml new file mode 100644 index 00000000..a89bf027 --- /dev/null +++ b/providers/openstack/hcp/1-35/cluster-addon/metrics-server/values.yaml @@ -0,0 +1,4 @@ +metrics-server: + fullnameOverride: metrics-server + args: + - --kubelet-insecure-tls diff --git a/providers/openstack/hcp/1-35/cluster-class/Chart.yaml b/providers/openstack/hcp/1-35/cluster-class/Chart.yaml new file mode 100644 index 00000000..13d1cadb --- /dev/null +++ b/providers/openstack/hcp/1-35/cluster-class/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +description: "OpenStack HCP (Hosted Control Plane) Cluster Class for K8s 1.35" +name: openstack-hcp-1-35-cluster-class +type: application +version: v1 diff --git a/providers/openstack/hcp/1-35/cluster-class/templates/_helpers.tpl b/providers/openstack/hcp/1-35/cluster-class/templates/_helpers.tpl new file mode 100644 index 00000000..2339c125 --- /dev/null +++ b/providers/openstack/hcp/1-35/cluster-class/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "cluster-class.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "cluster-class.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "cluster-class.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "cluster-class.labels" -}} +helm.sh/chart: {{ include "cluster-class.chart" . }} +{{ include "cluster-class.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "cluster-class.selectorLabels" -}} +app.kubernetes.io/name: {{ include "cluster-class.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "cluster-class.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "cluster-class.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/providers/openstack/hcp/1-35/cluster-class/templates/cluster-class.yaml b/providers/openstack/hcp/1-35/cluster-class/templates/cluster-class.yaml new file mode 100644 index 00000000..0cd5c804 --- /dev/null +++ b/providers/openstack/hcp/1-35/cluster-class/templates/cluster-class.yaml @@ -0,0 +1,664 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: ClusterClass +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }} +spec: + controlPlane: + ref: + apiVersion: controlplane.cluster.x-k8s.io/v1alpha1 + kind: HostedControlPlaneTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane + # No machineInfrastructure section - control plane is hosted in management cluster + infrastructure: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-cluster + workers: + machineDeployments: + - class: default-worker + template: + bootstrap: + ref: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 + kind: KubeadmConfigTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker + infrastructure: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker + variables: + # Control plane variables (hosted in management cluster) + - name: controlPlaneReplicas + required: false + schema: + openAPIV3Schema: + type: integer + minimum: 1 + default: 3 + description: "Number of hosted control plane replicas for high availability." + - name: gatewayName + required: false + schema: + openAPIV3Schema: + type: string + default: "" + description: "Name of the Gateway API resource for control plane ingress. Optional - control plane accessible via standard kubeconfig if not set." + - name: gatewayNamespace + required: false + schema: + openAPIV3Schema: + type: string + default: "default" + description: "Namespace of the Gateway API resource." + # Image variables + - name: imageName + required: false + schema: + openAPIV3Schema: + type: string + description: | + The base name of the OpenStack image used for provisioning servers. + If `imageIsOrc` is enabled, this name refers to an ORC image resource. + If `imageIsOrc` is disabled, the name is used to filter images available in the OpenStack project. In this case, the specified image must already exist within the project. + If `imageAddVersion` is enabled, the Kubernetes version will be appended to form the complete image name (e.g., imageName-v1.33.6) + default: "ubuntu-capi-image" + - name: imageIsOrc + required: false + schema: + openAPIV3Schema: + type: boolean + description: | + Indicates whether the image name refers to an ORC image resource. + If set to true (default), the `imageName` is interpreted as a reference to an ORC image. + If set to false, the `imageName` is used to filter images in the OpenStack project instead. + default: false + - name: imageAddVersion + required: false + schema: + openAPIV3Schema: + type: boolean + description: | + Add a suffix with the Kubernetes version to the imageName. E.g. imageName-v1.33.6. + default: true + - name: disableAPIServerFloatingIP + required: false + schema: + openAPIV3Schema: + type: boolean + default: true + example: true + description: "DisableAPIServerFloatingIP controls whether a floating IP should be attached to the API server. Set to true for hosted control plane since API server runs in management cluster." + # Network variables + - name: networkExternalID + required: false + schema: + openAPIV3Schema: + type: string + example: "ebfe5546-f09f-4f42-ab54-094e457d42ec" + format: "uuid4" + description: "networkExternalID is the ID of an external OpenStack Network. This is necessary to get public internet to the VMs in case there are several external networks." + - name: networkMTU + required: false + schema: + openAPIV3Schema: + type: integer + example: 1500 + description: "networkMTU sets the maximum transmission unit (MTU) value to address fragmentation for the private network ID." + - name: dnsNameservers + required: false + schema: + openAPIV3Schema: + type: array + description: "dnsNameservers is the list of nameservers for the OpenStack Subnet being created. Set this value when you need to create a new network/subnet which requires access to DNS." + default: ["9.9.9.9", "149.112.112.112"] + example: ["9.9.9.9", "149.112.112.112"] + items: + type: string + - name: nodeCIDR + required: false + schema: + openAPIV3Schema: + type: string + format: "cidr" + default: "10.8.0.0/20" + example: "10.8.0.0/20" + description: |- + nodeCIDR is the OpenStack Subnet to be created. + Cluster actuator will create a network, a subnet with nodeCIDR, + and a router connected to this subnet. + If you leave this empty, no network will be created. + # Workers + - name: workerFlavor + required: false + schema: + openAPIV3Schema: + type: string + default: "SCS-4V-8" + example: "SCS-4V-8" + description: "OpenStack instance flavor for worker nodes (default: SCS-4V-8, which requires workerRootDisk)." + - name: workerRootDisk + required: false + schema: + openAPIV3Schema: + type: integer + minimum: 0 + example: 25 + default: 50 + description: |- + Root disk size in GiB for worker nodes. + OpenStack volume will be created and used instead of an ephemeral disk defined in flavor. + Should be used for the diskless flavors (>= 20), otherwise set to 0. + - name: workerServerGroupID + required: false + schema: + openAPIV3Schema: + type: string + default: "" + example: "869fe071-1e56-46a9-9166-47c9f228e297" + description: "The server group to assign the worker nodes to." + - name: workerAdditionalBlockDevices + required: false + schema: + openAPIV3Schema: + type: array + default: [] + items: + type: object + properties: + name: + type: string + sizeGiB: + type: integer + default: 20 + type: + type: string + default: "__DEFAULT__" + required: ["name"] + # Access management + - name: sshKeyName + required: false + schema: + openAPIV3Schema: + type: string + default: "" + example: "capi-keypair" + description: "The ssh key name to inject in the nodes (for debugging)." + - name: securityGroups + required: false + schema: + openAPIV3Schema: + type: array + default: [] + example: ["security-group-1"] + description: |- + The names of extra security groups to assign to worker nodes. + Will be ignored if `securityGroupIDs` is used. + items: + type: string + - name: securityGroupIDs + required: false + schema: + openAPIV3Schema: + format: "uuid4" + type: array + default: [] + example: ["9ae2f488-30a3-4629-bd51-07acb8eb4278"] + description: "The UUIDs of extra security groups to assign to worker nodes" + items: + type: string + - name: workerSecurityGroups + required: false + schema: + openAPIV3Schema: + type: array + default: [] + example: ["security-group-1"] + description: |- + The names of extra security groups to assign to the worker nodes. + Will be ignored if `workerSecurityGroupIDs` is used. + items: + type: string + - name: workerSecurityGroupIDs + required: false + schema: + openAPIV3Schema: + format: "uuid4" + type: array + default: [] + example: ["9ae2f488-30a3-4629-bd51-07acb8eb4278"] + description: "The UUIDs of extra security groups to assign to the worker nodes" + items: + type: string + - name: identityRef + required: false + schema: + openAPIV3Schema: + type: object + default: {} + properties: + name: + type: string + example: "openstack" + default: "openstack" + description: "The name of the secret that carries the OpenStack clouds.yaml" + cloudName: + type: string + example: "openstack" + default: "openstack" + description: "The name of the cloud to use from the clouds.yaml" + # Kubernetes API server + - name: certSANs + required: false + schema: + openAPIV3Schema: + type: array + default: [] + example: ["mydomain.example"] + description: "certSANs sets extra Subject Alternative Names for the API Server signing cert." + items: + type: string + - name: apiServerLoadBalancer + required: false + schema: + openAPIV3Schema: + type: string + default: "none" + example: "none, octavia-amphora, octavia-ovn" + description: | + For hosted control plane, typically set to "none" since the API server runs in the management cluster. + You can choose from 3 options: + + none: + (default) No LoadBalancer solution will be deployed + + octavia-amphora: + Uses OpenStack's LoadBalancer service Octavia (provider:amphora) + + octavia-ovn: + Uses OpenStack's LoadBalancer service Octavia (provider:ovn) + - name: apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs + required: false + schema: + openAPIV3Schema: + type: array + example: ["192.168.10.0/24"] + description: |- + apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs restrict access to the Kubernetes API server on a network level. + Ensure that at least the outgoing IP of your Management Cluster is added to the list of allowed CIDRs. + Otherwise CAPO can't reconcile the target Cluster correctly. + This requires amphora as load balancer provider in version >= v2.12. + items: + type: string + # + # Patches + # + patches: + # + # Patches for control plane variables (apply to HostedControlPlaneTemplate) + # + - name: controlPlaneReplicas + description: "Sets the number of hosted control plane replicas." + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1alpha1 + kind: HostedControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/replicas" + valueFrom: + variable: controlPlaneReplicas + - name: gatewayName + description: "Sets the Gateway API resource for control plane ingress." + enabledIf: {{ `'{{ ne .gatewayName "" }}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1alpha1 + kind: HostedControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/gateway" + valueFrom: + template: | + name: {{ `{{ .gatewayName }}` }} + namespace: {{ `{{ .gatewayNamespace }}` }} + - name: certSANs + description: "certSANs sets extra Subject Alternative Names for the API Server signing cert." + enabledIf: {{ `'{{ if .certSANs }}true{{end}}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1alpha1 + kind: HostedControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/apiServer/certSANs" + valueFrom: + variable: certSANs + # + # Patches for OpenStackClusterTemplate resource. + # + - name: apiServerLoadBalancerOctaviaAmphora + description: "Takes care of the patches that should be applied when variable apiServerLoadBalancer is set to octavia-amphora." + enabledIf: {{ `'{{ eq .apiServerLoadBalancer "octavia-amphora" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: replace + path: "/spec/template/spec/apiServerLoadBalancer/enabled" + value: true + - op: add + path: "/spec/template/spec/apiServerLoadBalancer/provider" + value: "amphora" + - name: apiServerLoadBalancerOctaviaOVN + description: "Takes care of the patches that should be applied when variable apiServerLoadBalancer is set to octavia-ovn." + enabledIf: {{ `'{{ eq .apiServerLoadBalancer "octavia-ovn" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: replace + path: "/spec/template/spec/apiServerLoadBalancer/enabled" + value: true + - op: add + path: "/spec/template/spec/apiServerLoadBalancer/provider" + value: "ovn" + - name: apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs + description: "Takes care of the patches that should be applied when variable allowedCIDRs is set." + enabledIf: {{ `'{{ and .apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs (eq .apiServerLoadBalancer "octavia-amphora") }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/apiServerLoadBalancer/allowedCIDRs" + valueFrom: + variable: apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs + - name: networkExternalID + description: "Sets the ID of an external OpenStack Network. This is necessary to get public internet to the VMs." + enabledIf: {{ `'{{ if .networkExternalID }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/externalNetwork" + value: {} + - op: add + path: "/spec/template/spec/externalNetwork/id" + valueFrom: + variable: networkExternalID + - name: networkMTU + description: "Sets the network MTU when variable networkMTU exist in cluster resource." + enabledIf: {{ `'{{ if .networkMTU }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/networkMTU" + valueFrom: + variable: networkMTU + - name: identityRef + description: "Sets the OpenStack identity reference." + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: /spec/template/spec/identityRef + valueFrom: + variable: identityRef + - name: nodeCIDRSubnet + description: |- + Sets the NodeCIDR for the OpenStack Subnet to be created. + Cluster actuator will create a network, a subnet with NodeCIDR, + and a router connected to this subnet. + enabledIf: {{ `'{{ if .nodeCIDR }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/managedSubnets" + valueFrom: + template: | + - cidr: '{{ `{{ .nodeCIDR }}` }}' + dnsNameservers: + {{ `{{- range .dnsNameservers }}` }} + - {{ `{{ . }}` }} + {{ `{{- end }}` }} + - name: disableAPIServerFloatingIP + description: "DisableAPIServerFloatingIP controls whether a floating IP should be attached to the API server." + enabledIf: {{ `"{{ if .disableAPIServerFloatingIP }}true{{end}}"` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/disableAPIServerFloatingIP" + valueFrom: + variable: disableAPIServerFloatingIP + # + # Patches for worker's OpenStackMachineTemplate resources. + # Note: No control plane patches since control plane runs in management cluster + # + - name: workerImage + description: "Sets the OpenStack image name that is used for creating the worker servers." + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: /spec/template/spec/image + valueFrom: + template: | + {{ `{{ if .imageIsOrc }}imageRef{{ else }}filter{{ end }}` }}: + name: {{ `{{ .imageName }}{{ if .imageAddVersion }}-{{ .builtin.machineDeployment.version }}{{ end }}` }} + - name: workerFlavor + description: "Sets the openstack instance flavor for the worker nodes." + enabledIf: {{ `'{{ ne .workerFlavor "" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: replace + path: "/spec/template/spec/flavor" + valueFrom: + variable: workerFlavor + - name: workerRootDisk + description: "Sets the root disk size in GiB for worker nodes." + enabledIf: {{ `'{{ if .workerRootDisk }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/rootVolume" + valueFrom: + template: | + sizeGiB: {{ `{{ .workerRootDisk }}` }} + - name: workerServerGroupID + description: "Sets the server group to assign the worker nodes to." + enabledIf: {{ `'{{ ne .workerServerGroupID "" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/serverGroup" + valueFrom: + template: | + id: {{ `{{ .workerServerGroupID }}` }} + - name: workerAdditionalBlockDevices + enabledIf: {{ `'{{ if .workerAdditionalBlockDevices }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: /spec/template/spec/additionalBlockDevices + valueFrom: + template: | + {{ `{{- range .workerAdditionalBlockDevices }}` }} + - name: {{ `{{ .name }}` }} + sizeGiB: {{ `{{ .sizeGiB }}` }} + storage: + type: Volume + volume: + type: {{ `{{ .type }}` }} + {{ `{{- end }}` }} + # Note: The securityGroups patch must be placed before securityGroupIDs, workerSecurityGroups, and workerSecurityGroupIDs. + # The patch order ensures the last applied patch overwrites previous ones. + - name: securityGroups + description: "Sets the list of the openstack security groups for the worker instances." + enabledIf: {{ `'{{ if .securityGroups }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .securityGroups }} { filter: { name: {{ . }}}}, {{ end }} ]'` }} + # Note: The securityGroupIDs patch must be placed before workerSecurityGroups, workerSecurityGroupIDs and after securityGroupIDs. + # The patch order ensures the last applied patch overwrites previous ones. + - name: securityGroupIDs + description: "Sets the list of the openstack security groups for the worker instances by UUID." + enabledIf: {{ `'{{ if .securityGroupIDs }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .securityGroupIDs }} { id: {{ . }} }, {{ end }} ]'` }} + - name: sshKeyName + description: "Sets the ssh key to inject in the nodes." + enabledIf: {{ `'{{ ne .sshKeyName "" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/sshKeyName" + valueFrom: + variable: sshKeyName + # Note: The workerSecurityGroups patch must be placed before workerSecurityGroupIDs and after securityGroups and securityGroupIDs. + # The patch order ensures the last applied patch overwrites previous ones. + - name: workerSecurityGroups + description: "Sets the list of the openstack security groups for the worker instances." + enabledIf: {{ `'{{ if .workerSecurityGroups }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .workerSecurityGroups }} { filter: { name: {{ . }}}}, {{ end }} ]'` }} + # Note: The workerSecurityGroupIDs patch must be placed after securityGroups, securityGroupIDs and workerSecurityGroupIDs. + # The patch order ensures the last applied patch overwrites previous ones. + - name: workerSecurityGroupIDs + description: "Sets the list of the openstack security groups for the worker instances by UUID." + enabledIf: {{ `'{{ if .workerSecurityGroupIDs }}true{{end}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .workerSecurityGroupIDs }} { id: {{ . }} }, {{ end }} ]'` }} diff --git a/providers/openstack/hcp/1-35/cluster-class/templates/hosted-control-plane-template.yaml b/providers/openstack/hcp/1-35/cluster-class/templates/hosted-control-plane-template.yaml new file mode 100644 index 00000000..b1caeea8 --- /dev/null +++ b/providers/openstack/hcp/1-35/cluster-class/templates/hosted-control-plane-template.yaml @@ -0,0 +1,10 @@ +apiVersion: controlplane.cluster.x-k8s.io/v1alpha1 +kind: HostedControlPlaneTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane +spec: + template: + spec: + gateway: + namespace: overridden-by-patch + name: overridden-by-patch diff --git a/providers/openstack/hcp/1-35/cluster-class/templates/kubeadm-config-template-default-worker.yaml b/providers/openstack/hcp/1-35/cluster-class/templates/kubeadm-config-template-default-worker.yaml new file mode 100644 index 00000000..4c1494ed --- /dev/null +++ b/providers/openstack/hcp/1-35/cluster-class/templates/kubeadm-config-template-default-worker.yaml @@ -0,0 +1,13 @@ +apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +kind: KubeadmConfigTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker +spec: + template: + spec: + joinConfiguration: + nodeRegistration: + kubeletExtraArgs: + cloud-provider: external + provider-id: 'openstack:///{{ `{{ instance_id }}` }}' + name: '{{ `{{ local_hostname }}` }}' diff --git a/providers/openstack/hcp/1-35/cluster-class/templates/openstack-cluster-template.yaml b/providers/openstack/hcp/1-35/cluster-class/templates/openstack-cluster-template.yaml new file mode 100644 index 00000000..3b8ae609 --- /dev/null +++ b/providers/openstack/hcp/1-35/cluster-class/templates/openstack-cluster-template.yaml @@ -0,0 +1,43 @@ +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: OpenStackClusterTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-cluster +spec: + template: + spec: + identityRef: + cloudName: overridden-by-patch + name: overridden-by-patch + apiServerLoadBalancer: + enabled: false + disableAPIServerFloatingIP: true + managedSecurityGroups: + allNodesSecurityGroupRules: + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: VXLAN (Cilium) + portRangeMin: 8472 + portRangeMax: 8472 + protocol: udp + description: "Allow VXLAN traffic for Cilium" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: HealthCheck (Cilium) + portRangeMin: 4240 + portRangeMax: 4240 + protocol: tcp + description: "Allow HealthCheck traffic for Cilium" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Hubble (Cilium) + portRangeMin: 4244 + portRangeMax: 4244 + protocol: tcp + description: "Allow Hubble traffic for Cilium" diff --git a/providers/openstack/hcp/1-35/cluster-class/templates/openstack-machine-template-default-worker.yaml b/providers/openstack/hcp/1-35/cluster-class/templates/openstack-machine-template-default-worker.yaml new file mode 100644 index 00000000..920dbb0a --- /dev/null +++ b/providers/openstack/hcp/1-35/cluster-class/templates/openstack-machine-template-default-worker.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: OpenStackMachineTemplate +metadata: + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker +spec: + template: + spec: + flavor: overridden-by-patch + image: + imageRef: + name: overridden-by-patch diff --git a/providers/openstack/hcp/1-35/cluster-class/values.yaml b/providers/openstack/hcp/1-35/cluster-class/values.yaml new file mode 100644 index 00000000..5ebb9d61 --- /dev/null +++ b/providers/openstack/hcp/1-35/cluster-class/values.yaml @@ -0,0 +1,24 @@ +# Default values for hosted-control-plane cluster class + +# OpenStack credentials +identityRef: + name: "openstack" + cloudName: "openstack" + +# Network configuration +networkExternalID: "" +dnsNameservers: + - "9.9.9.9" + - "149.112.112.112" +nodeCIDR: "10.8.0.0/20" + +# Worker node configuration +workerFlavor: "SCS-2V-4" +workerRootDisk: 25 + +# Control plane configuration (hosted in management cluster) +controlPlaneReplicas: 3 + +# Gateway configuration for hosted control plane (optional) +gatewayName: "" +gatewayNamespace: "" diff --git a/providers/openstack/hcp/1-35/clusteraddon.yaml b/providers/openstack/hcp/1-35/clusteraddon.yaml new file mode 100644 index 00000000..d346ba22 --- /dev/null +++ b/providers/openstack/hcp/1-35/clusteraddon.yaml @@ -0,0 +1,21 @@ +apiVersion: clusteraddonconfig.x-k8s.io/v1alpha1 +clusterAddonVersion: clusteraddons.clusterstack.x-k8s.io/v1alpha1 +addonStages: + AfterControlPlaneInitialized: + - name: cni + action: apply + - name: metrics-server + action: apply + - name: csi + action: apply + - name: ccm + action: apply + BeforeClusterUpgrade: + - name: cni + action: apply + - name: metrics-server + action: apply + - name: csi + action: apply + - name: ccm + action: apply diff --git a/providers/openstack/hcp/1-35/stack.yaml b/providers/openstack/hcp/1-35/stack.yaml new file mode 100644 index 00000000..4e3c1659 --- /dev/null +++ b/providers/openstack/hcp/1-35/stack.yaml @@ -0,0 +1,7 @@ +provider: openstack +clusterStackName: hcp +kubernetesVersion: 1.35 + +addons: + ccm: 2.35.x + csi: 2.35.x From 9170707dd8ce8705df5d385e3111e6864f5d8ac3 Mon Sep 17 00:00:00 2001 From: Jan Schoone Date: Mon, 23 Feb 2026 17:13:49 +0000 Subject: [PATCH 04/11] docs: Rewrite all documentation for per-minor-version structure Rewrite all documentation to reflect the new per-Kubernetes-minor-version directory layout, OCI-based workflow, and v1beta2 ClusterClass variables. Tooling changes: - hack/show-matrix.sh: Add --markdown flag for GFM table output - hack/docugen.py: Add --matrix flag to embed version matrix in docs - hack/config-template.md: Update template for v1beta2, add !!matrix!! - justfile: Fix generate-docs to iterate all stacks with correct args New documentation: - docs/quickstart.md: Universal quickstart (OpenStack, Docker, HCP) - docs/providers/openstack/hcp.md: HCP architecture and configuration - docs/providers/openstack/scs-configuration.md: v1beta2 variable reference - docs/providers/docker/scs-configuration.md: Docker stack variable reference Removed: - docs/providers/openstack/quickstart.md (replaced by docs/quickstart.md) - docs/providers/openstack/configuration.md (replaced by scs-configuration.md) Assisted-by: Claude Code Signed-off-by: Jan Schoone --- README.md | 89 +++++- docs/overview.md | 234 +++++++------- docs/providers/docker/scs-configuration.md | 56 ++++ docs/providers/openstack/configuration.md | 84 ----- docs/providers/openstack/hcp.md | 153 +++++++++ docs/providers/openstack/quickstart.md | 290 ------------------ docs/providers/openstack/scs-configuration.md | 103 +++++++ docs/quickstart.md | 270 ++++++++++++++++ hack/config-template.md | 47 +-- hack/docugen.py | 258 +++++++++------- 10 files changed, 942 insertions(+), 642 deletions(-) create mode 100644 docs/providers/docker/scs-configuration.md delete mode 100644 docs/providers/openstack/configuration.md create mode 100644 docs/providers/openstack/hcp.md delete mode 100644 docs/providers/openstack/quickstart.md create mode 100644 docs/providers/openstack/scs-configuration.md create mode 100644 docs/quickstart.md diff --git a/README.md b/README.md index 78f40263..5c3f06f6 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,87 @@ # Cluster Stacks -:wave: Welcome to the SCS Cluster Stacks. The reference implementation for SCS KaaS. +Reference implementation of SCS Kubernetes-as-a-Service cluster stacks, built on +[Cluster API](https://cluster-api.sigs.k8s.io/) and managed by the +[Cluster Stack Operator](https://github.com/SovereignCloudStack/cluster-stack-operator) (CSO). -#### Useful links +## Quick start -- [Quick Start](providers/docker/scs/README.md) -- [Docs](https://docs.scs.community/docs/category/cluster-stacks) +```bash +# Prerequisites: kind, kubectl, helm, clusterctl, just +# See docs/quickstart.md for full details -## What is Cluster Stacks? +# Create a management cluster and install CAPI + provider +kind create cluster +clusterctl init --infrastructure openstack -Cluster Stacks is a comprehensive framework and set of reference -implementations for defining and managing Kubernetes clusters via the Cluster -API. Designed to support multiple providers and a broad range of Kubernetes -versions, it offers a standardized approach to configuring and operating -Kubernetes environments. For detailed information see [docs.scs.community](https://docs.scs.community/docs/container/components/cluster-stacks/components/cluster-stacks/overview). +# Install the CSO (auto-configures ttl.sh for development) +just install-cso + +# Build, publish, and generate ClusterStack resource for a specific version +just dev --version 1.35 +``` + +See [docs/quickstart.md](docs/quickstart.md) for a complete walkthrough. + +## Available cluster stacks + +| Provider | Stack | Description | Versions | +|------------|-------|------------------------------------------|----------------| +| OpenStack | scs | Standard SCS stack (dedicated VMs) | 1-32 .. 1-35 | +| OpenStack | hcp | Hosted Control Plane (CP as pods) | 1-33 .. 1-35 | +| Docker | scs | Local development stack | 1-32 .. 1-35 | + +## Repository structure + +``` +providers/ + / + / + 1-XX/ # per-Kubernetes-minor-version directory + stack.yaml # stack metadata and addon version pins + cluster-class/ # Helm chart: ClusterClass + infrastructure templates + cluster-addon/ # Helm chart: CNI, CCM, CSI, metrics-server + node-images/ # image build instructions (OpenStack only) +``` + +Each `1-XX/` directory is self-contained: it carries its own `stack.yaml`, +ClusterClass definition, and addon charts. There is no shared state between +minor versions. + +## Build system + +All workflows are driven by [`just`](https://just.systems): + +```bash +just build --version 1.35 # build locally to .release/ +just publish --version 1.35 # build + push to OCI registry +just dev --version 1.35 # publish + print ClusterStack YAML +just dev --install-cso --version 1.35 # also install/upgrade CSO +just matrix # show version/addon matrix +just update versions # update Kubernetes patch versions +just update addons # update addon chart versions +just generate-resources --version 1.35 # generate ClusterStack + Cluster YAML +just generate-docs # regenerate configuration docs +``` + +Set `PROVIDER` and `CLUSTER_STACK` environment variables (or use a `.env` file) +to target a different stack (default: `openstack`/`scs`). + +## Documentation + +- [Overview](docs/overview.md) -- architecture, versioning, and structure +- [Quickstart](docs/quickstart.md) -- end-to-end guide for all providers +- [OpenStack HCP](docs/providers/openstack/hcp.md) -- Hosted Control Plane stack + +Configuration references are generated from ClusterClass definitions via +`just generate-docs`. ## Releases -Releases for the providers openstack/scs and openstack/scs2 are published in -the [SCS registry](oci://registry.scs.community/kaas/cluster-stacks). +Releases are published as OCI artifacts to the +[SCS registry](https://registry.scs.community/kaas/cluster-stacks). ## Community - [Matrix](https://matrix.to/#/!NZpJdPGjAHISXnHUil:matrix.org) -- [notes](https://input.scs.community/2025-scs-team-container) - - +- [Meeting notes](https://input.scs.community/2025-scs-team-container) diff --git a/docs/overview.md b/docs/overview.md index 60d45ec4..3cb3c9e6 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -1,148 +1,152 @@ # Overview -## Cluster Stacks +## What is a Cluster Stack? -Cluster Stacks is a comprehensive framework and reference implementations for defining and managing Kubernetes clusters via the Cluster API. It is designed to cater to multiple providers and supports a broad range of Kubernetes versions, offering a standardized approach to managing and configuring Kubernetes clusters. +A cluster stack is a versioned bundle of everything needed to create and operate +a Kubernetes cluster via the Cluster API (CAPI). It packages three layers: -It encapsulates multiple layers, including node configuration, Cluster API setup, and application-level configurations, such as the Container Network Interface (CNI). By packaging these interdependent configurations, the cluster stack allows for efficient management and deployment of Kubernetes clusters, offering standardized, resilient, and self-managed Kubernetes environments. +1. **Cluster Class** -- a CAPI `ClusterClass` resource that defines the + infrastructure templates, machine configurations, and topology variables. +2. **Cluster Addons** -- Helm charts for core cluster components: CNI (Cilium), + Cloud Controller Manager, CSI driver, and metrics-server. +3. **Node Images** -- build instructions or references for the OS images that + run on cluster nodes. -## 🔧 Usage +These layers are published together as an OCI artifact. The +[Cluster Stack Operator](https://github.com/SovereignCloudStack/cluster-stack-operator) +(CSO) pulls these artifacts and installs them into a management cluster, making +the `ClusterClass` available for creating workload clusters. -Follow our [quickstart guide](providers/openstack/quickstart.md) for an introduction on how to deploy cluster stacks on openstack. +## Repository structure -## Layers of a Cluster Stack - -In essence, a cluster stack is an amalgamation of various components each of which serves a crucial role in setting up, maintaining, and operating a Kubernetes cluster. In the context of our framework, we categorize these components into three core layers: `cluster-class`, `cluster-addons`, and `node-images`. Let's delve deeper into understanding each of these layers: - -### 📚 Cluster Class - -The Cluster Class serves as a blueprint for creating and configuring Kubernetes clusters consistently. It encapsulates various aspects of a cluster, including: - -* The infrastructure provider details -* Networking configurations -* Cluster-class templating -* Other cluster-specific settings - -Essentially, it defines the desired configuration and properties of a Kubernetes cluster. It leverages the [ClusterClass](https://cluster-api.sigs.k8s.io/tasks/experimental-features/cluster-class/) feature of Cluster API, which provides a declarative, Kubernetes-style API for cluster creation, configuration, and management. Any change in this layer or in the node-image or cluster-addon layers triggers a version bump in the cluster class, hence the cluster stack. - -### 🎁 Cluster Addons - -Cluster Addons are core components or services required for the Kubernetes cluster to function correctly and efficiently. These are not user-facing applications but rather foundational services critical to the operation and management of a Kubernetes cluster. They're usually installed and configured after the cluster infrastructure has been provisioned and before the cluster is ready to serve workloads. - -Cluster addons encompass a variety of functionalities, including but not limited to: - -* Container Network Interfaces (CNI): These are plugins that facilitate container networking. A CNI is integral to setting up network connectivity and ensuring communication between pods in a Kubernetes cluster. -* Cloud Controller Manager (CCM): The CCM is a Kubernetes control plane component that embeds the cloud-specific control logic. Its role is to manage the communication with the underlying cloud services. -* Konnectivity service: This is a network proxy that enables connectivity from the control plane to nodes and vice versa. It is a critical component that supports Kubernetes API server connectivity. -* Metrics Server: A cluster-wide aggregator of resource usage data, Metrics Server collects CPU, memory, and other metrics from nodes and pods, enabling features like Horizontal Pod Autoscaling. - -It's important to note that cluster addons are not user-provided applications or services that can be installed multiple times, such as ingress controllers, application-level monitoring tools, or user-facing APIs. Those are left to the discretion and responsibility of the users, who install and manage them according to their specific needs and preferences. - -Each addon version is independent and can be updated separately. However, a change in this layer also necessitates a version bump in the cluster class and the cluster stack, which is reflected in the metadata.yaml. - -### 🎞️ Node Images - -Node images provide the foundation for the operating system environment on each node of a Kubernetes cluster. They are typically a minimal operating system distribution, like a lightweight Linux distro, which may also include container runtime components such as Docker or containerd. - -Node images are responsible for providing the necessary environment and dependencies to support Kubernetes components and workloads. This includes components like kubelet, kube-proxy, and other necessary system utilities and libraries. - -The version of a node image can be different from that of the cluster stack or the cluster class. However, an update to a node image will trigger a version bump in the cluster class and hence the cluster stack. - -In the cluster-stacks repository's directory structure, the build instructions for Node Images are always placed within the respective directory. The instructions outline the steps and configurations required to create the Node Image automatically. The specific method for releasing the Node Image may vary based on the provider's capabilities and requirements. - -During the development phase, the build instructions serve as a reference within the repository itself. These instructions may utilize tools like Packer or other image-building techniques. This allows for flexibility and customization, enabling users to define their Node Images according to specific needs and requirements. - -However, when it comes to the release of the cluster stack, the Node Image can be provided in different ways depending on the capabilities of the provider or the desired deployment method. Here are a few examples: - -1. **URL on a remote endpoint**: In some cases, `providers` may support deploying a Node Image directly from a URL. In this scenario, the Node Image referenced in the `cluster stack`, specifically in the `cluster class`, would be provided as a URL pointing to a pre-built image accessible remotely. -1. **Artifact**: If the provider supports artifacts, the Node Image can be released as an artifact, such as a qcow2 file. The artifact would be uploaded to the provider, and the `cluster stack` references the artifact for node provisioning. -1. **Build Instructions**: In cases where the provider doesn't support direct URL deployment or artifact-based provisioning, the build instructions defined within the repository become critical. The build instructions serve as a comprehensive guide to build the Node Image, specifying all the necessary steps and configurations. - -Regardless of the release method, the cluster stack, specifically the cluster class, references the appropriate Node Image to be used for node provisioning. - -By allowing flexibility in the release and deployment methods of Node Images, the cluster stack framework caters to various provider capabilities and user requirements. This adaptability ensures the cluster stack can be deployed in diverse environments while maintaining a consistent and manageable approach to managing Kubernetes clusters. - -## 🌐 IaaS Provider, Kubernetes Service Provider, and Cluster API -In the context of the `cluster-stacks`, we distinguish between two types of providers: - -An **IaaS Provider**, in general, offers Infrastructure as a Service - providing the fundamental compute, storage, and network resources on which workloads can be run. In the context of cluster-stacks, an IaaS Provider specifically refers to an entity that owns an API for their infrastructure. If an organization uses a common infrastructure API, such as OpenStack, they are not considered an IaaS Provider in this context. However, if the organization owns the API for its infrastructure, it becomes an IaaS Provider for the purposes of cluster-stacks. +``` +providers/ + / + / + 1-XX/ # one directory per Kubernetes minor version + stack.yaml # metadata: provider, name, k8s version, addon pins + cluster-class/ # Helm chart producing the ClusterClass + cluster-addon/ # Helm chart with CNI, CCM, CSI, metrics-server + node-images/ # image build definitions (provider-specific) + image-manager.yaml # OpenStack only: aggregated image references +``` -A **Kubernetes Service Provider**, on the other hand, is an entity that implements a cluster stack. They do so on top of the IaaS Providers, potentially spanning across multiple IaaS Providers. They use the IaaS Provider's infrastructure services and integrate them into their cluster stack implementations. +### Per-minor-version directories -The **Cluster API (CAPI)** is a Kubernetes project aimed at simplifying the process of managing Kubernetes clusters. It offers a declarative API that automates the creation, configuration, and management of clusters, providing a standardized way to interact with Kubernetes. The cluster stack approach leverages CAPI to deliver self-managed Kubernetes clusters. +Each `1-XX/` directory is completely self-contained. It carries its own +`stack.yaml`, ClusterClass templates, and addon charts. There is no inheritance +or sharing between minor versions -- changes to one version never affect another. -## 📌 Defining and Adding Providers -The structure of this repository is specifically designed to handle multiple providers, multiple cluster stacks per provider, and multiple Kubernetes versions per cluster stack. This organized structure allows us to effectively manage, develop, and maintain multiple cluster stacks across various Kubernetes versions and providers, all in a single repository. +This design makes it straightforward to: -### 📁 Repository Structure -The repository maintains a specific structure: +- Support different CAPI API versions across minors (e.g. `v1beta1` for 1-32, + `v1beta2` for 1-35). +- Pin addons to version ranges that match the Kubernetes minor + (e.g. CCM `2.34.x` for K8s 1.34). +- Drop old versions by simply removing their directory. -* Each IaaS Provider has a directory under providers. -* Each IaaS Provider can have multiple cluster stack implementations. -* Each cluster stack supports multiple Kubernetes major and minor versions. +### stack.yaml -``` -providers/ -└── / - └── / - └── / -``` +Each version directory contains a `stack.yaml` that serves as the single source +of truth for that version: -The directory structure for adding a new provider would look something like this: +```yaml +provider: openstack +clusterStackName: scs +kubernetesVersion: 1.35 +addons: # version pins used by `just update addons` + ccm: 2.35.x + csi: 2.35.x ``` -providers/// -# example -providers/openstack/scs/1-28 -``` -This granular, hierarchical structure allows us to manage different versions of Kubernetes and their associated cluster stacks across different providers. -We decided to support multiple Kubernetes major and minor versions to provide the flexibility to accommodate different implementation requirements of the provider. However, we deliberately chose not to support Kubernetes patch versions directly. The reason is the high frequency of patch versions release (often weekly), which would complicate maintenance efforts significantly. +The `addons` section declares SemVer ranges. When you run `just update addons`, +the build system resolves these ranges against upstream Helm repositories and +updates the `Chart.yaml` dependencies in the addon chart. -Instead, we represent Kubernetes patch version updates through changes in our cluster stack version. For instance, if a patch version of Kubernetes necessitates a change in the node-image or the cluster-class configuration, this would trigger a version bump in the corresponding cluster stack, hence the cluster class, as reflected in the metadata.yaml. +## Available stacks -In this way, our versioning system, our directory structure, and our approach to Kubernetes versioning are all interlinked, providing us a comprehensive, manageable, and resilient framework for maintaining various Kubernetes distributions or cluster stacks across multiple providers and versions. +### OpenStack / scs -## 📑 Versioning +The standard SCS cluster stack. Creates dedicated VMs for both control plane and +worker nodes on OpenStack. Supports Kubernetes 1.32 through 1.35. -Note: This section is subject to change, as our new tool [csctl](https://github.com/SovereignCloudStack/csctl) will incorporate future versioning capabilities. +- Versions 1-32 and below use CAPI `v1beta1` with role-prefixed variables + (`controlPlaneFlavor`, `workerFlavor`). +- Version 1-35 uses CAPI `v1beta2` with unified variables (`flavor`) and + per-role overrides via `topology.controlPlane.variables.overrides`. -A fundamental aspect of the cluster stack approach is the encapsulation of versioning within a cluster stack distribution. Each of the components can be updated independently, leading to a flexible and maintainable system. +### OpenStack / hcp -However, the critical point to understand here is the relationship between these component versions and the cluster stack version. Whenever there's a change or an update to either the cluster addon or the node image, the version of the cluster stack must be bumped. And due to the connection between the cluster class and the cluster stack, the cluster class version must be updated to match the new cluster stack version. +Hosted Control Plane stack. The Kubernetes control plane runs as pods in the +management cluster (using the +[teutonet Hosted Control Plane provider](https://github.com/teutonet/cluster-api-provider-hosted-control-plane)); +only worker nodes are OpenStack VMs. -The cluster stack version doesn't simply mirror the versions of its components, but rather, it reflects the "version of change". In essence, the cluster stack version is a reflection of the state of the entire stack as a whole at a particular point in time. Any change in the components warrants a new state, and therefore a new version of the cluster stack. +See [providers/openstack/hcp.md](providers/openstack/hcp.md) for details. -So, an update to the cluster addon component will bump the version of the cluster stack, irrespective of the existing version of the node image. The same applies vice versa. When such an update occurs, the version of the cluster class is also incremented to align with the new cluster stack version, maintaining the unity of the cluster stack framework. +### Docker / scs -This versioning approach ensures a clear and precise track of changes, promoting efficient management, and isolated testing. It offers enhanced resilience for the Kubernetes distribution or the cluster stack, ensuring safe and secure upgrades even in rapid update cycles. It's an efficient method of maintaining stability in the rapidly changing environment of a Kubernetes stack. +A lightweight stack for local development and CI. Uses the CAPI Docker +infrastructure provider. No cloud credentials required. -The versioning of the cluster stack is primarily managed through a file named metadata.yaml, located at the root directory of each cluster stack. This file serves as the source of truth for the versioning information of the cluster stack, cluster class, node images, and cluster addons. +## Versioning -Here is an example of how metadata.yaml could look like: -``` -apiVersion: metadata.clusterstack.x-k8s.io/v1alpha1 -versions: - clusterStack: v3 - kubernetes: v1.27.3 - components: - clusterAddon: v2 - nodeImage: v1 -``` -In this example, the cluster stack (and thus the cluster class) is on version 3, while the cluster addon is on version 2 and node image is on version 1. +A cluster stack version is a single integer (`v1`, `v2`, ...) that represents +the state of the entire bundle at a point in time. Any change to the +ClusterClass, addons, or node images produces a new version. -When there's a change or update in the node images or cluster addons, we would bump the version of the cluster stack and cluster class, while leaving the unaffected component's version intact. So if the node image was updated, the metadata.yaml might then look like this: +The full identifier of a cluster stack release is: ``` -apiVersion: metadata.clusterstack.x-k8s.io/v1alpha1 -versions: - clusterStack: v4 - kubernetes: v1.27.3 - components: - clusterAddon: v2 - nodeImage: v2 +----v ``` -Here, the cluster stack and cluster class versions were updated to v4, the node image version was bumped to v2 due to the changes, while the cluster addon remained on v2 as it was not affected by the update. - -This versioning approach allows us to keep track of changes across different components, manage these components effectively, and conduct isolated testing. This ensures that our Kubernetes distribution or cluster stack remains resilient, and we can perform safe and secure upgrades even in the face of rapid update cycles. The metadata.yaml plays a critical role in maintaining this structure and providing an accurate representation of the state of the whole stack at any given time. +For example: `openstack-scs-1-35-v3` means the third release of the `scs` stack +for Kubernetes 1.35 on OpenStack. + +Kubernetes patch versions are not part of the directory structure. A patch +version update (e.g. 1.35.1 to 1.35.2) is delivered by bumping the cluster +stack version, which updates the `kubernetesVersion` field and triggers a +rolling update of nodes. + +## Build system + +The build system uses [`just`](https://just.systems) as a task runner and a set +of bash scripts in `hack/`: + +| Command | Description | +|------------------------------------|------------------------------------------------| +| `just build --version 1.35` | Build locally to `.release/` | +| `just publish --version 1.35` | Build and push to OCI registry | +| `just dev --version 1.35` | Publish and print `ClusterStack` resource YAML | +| `just dev --install-cso --version 1.35` | Also install/upgrade CSO via Helm | +| `just install-cso` | Install CSO standalone | +| `just matrix` | Show version and addon matrix | +| `just update versions` | Update Kubernetes patch versions | +| `just update addons` | Update addon charts from upstream | +| `just generate-resources` | Generate `ClusterStack` + `Cluster` YAML | +| `just generate-docs` | Regenerate configuration documentation | +| `just clean` | Remove `.release/` build artifacts | + +### OCI workflow + +Cluster stack releases are OCI artifacts. For development, the build system +auto-configures [ttl.sh](https://ttl.sh) as a temporary registry (artifacts +expire after 24 hours). For production, set `OCI_REGISTRY` and +`OCI_REPOSITORY` environment variables to point to your registry. + +The CSO is installed via its Helm chart at +`oci://registry.scs.community/cluster-stacks/cso` and configured to pull from +whichever OCI registry you are publishing to. + +## Providers and the Cluster API + +The Cluster API (CAPI) provides a declarative, Kubernetes-native API for +creating and managing clusters. Each infrastructure provider (OpenStack, Docker, +etc.) implements the CAPI contracts for provisioning machines and networks. + +The CSO sits on top of CAPI: it manages the lifecycle of cluster stack releases, +installs ClusterClasses, and handles version upgrades. Users create `Cluster` +resources that reference a `ClusterClass` -- the CAPI topology controller then +reconciles the desired state into actual infrastructure. diff --git a/docs/providers/docker/scs-configuration.md b/docs/providers/docker/scs-configuration.md new file mode 100644 index 00000000..59be7fb9 --- /dev/null +++ b/docs/providers/docker/scs-configuration.md @@ -0,0 +1,56 @@ +# Configuration (Docker / scs) + +The Docker cluster stack is designed for local development and CI. It uses the +CAPI Docker infrastructure provider and requires no cloud credentials. + +## Version matrix + +| Version | K8s | CS Version | cilium | metrics-server | +|---------|-----|----------|---------|---------| +| 1-32 | 1.32 | - | 1.19.1 | 3.13.0 | +| 1-33 | 1.33 | - | 1.19.1 | 3.13.0 | +| 1-34 | 1.34 | - | 1.19.1 | 3.13.0 | +| 1-35 | 1.35 | - | 1.19.1 | 3.13.0 | + +## Example + +```yaml +apiVersion: cluster.x-k8s.io/v1beta2 +kind: Cluster +metadata: + name: my-docker-cluster + namespace: default +spec: + clusterNetwork: + pods: + cidrBlocks: + - 192.168.0.0/16 + serviceDomain: cluster.local + services: + cidrBlocks: + - 10.96.0.0/12 + topology: + class: docker-scs-1-35-v1 + controlPlane: + replicas: 1 + version: v1.35.0 + workers: + machineDeployments: + - class: default-worker + name: default-worker + replicas: 1 +``` + +## Available variables + +|Name|Type|Default|Example|Description|Required| +|----|----|-------|-------|-----------|--------| +|`imageRepository`|string|""|"registry.k8s.io"|Container registry to pull images from. If empty, the kubeadm default is used.|False| +|`certSANs`|array|[]|["mydomain.example"]|Extra Subject Alternative Names for the API Server signing certificate.|False| +|`oidcConfig.clientID`|string||"kubectl"|OIDC client ID for API server authentication.|| +|`oidcConfig.issuerURL`|string||"https://dex.example.com"|OIDC provider discovery URL (must be HTTPS).|| +|`oidcConfig.usernameClaim`|string|"preferred_username"|"email"|JWT claim to use as the username.|| +|`oidcConfig.groupsClaim`|string|"groups"|"groups"|JWT claim to use as groups.|| +|`oidcConfig.usernamePrefix`|string|"oidc:"|"oidc:"|Prefix for OIDC usernames.|| +|`oidcConfig.groupsPrefix`|string|"oidc:"|"oidc:"|Prefix for OIDC group names.|| +|`registryMirrors`|array|[]|[{"hostnameUpstream": "docker.io", "urlMirror": "https://mirror.example.com"}]|Container registry mirrors for node containerd/CRI-O configuration.|| diff --git a/docs/providers/openstack/configuration.md b/docs/providers/openstack/configuration.md deleted file mode 100644 index f72b29f2..00000000 --- a/docs/providers/openstack/configuration.md +++ /dev/null @@ -1,84 +0,0 @@ -# Configuration - -This page lists the custom configuration options available, including their default values and if they are optional. The following example shows how these variables can be used inside the `cluster.yaml` file under `spec.topology.variables`. - -## Example - -```yaml -apiVersion: cluster.x-k8s.io/v1beta1 -kind: Cluster -metadata: - name: - namespace: - labels: - managed-secret: cloud-config -spec: - clusterNetwork: - pods: - cidrBlocks: - - 192.168.0.0/16 - serviceDomain: cluster.local - services: - cidrBlocks: - - 10.96.0.0/12 - topology: - variables: // <-- variables from the table can be set here - - name: controller_flavor - value: "SCS-4V-8-20" - - name: worker_flavor - value: "SCS-4V-8-20" - - name: external_id - value: "ebfe5546-f09f-4f42-ab54-094e457d42ec" - class: openstack-alpha-1-29-v2 - controlPlane: - replicas: 2 - version: v1.29.3 - workers: - machineDeployments: - - class: openstack-alpha-1-29-v2 - failureDomain: nova - name: openstack-alpha-1-29-v2 - replicas: 4 -``` - -Variables from the table containing a `.` are to be used in an object with the part before the dot being the object name and the part behind the dot being the value names. The following example demonstrates this with `oidc_config`. - -```yaml ---- -topology: - variables: - - name: oidc_config - value: - issuer_url: "https://dex.k8s.scs.community" - client_id: "kubectl" -``` - -## Available variables - - -| Name | Type | Default | Example | Description | Required | -| ---------------------------------- | ------- | -------------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -| `external_id` | string | "" | "ebfe5546-f09f-4f42-ab54-094e457d42ec" | ExternalNetworkID is the ID of an external OpenStack Network. This is necessary to get public internet to the VMs. | False | -| `controller_flavor` | string | "SCS-2V-4-20s" | "SCS-2V-4-20s" | OpenStack instance flavor for control-plane nodes. | False | -| `worker_flavor` | string | "SCS-2V-4" | "SCS-2V-4" | OpenStack instance flavor for worker nodes. | False | -| `controller_root_disk` | integer | | 25 | Root disk size in GiB for control-plane nodes. OpenStack volume will be created and used instead of an ephemeral disk defined in flavor. Should only be used for the diskless flavors. | False | -| `worker_root_disk` | integer | 25 | 25 | Root disk size in GiB for worker nodes. OpenStack volume will be created and used instead of an ephemeral disk defined in flavor. Should be used for the diskless flavors. | False | -| `openstack_security_groups` | array | [] | ['security-group-1'] | The names of the security groups to assign to the instance | False | -| `cloud_name` | string | "openstack" | "openstack" | The name of the cloud to use from the clouds secret | False | -| `secret_name` | string | "openstack" | "openstack" | The name of the clouds secret | False | -| `controller_server_group_id` | string | "" | "3adf4e92-bb33-4e44-8ad3-afda9dfe8ec3" | The server group to assign the control plane nodes to. | False | -| `worker_server_group_id` | string | "" | "869fe071-1e56-46a9-9166-47c9f228e297" | The server group to assign the worker nodes to. | False | -| `ssh_key` | string | "" | "capi-keypair" | The ssh key to inject in the nodes. | False | -| `apiserver_loadbalancer` | string | "octavia-amphora" | "none, octavia-amphora, octavia-ovn" | "In this cluster-stack we have two kind of loadbalancers. Each of them has its own configuration variable. This setting here is to configure the loadbalancer that is placed in front of the apiserver.
You can choose from 2 options:

none:
No loadbalancer solution will be deployed

octavia-amphora:
(default) Uses openstack's loadbalancer service (provider:amphora)

octavia-ovn:
Uses openstack's loadbalancer service (provider:ovn)
| False | -| `dns_nameservers` | array | ['5.1.66.255', '185.150.99.255'] | ['5.1.66.255', '185.150.99.255'] | "DNSNameservers is the list of nameservers for the OpenStack Subnet being created. Set this value when you need to create a new network/subnet while the access through DNS is required.
| False | -| `node_cidr` | string | "10.8.0.0/20" | "10.8.0.0/20" | "NodeCIDR is the OpenStack Subnet to be created. Cluster actuator will create a network, a subnet with NodeCIDR, and a router connected to this subnet. If you leave this empty, no network will be created.
| False | -| `certSANs` | array | [] | ['mydomain.example'] | CertSANs sets extra Subject Alternative Names for the API Server signing cert. | False | -| `oidc_config.client_id` | string | | kubectl | A client id that all tokens must be issued for. | | -| `oidc_config.issuer_url` | string | | `https://dex.example.com` | URL of the provider that allows the API server to dis cover public signing keys. Only URLs that use the https:// scheme are acc epted. This is typically the provider's discovery URL, changed to have an emp ty path | | -| `oidc_config.username_claim` | string | preferred_username | preferred_username | JWT claim to use as the user name. By default sub, whi ch is expected to be a unique identifier of the end user. Admins can choose oth er claims, such as email or name, depending on their provider. However, cla ims other than email will be prefixed with the issuer URL to prevent naming cla shes with other plugins. | | -| `oidc_config.groups_claim` | string | groups | groups | JWT claim to use as the user's group. If the claim is present it must be an array of strings. | | -| `oidc_config.username_prefix` | string | oidc: | oidc: | Prefix prepended to username claims to prevent cla shes with existing names (such as system: users). For example, the value oid c: will create usernames like oidc:jane.doe. If this flag isn't provided and --o idc-username-claim is a value other than email the prefix defaults to ( Iss uer URL )# where ( Issuer URL ) is the value of --oidc-issuer-url. The value - c an be used to disable all prefixing. | | -| `oidc_config.groups_prefix` | string | oidc: | oidc: | Prefix prepended to group claims to prevent clashes wit h existing names (such as system: groups). For example, the value oidc: will cre ate group names like oidc:engineering and oidc:infra. | | -| `network_mtu` | integer | | 1500 | NetworkMTU sets the maximum transmission unit (MTU) value to address fragmentation for the private network ID. | False | -| `controlPlaneAvailabilityZones` | array | | ['nova'] | ControlPlaneAvailabilityZones is the set of availability zones which control plane machines may be deployed to. | False | -| `controlPlaneOmitAvailabilityZone` | boolean | | True | ControlPlaneOmitAvailabilityZone causes availability zone to be omitted when creating control plane nodes, allowing the Nova scheduler to make a decision on which availability zone to use based on other scheduling constraints. | False | diff --git a/docs/providers/openstack/hcp.md b/docs/providers/openstack/hcp.md new file mode 100644 index 00000000..a1cf7238 --- /dev/null +++ b/docs/providers/openstack/hcp.md @@ -0,0 +1,153 @@ +# OpenStack HCP (Hosted Control Plane) + +The `hcp` cluster stack runs the Kubernetes control plane as pods inside the +management cluster, using the +[teutonet Hosted Control Plane provider](https://github.com/teutonet/cluster-api-provider-hosted-control-plane). +Only worker nodes are created as OpenStack VMs. + +## How it differs from the `scs` stack + +| Aspect | scs | hcp | +|----------------------------|-------------------------------------------|--------------------------------------------| +| Control plane | Dedicated OpenStack VMs | Pods in the management cluster | +| Control plane template | `KubeadmControlPlaneTemplate` | `HostedControlPlaneTemplate` (v1alpha1) | +| CAPI API version | `v1beta2` (1-35) | `v1beta1` | +| CP machine infrastructure | `OpenStackMachineTemplate` | None (no CP VMs) | +| API server load balancer | Configurable (Octavia / none) | `none` by default (uses Gateway API) | +| API server floating IP | Configurable | Disabled by default | +| Variable model | Unified (`flavor`, `rootDisk`) | Worker-prefixed (`workerFlavor`, etc.) | +| Unique variables | `oidcConfig`, `registryMirrors` | `gatewayName`, `gatewayNamespace`, `controlPlaneReplicas` | + +## Architecture + +``` +Management Cluster + +-- CP pods (etcd, apiserver, controller-manager, scheduler) + +-- HostedControlPlane resource + +-- Gateway API ingress (optional) + +OpenStack + +-- Worker VMs only + +-- Cilium CNI + +-- CCM + CSI (as DaemonSets/Deployments on workers) +``` + +The control plane is exposed to worker nodes via the management cluster's +network. For external access to the API server, configure Gateway API resources +using the `gatewayName` and `gatewayNamespace` variables. + +## Prerequisites + +In addition to the standard [quickstart prerequisites](../../quickstart.md): + +1. Install the teutonet Hosted Control Plane provider in the management cluster: + + ```bash + kubectl apply -f https://github.com/teutonet/cluster-api-provider-hosted-control-plane/releases/latest/download/install.yaml + ``` + +2. (Optional) Set up a Gateway API implementation (e.g., Envoy Gateway, Cilium + Gateway API) if you want external access to the hosted API servers. + +## Available versions + +| Version | Kubernetes | Notes | +|---------|------------|----------------------------| +| 1-33 | 1.33 | Initial HCP release | +| 1-34 | 1.34 | Updated CCM/CSI | +| 1-35 | 1.35 | Updated CCM/CSI | + +## Usage + +### Build and publish + +```bash +export PROVIDER=openstack +export CLUSTER_STACK=hcp + +just dev --install-cso --version 1.35 +``` + +### Create a cluster + +```yaml +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: my-hcp-cluster + namespace: my-tenant + labels: + managed-secret: cloud-config +spec: + clusterNetwork: + pods: + cidrBlocks: + - 192.168.0.0/16 + serviceDomain: cluster.local + services: + cidrBlocks: + - 10.96.0.0/12 + topology: + class: openstack-hcp-1-35-v1 + controlPlane: + replicas: 3 + version: v1.35.0 + workers: + machineDeployments: + - class: default-worker + name: default-worker + replicas: 3 + variables: + - name: workerFlavor + value: "SCS-4V-8-20" + - name: networkExternalID + value: "your-external-network-uuid" +``` + +### With Gateway API ingress + +To expose the hosted API server externally, configure a Gateway resource in the +management cluster and reference it: + +```yaml + variables: + - name: gatewayName + value: "cluster-gateway" + - name: gatewayNamespace + value: "gateway-system" +``` + +## Configuration variables + +### HCP-specific variables + +| Variable | Type | Default | Description | +|--------------------------|---------|-------------|--------------------------------------------------| +| `controlPlaneReplicas` | integer | 3 | Number of hosted control plane replicas | +| `gatewayName` | string | "" | Gateway API resource name for API server ingress | +| `gatewayNamespace` | string | "default" | Namespace of the Gateway resource | +| `disableAPIServerFloatingIP` | boolean | true | Disable floating IP for API server | +| `apiServerLoadBalancer` | string | "none" | Load balancer type (typically `none` for HCP) | + +### Worker variables + +| Variable | Type | Default | Description | +|------------------------------|---------|-----------------|--------------------------------------------| +| `workerFlavor` | string | "SCS-2V-4" | OpenStack flavor for worker nodes | +| `workerRootDisk` | integer | 25 | Root disk size in GiB | +| `workerServerGroupID` | string | "" | Anti-affinity server group for workers | +| `workerAdditionalBlockDevices` | array | [] | Extra Cinder volumes for workers | +| `workerSecurityGroups` | array | [] | Security group names for workers | +| `workerSecurityGroupIDs` | array | [] | Security group UUIDs for workers | + +### Shared variables (same as scs) + +| Variable | Type | Default | Description | +|---------------------|---------|---------------------------------|------------------------------------------| +| `imageName` | string | "ubuntu-capi-image" | Base OS image name | +| `networkExternalID` | string | "" | External network UUID | +| `networkMTU` | integer | (provider default) | MTU for cluster network | +| `dnsNameservers` | array | ["5.1.66.255", "185.150.99.255"]| DNS nameservers | +| `nodeCIDR` | string | "10.8.0.0/20" | Subnet CIDR for cluster nodes | +| `sshKey` | string | "" | SSH key to inject into nodes | +| `certSANs` | array | [] | Extra SANs for API server certificate | diff --git a/docs/providers/openstack/quickstart.md b/docs/providers/openstack/quickstart.md deleted file mode 100644 index 00bccef8..00000000 --- a/docs/providers/openstack/quickstart.md +++ /dev/null @@ -1,290 +0,0 @@ -# Quickstart - -This quickstart guide contains steps to install the [Cluster Stack Operator][CSO] (CSO) utilizing the [Cluster Stack Provider OpenStack][CSPO] (CSPO) to provide [ClusterClasses][ClusterClass] which can be used with the [Kubernetes Cluster API][CAPI] to create Kubernetes Clusters. - -This section guides you through all the necessary steps to create a workload Kubernetes cluster on top of the OpenStack infrastructure. The guide describes a path that utilizes the `clusterctl` CLI tool to manage the lifecycle of a CAPI management cluster and employs `kind` to create a local non-production managemnt cluster. - -Note that it is a common practice to create a temporary, local [bootstrap cluster](https://cluster-api.sigs.k8s.io/reference/glossary#bootstrap-cluster) which is then used to provision a target [management cluster](https://cluster-api.sigs.k8s.io/reference/glossary#management-cluster) on the selected infrastructure. - -## Prerequisites - -- Install [Docker](https://docs.docker.com/get-docker/) and [kind](https://helm.sh/docs/intro/install/) -- Install [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) -- Install [Helm](https://helm.sh/docs/intro/install/) -- Install [clusterctl](https://cluster-api.sigs.k8s.io/user/quick-start.html#install-clusterctl) -- Install [go](https://go.dev/doc/install) -- Install [jq](https://jqlang.github.io/jq/) - -## Initialize the management cluster - -Create the kind cluster: - -```bash -kind create cluster -``` - -Transform the Kubernetes cluster into a management cluster by using `clusterctl init` and bootstrap it with CAPI and Cluster API Provider OpenStack ([CAPO](https://github.com/kubernetes-sigs/cluster-api-provider-openstack)) components: - -```bash -export CLUSTER_TOPOLOGY=true -export EXP_CLUSTER_RESOURCE_SET=true -export EXP_RUNTIME_SDK=true -clusterctl init --infrastructure openstack -kubectl apply -f https://github.com/k-orc/openstack-resource-controller/releases/latest/download/install.yaml -``` - -Note that the manual deployment of the openstack resource controller (ORC) is required since capo-0.12. If you use `clusterctl upgrade` to upgrade capo from earlier version, you'll also need to manually add ORC to the management host/cluster. - -### CSO and CSPO variables preparation (CSP) - -The CSO and CSPO must be directed to the Cluster Stacks repository housing releases for the OpenStack provider. -Modify and export the following environment variables if you wish to redirect CSO and CSPO to an alternative Git repository - -Be aware that GitHub enforces limitations on the number of API requests per unit of time. To overcome this, -it is recommended to configure a [personal access token](https://github.com/settings/personal-access-tokens/new) for authenticated calls. This will significantly increase the rate limit for GitHub API requests. -Fine grained PAT with `Public Repositories (read-only)` is enough. - -```bash -export GIT_PROVIDER_B64=Z2l0aHVi # github -export GIT_ORG_NAME_B64=U292ZXJlaWduQ2xvdWRTdGFjaw== # SovereignCloudStack -export GIT_REPOSITORY_NAME_B64=Y2x1c3Rlci1zdGFja3M= # cluster-stacks -export GIT_ACCESS_TOKEN_B64=$(echo -n '' | base64 -w0) -``` - -### CSO and CSPO deployment (CSP) - -Install the [envsubst](https://github.com/drone/envsubst) Go package. It is required to enable the expansion of variables specified in CSPO and CSO manifests. - -```bash -GOBIN=/tmp go install github.com/drone/envsubst/v2/cmd/envsubst@latest -``` -Note: On typical Linux distros, you will have a binary `/usr/bin/envsubst` from the gettext package that does *not* work. - -Get the latest CSO release version and apply CSO manifests to the management cluster. - -```bash -# Get the latest CSO release version and apply CSO manifests -curl -sSL https://github.com/SovereignCloudStack/cluster-stack-operator/releases/latest/download/cso-infrastructure-components.yaml | /tmp/envsubst | kubectl apply -f - -``` - -Get the latest CSPO release version and apply CSPO manifests to the management cluster. - -```bash -# Get the latest CSPO release version and apply CSPO manifests -curl -sSL https://github.com/sovereignCloudStack/cluster-stack-provider-openstack/releases/latest/download/cspo-infrastructure-components.yaml | /tmp/envsubst | kubectl apply -f - -``` - -## Define a namespace for a tenant (CSP/per tenant) - -```sh -export CS_NAMESPACE=my-tenant -``` - -### Deploy CSP-helper chart - -The csp-helper chart is meant to create per tenant credentials as well as the tenants namespace where all resources for this tenant will live in. - -Cloud and secret name default to `openstack`. - -Example `clouds.yaml` - -```yaml -clouds: - openstack: - auth: - auth_url: https://api.gx-scs.sovereignit.cloud:5000/v3 - application_credential_id: "" - application_credential_secret: "" - region_name: "RegionOne" - interface: "public" - identity_api_version: 3 - auth_type: "v3applicationcredential" -``` - -```bash -helm upgrade -i csp-helper-"${CS_NAMESPACE}" -n "${CS_NAMESPACE}" --create-namespace https://github.com/SovereignCloudStack/openstack-csp-helper/releases/latest/download/openstack-csp-helper.tgz -f path/to/clouds.yaml -``` - -## Create Cluster Stack definition (CSP/per tenant) - -Configure the Cluster Stack you want to use: - -```sh -# the name of the cluster stack (must match a name of a directory in https://github.com/SovereignCloudStack/cluster-stacks/tree/main/providers/openstack) -export CS_NAME=scs - -# the kubernetes version of the cluster stack (must match a tag for the kubernetes version and the stack version) -export CS_K8S_VERSION=1.29 - -# the version of the cluster stack (must match a tag for the kubernetes version and the stack version) -export CS_VERSION=v1 -export CS_CHANNEL=stable - -# must match a cloud section name in the used clouds.yaml -export CS_CLOUDNAME=openstack -export CS_SECRETNAME="${CS_CLOUDNAME}" -``` - -This will use the cluster-stack as defined in the `providers/openstack/scs` directory. - -```bash -cat >clusterstack.yaml < cluster.yaml < kubeconfig.yaml -# Communicate with the workload cluster -kubectl --kubeconfig kubeconfig.yaml get nodes -``` - -## Check the workload cluster health - -```bash -$ kubectl --kubeconfig kubeconfig.yaml get pods -A -NAMESPACE NAME READY STATUS RESTARTS AGE -kube-system cilium-8mzrx 1/1 Running 0 7m58s -kube-system cilium-jdxqm 1/1 Running 0 6m43s -kube-system cilium-operator-6bb4c7d6b6-c77tn 1/1 Running 0 7m57s -kube-system cilium-operator-6bb4c7d6b6-l2df8 1/1 Running 0 7m58s -kube-system cilium-p9tkv 1/1 Running 0 6m44s -kube-system cilium-thbc8 1/1 Running 0 6m45s -kube-system coredns-5dd5756b68-k68j4 1/1 Running 0 8m3s -kube-system coredns-5dd5756b68-vjg9r 1/1 Running 0 8m3s -kube-system etcd-cs-cluster-pwblg-xkptx 1/1 Running 0 8m3s -kube-system kube-apiserver-cs-cluster-pwblg-xkptx 1/1 Running 0 8m3s -kube-system kube-controller-manager-cs-cluster-pwblg-xkptx 1/1 Running 0 8m3s -kube-system kube-proxy-54f8w 1/1 Running 0 6m44s -kube-system kube-proxy-8z8kb 1/1 Running 0 6m43s -kube-system kube-proxy-jht46 1/1 Running 0 8m3s -kube-system kube-proxy-mt69p 1/1 Running 0 6m45s -kube-system kube-scheduler-cs-cluster-pwblg-xkptx 1/1 Running 0 8m3s -kube-system metrics-server-6578bd6756-vztzf 1/1 Running 0 7m57s -kube-system openstack-cinder-csi-controllerplugin-776696786b-ksf77 6/6 Running 0 7m57s -kube-system openstack-cinder-csi-nodeplugin-96dlg 3/3 Running 0 6m43s -kube-system openstack-cinder-csi-nodeplugin-crhc4 3/3 Running 0 6m44s -kube-system openstack-cinder-csi-nodeplugin-d7rzz 3/3 Running 0 7m58s -kube-system openstack-cinder-csi-nodeplugin-nkgq6 3/3 Running 0 6m44s -kube-system openstack-cloud-controller-manager-hp2n2 1/1 Running 0 7m9s -``` - -[CAPI]: https://cluster-api.sigs.k8s.io/ -[CSO]: https://github.com/sovereignCloudStack/cluster-stack-operator/ -[CSPO]: https://github.com/SovereignCloudStack/cluster-stacks/tree/main/providers/openstack -[ClusterClass]: https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20210526-cluster-class-and-managed-topologies.md diff --git a/docs/providers/openstack/scs-configuration.md b/docs/providers/openstack/scs-configuration.md new file mode 100644 index 00000000..55819bf2 --- /dev/null +++ b/docs/providers/openstack/scs-configuration.md @@ -0,0 +1,103 @@ +# Configuration + +This page lists the custom configuration options available, including their default values and if they are optional. The following example shows how these variables can be used inside the `cluster.yaml` file under `spec.topology.variables`. + +## Version matrix + +| Version | K8s | CS Version | cilium | metrics-server | os-csi | os-ccm | +|---------|-----|----------|---------|---------|---------|---------| +| 1-32 | 1.32 | - | 1.19.1 | 3.13.0 | 2.32.x | 2.32.x | +| 1-33 | 1.33 | - | 1.19.1 | 3.13.0 | 2.33.x | 2.33.x | +| 1-34 | 1.34 | - | 1.19.1 | 3.13.0 | 2.34.x | 2.34.x | +| 1-35 | 1.35 | - | 1.19.1 | 3.13.0 | 2.35.x | 2.35.x | + +## Example + +```yaml +apiVersion: cluster.x-k8s.io/v1beta2 +kind: Cluster +metadata: + name: my-cluster + namespace: my-namespace + labels: + managed-secret: cloud-config +spec: + clusterNetwork: + pods: + cidrBlocks: + - 192.168.0.0/16 + serviceDomain: cluster.local + services: + cidrBlocks: + - 10.96.0.0/12 + topology: + variables: # <-- variables from the table below can be set here + - name: flavor + value: "SCS-4V-8-20" + - name: networkExternalID + value: "ebfe5546-f09f-4f42-ab54-094e457d42ec" + class: openstack-scs-1-35-v1 + controlPlane: + replicas: 3 + variables: + overrides: + - name: flavor + value: "SCS-4V-8-50" + version: v1.35.0 + workers: + machineDeployments: + - class: default-worker + name: md-0 + replicas: 3 +``` + +Variables of type `object` are set as nested values. The following example demonstrates this with `oidcConfig`: + +```yaml +... +topology: + variables: + - name: oidcConfig + value: + issuerURL: "https://dex.k8s.scs.community" + clientID: "kubectl" +... +``` + +In v1beta2, per-role overrides (e.g. different flavors for control plane and workers) are set via `topology.controlPlane.variables.overrides` and `topology.workers.machineDeployments[].variables.overrides` instead of separate variable names. + +## Available variables + +> **Note:** This table documents the **1-35** (v1beta2) variable set with unified +> variable names. Older versions (1-32, 1-33) use role-prefixed names like +> `controlPlaneFlavor` / `workerFlavor` instead of the unified `flavor`. + +|Name|Type|Default|Example|Description|Required| +|----|----|-------|-------|-----------|--------| +|`imageName`|string|"ubuntu-capi-image"|"ubuntu-capi-image"|Base name of the OpenStack image for cluster nodes.|False| +|`imageIsOrc`|boolean|false|true|Whether the image name refers to an ORC (OpenStack Resource Controller) image resource instead of a Glance image.|False| +|`imageAddVersion`|boolean|true|false|Append the Kubernetes version suffix to the image name (e.g. `ubuntu-capi-image-v1.35`).|False| +|`networkExternalID`|string|""|"ebfe5546-f09f-4f42-ab54-094e457d42ec"|ID of the external OpenStack network for public internet access.|False| +|`networkMTU`|integer||1500|Maximum transmission unit (MTU) for the private cluster network.|False| +|`dnsNameservers`|array|["5.1.66.255", "185.150.99.255"]|["8.8.8.8"]|DNS nameservers for the cluster subnet.|False| +|`nodeCIDR`|string|"10.8.0.0/20"|"10.8.0.0/20"|CIDR for the cluster subnet. A network, subnet, and router will be created.|False| +|`flavor`|string|"SCS-2V-4-20s"|"SCS-4V-8-20"|OpenStack instance flavor for all nodes. Override per role using topology variable overrides.|False| +|`rootDisk`|integer|0|50|Root disk size in GiB. When set, an OpenStack volume is used instead of the ephemeral disk from the flavor.|False| +|`serverGroupID`|string|""|"3adf4e92-bb33-4e44-8ad3-afda9dfe8ec3"|Server group for anti-affinity placement. Override per role using topology variable overrides.|False| +|`additionalBlockDevices`|array|[]|[{"name": "data", "sizeGiB": 100, "type": "Volume"}]|Additional Cinder volumes to attach to nodes.|False| +|`sshKey`|string|""|"capi-keypair"|SSH key pair name to inject into nodes.|False| +|`apiServerLoadBalancer`|string|"octavia-ovn"|"none"|Load balancer for the API server. Options: `none`, `octavia-amphora`, `octavia-ovn`.|False| +|`apiServerAllowedCIDRs`|array|[]|["10.0.0.0/8"]|CIDRs allowed to access the API server load balancer.|False| +|`disableAPIServerFloatingIP`|boolean|false|true|Disable floating IP for the API server.|False| +|`certSANs`|array|[]|["mydomain.example"]|Extra Subject Alternative Names for the API server certificate.|False| +|`controlPlaneAvailabilityZones`|array|[]|["nova"]|Availability zones for control plane nodes.|False| +|`controlPlaneOmitAvailabilityZone`|boolean|false|true|Omit availability zone when creating control plane nodes, letting Nova schedule freely.|False| +|`identityRef.name`|string|"openstack"|"openstack"|Name of the Secret containing OpenStack credentials.|False| +|`identityRef.cloudName`|string|"openstack"|"openstack"|Cloud name within the credentials Secret.|False| +|`oidcConfig.clientID`|string||"kubectl"|OIDC client ID for API server authentication.|| +|`oidcConfig.issuerURL`|string||"https://dex.example.com"|OIDC provider discovery URL (must be HTTPS).|| +|`oidcConfig.usernameClaim`|string|"preferred_username"|"email"|JWT claim to use as the username.|| +|`oidcConfig.groupsClaim`|string|"groups"|"groups"|JWT claim to use as groups.|| +|`oidcConfig.usernamePrefix`|string|"oidc:"|"oidc:"|Prefix for OIDC usernames.|| +|`oidcConfig.groupsPrefix`|string|"oidc:"|"oidc:"|Prefix for OIDC group names.|| +|`registryMirrors`|array|[]|[{"hostnameUpstream": "docker.io", "urlMirror": "https://mirror.example.com"}]|Container registry mirrors for node containerd configuration.|| diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 00000000..59015424 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,270 @@ +# Quickstart + +This guide walks through creating a CAPI management cluster and deploying a +workload cluster using one of the available cluster stacks. It covers OpenStack, +Docker (local development), and HCP (Hosted Control Plane) variants. + +## Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) and + [kind](https://kind.sigs.k8s.io/) +- [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) +- [Helm](https://helm.sh/docs/intro/install/) +- [clusterctl](https://cluster-api.sigs.k8s.io/user/quick-start.html#install-clusterctl) +- [just](https://just.systems/man/en/installation.html) + +For OpenStack stacks you also need: + +- An OpenStack cloud with application credentials +- A `clouds.yaml` with your credentials + +## 1. Create a management cluster + +```bash +kind create cluster +``` + +## 2. Install CAPI and the infrastructure provider + +### OpenStack (scs and hcp stacks) + +```bash +clusterctl init --infrastructure openstack +``` + +For clusters using the [OpenStack Resource Controller](https://github.com/k-orc/openstack-resource-controller) +image format (ORC), also install ORC: + +```bash +kubectl apply -f https://github.com/k-orc/openstack-resource-controller/releases/latest/download/install.yaml +``` + +### Docker (local development) + +```bash +clusterctl init --infrastructure docker +``` + +### HCP (Hosted Control Plane) + +The HCP stack requires both the OpenStack provider and the teutonet Hosted +Control Plane provider: + +```bash +clusterctl init --infrastructure openstack +# Install the Hosted Control Plane provider (see teutonet docs for latest URL) +kubectl apply -f https://github.com/teutonet/cluster-api-provider-hosted-control-plane/releases/latest/download/install.yaml +``` + +## 3. Install the Cluster Stack Operator + +The quickest way during development: + +```bash +just install-cso +``` + +This installs the CSO Helm chart from +`oci://registry.scs.community/cluster-stacks/cso` and auto-configures +[ttl.sh](https://ttl.sh) as a temporary OCI registry. + +For production, set `OCI_REGISTRY` and `OCI_REPOSITORY` before running: + +```bash +export OCI_REGISTRY=registry.example.com +export OCI_REPOSITORY=kaas/cluster-stacks +just install-cso +``` + +## 4. Build and publish a cluster stack + +```bash +# Build and publish all versions for the default stack (openstack/scs) +just publish --all + +# Or target a specific version +just publish --version 1.35 + +# For a different stack, set the environment +PROVIDER=docker CLUSTER_STACK=scs just publish --version 1.35 +``` + +The `dev` recipe combines publish with generating the `ClusterStack` resource: + +```bash +just dev --version 1.35 +``` + +This prints a `ClusterStack` YAML that you can pipe to `kubectl apply -f -`. + +To also install/upgrade the CSO in one step: + +```bash +just dev --install-cso --version 1.35 +``` + +## 5. Deploy OpenStack credentials (OpenStack only) + +Create a namespace for your tenant and deploy credentials using the +[csp-helper chart](https://github.com/SovereignCloudStack/openstack-csp-helper): + +```bash +export CS_NAMESPACE=my-tenant + +helm upgrade -i csp-helper-"${CS_NAMESPACE}" \ + -n "${CS_NAMESPACE}" --create-namespace \ + https://github.com/SovereignCloudStack/openstack-csp-helper/releases/latest/download/openstack-csp-helper.tgz \ + -f path/to/clouds.yaml +``` + +## 6. Apply the ClusterStack resource + +If you used `just dev`, the output already contains this. Otherwise, generate it: + +```bash +just generate-resources --version 1.35 --clusterstack-only +``` + +Example output: + +```yaml +apiVersion: clusterstack.x-k8s.io/v1alpha1 +kind: ClusterStack +metadata: + name: openstack-1-35 + namespace: cluster +spec: + provider: openstack + name: scs + kubernetesVersion: "1.35" + channel: custom + autoSubscribe: false + versions: + - v1 # matches your published version +``` + +Apply it and wait for the `ClusterClass` to become available: + +```bash +kubectl apply -f clusterstack.yaml +kubectl get clusterclass -w +``` + +## 7. Create a workload cluster + +Generate a `Cluster` resource: + +```bash +just generate-resources --version 1.35 --cluster-only +``` + +Or write one manually. Here is a minimal example for OpenStack/scs 1-35: + +```yaml +apiVersion: cluster.x-k8s.io/v1beta2 +kind: Cluster +metadata: + name: my-cluster + namespace: my-tenant + labels: + managed-secret: cloud-config +spec: + clusterNetwork: + pods: + cidrBlocks: + - 192.168.0.0/16 + serviceDomain: cluster.local + services: + cidrBlocks: + - 10.96.0.0/12 + topology: + class: openstack-scs-1-35-v1 + controlPlane: + replicas: 3 + version: v1.35.0 + workers: + machineDeployments: + - class: default-worker + name: default-worker + replicas: 3 + variables: + - name: flavor + value: "SCS-4V-8-20" + - name: networkExternalID + value: "your-external-network-uuid" +``` + +Apply and monitor: + +```bash +kubectl apply -f cluster.yaml +clusterctl -n my-tenant describe cluster my-cluster +``` + +## 8. Access the workload cluster + +```bash +clusterctl -n my-tenant get kubeconfig my-cluster > kubeconfig.yaml +kubectl --kubeconfig kubeconfig.yaml get nodes +``` + +## Docker variant + +For the Docker provider, no cloud credentials or external network are needed: + +```bash +export PROVIDER=docker +export CLUSTER_STACK=scs + +just dev --install-cso --version 1.35 +# Apply the printed ClusterStack YAML, then create a cluster +just generate-resources --version 1.35 --cluster-only | kubectl apply -f - +``` + +## HCP variant + +The HCP stack creates control plane pods in the management cluster instead of +dedicated VMs. Worker-specific variables use the `worker` prefix +(`workerFlavor`, `workerRootDisk`): + +```yaml +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: my-hcp-cluster + namespace: my-tenant + labels: + managed-secret: cloud-config +spec: + topology: + class: openstack-hcp-1-35-v1 + controlPlane: + replicas: 3 + version: v1.35.0 + workers: + machineDeployments: + - class: default-worker + name: default-worker + replicas: 3 + variables: + - name: workerFlavor + value: "SCS-4V-8-20" + - name: networkExternalID + value: "your-external-network-uuid" + - name: gatewayName + value: "my-gateway" + - name: gatewayNamespace + value: "default" +``` + +See [providers/openstack/hcp.md](providers/openstack/hcp.md) for full details. + +## Useful commands + +```bash +just matrix # show version/addon matrix for all stacks +just update versions # update Kubernetes patch versions +just update addons # update addon chart versions +just generate-docs # regenerate configuration reference docs +just clean # remove build artifacts +``` diff --git a/hack/config-template.md b/hack/config-template.md index 8f05340b..091ee975 100644 --- a/hack/config-template.md +++ b/hack/config-template.md @@ -2,14 +2,18 @@ This page lists the custom configuration options available, including their default values and if they are optional. The following example shows how these variables can be used inside the `cluster.yaml` file under `spec.topology.variables`. +## Version matrix + +!!matrix!! + ## Example ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: Cluster metadata: - name: - namespace: + name: my-cluster + namespace: my-namespace labels: managed-secret: cloud-config spec: @@ -22,38 +26,41 @@ spec: cidrBlocks: - 10.96.0.0/12 topology: - variables: // <-- variables from the table can be set here - - name: controller_flavor + variables: # <-- variables from the table below can be set here + - name: flavor value: "SCS-4V-8-20" - - name: worker_flavor - value: "SCS-4V-8-20" - - name: external_id + - name: networkExternalID value: "ebfe5546-f09f-4f42-ab54-094e457d42ec" - class: openstack-alpha-1-29-v2 + class: openstack-scs-1-35-v1 controlPlane: - replicas: 2 - version: v1.29.3 + replicas: 3 + variables: + overrides: + - name: flavor + value: "SCS-4V-8-50" + version: v1.35.0 workers: machineDeployments: - - class: openstack-alpha-1-29-v2 - failureDomain: nova - name: openstack-alpha-1-29-v2 - replicas: 4 + - class: default-worker + name: md-0 + replicas: 3 ``` -Variables from the table containing a `.` are to be used in an object with the part before the dot being the object name and the part behind the dot being the value names. The following example demonstrates this with `oidc_config`. +Variables of type `object` are set as nested values. The following example demonstrates this with `oidcConfig`: ```yaml ... topology: variables: - - name: oidc_config + - name: oidcConfig value: - issuer_url: "https://dex.k8s.scs.community" - client_id: "kubectl" + issuerURL: "https://dex.k8s.scs.community" + clientID: "kubectl" ... ``` +In v1beta2, per-role overrides (e.g. different flavors for control plane and workers) are set via `topology.controlPlane.variables.overrides` and `topology.workers.machineDeployments[].variables.overrides` instead of separate variable names. + ## Available variables -!!table!! \ No newline at end of file +!!table!! diff --git a/hack/docugen.py b/hack/docugen.py index 1f58fdd7..097bea33 100755 --- a/hack/docugen.py +++ b/hack/docugen.py @@ -1,149 +1,171 @@ #!/usr/bin/env python3 """ -Generate markdown table from cluster-class variable definitions. +Generate markdown documentation from ClusterClass variable definitions. -This script temporarily renders the helm template of the cluster-class. -Parses the `Cluster.spec.topology.variables` definitions and -generates a markdown table for documentation purposes. +Renders the cluster-class Helm template, parses the topology variables +(openAPIV3Schema), and outputs a markdown table of all configurable options. + +Usage: + ./hack/docugen.py + ./hack/docugen.py --output docs/configuration.md + ./hack/docugen.py --template hack/config-template.md + ./hack/docugen.py --matrix + ./hack/docugen.py --dry-run """ import argparse import subprocess -from pathlib import Path import sys +from pathlib import Path import yaml -BASE_PATH = Path(__file__).parent.parent -TEMPLATE_PATH = BASE_PATH.joinpath("providers", "openstack", "scs", "cluster-class") -DOCS_TMPL_PATH = BASE_PATH.joinpath("hack", "config-template.md") -DOCS_OUT_PATH = BASE_PATH.joinpath("docs", "providers", "openstack", "configuration.md") - - -def generate_row(content: list): - """Generate string of markdown table row.""" - row_tmpl = "|{content}|" - row_str = "|".join(content) - - return row_tmpl.format(content=row_str) - - -def parse_variable(tmpl: dict) -> list: - """ - Parse schema of simple cluster-stack variable type. (String, Boolean, Integer, Array) - Parameters: - tmpl (dict): Dictionary of variable schema, - parsed from cluster-class.yaml via yaml.safe_load(). +def generate_row(columns: list) -> str: + """Generate a markdown table row from a list of column values.""" + return "|" + "|".join(str(c) for c in columns) + "|" - Returns: List of variable schema properties. - Each entry represents a column in the final markdown table row. - """ - var_name = tmpl["name"] - var_required = tmpl["required"] - var_schema = tmpl["schema"]["openAPIV3Schema"] - var_type = var_schema["type"] - var_default = var_schema.get("default", "") - var_example = var_schema.get("example", "") - var_desc = var_schema.get("description", "TODO") +def parse_variable(var: dict) -> list: + """Parse a simple variable (string, boolean, integer, array) into table columns.""" + name = var["name"] + required = var["required"] + schema = var["schema"]["openAPIV3Schema"] - row = [] - row.append(f"`{var_name}`") - row.append(var_type) + var_type = schema["type"] + default = schema.get("default", "") + example = schema.get("example", "") + description = schema.get("description", "TODO").replace("\n", "
") - # Make sure that example and default values are quoted in the table if var_type == "string": - row.append(f'"{var_default}"') - row.append(f'"{var_example}"') - - # Cast any non-string type example / default to string - if var_type in ["array", "integer", "boolean"]: - row.append(str(var_default)) - row.append(str(var_example)) - - row.append(var_desc.replace("\n", "
")) - row.append(str(var_required)) # Convert boolean variable to string - - return row - - -def parse_object(tmpl: dict) -> list: - """ - Parse cluster-stack variable schema of type object. - Separated due to nesting in YAML format. - Parameters: - tmpl (dict): Dictionary of object schema, - parsed from cluster-class.yaml via yaml.safe_load(). - - Returns: - List of object properties. - Each entry represents a row in the final markdown table as a list, - consisting of the rows column values. - """ - var_name = tmpl["name"] - props = tmpl["schema"]["openAPIV3Schema"]["properties"] - - object_list = [] - for prop in props: - row = [] - row.append(f"`{var_name}.{prop}`") - row.append(props[prop]["type"]) - row.append(props[prop].get("default", "")) - row.append(props[prop].get("example", "")) - row.append(props[prop].get("description", "TODO")) - # append dummy row for required field - row.append("") - - object_list.append(row) - return object_list + default = f'"{default}"' + example = f'"{example}"' + else: + default = str(default) + example = str(example) + + return [f"`{name}`", var_type, default, example, description, str(required)] + + +def parse_object(var: dict) -> list: + """Parse an object variable into multiple table rows (one per property).""" + name = var["name"] + props = var["schema"]["openAPIV3Schema"]["properties"] + rows = [] + + for prop_name, prop_schema in props.items(): + rows.append([ + f"`{name}.{prop_name}`", + prop_schema["type"], + prop_schema.get("default", ""), + prop_schema.get("example", ""), + prop_schema.get("description", "TODO"), + "", + ]) + + return rows + + +def render_cluster_class(stack_dir: Path) -> dict: + """Render the cluster-class Helm template and return parsed YAML.""" + cluster_class_dir = stack_dir / "cluster-class" + if not cluster_class_dir.exists(): + print(f"cluster-class directory not found: {cluster_class_dir}", file=sys.stderr) + sys.exit(1) + + result = subprocess.run( + ["helm", "template", "docugen", str(cluster_class_dir), + "-s", "templates/cluster-class.yaml"], + capture_output=True, check=False, + ) + if result.returncode != 0: + print(f"helm template failed:\n{result.stderr.decode()}", file=sys.stderr) + sys.exit(1) -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "--dry-run", action="store_true", help="Only print result to stdout." - ) + return yaml.safe_load(result.stdout.decode("utf-8")) - args = parser.parse_args() - cmd = [ - "helm", - "template", - "docugen", - TEMPLATE_PATH, - "-s", - "templates/cluster-class.yaml", +def generate_table(template: dict) -> str: + """Generate a markdown table from ClusterClass topology variables.""" + rows = [ + "|Name|Type|Default|Example|Description|Required|", + "|----|----|-------|-------|-----------|--------|", ] - with open(DOCS_TMPL_PATH, "r") as f: - tmpl = f.read() + for var in template["spec"]["variables"]: + var_type = var["schema"]["openAPIV3Schema"]["type"] + if var_type == "object": + for row in parse_object(var): + rows.append(generate_row(row)) + else: + rows.append(generate_row(parse_variable(var))) - cmdout = subprocess.run(cmd, capture_output=True, check=False) - rendered_template = cmdout.stdout.decode("utf-8") + return "\n".join(rows) - template = yaml.safe_load(rendered_template) - result_table = [] - result_table.append("|Name|Type|Default|Example|Description|Required|") - result_table.append("|----|----|-------|-------|-----------|--------|") +def main(): + parser = argparse.ArgumentParser( + description="Generate docs from ClusterClass variables", + ) + parser.add_argument( + "stack_dir", type=Path, + help="Path to the cluster stack directory", + ) + parser.add_argument( + "--template", type=Path, default=None, + help="Markdown template file with !!table!! placeholder", + ) + parser.add_argument( + "--output", "-o", type=Path, default=None, + help="Output file path (default: stdout)", + ) + parser.add_argument( + "--matrix", action="store_true", + help="Include version matrix from show-matrix.sh --markdown", + ) + parser.add_argument( + "--dry-run", action="store_true", + help="Print to stdout even if --output is set", + ) + args = parser.parse_args() - for var in template["spec"]["variables"]: - if var["schema"]["openAPIV3Schema"]["type"] in ["object"]: - parsed = parse_object(var) - for object_property in parsed: - result_table.append(generate_row(object_property)) + # Render and parse + template = render_cluster_class(args.stack_dir) + table = generate_table(template) + + # Generate version matrix if requested + matrix = "" + if args.matrix: + # Derive the stack base directory (parent of the version dir) + stack_base = args.stack_dir.parent + script = Path(__file__).parent / "show-matrix.sh" + result = subprocess.run( + ["bash", str(script), "--markdown", str(stack_base)], + capture_output=True, check=False, + ) + if result.returncode == 0: + matrix = result.stdout.decode("utf-8").strip() else: - parsed = parse_variable(var) - result_table.append(generate_row(parsed)) - - output = tmpl.replace("!!table!!", "\n".join(result_table)) - - if args.dry_run: + print(f"show-matrix.sh failed:\n{result.stderr.decode()}", file=sys.stderr) + + # Apply template if provided + if args.template and args.template.exists(): + output = args.template.read_text() + output = output.replace("!!table!!", table) + output = output.replace("!!matrix!!", matrix) + else: + output = table + + # Output + if args.dry_run or args.output is None: print(output) - sys.exit() + else: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(output) + print(f"Written: {args.output}", file=sys.stderr) - print(f"Writing output to file {DOCS_OUT_PATH}") - with open(DOCS_OUT_PATH, "w") as f: - f.write(output) + +if __name__ == "__main__": + main() From 9af6ce46f7db795e312b6c3496610d542fea0c96 Mon Sep 17 00:00:00 2001 From: Jan Schoone Date: Mon, 23 Feb 2026 20:13:32 +0000 Subject: [PATCH 05/11] feat(stacks): Add csctl.yaml to all per-minor-version directories Re-add csctl.yaml files adapted for the per-minor-version structure. Each version directory now contains its own csctl.yaml with the correct provider type, cluster stack name, and Kubernetes minor version. Assisted-by: Claude Code Signed-off-by: Jan Schoone --- providers/docker/scs/1-32/csctl.yaml | 7 +++++++ providers/docker/scs/1-33/csctl.yaml | 7 +++++++ providers/docker/scs/1-34/csctl.yaml | 7 +++++++ providers/docker/scs/1-35/csctl.yaml | 7 +++++++ providers/openstack/hcp/1-33/csctl.yaml | 7 +++++++ providers/openstack/hcp/1-34/csctl.yaml | 7 +++++++ providers/openstack/hcp/1-35/csctl.yaml | 7 +++++++ providers/openstack/scs/1-32/csctl.yaml | 7 +++++++ providers/openstack/scs/1-33/csctl.yaml | 7 +++++++ providers/openstack/scs/1-34/csctl.yaml | 7 +++++++ providers/openstack/scs/1-35/csctl.yaml | 7 +++++++ 11 files changed, 77 insertions(+) create mode 100644 providers/docker/scs/1-32/csctl.yaml create mode 100644 providers/docker/scs/1-33/csctl.yaml create mode 100644 providers/docker/scs/1-34/csctl.yaml create mode 100644 providers/docker/scs/1-35/csctl.yaml create mode 100644 providers/openstack/hcp/1-33/csctl.yaml create mode 100644 providers/openstack/hcp/1-34/csctl.yaml create mode 100644 providers/openstack/hcp/1-35/csctl.yaml create mode 100644 providers/openstack/scs/1-32/csctl.yaml create mode 100644 providers/openstack/scs/1-33/csctl.yaml create mode 100644 providers/openstack/scs/1-34/csctl.yaml create mode 100644 providers/openstack/scs/1-35/csctl.yaml diff --git a/providers/docker/scs/1-32/csctl.yaml b/providers/docker/scs/1-32/csctl.yaml new file mode 100644 index 00000000..ccdc9cf1 --- /dev/null +++ b/providers/docker/scs/1-32/csctl.yaml @@ -0,0 +1,7 @@ +apiVersion: csctl.clusterstack.x-k8s.io/v1alpha1 +config: + clusterStackName: scs + kubernetesVersion: v1.32 + provider: + apiVersion: docker.csctl.clusterstack.x-k8s.io/v1alpha1 + type: docker diff --git a/providers/docker/scs/1-33/csctl.yaml b/providers/docker/scs/1-33/csctl.yaml new file mode 100644 index 00000000..5c2283be --- /dev/null +++ b/providers/docker/scs/1-33/csctl.yaml @@ -0,0 +1,7 @@ +apiVersion: csctl.clusterstack.x-k8s.io/v1alpha1 +config: + clusterStackName: scs + kubernetesVersion: v1.33 + provider: + apiVersion: docker.csctl.clusterstack.x-k8s.io/v1alpha1 + type: docker diff --git a/providers/docker/scs/1-34/csctl.yaml b/providers/docker/scs/1-34/csctl.yaml new file mode 100644 index 00000000..83c6a077 --- /dev/null +++ b/providers/docker/scs/1-34/csctl.yaml @@ -0,0 +1,7 @@ +apiVersion: csctl.clusterstack.x-k8s.io/v1alpha1 +config: + clusterStackName: scs + kubernetesVersion: v1.34 + provider: + apiVersion: docker.csctl.clusterstack.x-k8s.io/v1alpha1 + type: docker diff --git a/providers/docker/scs/1-35/csctl.yaml b/providers/docker/scs/1-35/csctl.yaml new file mode 100644 index 00000000..1d484941 --- /dev/null +++ b/providers/docker/scs/1-35/csctl.yaml @@ -0,0 +1,7 @@ +apiVersion: csctl.clusterstack.x-k8s.io/v1alpha1 +config: + clusterStackName: scs + kubernetesVersion: v1.35 + provider: + apiVersion: docker.csctl.clusterstack.x-k8s.io/v1alpha1 + type: docker diff --git a/providers/openstack/hcp/1-33/csctl.yaml b/providers/openstack/hcp/1-33/csctl.yaml new file mode 100644 index 00000000..6cb8a11b --- /dev/null +++ b/providers/openstack/hcp/1-33/csctl.yaml @@ -0,0 +1,7 @@ +apiVersion: csctl.clusterstack.x-k8s.io/v1alpha1 +config: + clusterStackName: hcp + kubernetesVersion: v1.33 + provider: + apiVersion: openstack.csctl.clusterstack.x-k8s.io/v1alpha1 + type: openstack diff --git a/providers/openstack/hcp/1-34/csctl.yaml b/providers/openstack/hcp/1-34/csctl.yaml new file mode 100644 index 00000000..81f85d82 --- /dev/null +++ b/providers/openstack/hcp/1-34/csctl.yaml @@ -0,0 +1,7 @@ +apiVersion: csctl.clusterstack.x-k8s.io/v1alpha1 +config: + clusterStackName: hcp + kubernetesVersion: v1.34 + provider: + apiVersion: openstack.csctl.clusterstack.x-k8s.io/v1alpha1 + type: openstack diff --git a/providers/openstack/hcp/1-35/csctl.yaml b/providers/openstack/hcp/1-35/csctl.yaml new file mode 100644 index 00000000..cc70605d --- /dev/null +++ b/providers/openstack/hcp/1-35/csctl.yaml @@ -0,0 +1,7 @@ +apiVersion: csctl.clusterstack.x-k8s.io/v1alpha1 +config: + clusterStackName: hcp + kubernetesVersion: v1.35 + provider: + apiVersion: openstack.csctl.clusterstack.x-k8s.io/v1alpha1 + type: openstack diff --git a/providers/openstack/scs/1-32/csctl.yaml b/providers/openstack/scs/1-32/csctl.yaml new file mode 100644 index 00000000..2b25389b --- /dev/null +++ b/providers/openstack/scs/1-32/csctl.yaml @@ -0,0 +1,7 @@ +apiVersion: csctl.clusterstack.x-k8s.io/v1alpha1 +config: + clusterStackName: scs + kubernetesVersion: v1.32 + provider: + apiVersion: openstack.csctl.clusterstack.x-k8s.io/v1alpha1 + type: openstack diff --git a/providers/openstack/scs/1-33/csctl.yaml b/providers/openstack/scs/1-33/csctl.yaml new file mode 100644 index 00000000..f468ad56 --- /dev/null +++ b/providers/openstack/scs/1-33/csctl.yaml @@ -0,0 +1,7 @@ +apiVersion: csctl.clusterstack.x-k8s.io/v1alpha1 +config: + clusterStackName: scs + kubernetesVersion: v1.33 + provider: + apiVersion: openstack.csctl.clusterstack.x-k8s.io/v1alpha1 + type: openstack diff --git a/providers/openstack/scs/1-34/csctl.yaml b/providers/openstack/scs/1-34/csctl.yaml new file mode 100644 index 00000000..5e8fd276 --- /dev/null +++ b/providers/openstack/scs/1-34/csctl.yaml @@ -0,0 +1,7 @@ +apiVersion: csctl.clusterstack.x-k8s.io/v1alpha1 +config: + clusterStackName: scs + kubernetesVersion: v1.34 + provider: + apiVersion: openstack.csctl.clusterstack.x-k8s.io/v1alpha1 + type: openstack diff --git a/providers/openstack/scs/1-35/csctl.yaml b/providers/openstack/scs/1-35/csctl.yaml new file mode 100644 index 00000000..6c5abf1b --- /dev/null +++ b/providers/openstack/scs/1-35/csctl.yaml @@ -0,0 +1,7 @@ +apiVersion: csctl.clusterstack.x-k8s.io/v1alpha1 +config: + clusterStackName: scs + kubernetesVersion: v1.35 + provider: + apiVersion: openstack.csctl.clusterstack.x-k8s.io/v1alpha1 + type: openstack From e0b5a4639c6583aaa664be6940943f1df9dcc106 Mon Sep 17 00:00:00 2001 From: Nils Arnold Date: Wed, 15 Apr 2026 12:53:12 +0200 Subject: [PATCH 06/11] Improve hack scripts Signed-off-by: Nils Arnold --- Makefile | 303 ------------------------------- hack/build.sh | 107 +++++++++-- hack/generate-image-manifests.sh | 43 ++++- hack/generate-resources.sh | 88 ++++++++- hack/show-matrix.sh | 30 ++- hack/update.sh | 87 ++++++++- 6 files changed, 312 insertions(+), 346 deletions(-) delete mode 100644 Makefile diff --git a/Makefile b/Makefile deleted file mode 100644 index e5d63845..00000000 --- a/Makefile +++ /dev/null @@ -1,303 +0,0 @@ -# Copyright 2023 The Kubernetes Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -SHELL = /usr/bin/env bash -o pipefail -.SHELLFLAGS = -ec -.DEFAULT_GOAL:=help - -##@ General - -# The help target prints out all targets with their descriptions organized -# beneath their categories. The categories are represented by '##@' and the -# target descriptions by '##'. The awk commands is responsible for reading the -# entire set of makefiles included in this invocation, looking for lines of the -# file as xyz: ## something, and then pretty-format the target and help. Then, -# if there's a line with ##@ something, that gets pretty-printed as a category. -# More info on the usage of ANSI control characters for terminal formatting: -# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters -# More info on the awk command: -# http://linuxcommand.org/lc3_adv_awk.php - -.PHONY: ensure-connected-to-mgt-cluster -ensure-connected-to-mgt-cluster: - ./hack/ensure-connected-to-mgt-cluster.sh - -.PHONY: help -help: ## Display this help. - @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) - -############# -# Variables # -############# - -TIMEOUT := $(shell command -v timeout || command -v gtimeout) - -# Directories -BIN_DIR := bin -TOOLS_DIR := hack/tools -TOOLS_BIN_DIR := $(TOOLS_DIR)/$(BIN_DIR) -export PATH := $(abspath $(TOOLS_BIN_DIR)):$(PATH) -export GOBIN := $(abspath $(TOOLS_BIN_DIR)) - -ARTIFACTS_PATH := $(ROOT_DIR)/_artifacts -# Docker -RM := --rm -TTY := -t - -##@ Binaries -############ -# Binaries # -############ - -KUSTOMIZE := $(abspath $(TOOLS_BIN_DIR)/kustomize) -kustomize: $(KUSTOMIZE) ## Build a local copy of kustomize -$(KUSTOMIZE): # Build kustomize from tools folder. - go install sigs.k8s.io/kustomize/kustomize/v5@v5.1.0 - -ENVSUBST := $(abspath $(TOOLS_BIN_DIR)/envsubst) -envsubst: $(ENVSUBST) ## Build a local copy of envsubst -$(ENVSUBST): # Build envsubst from tools folder. - go install github.com/drone/envsubst/v2/cmd/envsubst@latest - -CTLPTL := $(abspath $(TOOLS_BIN_DIR)/ctlptl) -ctlptl: $(CTLPTL) ## Build a local copy of ctlptl -$(CTLPTL): - go install github.com/tilt-dev/ctlptl/cmd/ctlptl@v0.8.20 - -CLUSTERCTL := $(abspath $(TOOLS_BIN_DIR)/clusterctl) -clusterctl: $(CLUSTERCTL) ## Build a local copy of clusterctl -$(CLUSTERCTL): - curl -sSLf https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.5.1/clusterctl-linux-amd64 -o $(CLUSTERCTL) - chmod a+rx $(CLUSTERCTL) - -HELM := $(abspath $(TOOLS_BIN_DIR)/helm) -helm: $(HELM) ## Build a local copy of helm -$(HELM): - go install helm.sh/helm/v3/cmd/helm@v3.12.3 - -KIND := $(abspath $(TOOLS_BIN_DIR)/kind) -kind: $(KIND) ## Build a local copy of kind -$(KIND): - go install sigs.k8s.io/kind@v0.20.0 - -all-tools: $(KIND) $(CTLPTL) $(KIND) $(ENVSUBST) $(KUSTOMIZE) $(CLUSTERCTL) $(HELM) - echo 'done' - -.PHONY: basics -basics: $(KIND) $(CTLPTL) $(KIND) $(ENVSUBST) $(KUSTOMIZE) $(CLUSTERCTL) - @./hack/ensure-env-variables.sh CAPI_VERSION CAPD_VERSION NAMESPACE \ - CLUSTER_CLASS_NAME K8S_VERSION CLUSTER_NAME PROVIDER - @mkdir -p build - -##@ Development -############### -# Development # -############### - -.PHONY: cluster -kind-cluster: basics ## Creates kind-dev Cluster - ./hack/kind-dev.sh - kubectl config set-context --current --namespace $(NAMESPACE) - -.PHONY: watch -watch: ## Show the current state of the CRDs and events. - watch -c "kubectl -n $(NAMESPACE) get cluster; echo; kubectl -n $(NAMESPACE) get machine; echo; kubectl -n $(NAMESPACE) get dockermachine; echo; echo Events; kubectl -A get events --sort-by=metadata.creationTimestamp | tail -5" - -##@ Clean -######### -# Clean # -######### -.PHONY: clean -clean: basics ## Remove all generated files - $(MAKE) clean-bin - -.PHONY: clean-bin -clean-bin: ## Remove all generated helper binaries - rm -rf $(TOOLS_BIN_DIR) - -.PHONY: clean-release -clean-release: ## Remove the release folder - rm -rf $(RELEASE_DIR) - -.PHONY: clean-release-git -clean-release-git: ## Restores the git files usually modified during a release - git restore ./*manager_config_patch.yaml ./*manager_pull_policy.yaml - -##@ Releasing -############# -# Releasing # -############# -## latest git tag for the commit, e.g., v0.3.10 -RELEASE_TAG ?= $(shell git describe --abbrev=0 2>/dev/null) -# the previous release tag, e.g., v0.3.9, excluding pre-release tags -PREVIOUS_TAG ?= $(shell git tag -l | grep -E "^v[0-9]+\.[0-9]+\.[0-9]." | sort -V | grep -B1 $(RELEASE_TAG) | head -n 1 2>/dev/null) -RELEASE_DIR ?= out -RELEASE_NOTES_DIR := _releasenotes - -$(RELEASE_DIR): - mkdir -p $(RELEASE_DIR)/ - -$(RELEASE_NOTES_DIR): - mkdir -p $(RELEASE_NOTES_DIR)/ - -.PHONY: test-release -test-release: basics - $(MAKE) set-manifest-image MANIFEST_IMG=$(IMAGE_PREFIX)/capd-staging MANIFEST_TAG=$(TAG) - $(MAKE) set-manifest-pull-policy PULL_POLICY=IfNotPresent - $(MAKE) release-manifests - -.PHONY: release-manifests -release-manifests: basics generate-manifests generate-go-deepcopy $(RELEASE_DIR) cluster-templates ## Builds the manifests to publish with a release - $(KUSTOMIZE) build config/default > $(RELEASE_DIR)/infrastructure-components.yaml - ## Build capd-components (aggregate of all of the above). - cp metadata.yaml $(RELEASE_DIR)/metadata.yaml - cp templates/cluster-templates/cluster-template* $(RELEASE_DIR)/ - cp templates/cluster-templates/cluster-class* $(RELEASE_DIR)/ - -.PHONY: release-notes -release-notes: basics $(RELEASE_NOTES_DIR) $(RELEASE_NOTES) - go run ./hack/tools/release/notes.go --from=$(PREVIOUS_TAG) > $(RELEASE_NOTES_DIR)/$(RELEASE_TAG).md - - -##@ Testing -########### -# Testing # -########### -ARTIFACTS ?= _artifacts -$(ARTIFACTS): - mkdir -p $(ARTIFACTS)/ - -##@ cluster-class -################# -# cluster-class # -################# - -.PHONY: re-apply-cluster-class-docker -re-apply-cluster-class-docker: ensure-connected-to-mgt-cluster basics ## Re-apply only a cluster-class. - $(HELM) -n $(NAMESPACE) template docker-$(CLUSTER_CLASS_NAME)-$(K8S_VERSION) providers/docker/$(CLUSTER_CLASS_NAME)/$(K8S_VERSION)/cluster-class | kubectl -n $(NAMESPACE) apply -f - - -.PHONY: delete-cluster-class-docker -delete-cluster-class-docker: ensure-connected-to-mgt-cluster basics ## Delete a cluster-class. - $(HELM) -n $(NAMESPACE) template docker-$(CLUSTER_CLASS_NAME)-$(K8S_VERSION) providers/docker/$(CLUSTER_CLASS_NAME)/$(K8S_VERSION)/cluster-class | kubectl -n $(NAMESPACE) delete -f - - -##@ cluster-addon -################# -# cluster-addon # -################# - -## working on cluster-addon -.PHONY: generate-deps-cluster-addon-docker -generate-deps-cluster-addon-docker: ensure-connected-to-mgt-cluster basics ## Build a cluster-class. - cd providers/docker/$(CLUSTER_CLASS_NAME)/$(K8S_VERSION)/cluster-addon && rm -rf ./charts/* && helm dependency update - -.PHONY: package-cluster-addon-docker -package-cluster-addon-docker: ensure-connected-to-mgt-cluster basics ## Build a cluster-class. - @mkdir -p .helm - $(HELM) package providers/docker/$(CLUSTER_CLASS_NAME)/$(K8S_VERSION)/cluster-addon -d .helm - -##@ Main Targets -################ -# Main Targets # -################ -.PHONY: delete-bootstrap-cluster -delete-bootstrap-cluster: ensure-connected-to-mgt-cluster basics ## Deletes Kind-dev Cluster - $(CTLPTL) delete cluster kind-scs-cluster-stacks - $(CTLPTL) delete registry cluster-stacks-registry - -.PHONY: create-bootstrap-cluster -create-bootstrap-cluster: basics kind-cluster ## Create mgt-cluster and install capi-stack. - EXP_RUNTIME_SDK=true CLUSTER_TOPOLOGY=true DISABLE_VERSIONCHECK="true" $(CLUSTERCTL) init --core cluster-api:$(CAPI_VERSION) --bootstrap kubeadm:$(CAPI_VERSION) --control-plane kubeadm:$(CAPI_VERSION) - # kubectl apply -f https://github.com/kubernetes-sigs/cluster-api-addon-provider-helm/releases/download/$(CAAPH_VERSION)/add-on-components.yaml - kubectl wait -n cert-manager deployment cert-manager --for=condition=Available --timeout=300s - kubectl wait -n capi-kubeadm-bootstrap-system deployment capi-kubeadm-bootstrap-controller-manager --for=condition=Available --timeout=300s - kubectl wait -n capi-kubeadm-control-plane-system deployment capi-kubeadm-control-plane-controller-manager --for=condition=Available --timeout=300s - kubectl wait -n capi-system deployment capi-controller-manager --for=condition=Available --timeout=300s - -.PHONY: install-provider-docker -install-provider-docker: create-bootstrap-cluster ## Install Docker Infrastructure provider. - # hangs for ever waiting for cert-manger to get available if called twice. - if kubectl get deployments.apps -n capd-system capd-controller-manager > /dev/null 2>&1; then \ - echo "capd is already installed" ; \ - else \ - echo "installing capd" ; \ - DISABLE_VERSIONCHECK="true" $(CLUSTERCTL) init --infrastructure docker:$(CAPD_VERSION); \ - fi - kubectl wait -n capd-system deployment capd-controller-manager --for=condition=Available --timeout=300s - -.PHONY: prepare-provider-docker -prepare-provider-docker: install-provider-docker ## Prepares the Docker Environment. - kubectl create namespace $(NAMESPACE) --dry-run=client -o yaml | kubectl apply -f - - -.PHONY: apply-cluster-class-docker -apply-cluster-class-docker: $(HELM) prepare-provider-docker package-cluster-addon-docker ## Applies all resources and node-images. - $(HELM) -n $(NAMESPACE) template docker-$(CLUSTER_CLASS_NAME)-$(K8S_VERSION) \ - providers/docker/$(CLUSTER_CLASS_NAME)/$(K8S_VERSION)/cluster-class > build/cluster-class-created.yaml - kubectl -n $(NAMESPACE) apply -f build/cluster-class-created.yaml - # create the docker image. Start first build in background to - docker build -t docker-ferrol-1-27-controlplaneamd64-v1:dev \ - --file providers/docker/$(CLUSTER_CLASS_NAME)/$(K8S_VERSION)/node-images/Dockerfile.controlplane \ - providers/docker/$(CLUSTER_CLASS_NAME)/$(K8S_VERSION)/node-images/ & \ - docker build -t docker-ferrol-1-27-workeramd64-v1:dev \ - --file providers/docker/$(CLUSTER_CLASS_NAME)/$(K8S_VERSION)/node-images/Dockerfile.worker \ - providers/docker/$(CLUSTER_CLASS_NAME)/$(K8S_VERSION)/node-images/ - @echo "" - @echo "Done" - -.PHONY: create-workload-cluster -create-workload-cluster: apply-cluster-class-docker basics ## Creates a workload cluster. - cat providers/docker/$(CLUSTER_CLASS_NAME)/$(K8S_VERSION)/topology-$(PROVIDER).yaml | $(ENVSUBST) - > build/topology.yaml - kubectl apply -f build/topology.yaml - # Wait for the kubeconfig to become available. - ${TIMEOUT} --foreground 5m bash -c "while ! kubectl -n $(NAMESPACE) get secrets | grep $(CLUSTER_NAME)-kubeconfig; do date; echo waiting for secret $(CLUSTER_NAME)-kubeconfig; sleep 1; done" - # Get kubeconfig and store it locally. - @mkdir -p .kubeconfigs - kubectl -n $(NAMESPACE) get secrets $(CLUSTER_NAME)-kubeconfig -o json | jq -r .data.value | base64 --decode > .kubeconfigs/.$(CLUSTER_NAME)-kubeconfig - if [ ! -s ".kubeconfigs/.$(CLUSTER_NAME)-kubeconfig" ]; then echo "failed to create .kubeconfigs/.$(CLUSTER_NAME)-kubeconfig"; exit 1; fi - ${TIMEOUT} --foreground 15m bash -c "while ! kubectl --kubeconfig=.kubeconfigs/.$(CLUSTER_NAME)-kubeconfig -n $(NAMESPACE) get nodes | \ - grep control-plane; do echo 'Waiting for control-plane in workload-cluster'; sleep 1; done" - chmod a=,u=rw .kubeconfigs/.$(CLUSTER_NAME)-kubeconfig - @echo "" - @echo 'Access to workload API server successful.' - @echo 'use KUBECONFIG=.kubeconfigs/.$(CLUSTER_NAME)-kubeconfig to access the workload cluster' - -.PHONY: install-cluster-addons -install-addons-in-workload-cluster: $(HELM) - # Cluster Addons are software tools which provide required functionality to the cluster. - # Example: CNI (Container Network Interface) - # Hint: Applications like PostgreSQL or Prometheus are not cluster addons. - - @# The "ugly" replacement of policy/v1beta1 needs to be done, since `helm template` - @# has no access to the cluster, and the new --dry-run=server is not released yet. - @# Replace the `sed` with `--dry-run=server` when helm supports --dry-run=server. - @# It should be included in helm 3.13.0 - KUBECONFIG=.kubeconfigs/.$(CLUSTER_NAME)-kubeconfig helm template \ - ./providers/docker/$(CLUSTER_CLASS_NAME)/$(K8S_VERSION)/cluster-addon \ - | sed 's#apiVersion: policy/v1beta1#apiVersion: policy/v1#' \ - > build/cluster-addons-$(CLUSTER_NAME).yaml - KUBECONFIG=.kubeconfigs/.$(CLUSTER_NAME)-kubeconfig kubectl apply -f build/cluster-addons-$(CLUSTER_NAME).yaml - - -.PHONY: release-docker -release-docker: clean-release $(HELM) ## Builds and push container images using the latest git tag for the commit. - # @if [ -z "${RELEASE_TAG}" ]; then echo "RELEASE_TAG is not set"; exit 1; fi - # @if ! [ -z "$$(git status --porcelain)" ]; then echo "Your local git repository contains uncommitted changes, use git clean before proceeding."; exit 1; fi - # git checkout "${RELEASE_TAG}" - @./hack/ensure-env-variables.sh RELEASE_CLUSTER_CLASS RELEASE_KUBERNETES_VERSION - @mkdir -p .release - cp providers/docker/$(RELEASE_CLUSTER_CLASS)/$(RELEASE_KUBERNETES_VERSION)/cluster-addon-values.yaml .release/ - cp providers/docker/$(RELEASE_CLUSTER_CLASS)/$(RELEASE_KUBERNETES_VERSION)/node-images.yaml .release/ - cp providers/docker/$(RELEASE_CLUSTER_CLASS)/$(RELEASE_KUBERNETES_VERSION)/metadata.yaml .release/ - cp providers/docker/$(RELEASE_CLUSTER_CLASS)/$(RELEASE_KUBERNETES_VERSION)/topology-* .release/ - $(HELM) package providers/docker/$(RELEASE_CLUSTER_CLASS)/$(RELEASE_KUBERNETES_VERSION)/cluster-addon -d .release/ - $(HELM) package providers/docker/$(RELEASE_CLUSTER_CLASS)/$(RELEASE_KUBERNETES_VERSION)/cluster-class -d .release/ diff --git a/hack/build.sh b/hack/build.sh index 3ae57af5..d09cc23c 100755 --- a/hack/build.sh +++ b/hack/build.sh @@ -41,6 +41,54 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +YQ_VERSION="$(yq --version 2>/dev/null || true)" + +require_command() { + local name="$1" + + if ! command -v "$name" >/dev/null 2>&1; then + echo "$name not found. Please install $name and try again." >&2 + exit 1 + fi +} + +extract_k8s_minor_version() { + echo "$1" | sed -E -n 's/^([0-9]+\.[0-9]+)(\.[0-9]+)?$/\1/p' +} + +extract_latest_release_number() { + local prefix="$1" + + awk -v prefix="${prefix}-v" 'index($0, prefix) == 1 { + suffix = substr($0, length(prefix) + 1) + if (suffix ~ /^[0-9]+$/) { + print suffix + } + }' | sort -n | tail -1 +} + +yq_edit_in_place() { + local expression="$1" + local file="$2" + + if [[ "$YQ_VERSION" == *"https://github.com/mikefarah/yq/"* ]]; then + yq -i "$expression" "$file" + else + yq -y -i "$expression" "$file" + fi +} + +yq_has_path() { + local expression="$1" + local file="$2" + + yq -e "$expression" "$file" >/dev/null 2>&1 +} + +require_command yq +require_command curl +require_command jq +require_command helm # ============================================ # Argument parsing @@ -185,15 +233,25 @@ get_release_version() { return fi + local oras_opts=() + if [[ -n "${OCI_USERNAME:-}" && -n "${OCI_PASSWORD:-}" ]]; then + oras_opts+=(--username "$OCI_USERNAME" --password "$OCI_PASSWORD") + elif [[ -n "${OCI_ACCESS_TOKEN:-}" ]]; then + oras_opts+=(--password "$OCI_ACCESS_TOKEN") + fi + # Query OCI for existing stable versions + require_command oras + local latest=0 - if command -v oras >/dev/null 2>&1; then - local tags - tags=$(oras repo tags "${OCI_REGISTRY}/${OCI_REPOSITORY}" 2>/dev/null || echo "") - if [[ -n "$tags" ]]; then - latest=$(echo "$tags" | grep -oP "^${tag_prefix}-v\K[0-9]+" | sort -n | tail -1 || echo "0") - latest="${latest:-0}" - fi + local tags + if ! tags=$(oras repo tags "${OCI_REGISTRY}/${OCI_REPOSITORY}" "${oras_opts[@]}" 2>/dev/null); then + echo "Failed to query OCI tags from ${OCI_REGISTRY}/${OCI_REPOSITORY}." >&2 + exit 1 + fi + if [[ -n "$tags" ]]; then + latest=$(echo "$tags" | extract_latest_release_number "$tag_prefix" || echo "0") + latest="${latest:-0}" fi echo "v$((latest + 1))" @@ -219,12 +277,15 @@ resolve_k8s_version() { if [[ "$provider" == "docker" ]]; then # Query Docker Hub for kindest/node tags local latest - latest=$(curl -sfL "https://registry.hub.docker.com/v2/repositories/kindest/node/tags?page_size=100&name=v${version}." 2>/dev/null \ + if ! latest=$(curl -sfL "https://registry.hub.docker.com/v2/repositories/kindest/node/tags?page_size=100&name=v${version}." 2>/dev/null \ | jq -r '.results[].name' 2>/dev/null \ | grep -E "^v${version}\.[0-9]+$" \ | sed 's/^v//' \ | sort -V \ - | tail -1) || true + | tail -1); then + echo "Failed to resolve the latest Docker patch version for Kubernetes ${version}." >&2 + exit 1 + fi if [[ -n "$latest" ]]; then echo "$latest" return @@ -236,21 +297,24 @@ resolve_k8s_version() { github_headers=(-H "Authorization: token $GITHUB_TOKEN") fi local latest - latest=$(curl -sfL "${github_headers[@]+"${github_headers[@]}"}" \ + if ! latest=$(curl -sfL "${github_headers[@]+"${github_headers[@]}"}" \ "https://api.github.com/repos/kubernetes/kubernetes/releases?per_page=100" 2>/dev/null \ | jq -r '.[].tag_name' 2>/dev/null \ | grep -E "^v${version}\.[0-9]+$" \ | sed 's/^v//' \ | sort -V \ - | tail -1) || true + | tail -1); then + echo "Failed to resolve the latest GitHub patch version for Kubernetes ${version}." >&2 + exit 1 + fi if [[ -n "$latest" ]]; then echo "$latest" return fi fi - # Fallback: return with .0 - echo "${version}.0" + echo "Could not resolve a stable patch version for Kubernetes ${version}." >&2 + exit 1 } # ============================================ @@ -298,7 +362,7 @@ build_version_dir() { local k8s_version k8s_version=$(resolve_k8s_version "$k8s_version_raw" "$provider") local k8s_short - k8s_short=$(echo "$k8s_version" | grep -oP '^\d+\.\d+') + k8s_short=$(extract_k8s_minor_version "$k8s_version") local k8s_dash="${k8s_short//./-}" echo "" @@ -322,17 +386,22 @@ build_version_dir() { # Patch cluster-class Chart.yaml: set name and version local class_chart="$work_dir/cluster-class/Chart.yaml" - yq -i ".name = \"${provider}-${stack_name}-${k8s_dash}-cluster-class\"" "$class_chart" - yq -i ".version = \"${release_version}\"" "$class_chart" + yq_edit_in_place ".name = \"${provider}-${stack_name}-${k8s_dash}-cluster-class\"" "$class_chart" + yq_edit_in_place ".version = \"${release_version}\"" "$class_chart" # Generate csctl.yaml for release artifact (backwards compatibility) generate_csctl_yaml "$provider" "$stack_name" "$k8s_version" "$work_dir/csctl.yaml" # Patch cluster-class values.yaml image names (if the field exists) local class_values="$work_dir/cluster-class/values.yaml" - if [[ -f "$class_values" ]] && yq -e '.images.controlPlane.name' "$class_values" >/dev/null 2>&1; then - yq -i ".images.controlPlane.name = \"ubuntu-capi-image-v${k8s_version}\"" "$class_values" - yq -i ".images.worker.name = \"ubuntu-capi-image-v${k8s_version}\"" "$class_values" + if [[ -f "$class_values" ]]; then + if yq_has_path '.images.controlPlane.name' "$class_values"; then + yq_edit_in_place ".images.controlPlane.name = \"ubuntu-capi-image-v${k8s_version}\"" "$class_values" + yq_edit_in_place ".images.worker.name = \"ubuntu-capi-image-v${k8s_version}\"" "$class_values" + elif yq_has_path '.images.controlPlane[0].name' "$class_values"; then + yq_edit_in_place ".images.controlPlane[0].name = \"registry.scs.community/docker.io/kindest/node:v${k8s_version}\"" "$class_values" + yq_edit_in_place ".images.worker[0].name = \"registry.scs.community/docker.io/kindest/node:v${k8s_version}\"" "$class_values" + fi fi # ---- Package cluster-class ---- diff --git a/hack/generate-image-manifests.sh b/hack/generate-image-manifests.sh index d1461f83..6e095562 100755 --- a/hack/generate-image-manifests.sh +++ b/hack/generate-image-manifests.sh @@ -35,6 +35,23 @@ set -euo pipefail +require_command() { + local name="$1" + + if ! command -v "$name" >/dev/null 2>&1; then + echo "$name not found. Please install $name and try again." >&2 + exit 1 + fi +} + +extract_k8s_minor_version() { + echo "$1" | sed -E -n 's/^([0-9]+\.[0-9]+)(\.[0-9]+)?$/\1/p' +} + +require_command yq +require_command curl +require_command jq + # Defaults BASE_URL="${IMAGE_BASE_URL:-https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images}" CLOUD_NAME="${CLOUD_NAME:-openstack}" @@ -107,7 +124,14 @@ ubuntu_for_minor() { # Check provider # ============================================ -FIRST_STACK=$(ls "$BASE_DIR"/1-*/stack.yaml 2>/dev/null | head -1) +FIRST_STACK="" +for stack_file in "$BASE_DIR"/1-*/stack.yaml; do + if [[ -f "$stack_file" ]]; then + FIRST_STACK="$stack_file" + break + fi +done + if [[ -z "$FIRST_STACK" ]]; then echo "No stack.yaml found in $BASE_DIR/1-*/" >&2 exit 1 @@ -138,19 +162,24 @@ resolve_k8s_version() { github_headers=(-H "Authorization: token $GITHUB_TOKEN") fi local latest - latest=$(curl -sfL "${github_headers[@]+"${github_headers[@]}"}" \ + if ! latest=$(curl -sfL "${github_headers[@]+"${github_headers[@]}"}" \ "https://api.github.com/repos/kubernetes/kubernetes/releases?per_page=100" 2>/dev/null \ | jq -r '.[].tag_name' 2>/dev/null \ | grep -E "^v${version}\.[0-9]+$" \ | sed 's/^v//' \ | sort -V \ - | tail -1) || true + | tail -1); then + echo "Failed to resolve the latest GitHub patch version for Kubernetes ${version}." >&2 + exit 1 + fi if [[ -n "$latest" ]]; then echo "$latest" - else - echo "${version}.0" + return fi + + echo "Could not resolve a stable patch version for Kubernetes ${version}." >&2 + exit 1 } # ============================================ @@ -172,12 +201,12 @@ for version_dir in "$BASE_DIR"/1-*/; do [[ -f "$stack_yaml" ]] || continue k8s_version_raw=$(yq -r '.kubernetesVersion' "$stack_yaml") - k8s_short=$(echo "$k8s_version_raw" | grep -oP '^\d+\.\d+') + k8s_short=$(extract_k8s_minor_version "$k8s_version_raw") k8s_minor=$(echo "$k8s_short" | cut -d. -f2) # Filter by --version if specified if [[ -n "$TARGET_VERSION" ]]; then - target_short=$(echo "$TARGET_VERSION" | grep -oP '^\d+\.\d+') + target_short=$(extract_k8s_minor_version "$TARGET_VERSION") if [[ "$k8s_short" != "$target_short" ]]; then continue fi diff --git a/hack/generate-resources.sh b/hack/generate-resources.sh index 6a7c2893..57a4f29f 100755 --- a/hack/generate-resources.sh +++ b/hack/generate-resources.sh @@ -31,6 +31,80 @@ set -euo pipefail +require_command() { + local name="$1" + + if ! command -v "$name" >/dev/null 2>&1; then + echo "$name not found. Please install $name and try again." >&2 + exit 1 + fi +} + +extract_latest_release_number() { + local prefix="$1" + + awk -v prefix="${prefix}-v" 'index($0, prefix) == 1 { + suffix = substr($0, length(prefix) + 1) + if (suffix ~ /^[0-9]+$/) { + print suffix + } + }' | sort -n | tail -1 +} + +require_command yq +require_command curl +require_command jq + +resolve_k8s_version() { + local version="$1" + local provider="$2" + + if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "$version" + return + fi + + if [[ "$provider" == "docker" ]]; then + local latest_docker + if ! latest_docker=$(curl -sfL "https://registry.hub.docker.com/v2/repositories/kindest/node/tags?page_size=100&name=v${version}." 2>/dev/null | \ + jq -r '.results[].name' 2>/dev/null | \ + grep -E "^v${version}\.[0-9]+$" | \ + sed 's/^v//' | \ + sort -V | \ + tail -1); then + echo "Failed to resolve the latest Docker patch version for Kubernetes ${version}." >&2 + exit 1 + fi + if [[ -n "$latest_docker" ]]; then + echo "$latest_docker" + return + fi + else + local github_headers=() + if [[ -n "${GITHUB_TOKEN:-}" ]]; then + github_headers=(-H "Authorization: token $GITHUB_TOKEN") + fi + local latest_github + if ! latest_github=$(curl -sfL "${github_headers[@]+"${github_headers[@]}"}" \ + "https://api.github.com/repos/kubernetes/kubernetes/releases?per_page=100" 2>/dev/null | \ + jq -r '.[].tag_name' 2>/dev/null | \ + grep -E "^v${version}\.[0-9]+$" | \ + sed 's/^v//' | \ + sort -V | \ + tail -1); then + echo "Failed to resolve the latest GitHub patch version for Kubernetes ${version}." >&2 + exit 1 + fi + if [[ -n "$latest_github" ]]; then + echo "$latest_github" + return + fi + fi + + echo "Could not resolve a stable patch version for Kubernetes ${version}." >&2 + exit 1 +} + # ============================================ # Argument parsing # ============================================ @@ -96,8 +170,7 @@ PROVIDER=$(yq -r '.provider' "$STACK_YAML") CLUSTER_STACK=$(yq -r '.clusterStackName' "$STACK_YAML") K8S_VERSION_RAW=$(yq -r '.kubernetesVersion' "$STACK_YAML") -# Use full version from stack.yaml if it has patch, otherwise use the minor -K8S_FULL="$K8S_VERSION_RAW" +K8S_FULL=$(resolve_k8s_version "$K8S_VERSION_RAW" "$PROVIDER") # Auto-detect CS version (if not specified) # Priority: 1. local .release/ build output 2. OCI registry 3. fall back to v1 @@ -118,7 +191,7 @@ if [[ -z "$CS_VERSION" ]]; then # 2. Try OCI registry elif [[ -n "${OCI_REGISTRY:-}" && -n "${OCI_REPOSITORY:-}" ]] && command -v oras >/dev/null 2>&1; then LATEST=$(oras repo tags "${OCI_REGISTRY}/${OCI_REPOSITORY}" 2>/dev/null | \ - grep -oP "^${TAG_PREFIX}-v\K[0-9]+" | sort -n | tail -1 || echo "") + extract_latest_release_number "$TAG_PREFIX" || echo "") if [[ -n "$LATEST" ]]; then CS_VERSION="v${LATEST}" echo "# Auto-detected CS version: ${CS_VERSION} (from ${OCI_REGISTRY}/${OCI_REPOSITORY})" >&2 @@ -147,7 +220,7 @@ metadata: spec: provider: ${PROVIDER} name: ${CLUSTER_STACK} - kubernetesVersion: "${K8S_VERSION}" + kubernetesVersion: "${K8S_FULL}" channel: custom autoSubscribe: false versions: @@ -160,11 +233,16 @@ fi # ============================================ if [[ "$CLUSTERSTACK_ONLY" != "true" ]]; then + CLUSTER_API_VERSION="cluster.x-k8s.io/v1beta2" + if [[ "$CLUSTER_STACK" == "hcp" ]]; then + CLUSTER_API_VERSION="cluster.x-k8s.io/v1beta1" + fi + CLUSTER_CLASS="${PROVIDER}-${CLUSTER_STACK}-${K8S_DASH}-${CS_VERSION}" cat </dev/null 2>&1; then + echo "$name not found. Please install $name and try again." >&2 + exit 1 + fi +} + +extract_k8s_minor_version() { + echo "$1" | sed -E -n 's/^([0-9]+\.[0-9]+)(\.[0-9]+)?$/\1/p' +} + +extract_latest_release_number() { + local prefix="$1" + + awk -v prefix="${prefix}-v" 'index($0, prefix) == 1 { + suffix = substr($0, length(prefix) + 1) + if (suffix ~ /^[0-9]+$/) { + print suffix + } + }' | sort -n | tail -1 +} + +require_command yq + MARKDOWN=false BASE_DIR="" @@ -126,7 +152,7 @@ for version_dir in "$BASE_DIR"/1-*/; do [[ -f "$stack_yaml" ]] || continue k8s_version=$(yq -r '.kubernetesVersion' "$stack_yaml") - k8s_short=$(echo "$k8s_version" | grep -oP '^\d+\.\d+') + k8s_short=$(extract_k8s_minor_version "$k8s_version") k8s_dash="${k8s_short//./-}" # Query OCI for CS version @@ -134,7 +160,7 @@ for version_dir in "$BASE_DIR"/1-*/; do if [[ -n "${OCI_REGISTRY:-}" && -n "${OCI_REPOSITORY:-}" ]] && command -v oras >/dev/null 2>&1; then TAG_PREFIX="${PROVIDER_NAME}-${STACK_NAME}-${k8s_dash}" LATEST=$(oras repo tags "${OCI_REGISTRY}/${OCI_REPOSITORY}" 2>/dev/null | \ - grep -oP "^${TAG_PREFIX}-v\K[0-9]+" | sort -n | tail -1 || echo "") + extract_latest_release_number "$TAG_PREFIX" || echo "") [[ -n "$LATEST" ]] && CS_VERSION="v${LATEST}" fi diff --git a/hack/update.sh b/hack/update.sh index 61823ebc..93ec3640 100755 --- a/hack/update.sh +++ b/hack/update.sh @@ -39,6 +39,47 @@ set -euo pipefail readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +readonly YQ_VERSION="$(yq --version 2>/dev/null || true)" + +require_command() { + local name="$1" + + if ! command -v "$name" >/dev/null 2>&1; then + echo "$name not found. Please install $name and try again." >&2 + exit 1 + fi +} + +extract_k8s_minor_version() { + echo "$1" | sed -E -n 's/^([0-9]+\.[0-9]+)(\.[0-9]+)?$/\1/p' +} + +extract_k8s_minor_number() { + echo "$1" | sed -E -n 's/^v?[0-9]+\.([0-9]+)(\.[0-9]+)?$/\1/p' +} + +yq_edit_in_place() { + local expression="$1" + local file="$2" + + if [[ "$YQ_VERSION" == *"https://github.com/mikefarah/yq/"* ]]; then + yq -i "$expression" "$file" + else + yq -y -i "$expression" "$file" + fi +} + +require_command yq + +require_versions_tools() { + require_command curl + require_command jq +} + +require_addons_tools() { + require_command helm + require_command jq +} # ============================================ # Defaults @@ -254,7 +295,7 @@ update_image_manager() { # Extract minor from version string (e.g., "v1.34.4" → "34") local minor - minor=$(echo "$ver" | grep -oP '\d+\.\K\d+(?=\.\d+)') + minor=$(extract_k8s_minor_number "$ver") if [[ -n "$minor" ]]; then existing_versions["$minor"]="${ver}|${url}|${chk}" fi @@ -405,7 +446,7 @@ cmd_versions() { local k8s_version_raw k8s_version_raw=$(yq -r '.kubernetesVersion' "$stack_yaml") local k8s_short - k8s_short=$(echo "$k8s_version_raw" | grep -oP '^\d+\.\d+') + k8s_short=$(extract_k8s_minor_version "$k8s_version_raw") local k8s_minor k8s_minor=$(echo "$k8s_short" | cut -d. -f2) @@ -430,13 +471,18 @@ cmd_versions() { change "$dir_name: K8s $k8s_version_raw → $latest_patch" changes=$((changes + 1)) if [[ "$DRY_RUN" != "true" ]]; then - yq -i ".kubernetesVersion = \"$latest_patch\"" "$stack_yaml" + yq_edit_in_place ".kubernetesVersion = \"$latest_patch\"" "$stack_yaml" info "Updated $stack_yaml" fi fi else - # Minor-only (e.g., "1.34") — report latest available patch - ok "$dir_name: K8s $k8s_version_raw (minor-only, latest patch: $latest_patch)" + # Minor-only (e.g., "1.34") — pin to the latest patch for deterministic builds. + change "$dir_name: K8s $k8s_version_raw → $latest_patch" + changes=$((changes + 1)) + if [[ "$DRY_RUN" != "true" ]]; then + yq_edit_in_place ".kubernetesVersion = \"$latest_patch\"" "$stack_yaml" + info "Updated $stack_yaml" + fi fi done @@ -462,6 +508,7 @@ cmd_versions() { cmd_addons() { local base_dir="$1" + local query_failures=0 echo "Updating addons: $base_dir" echo "" @@ -481,7 +528,10 @@ cmd_addons() { dep_repo=$(yq -r ".dependencies[$i].repository // \"\"" "$chart_file") [[ -z "$dep_repo" ]] && continue if [[ -z "${repos_added[$dep_name]+_}" ]]; then - helm repo add "$dep_name" "$dep_repo" >/dev/null 2>&1 || true + if ! helm repo add --force-update "$dep_name" "$dep_repo" >/dev/null 2>&1; then + echo "Failed to add Helm repo $dep_name ($dep_repo)." >&2 + exit 1 + fi repos_added["$dep_name"]="$dep_repo" fi done @@ -490,7 +540,10 @@ cmd_addons() { if [[ ${#repos_added[@]} -gt 0 ]]; then info "Updating Helm repos..." - helm repo update > /dev/null 2>&1 + if ! helm repo update > /dev/null 2>&1; then + echo "Failed to update Helm repositories." >&2 + exit 1 + fi fi echo "" @@ -538,7 +591,7 @@ cmd_addons() { if [[ "$is_tied" == "true" ]]; then # For K8s-tied addons, match by prefix from stack.yaml range local k8s_minor - k8s_minor=$(yq -r '.kubernetesVersion' "$stack_yaml" | grep -oP '\.\K\d+') + k8s_minor=$(extract_k8s_minor_number "$(yq -r '.kubernetesVersion' "$stack_yaml")") latest_version=$(helm search repo "$dep_name/$dep_name" --versions -o json 2>/dev/null | \ jq -r --arg minor "$k8s_minor" \ '[.[] | select(.version | startswith("2." + $minor + "."))] | .[0].version // empty' 2>/dev/null) || true @@ -548,7 +601,8 @@ cmd_addons() { fi if [[ -z "$latest_version" ]]; then - info "$dep_name: could not query upstream" + warn "$dep_name: could not query upstream" + query_failures=$((query_failures + 1)) continue fi @@ -561,7 +615,7 @@ cmd_addons() { total_updates=$((total_updates + 1)) if [[ "$DRY_RUN" != "true" ]]; then - yq -i ".dependencies[$i].version = \"$latest_version\"" "$chart_file" + yq_edit_in_place ".dependencies[$i].version = \"$latest_version\"" "$chart_file" info "Updated $chart_file" fi done @@ -569,6 +623,11 @@ cmd_addons() { echo "" done + if [[ "$query_failures" -gt 0 ]]; then + echo "$query_failures addon query failure(s) encountered." >&2 + exit 1 + fi + if [[ "$DRY_RUN" == "true" ]]; then if [[ "$total_updates" -gt 0 ]]; then echo "$total_updates addon update(s) available (dry-run: no changes written)." @@ -626,6 +685,14 @@ run_all() { main() { parse_args "$@" + if [[ -z "$SUBCOMMAND" || "$SUBCOMMAND" != "addons" ]]; then + require_versions_tools + fi + + if [[ -z "$SUBCOMMAND" || "$SUBCOMMAND" != "versions" ]]; then + require_addons_tools + fi + if [[ "$RUN_ALL" == true ]]; then run_all "$SUBCOMMAND" return From 9388a475b12248e36a5f549061502601d2a81d4e Mon Sep 17 00:00:00 2001 From: Nils Arnold Date: Wed, 15 Apr 2026 12:53:12 +0200 Subject: [PATCH 07/11] Update scs ClusterStack and add scs2 changes Signed-off-by: Nils Arnold --- .../templates/cluster-class.yaml | 6 +- .../templates/cluster-class.yaml | 6 +- .../templates/cluster-class.yaml | 6 +- .../scs/1-32/cluster-addon/ccm/Chart.yaml | 14 +- .../scs/1-32/cluster-addon/ccm/values.yaml | 18 +- .../scs/1-32/cluster-addon/cni/Chart.yaml | 10 +- .../scs/1-32/cluster-addon/cni/values.yaml | 7 +- .../scs/1-32/cluster-addon/csi/Chart.yaml | 8 +- .../scs/1-32/cluster-addon/csi/values.yaml | 35 +- .../cluster-addon/metrics-server/Chart.yaml | 14 +- .../scs/1-32/cluster-class/Chart.yaml | 4 +- .../templates/cluster-class.yaml | 937 +++++++++++------- .../1-32/cluster-class/templates/image.yaml | 30 - ...eadm-config-template-worker-openstack.yaml | 10 +- .../kubeadm-control-plane-template.yaml | 60 +- .../templates/openstack-cluster-template.yaml | 56 +- ...nstack-machine-template-control-plane.yaml | 10 +- .../openstack-machine-template-worker.yaml | 10 +- .../scs/1-32/cluster-class/values.yaml | 62 +- .../openstack/scs/1-32/clusteraddon.yaml | 9 + providers/openstack/scs/1-32/stack.yaml | 3 +- .../scs/1-33/cluster-addon/cni/Chart.yaml | 2 +- .../templates/cluster-class.yaml | 668 +++++++------ ...eadm-config-template-worker-openstack.yaml | 8 +- .../kubeadm-control-plane-template.yaml | 54 +- .../templates/openstack-cluster-template.yaml | 46 + .../scs/1-33/cluster-class/values.yaml | 51 + .../openstack/scs/1-33/clusteraddon.yaml | 9 + providers/openstack/scs/1-33/stack.yaml | 3 +- .../scs/1-34/cluster-addon/cni/Chart.yaml | 2 +- .../templates/cluster-class.yaml | 663 +++++++------ ...eadm-config-template-worker-openstack.yaml | 8 +- .../kubeadm-control-plane-template.yaml | 54 +- .../templates/openstack-cluster-template.yaml | 46 + .../scs/1-34/cluster-class/values.yaml | 50 + .../openstack/scs/1-34/clusteraddon.yaml | 9 + providers/openstack/scs/1-34/stack.yaml | 3 +- .../scs/1-35/cluster-addon/cni/Chart.yaml | 2 +- .../templates/cluster-class.yaml | 30 +- .../templates/openstack-cluster-template.yaml | 2 +- providers/openstack/scs/1-35/stack.yaml | 3 +- providers/openstack/scs/image-manager.yaml | 24 +- 42 files changed, 1869 insertions(+), 1183 deletions(-) delete mode 100644 providers/openstack/scs/1-32/cluster-class/templates/image.yaml diff --git a/providers/openstack/hcp/1-33/cluster-class/templates/cluster-class.yaml b/providers/openstack/hcp/1-33/cluster-class/templates/cluster-class.yaml index 0cd5c804..dac6ba2c 100644 --- a/providers/openstack/hcp/1-33/cluster-class/templates/cluster-class.yaml +++ b/providers/openstack/hcp/1-33/cluster-class/templates/cluster-class.yaml @@ -135,9 +135,9 @@ spec: schema: openAPIV3Schema: type: string - default: "SCS-4V-8" + default: {{ .Values.workerFlavor | quote }} example: "SCS-4V-8" - description: "OpenStack instance flavor for worker nodes (default: SCS-4V-8, which requires workerRootDisk)." + description: "OpenStack instance flavor for worker nodes." - name: workerRootDisk required: false schema: @@ -145,7 +145,7 @@ spec: type: integer minimum: 0 example: 25 - default: 50 + default: {{ .Values.workerRootDisk }} description: |- Root disk size in GiB for worker nodes. OpenStack volume will be created and used instead of an ephemeral disk defined in flavor. diff --git a/providers/openstack/hcp/1-34/cluster-class/templates/cluster-class.yaml b/providers/openstack/hcp/1-34/cluster-class/templates/cluster-class.yaml index 0cd5c804..dac6ba2c 100644 --- a/providers/openstack/hcp/1-34/cluster-class/templates/cluster-class.yaml +++ b/providers/openstack/hcp/1-34/cluster-class/templates/cluster-class.yaml @@ -135,9 +135,9 @@ spec: schema: openAPIV3Schema: type: string - default: "SCS-4V-8" + default: {{ .Values.workerFlavor | quote }} example: "SCS-4V-8" - description: "OpenStack instance flavor for worker nodes (default: SCS-4V-8, which requires workerRootDisk)." + description: "OpenStack instance flavor for worker nodes." - name: workerRootDisk required: false schema: @@ -145,7 +145,7 @@ spec: type: integer minimum: 0 example: 25 - default: 50 + default: {{ .Values.workerRootDisk }} description: |- Root disk size in GiB for worker nodes. OpenStack volume will be created and used instead of an ephemeral disk defined in flavor. diff --git a/providers/openstack/hcp/1-35/cluster-class/templates/cluster-class.yaml b/providers/openstack/hcp/1-35/cluster-class/templates/cluster-class.yaml index 0cd5c804..dac6ba2c 100644 --- a/providers/openstack/hcp/1-35/cluster-class/templates/cluster-class.yaml +++ b/providers/openstack/hcp/1-35/cluster-class/templates/cluster-class.yaml @@ -135,9 +135,9 @@ spec: schema: openAPIV3Schema: type: string - default: "SCS-4V-8" + default: {{ .Values.workerFlavor | quote }} example: "SCS-4V-8" - description: "OpenStack instance flavor for worker nodes (default: SCS-4V-8, which requires workerRootDisk)." + description: "OpenStack instance flavor for worker nodes." - name: workerRootDisk required: false schema: @@ -145,7 +145,7 @@ spec: type: integer minimum: 0 example: 25 - default: 50 + default: {{ .Values.workerRootDisk }} description: |- Root disk size in GiB for worker nodes. OpenStack volume will be created and used instead of an ephemeral disk defined in flavor. diff --git a/providers/openstack/scs/1-32/cluster-addon/ccm/Chart.yaml b/providers/openstack/scs/1-32/cluster-addon/ccm/Chart.yaml index 1b767285..b66156ac 100644 --- a/providers/openstack/scs/1-32/cluster-addon/ccm/Chart.yaml +++ b/providers/openstack/scs/1-32/cluster-addon/ccm/Chart.yaml @@ -1,10 +1,10 @@ apiVersion: v2 -dependencies: -- alias: openstack-cloud-controller-manager - name: openstack-cloud-controller-manager - repository: https://kubernetes.github.io/cloud-provider-openstack - version: 2.32.0 -description: CCM -name: openstack-scs-1-32-cluster-addon type: application +description: CCM +name: CCM version: v1 +dependencies: + - alias: openstack-cloud-controller-manager + name: openstack-cloud-controller-manager + repository: https://kubernetes.github.io/cloud-provider-openstack + version: 2.32.0 diff --git a/providers/openstack/scs/1-32/cluster-addon/ccm/values.yaml b/providers/openstack/scs/1-32/cluster-addon/ccm/values.yaml index 770706c7..3f290366 100644 --- a/providers/openstack/scs/1-32/cluster-addon/ccm/values.yaml +++ b/providers/openstack/scs/1-32/cluster-addon/ccm/values.yaml @@ -1,13 +1,21 @@ openstack-cloud-controller-manager: secret: enabled: true - name: cloud-config - create: false + name: ccm-cloud-config + create: true nodeSelector: - node-role.kubernetes.io/control-plane: "" tolerations: - key: node.cloudprovider.kubernetes.io/uninitialized value: "true" effect: NoSchedule - - key: node-role.kubernetes.io/control-plane - effect: NoSchedule + extraVolumes: + - name: clouds-yaml + secret: + secretName: clouds-yaml + extraVolumeMounts: + - name: clouds-yaml + readOnly: true + mountPath: /etc/openstack + cloudConfig: + global: + use-clouds: true diff --git a/providers/openstack/scs/1-32/cluster-addon/cni/Chart.yaml b/providers/openstack/scs/1-32/cluster-addon/cni/Chart.yaml index 10b69016..95516e98 100644 --- a/providers/openstack/scs/1-32/cluster-addon/cni/Chart.yaml +++ b/providers/openstack/scs/1-32/cluster-addon/cni/Chart.yaml @@ -1,10 +1,10 @@ apiVersion: v2 +type: application +description: CNI +name: CNI +version: v1 dependencies: - alias: cilium name: cilium repository: https://helm.cilium.io/ - version: 1.19.1 -description: CNI -name: openstack-scs-1-32-cluster-addon -type: application -version: v1 + version: 1.19.2 diff --git a/providers/openstack/scs/1-32/cluster-addon/cni/values.yaml b/providers/openstack/scs/1-32/cluster-addon/cni/values.yaml index 195534d0..8a312f0c 100644 --- a/providers/openstack/scs/1-32/cluster-addon/cni/values.yaml +++ b/providers/openstack/scs/1-32/cluster-addon/cni/values.yaml @@ -1,9 +1,14 @@ cilium: + namespaceOverride: kube-system tls: secretsNamespace: - name: kube-system + name: "kube-system" sessionAffinity: true sctp: enabled: true ipam: mode: "kubernetes" + gatewayAPI: + enabled: true + secretsNamespace: + name: "kube-system" diff --git a/providers/openstack/scs/1-32/cluster-addon/csi/Chart.yaml b/providers/openstack/scs/1-32/cluster-addon/csi/Chart.yaml index c5ed4d75..7d2ed6f0 100644 --- a/providers/openstack/scs/1-32/cluster-addon/csi/Chart.yaml +++ b/providers/openstack/scs/1-32/cluster-addon/csi/Chart.yaml @@ -1,10 +1,10 @@ apiVersion: v2 +type: application +description: CSI +name: CSI +version: v1 dependencies: - alias: openstack-cinder-csi name: openstack-cinder-csi repository: https://kubernetes.github.io/cloud-provider-openstack version: 2.32.2 -description: CSI -name: openstack-scs-1-32-cluster-addon -type: application -version: v1 diff --git a/providers/openstack/scs/1-32/cluster-addon/csi/values.yaml b/providers/openstack/scs/1-32/cluster-addon/csi/values.yaml index 83817b87..4e648a4f 100644 --- a/providers/openstack/scs/1-32/cluster-addon/csi/values.yaml +++ b/providers/openstack/scs/1-32/cluster-addon/csi/values.yaml @@ -1,8 +1,36 @@ openstack-cinder-csi: secret: enabled: true - name: cloud-config - create: false + name: csi-cloud-config + create: true + filename: cloud.conf + data: + cloud.conf: |- + [Global] + use-clouds = "true" + clouds-file = /etc/openstack/clouds.yaml + storageClass: + delete: + isDefault: true + csi: + plugin: + volumes: + - name: clouds-yaml + secret: + secretName: clouds-yaml + - name: cloud-conf + secret: + secretName: csi-cloud-config + volumeMounts: + - name: clouds-yaml + readOnly: true + mountPath: /etc/openstack + - name: cloud-conf + readOnly: true + mountPath: /etc/kubernetes + - name: cloud-conf + readOnly: true + mountPath: /etc/config nodeSelector: node-role.kubernetes.io/control-plane: "" tolerations: @@ -11,6 +39,3 @@ openstack-cinder-csi: effect: NoSchedule - key: node-role.kubernetes.io/control-plane effect: NoSchedule - storageClass: - delete: - isDefault: true diff --git a/providers/openstack/scs/1-32/cluster-addon/metrics-server/Chart.yaml b/providers/openstack/scs/1-32/cluster-addon/metrics-server/Chart.yaml index 005860b5..2ac06b1a 100644 --- a/providers/openstack/scs/1-32/cluster-addon/metrics-server/Chart.yaml +++ b/providers/openstack/scs/1-32/cluster-addon/metrics-server/Chart.yaml @@ -1,10 +1,10 @@ apiVersion: v2 -dependencies: - - alias: metrics-server - name: metrics-server - repository: https://kubernetes-sigs.github.io/metrics-server/ - version: 3.13.0 -description: Metrics Server -name: openstack-scs-1-32-cluster-addon type: application +description: Metrics Server +name: metrics-server version: v1 +dependencies: + - name: "metrics-server" + version: "3.13.0" + repository: "https://kubernetes-sigs.github.io/metrics-server/" + alias: "metrics-server" diff --git a/providers/openstack/scs/1-32/cluster-class/Chart.yaml b/providers/openstack/scs/1-32/cluster-class/Chart.yaml index 642bef98..fa6d1f5d 100644 --- a/providers/openstack/scs/1-32/cluster-class/Chart.yaml +++ b/providers/openstack/scs/1-32/cluster-class/Chart.yaml @@ -1,9 +1,9 @@ apiVersion: v2 -description: 'This chart installs and configures: +description: "This chart installs and configures: * Openstack scs Cluster Class - ' + " name: openstack-scs-1-32-cluster-class type: application version: v1 diff --git a/providers/openstack/scs/1-32/cluster-class/templates/cluster-class.yaml b/providers/openstack/scs/1-32/cluster-class/templates/cluster-class.yaml index feeba43b..c9131e4c 100644 --- a/providers/openstack/scs/1-32/cluster-class/templates/cluster-class.yaml +++ b/providers/openstack/scs/1-32/cluster-class/templates/cluster-class.yaml @@ -1,275 +1,377 @@ -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: {{ .Release.Name }}-{{ .Chart.Version }} spec: controlPlane: - ref: - apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + templateRef: + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 kind: KubeadmControlPlaneTemplate name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane machineInfrastructure: - ref: + templateRef: kind: OpenStackMachineTemplate apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane infrastructure: - ref: + templateRef: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackClusterTemplate name: {{ .Release.Name }}-{{ .Chart.Version }}-cluster workers: machineDeployments: - class: default-worker - template: - bootstrap: - ref: - apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 - kind: KubeadmConfigTemplate - name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker - infrastructure: - ref: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate - name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker + bootstrap: + templateRef: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 + kind: KubeadmConfigTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker + infrastructure: + templateRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker variables: - - name: external_id + # Image variables + - name: imageName required: false schema: openAPIV3Schema: type: string - example: "ebfe5546-f09f-4f42-ab54-094e457d42ec" - format: "uuid4" - description: "ExternalNetworkID is the ID of an external OpenStack Network. This is necessary to get public internet to the VMs." - - name: controller_flavor + description: | + The base name of the OpenStack image used for provisioning servers. + If `imageIsOrc` is enabled, this name refers to an ORC image resource. + If `imageIsOrc` is disabled, the name is used to filter images available in the OpenStack project. In this case, the specified image must already exist within the project. + If `imageAddVersion` is enabled, the Kubernetes version will be appended to form the complete image name (e.g., imageName-v1.32.5) + default: {{ .Values.variables.imageName | quote }} + - name: imageIsOrc required: false schema: openAPIV3Schema: - type: string - default: "SCS-2V-4-20s" - example: "SCS-2V-4-20s" - description: "OpenStack instance flavor for control-plane nodes." - - name: worker_flavor + type: boolean + description: | + Indicates whether the image name refers to an ORC image resource. + If set to true (default), the `imageName` is interpreted as a reference to an ORC image. + If set to false, the `imageName` is used to filter images in the OpenStack project instead. + default: {{ .Values.variables.imageIsOrc }} + - name: imageAddVersion required: false schema: openAPIV3Schema: - type: string - default: "SCS-2V-4" - example: "SCS-2V-4" - description: "OpenStack instance flavor for worker nodes." - - name: controller_root_disk + type: boolean + description: | + Add a suffix with the Kubernetes version to the imageName. E.g. imageName-v1.32.5. + default: {{ .Values.variables.imageAddVersion }} + # Network variables + - name: networkExternalID required: false schema: openAPIV3Schema: - type: integer - minimum: 1 - example: 25 - description: "Root disk size in GiB for control-plane nodes. OpenStack volume will be created and used instead of an ephemeral disk defined in flavor. Should only be used for the diskless flavors." - - name: worker_root_disk + type: string + example: "ebfe5546-f09f-4f42-ab54-094e457d42ec" + format: "uuid4" + description: |- + ID of an external OpenStack network. Required when multiple + external networks exist and VMs need public internet access. + - name: networkMTU required: false schema: openAPIV3Schema: type: integer - minimum: 1 - default: 25 - example: 25 - description: "Root disk size in GiB for worker nodes. OpenStack volume will be created and used instead of an ephemeral disk defined in flavor. Should be used for the diskless flavors." - - name: openstack_security_groups + example: 1500 + description: "MTU for the private cluster network. Set this to avoid fragmentation issues." + - name: dnsNameservers required: false schema: openAPIV3Schema: type: array - default: [] - example: ["security-group-1"] - description: "The names of the security groups to assign to the instance" + description: "DNS nameservers for the cluster subnet. Only used when a new network/subnet is created." + default: {{ .Values.variables.dnsNameservers | toJson }} + example: ["9.9.9.9", "149.112.112.112"] items: type: string - - name: cloud_name + - name: nodeCIDR required: false schema: openAPIV3Schema: type: string - default: "openstack" - example: "openstack" - description: "The name of the cloud to use from the clouds secret" - - name: secret_name + format: "cidr" + default: {{ .Values.variables.nodeCIDR | quote }} + example: "10.8.0.0/20" + description: |- + CIDR for the cluster subnet. CAPO will create a network, subnet, + and router. Leave empty to skip network creation. + # Machine configuration + # These apply to all nodes by default. Use topology.controlPlane.variables.overrides + # or topology.workers.machineDeployments[].variables.overrides to differentiate. + - name: flavor required: false schema: openAPIV3Schema: type: string - default: "openstack" - example: "openstack" - description: "The name of the clouds secret" - - name: controller_server_group_id + default: {{ .Values.variables.flavor | quote }} + example: "SCS-2V-4" + description: |- + OpenStack instance flavor. Applies to all nodes by default. + Override per control plane or worker via topology variables overrides. + - name: rootDisk + required: false + schema: + openAPIV3Schema: + type: integer + minimum: 0 + example: 50 + default: {{ .Values.variables.rootDisk }} + description: |- + Root disk size in GiB. OpenStack volume will be created and used + instead of an ephemeral disk defined in the flavor. + Set to 0 to use the flavor's ephemeral disk. + - name: serverGroupID required: false schema: openAPIV3Schema: type: string - default: "" + default: {{ .Values.variables.serverGroupID | quote }} example: "3adf4e92-bb33-4e44-8ad3-afda9dfe8ec3" - description: "The server group to assign the control plane nodes to." - - name: worker_server_group_id + description: "Server group for anti-affinity placement. Override per CP/worker via topology." + - name: additionalBlockDevices required: false schema: openAPIV3Schema: - type: string - default: "" - example: "869fe071-1e56-46a9-9166-47c9f228e297" - description: "The server group to assign the worker nodes to." - - name: ssh_key + type: array + default: {{ .Values.variables.additionalBlockDevices | toJson }} + description: |- + Additional block devices (Cinder volumes) to attach to nodes. + Override per CP/worker via topology. + items: + type: object + properties: + name: + type: string + sizeGiB: + type: integer + default: 20 + type: + type: string + default: "__DEFAULT__" + required: ["name"] + # Cluster-level (control plane only, managed by OpenStackClusterTemplate) + - name: controlPlaneAvailabilityZones + required: false + schema: + openAPIV3Schema: + type: array + default: {{ .Values.variables.controlPlaneAvailabilityZones | toJson }} + example: ["nova"] + description: "Availability zones for control plane nodes (OpenStack cluster-level setting)." + items: + type: string + - name: controlPlaneOmitAvailabilityZone + required: false + schema: + openAPIV3Schema: + type: boolean + default: {{ .Values.variables.controlPlaneOmitAvailabilityZone }} + description: |- + Omit availability zone when creating control plane nodes, letting the + Nova scheduler decide based on other scheduling constraints. + # Access management + - name: sshKeyName required: false schema: openAPIV3Schema: type: string - default: "" + default: {{ .Values.variables.sshKeyName | quote }} example: "capi-keypair" - description: "The ssh key to inject in the nodes." - - name: apiserver_loadbalancer + description: "SSH key to inject into all nodes (for debugging)." + - name: securityGroups required: false schema: openAPIV3Schema: - type: string - default: "octavia-amphora" - example: "none, octavia-amphora, octavia-ovn" - description: | - "In this cluster-stack we have two kind of loadbalancers. Each of them has its own configuration variable. This setting here is to configure the loadbalancer that is placed in front of the apiserver. - You can choose from 2 options: - - none: - No loadbalancer solution will be deployed - - octavia-amphora: - (default) Uses openstack's loadbalancer service (provider:amphora) - - octavia-ovn: - Uses openstack's loadbalancer service (provider:ovn) - - name: dns_nameservers + type: array + default: {{ .Values.variables.securityGroups | toJson }} + example: ["security-group-1"] + description: |- + Extra security groups by name for all nodes. + Ignored if securityGroupIDs is set. Override per CP/worker via topology. + items: + type: string + - name: securityGroupIDs required: false schema: openAPIV3Schema: + format: "uuid4" type: array - description: | - "DNSNameservers is the list of nameservers for the OpenStack Subnet - being created. Set this value when you need to create a new network/subnet - while the access through DNS is required." - default: ["5.1.66.255", "185.150.99.255"] - example: ["5.1.66.255", "185.150.99.255"] + default: {{ .Values.variables.securityGroupIDs | toJson }} + example: ["9ae2f488-30a3-4629-bd51-07acb8eb4278"] + description: |- + Extra security groups by UUID for all nodes. + Takes precedence over securityGroups. Override per CP/worker via topology. items: type: string - - name: node_cidr + - name: identityRef required: false schema: openAPIV3Schema: - type: string - format: "cidr" - default: "10.8.0.0/20" - example: "10.8.0.0/20" - description: | - "NodeCIDR is the OpenStack Subnet to be created. Cluster actuator - will create a network, a subnet with NodeCIDR, and a router - connected to this subnet. If you leave this empty, no network will be created." + type: object + default: {{ .Values.variables.identityRef | toJson }} + properties: + name: + type: string + example: "openstack" + default: {{ .Values.variables.identityRef.name | quote }} + description: "The name of the secret that carries the OpenStack clouds.yaml" + cloudName: + type: string + example: "openstack" + default: {{ .Values.variables.identityRef.cloudName | quote }} + description: "The name of the cloud to use from the clouds.yaml" + # API server + - name: disableAPIServerFloatingIP + required: false + schema: + openAPIV3Schema: + type: boolean + default: {{ .Values.variables.disableAPIServerFloatingIP }} + description: "Disable the floating IP on the API server load balancer." - name: certSANs required: false schema: openAPIV3Schema: type: array - default: [] + default: {{ .Values.variables.certSANs | toJson }} example: ["mydomain.example"] - description: "CertSANs sets extra Subject Alternative Names for the API Server signing cert." + description: "Extra Subject Alternative Names for the API server TLS certificate." + items: + type: string + - name: apiServerLoadBalancer + required: false + schema: + openAPIV3Schema: + type: string + default: {{ .Values.variables.apiServerLoadBalancer | quote }} + example: "octavia-ovn" + description: |- + Load balancer in front of the API server. + Options: none, octavia-amphora, octavia-ovn (default). + - name: apiServerAllowedCIDRs + required: false + schema: + openAPIV3Schema: + type: array + example: ["192.168.10.0/24"] + description: |- + Restrict access to the API server to these CIDRs (network-level ACL). + Requires amphora as load balancer provider (CAPO >= v2.12). + Ensure the management cluster's outgoing IP is included, + otherwise CAPO cannot reconcile the workload cluster. items: type: string - - name: oidc_config + - name: oidcConfig required: false schema: openAPIV3Schema: type: object properties: - client_id: + clientID: type: string example: "kubectl" description: "A client id that all tokens must be issued for." - issuer_url: + issuerURL: type: string example: "https://dex.k8s.scs.community" - description: "URL of the provider that allows the API server to -dis cover public signing keys. Only URLs that use the https:// scheme are -acc epted. This is typically the provider's discovery URL, changed to have an -emp ty path" - username_claim: + description: >- + URL of the provider that allows the API server to + discover public signing keys. Only URLs that use the https:// scheme are + accepted. This is typically the provider's discovery URL, changed to have an + empty path. + usernameClaim: type: string example: "preferred_username" - default: "preferred_username" - description: "JWT claim to use as the user name. By default sub, -whi ch is expected to be a unique identifier of the end user. Admins can choose -oth er claims, such as email or name, depending on their provider. However, -cla ims other than email will be prefixed with the issuer URL to prevent naming -cla shes with other plugins." - groups_claim: + default: {{ .Values.variables.oidcConfig.usernameClaim | quote }} + description: >- + JWT claim to use as the user name. By default sub, + which is expected to be a unique identifier of the end user. Admins can choose + other claims, such as email or name, depending on their provider. However, + claims other than email will be prefixed with the issuer URL to prevent naming + clashes with other plugins. + groupsClaim: type: string example: "groups" - default: "groups" - description: "JWT claim to use as the user's group. If the claim -is present it must be an array of strings." - username_prefix: + default: {{ .Values.variables.oidcConfig.groupsClaim | quote }} + description: "JWT claim to use as the user's group. If the claim is present it must be an array of strings." + usernamePrefix: type: string example: "oidc:" - default: "oidc:" - description: "Prefix prepended to username claims to prevent -cla shes with existing names (such as system: users). For example, the value -oid c: will create usernames like oidc:jane.doe. If this flag isn't provided and ---o idc-username-claim is a value other than email the prefix defaults to ( -Iss uer URL )# where ( Issuer URL ) is the value of --oidc-issuer-url. The value -- c an be used to disable all prefixing." - groups_prefix: + default: {{ .Values.variables.oidcConfig.usernamePrefix | quote }} + description: >- + Prefix prepended to username claims to prevent + clashes with existing names (such as system: users). For example, the value + oidc: will create usernames like oidc:jane.doe. If this flag isn't provided and + --oidc-username-claim is a value other than email the prefix defaults to ( + Issuer URL )# where ( Issuer URL ) is the value of --oidc-issuer-url. The value + - can be used to disable all prefixing. + groupsPrefix: type: string example: "oidc:" - default: "oidc:" - description: "Prefix prepended to group claims to prevent clashes -wit h existing names (such as system: groups). For example, the value oidc: will -cre ate group names like oidc:engineering and oidc:infra." - - name: network_mtu - required: false - schema: - openAPIV3Schema: - type: integer - example: 1500 - description: "NetworkMTU sets the maximum transmission unit (MTU) value to address fragmentation for the private network ID." - - name: controlPlaneAvailabilityZones + default: {{ .Values.variables.oidcConfig.groupsPrefix | quote }} + description: >- + Prefix prepended to group claims to prevent clashes + with existing names (such as system: groups). For example, the value oidc: will + create group names like oidc:engineering and oidc:infra. + # Container runtime + - name: registryMirrors required: false schema: openAPIV3Schema: type: array - example: ["nova"] - description: "ControlPlaneAvailabilityZones is the set of availability zones which control plane machines may be deployed to." + default: {{ .Values.variables.registryMirrors | toJson }} + description: "Registry mirrors for upstream container registries. Configures both containerd and CRI-O to pull through a mirror." items: - type: string - - name: controlPlaneOmitAvailabilityZone - required: false - schema: - openAPIV3Schema: - type: boolean - example: true - description: "ControlPlaneOmitAvailabilityZone causes availability zone to be omitted when creating control plane nodes, allowing the Nova scheduler to make a decision on which availability zone to use based on other scheduling constraints." + type: object + properties: + hostnameUpstream: + type: string + example: "docker.io" + description: "The hostname of the upstream registry." + urlUpstream: + type: string + example: "https://registry-1.docker.io" + description: "The server URL of the upstream registry." + urlMirror: + type: string + example: "https://registry.example.com/v2/dockerhub" + description: "The URL of the mirror registry." + certMirror: + type: string + example: "" + description: "TLS certificate of the mirror in PEM format (optional)." + # + # Patches + # patches: - - name: k8s_version - description: "Sets the openstack node image for workers and the controlplane to the cluster-api image with the version mentioned in spec.topology.version." + # + # Patches for OpenStackClusterTemplate resource. + # + - name: apiServerLoadBalancerOctaviaAmphora + description: "Takes care of the patches that should be applied when variable apiServerLoadBalancer is set to octavia-amphora." + enabledIf: {{ `'{{ eq .apiServerLoadBalancer "octavia-amphora" }}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate + kind: OpenStackClusterTemplate matchResources: - controlPlane: true - machineDeploymentClass: - names: - - default-worker + infrastructureCluster: true jsonPatches: - - op: replace - path: "/spec/template/spec/image/filter/name" - valueFrom: - template: ubuntu-capi-image-{{ `{{ .builtin.cluster.topology.version }}` }} - - name: apiserver_loadbalancer_octavia-amphora - description: "Takes care of the patches that should be applied when variable apiserver_loadbalancer is set to octavia-amphora." - enabledIf: {{ `'{{ eq .apiserver_loadbalancer "octavia-amphora" }}'` }} + - op: replace + path: "/spec/template/spec/apiServerLoadBalancer/enabled" + value: true + - op: add + path: "/spec/template/spec/apiServerLoadBalancer/provider" + value: "amphora" + - name: apiServerLoadBalancerOctaviaOVN + description: "Takes care of the patches that should be applied when variable apiServerLoadBalancer is set to octavia-ovn." + enabledIf: {{ `'{{ eq .apiServerLoadBalancer "octavia-ovn" }}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 @@ -277,15 +379,15 @@ cre ate group names like oidc:engineering and oidc:infra." matchResources: infrastructureCluster: true jsonPatches: - - op: replace - path: "/spec/template/spec/apiServerLoadBalancer/enabled" - value: true - - op: add - path: "/spec/template/spec/apiServerLoadBalancer/provider" - value: "amphora" - - name: apiserver_loadbalancer_octavia-ovn - description: "Takes care of the patches that should be applied when variable apiserver_loadbalancer is set to octavia-ovn." - enabledIf: {{ `'{{ eq .apiserver_loadbalancer "octavia-ovn" }}'` }} + - op: replace + path: "/spec/template/spec/apiServerLoadBalancer/enabled" + value: true + - op: add + path: "/spec/template/spec/apiServerLoadBalancer/provider" + value: "ovn" + - name: apiServerAllowedCIDRs + description: "Restricts API server access to the given CIDRs (requires amphora LB)." + enabledIf: {{ `'{{ and .apiServerAllowedCIDRs (eq .apiServerLoadBalancer "octavia-amphora") }}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 @@ -293,79 +395,71 @@ cre ate group names like oidc:engineering and oidc:infra." matchResources: infrastructureCluster: true jsonPatches: - - op: replace - path: "/spec/template/spec/apiServerLoadBalancer/enabled" - value: true - - op: add - path: "/spec/template/spec/apiServerLoadBalancer/provider" - value: "ovn" - - name: controller_flavor - description: "Sets the openstack instance flavor for the KubeadmControlPlane." - enabledIf: {{ `'{{ ne .controller_flavor "" }}'` }} + - op: add + path: "/spec/template/spec/apiServerLoadBalancer/allowedCIDRs" + valueFrom: + variable: apiServerAllowedCIDRs + - name: networkExternalID + description: "Sets the ID of an external OpenStack Network. This is necessary to get public internet to the VMs." + enabledIf: {{ `'{{ if .networkExternalID }}true{{end}}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate + kind: OpenStackClusterTemplate matchResources: - controlPlane: true + infrastructureCluster: true jsonPatches: - - op: replace - path: "/spec/template/spec/flavor" + - op: add + path: "/spec/template/spec/externalNetwork" + value: {} + - op: add + path: "/spec/template/spec/externalNetwork/id" valueFrom: - variable: controller_flavor - - name: worker_flavor - description: "Sets the openstack instance flavor for the worker nodes." - enabledIf: {{ `'{{ ne .worker_flavor "" }}'` }} + variable: networkExternalID + - name: networkMTU + description: "Sets the network MTU when variable networkMTU exist in cluster resource." + enabledIf: {{ `'{{ if .networkMTU }}true{{end}}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate + kind: OpenStackClusterTemplate matchResources: - controlPlane: false - machineDeploymentClass: - names: - - default-worker + infrastructureCluster: true jsonPatches: - - op: replace - path: "/spec/template/spec/flavor" + - op: add + path: "/spec/template/spec/networkMTU" valueFrom: - variable: worker_flavor - - name: controller_root_disk - description: "Sets the root disk size in GiB for control-plane nodes." - enabledIf: {{ `"{{ if .controller_root_disk }}true{{end}}"` }} + variable: networkMTU + - name: controlPlaneAvailabilityZones + description: "Sets the availability zones which control plane machines may be deployed to." + enabledIf: {{ `'{{ if .controlPlaneAvailabilityZones }}true{{end}}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate + kind: OpenStackClusterTemplate matchResources: - controlPlane: true + infrastructureCluster: true jsonPatches: - op: add - path: "/spec/template/spec/rootVolume" + path: "/spec/template/spec/controlPlaneAvailabilityZones" valueFrom: - template: | - sizeGiB: {{"{{"}} .controller_root_disk {{"}}"}} - - name: worker_root_disk - description: "Sets the root disk size in GiB for worker nodes." - enabledIf: {{ `"{{ if .worker_root_disk }}true{{end}}"` }} + variable: controlPlaneAvailabilityZones + - name: controlPlaneOmitAvailabilityZone + description: "Causes availability zone to be omitted when creating control plane nodes." + enabledIf: {{ `'{{ if .controlPlaneOmitAvailabilityZone }}true{{end}}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate + kind: OpenStackClusterTemplate matchResources: - controlPlane: false - machineDeploymentClass: - names: - - default-worker + infrastructureCluster: true jsonPatches: - op: add - path: "/spec/template/spec/rootVolume" + path: "/spec/template/spec/controlPlaneOmitAvailabilityZone" valueFrom: - template: | - sizeGiB: {{"{{"}} .worker_root_disk {{"}}"}} - - name: external_id - description: "Sets the ID of an external OpenStack Network. This is necessary to get public internet to the VMs." - enabledIf: {{ `"{{ if .external_id }}true{{end}}"` }} + variable: controlPlaneOmitAvailabilityZone + - name: identityRef + description: "Sets the OpenStack identity reference." definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 @@ -373,16 +467,16 @@ cre ate group names like oidc:engineering and oidc:infra." matchResources: infrastructureCluster: true jsonPatches: - - op: add - path: "/spec/template/spec/externalNetwork" - value: {} - - op: add - path: "/spec/template/spec/externalNetwork/id" - valueFrom: - variable: external_id - - name: network_mtu - description: "Sets the network MTU when variable network_mtu exist in cluster resource." - enabledIf: {{ `"{{ if .network_mtu }}true{{end}}"` }} + - op: add + path: /spec/template/spec/identityRef + valueFrom: + variable: identityRef + - name: nodeCIDRSubnet + description: |- + Sets the NodeCIDR for the OpenStack Subnet to be created. + Cluster actuator will create a network, a subnet with NodeCIDR, + and a router connected to this subnet. + enabledIf: {{ `'{{ if .nodeCIDR }}true{{end}}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 @@ -391,12 +485,17 @@ cre ate group names like oidc:engineering and oidc:infra." infrastructureCluster: true jsonPatches: - op: add - path: "/spec/template/spec/networkMTU" + path: "/spec/template/spec/managedSubnets" valueFrom: - variable: network_mtu - - name: controlPlaneAvailabilityZones - description: "Sets the availability zones which control plane machines may be deployed to." - enabledIf: {{ `"{{ if .controlPlaneAvailabilityZones }}true{{end}}"` }} + template: | + - cidr: '{{ `{{ .nodeCIDR }}` }}' + dnsNameservers: + {{ `{{- range .dnsNameservers }}` }} + - {{ `{{ . }}` }} + {{ `{{- end }}` }} + - name: disableAPIServerFloatingIP + description: "DisableAPIServerFloatingIP controls whether a floating IP should be attached to the API server." + enabledIf: {{ `"{{ if .disableAPIServerFloatingIP }}true{{end}}"` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 @@ -405,54 +504,73 @@ cre ate group names like oidc:engineering and oidc:infra." infrastructureCluster: true jsonPatches: - op: add - path: "/spec/template/spec/controlPlaneAvailabilityZones" + path: "/spec/template/spec/disableAPIServerFloatingIP" valueFrom: - variable: controlPlaneAvailabilityZones - - name: controlPlaneOmitAvailabilityZone - description: "Causes availability zone to be omitted when creating control plane nodes." - enabledIf: {{ `"{{ if .controlPlaneOmitAvailabilityZone }}true{{end}}"` }} + variable: disableAPIServerFloatingIP + # + # Patches for OpenStackMachineTemplate resources (image). + # Image patches must stay separate because they use different builtin variables: + # - .builtin.controlPlane.version for CP + # - .builtin.machineDeployment.version for workers + # + - name: controlPlaneImage + description: "Sets the OpenStack image for control plane nodes." definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackClusterTemplate + kind: OpenStackMachineTemplate matchResources: - infrastructureCluster: true + controlPlane: true jsonPatches: - - op: add - path: "/spec/template/spec/controlPlaneOmitAvailabilityZone" - valueFrom: - variable: controlPlaneOmitAvailabilityZone - - name: openstack_security_groups - description: "Sets the list of the openstack security groups for the worker and the controlplane instances." - enabledIf: {{ `"{{ if .openstack_security_groups }}true{{end}}"` }} + - op: add + path: /spec/template/spec/image + valueFrom: + template: | + {{ `{{ if .imageIsOrc }}imageRef{{ else }}filter{{ end }}` }}: + name: {{ `{{ .imageName }}{{ if .imageAddVersion }}-{{ .builtin.controlPlane.version }}{{ end }}` }} + - name: workerImage + description: "Sets the OpenStack image for worker nodes." definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate matchResources: - controlPlane: true machineDeploymentClass: names: - default-worker jsonPatches: - - op: add - path: "/spec/template/spec/securityGroups" - valueFrom: - template: {{ `"[ {{ range .openstack_security_groups }} { filter: { name: {{ . }}}}, {{ end }} ]"` }} - - name: cloud_name - description: "Sets the name of the cloud to use from the clouds secret." - enabledIf: {{ `'{{ ne .cloud_name "" }}'` }} + - op: add + path: /spec/template/spec/image + valueFrom: + template: | + {{ `{{ if .imageIsOrc }}imageRef{{ else }}filter{{ end }}` }}: + name: {{ `{{ .imageName }}{{ if .imageAddVersion }}-{{ .builtin.machineDeployment.version }}{{ end }}` }} + # + # Unified machine patches — target both CP and workers. + # Users can override per CP/worker via topology.controlPlane.variables.overrides + # and topology.workers.machineDeployments[].variables.overrides. + # + - name: flavor + description: "Sets the OpenStack instance flavor for all nodes." + enabledIf: {{ `'{{ ne .flavor "" }}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackClusterTemplate + kind: OpenStackMachineTemplate matchResources: - infrastructureCluster: true + controlPlane: true + machineDeploymentClass: + names: + - default-worker jsonPatches: - - op: replace - path: "/spec/template/spec/identityRef/cloudName" - valueFrom: - variable: cloud_name + - op: replace + path: "/spec/template/spec/flavor" + valueFrom: + variable: flavor + - name: rootDisk + description: "Sets the root disk size in GiB. 0 means use ephemeral disk from flavor." + enabledIf: {{ `'{{ if .rootDisk }}true{{end}}'` }} + definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate @@ -462,24 +580,33 @@ cre ate group names like oidc:engineering and oidc:infra." names: - default-worker jsonPatches: - - op: replace - path: "/spec/template/spec/identityRef/cloudName" - valueFrom: - variable: cloud_name - - name: secret_name - description: "Sets the name of the clouds secret." - enabledIf: {{ `'{{ ne .secret_name "" }}'` }} + - op: add + path: "/spec/template/spec/rootVolume" + valueFrom: + template: | + sizeGiB: {{ `{{ .rootDisk }}` }} + - name: serverGroupID + description: "Sets the server group for anti-affinity." + enabledIf: {{ `'{{ ne .serverGroupID "" }}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackClusterTemplate + kind: OpenStackMachineTemplate matchResources: - infrastructureCluster: true + controlPlane: true + machineDeploymentClass: + names: + - default-worker jsonPatches: - - op: replace - path: "/spec/template/spec/identityRef/name" - valueFrom: - variable: secret_name + - op: add + path: "/spec/template/spec/serverGroup" + valueFrom: + template: | + id: {{ `{{ .serverGroupID }}` }} + - name: additionalBlockDevices + description: "Attaches additional Cinder volumes to nodes." + enabledIf: {{ `'{{ if .additionalBlockDevices }}true{{end}}'` }} + definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate @@ -489,46 +616,59 @@ cre ate group names like oidc:engineering and oidc:infra." names: - default-worker jsonPatches: - - op: replace - path: "/spec/template/spec/identityRef/name" - valueFrom: - variable: secret_name - - name: controller_server_group_id - description: "Sets the server group to assign the control plane nodes to." - enabledIf: {{ `'{{ ne .controller_server_group_id "" }}'` }} + - op: add + path: /spec/template/spec/additionalBlockDevices + valueFrom: + template: | + {{ `{{- range .additionalBlockDevices }}` }} + - name: {{ `{{ .name }}` }} + sizeGiB: {{ `{{ .sizeGiB }}` }} + storage: + type: Volume + volume: + type: {{ `{{ .type }}` }} + {{ `{{- end }}` }} + # + # Access patches — target both CP and workers. + # securityGroupIDs takes precedence over securityGroups (last patch wins). + # + - name: securityGroups + description: "Sets security groups by name for all nodes." + enabledIf: {{ `'{{ if .securityGroups }}true{{end}}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate matchResources: controlPlane: true + machineDeploymentClass: + names: + - default-worker jsonPatches: - - op: add - path: "/spec/template/spec/serverGroup" - valueFrom: - template: | - id: {{"{{"}} .controller_server_group_id {{"}}"}} - - name: worker_server_group_id - description: "Sets the server group to assign the worker nodes to." - enabledIf: {{ `'{{ ne .worker_server_group_id "" }}'` }} + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .securityGroups }} { filter: { name: {{ . }}}}, {{ end }} ]'` }} + - name: securityGroupIDs + description: "Sets security groups by UUID for all nodes. Takes precedence over securityGroups." + enabledIf: {{ `'{{ if .securityGroupIDs }}true{{end}}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate matchResources: - controlPlane: false + controlPlane: true machineDeploymentClass: names: - default-worker jsonPatches: - - op: add - path: "/spec/template/spec/serverGroup" - valueFrom: - template: | - id: {{"{{"}} .worker_server_group_id {{"}}"}} - - name: ssh_key - description: "Sets the ssh key to inject in the nodes." - enabledIf: {{ `'{{ ne .ssh_key "" }}'` }} + - op: add + path: "/spec/template/spec/securityGroups" + valueFrom: + template: {{ `'[ {{ range .securityGroupIDs }} { id: {{ . }} }, {{ end }} ]'` }} + - name: sshKeyName + description: "Sets the SSH key to inject into all nodes." + enabledIf: {{ `'{{ ne .sshKeyName "" }}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 @@ -539,74 +679,185 @@ cre ate group names like oidc:engineering and oidc:infra." names: - default-worker jsonPatches: - - op: add - path: "/spec/template/spec/sshKeyName" - valueFrom: - variable: ssh_key + - op: add + path: "/spec/template/spec/sshKeyName" + valueFrom: + variable: sshKeyName + # - name: certSANs - description: "CertSANs sets extra Subject Alternative Names for the API Server signing cert." - enabledIf: {{ `"{{ if .certSANs }}true{{end}}"` }} + description: "certSANs sets extra Subject Alternative Names for the API Server signing cert." + enabledIf: {{ `'{{ if .certSANs }}true{{end}}'` }} definitions: - selector: - apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 kind: KubeadmControlPlaneTemplate matchResources: controlPlane: true jsonPatches: - - op: add - path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/certSANs" - valueFrom: - variable: certSANs - - name: oidc_config + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/certSANs" + valueFrom: + variable: certSANs + - name: oidcConfig description: "Configure API Server to use external authentication service." - enabledIf: {{ `"{{ if and .oidc_config .oidc_config.client_id .oidc_config.issuer_url }}true{{end}}"` }} + enabledIf: {{ `'{{ if and .oidcConfig .oidcConfig.clientID .oidcConfig.issuerURL }}true{{end}}'` }} definitions: - selector: - apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 + kind: KubeadmControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" + valueFrom: + template: | + name: oidc-client-id + value: {{ `'{{ .oidcConfig.clientID }}'` }} + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" + valueFrom: + template: | + name: oidc-issuer-url + value: {{ `'{{ .oidcConfig.issuerURL }}'` }} + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" + valueFrom: + template: | + name: oidc-username-claim + value: {{ `'{{ .oidcConfig.usernameClaim }}'` }} + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" + valueFrom: + template: | + name: oidc-groups-claim + value: {{ `'{{ .oidcConfig.groupsClaim }}'` }} + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" + valueFrom: + template: | + name: oidc-username-prefix + value: {{ `'{{ .oidcConfig.usernamePrefix }}'` }} + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" + valueFrom: + template: | + name: oidc-groups-prefix + value: {{ `'{{ .oidcConfig.groupsPrefix }}'` }} + # + # Registry mirror patches + # + - name: registryMirrorsControlPlane + description: "Configure registry mirrors on control plane nodes (containerd + CRI-O)." + enabledIf: {{ `'{{ if .registryMirrors }}true{{end}}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 kind: KubeadmControlPlaneTemplate matchResources: controlPlane: true jsonPatches: - op: add - path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-client-id" - valueFrom: - variable: oidc_config.client_id - - op: add - path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-issuer-url" - valueFrom: - variable: oidc_config.issuer_url - - op: add - path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-username-claim" - valueFrom: - variable: oidc_config.username_claim - - op: add - path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-groups-claim" - valueFrom: - variable: oidc_config.groups_claim - - op: add - path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-username-prefix" + path: "/spec/template/spec/kubeadmConfigSpec/files" valueFrom: - variable: oidc_config.username_prefix + template: | + {{ `{{- range $r := .registryMirrors }}` }} + - content: | + server = "{{ `{{ $r.urlUpstream }}` }}" + [host."{{ `{{ $r.urlMirror }}` }}"] + capabilities = ["pull","resolve"] + override_path = true + owner: root:root + path: /etc/containerd/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/hosts.toml + permissions: "0644" + - content: | + [[registry]] + prefix = "{{ `{{ $r.hostnameUpstream }}` }}" + location = "{{ `{{ $r.hostnameUpstream }}` }}" + [[registry.mirror]] + location = "{{ `{{ $r.urlMirror }}` }}" + owner: root:root + path: /etc/containers/registries.conf.d/50-mirror-{{ `{{ $r.hostnameUpstream }}` }}.conf + permissions: "0644" + {{ `{{- if $r.certMirror }}` }} + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containerd/certs/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containers/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + {{ `{{- end }}` }} + {{ `{{- end }}` }} + - path: /etc/kube-proxy-config.yaml + owner: root:root + permissions: "0644" + content: | + --- + apiVersion: kubeproxy.config.k8s.io/v1alpha1 + kind: KubeProxyConfiguration + metricsBindAddress: "0.0.0.0:10249" + + - path: /etc/kube-proxy-patch.sh + owner: root:root + permissions: "0755" + content: | + #!/usr/bin/env bash + set -euo pipefail + dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + kubeadm_file="/etc/kubeadm.yml" + [[ -f ${kubeadm_file} ]] || kubeadm_file="/run/kubeadm/kubeadm.yaml" + [[ -f ${kubeadm_file} ]] || exit 0 + [[ -f ${dir}/kube-proxy-config.yaml ]] || exit 0 + cat "${dir}/kube-proxy-config.yaml" >> "${kubeadm_file}" + rm -f "${dir}/kube-proxy-config.yaml" + echo success > /tmp/kube-proxy-patch - op: add - path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-groups-prefix" - valueFrom: - variable: oidc_config.groups_prefix - - name: subnet - description: "Sets the NodeCIDR for the OpenStack Subnet to be created. Cluster actuator will create a network, a subnet with NodeCIDR, and a router connected to this subnet." - enabledIf: {{ `"{{ if .node_cidr }}true{{end}}"` }} + path: "/spec/template/spec/kubeadmConfigSpec/preKubeadmCommands/-" + value: "bash /etc/kube-proxy-patch.sh" + - name: registryMirrorsWorker + description: "Configure registry mirrors on worker nodes (containerd + CRI-O)." + enabledIf: {{ `'{{ if .registryMirrors }}true{{end}}'` }} definitions: - selector: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackClusterTemplate + apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 + kind: KubeadmConfigTemplate matchResources: - infrastructureCluster: true + machineDeploymentClass: + names: + - default-worker jsonPatches: - op: add - path: "/spec/template/spec/managedSubnets" + path: "/spec/template/spec/files" valueFrom: template: | - - cidr: '{{"{{"}} .node_cidr {{"}}"}}' - dnsNameservers: - {{`{{- range .dns_nameservers }}`}} - - {{`{{ . }}`}} - {{`{{- end }}`}} + {{ `{{- range $r := .registryMirrors }}` }} + - content: | + server = "{{ `{{ $r.urlUpstream }}` }}" + [host."{{ `{{ $r.urlMirror }}` }}"] + capabilities = ["pull","resolve"] + override_path = true + owner: root:root + path: /etc/containerd/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/hosts.toml + permissions: "0644" + - content: | + [[registry]] + prefix = "{{ `{{ $r.hostnameUpstream }}` }}" + location = "{{ `{{ $r.hostnameUpstream }}` }}" + [[registry.mirror]] + location = "{{ `{{ $r.urlMirror }}` }}" + owner: root:root + path: /etc/containers/registries.conf.d/50-mirror-{{ `{{ $r.hostnameUpstream }}` }}.conf + permissions: "0644" + {{ `{{- if $r.certMirror }}` }} + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containerd/certs/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containers/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + {{ `{{- end }}` }} + {{ `{{- end }}` }} diff --git a/providers/openstack/scs/1-32/cluster-class/templates/image.yaml b/providers/openstack/scs/1-32/cluster-class/templates/image.yaml deleted file mode 100644 index 2850e378..00000000 --- a/providers/openstack/scs/1-32/cluster-class/templates/image.yaml +++ /dev/null @@ -1,30 +0,0 @@ -apiVersion: openstack.k-orc.cloud/v1alpha1 -kind: Image -metadata: - name: '{{ .Values.images.worker.name }}' -spec: - cloudCredentialsRef: - cloudName: '{{ .Values.identityRef.cloudName }}' - secretName: '{{ .Values.identityRef.name }}' - managementPolicy: managed - resource: - content: - diskFormat: qcow2 - download: - hash: - algorithm: sha256 - value: 8bfad5cb331480a7530952d8d6ff41155c28b2941f8be7a3f0318ebd1569cf83 - url: https://swift.services.a.regiocloud.tech/swift/v1/AUTH_b182637428444b9aa302bb8d5a5a418c/openstack-k8s-capi-images/ubuntu-2204-kube-v1.32/ubuntu-2204-kube-v1.32.5.qcow2 - properties: - architecture: x86_64 - hardware: - diskBus: scsi - qemuGuestAgent: true - rngModel: virtio - scsiModel: virtio-scsi - vifModel: virtio - minDiskGB: 20 - minMemoryMB: 2048 - operatingSystem: - distro: ubuntu - version: '22.04' diff --git a/providers/openstack/scs/1-32/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml b/providers/openstack/scs/1-32/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml index 2e9ff290..732d78b3 100644 --- a/providers/openstack/scs/1-32/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml +++ b/providers/openstack/scs/1-32/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml @@ -1,4 +1,4 @@ -apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 kind: KubeadmConfigTemplate metadata: name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker @@ -8,6 +8,8 @@ spec: joinConfiguration: nodeRegistration: kubeletExtraArgs: - cloud-provider: external - provider-id: openstack:///'{{"{{"}} instance_id {{"}}"}}' - name: '{{"{{"}} local_hostname {{"}}"}}' + - name: cloud-provider + value: external + - name: provider-id + value: 'openstack:///{{ `{{ instance_id }}` }}' + name: '{{ `{{ local_hostname }}` }}' diff --git a/providers/openstack/scs/1-32/cluster-class/templates/kubeadm-control-plane-template.yaml b/providers/openstack/scs/1-32/cluster-class/templates/kubeadm-control-plane-template.yaml index 14e46f18..b199f36c 100644 --- a/providers/openstack/scs/1-32/cluster-class/templates/kubeadm-control-plane-template.yaml +++ b/providers/openstack/scs/1-32/cluster-class/templates/kubeadm-control-plane-template.yaml @@ -1,4 +1,4 @@ -apiVersion: controlplane.cluster.x-k8s.io/v1beta1 +apiVersion: controlplane.cluster.x-k8s.io/v1beta2 kind: KubeadmControlPlaneTemplate metadata: name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane @@ -7,28 +7,42 @@ spec: spec: kubeadmConfigSpec: clusterConfiguration: - apiServer: - extraArgs: - cloud-provider: external controllerManager: extraArgs: - cloud-provider: external - bind-address: 0.0.0.0 - secure-port: "10257" + - name: cloud-provider + value: external + - name: bind-address + value: 0.0.0.0 + - name: secure-port + value: "10257" + - name: profiling + value: "false" + - name: terminated-pod-gc-threshold + value: "100" scheduler: extraArgs: - bind-address: 0.0.0.0 - secure-port: "10259" + - name: bind-address + value: 0.0.0.0 + - name: secure-port + value: "10259" + - name: profiling + value: "false" etcd: local: dataDir: /var/lib/etcd extraArgs: - listen-metrics-urls: http://0.0.0.0:2381 - auto-compaction-mode: periodic - auto-compaction-retention: 8h - election-timeout: "2500" - heartbeat-interval: "250" - snapshot-count: "6400" + - name: listen-metrics-urls + value: http://0.0.0.0:2381 + - name: auto-compaction-mode + value: periodic + - name: auto-compaction-retention + value: 8h + - name: election-timeout + value: "2500" + - name: heartbeat-interval + value: "250" + - name: snapshot-count + value: "6400" files: - content: | --- @@ -80,12 +94,16 @@ spec: initConfiguration: nodeRegistration: kubeletExtraArgs: - cloud-provider: external - provider-id: openstack:///'{{"{{"}} instance_id {{"}}"}}' - name: '{{"{{"}} local_hostname {{"}}"}}' + - name: cloud-provider + value: external + - name: provider-id + value: 'openstack:///{{ `{{ instance_id }}` }}' + name: '{{ `{{ local_hostname }}` }}' joinConfiguration: nodeRegistration: kubeletExtraArgs: - cloud-provider: external - provider-id: openstack:///'{{"{{"}} instance_id {{"}}"}}' - name: '{{"{{"}} local_hostname {{"}}"}}' + - name: cloud-provider + value: external + - name: provider-id + value: 'openstack:///{{ `{{ instance_id }}` }}' + name: '{{ `{{ local_hostname }}` }}' diff --git a/providers/openstack/scs/1-32/cluster-class/templates/openstack-cluster-template.yaml b/providers/openstack/scs/1-32/cluster-class/templates/openstack-cluster-template.yaml index ce689a2a..3cc03fbb 100644 --- a/providers/openstack/scs/1-32/cluster-class/templates/openstack-cluster-template.yaml +++ b/providers/openstack/scs/1-32/cluster-class/templates/openstack-cluster-template.yaml @@ -1,3 +1,4 @@ +--- apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackClusterTemplate metadata: @@ -6,13 +7,10 @@ spec: template: spec: identityRef: - cloudName: {{ .Values.identityRef.cloudName }} - name: {{ .Values.identityRef.name }} + cloudName: overridden-by-patch + name: overridden-by-patch apiServerLoadBalancer: - enabled: {{ .Values.openstack_loadbalancer_apiserver }} -{{- if .Values.restrict_kubeapi }} - allowedCIDRs: {{ .Values.restrict_kubeapi }} -{{- end }} + enabled: false managedSecurityGroups: allNodesSecurityGroupRules: - remoteManagedGroups: @@ -45,3 +43,49 @@ spec: portRangeMax: 4244 protocol: tcp description: "Allow Hubble traffic for Cilium" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus kube-proxy exporter + portRangeMin: 10249 + portRangeMax: 10249 + protocol: tcp + description: "Allow Prometheus traffic for kube-proxy exporter" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus kube-controller-manager exporter + portRangeMin: 10257 + portRangeMax: 10257 + protocol: tcp + description: "Allow Prometheus traffic for kube-controller-manager exporter" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus kube-scheduler exporter + portRangeMin: 10259 + portRangeMax: 10259 + protocol: tcp + description: "Allow Prometheus traffic for kube-scheduler exporter" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus node exporter + portRangeMin: 9100 + portRangeMax: 9100 + protocol: tcp + description: "Allow Prometheus traffic for scraping node exporter" + controlPlaneNodesSecurityGroupRules: + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus etcd exporter + portRangeMin: 2381 + portRangeMax: 2381 + protocol: tcp + description: "Allow Prometheus traffic for scraping etcd exporter" \ No newline at end of file diff --git a/providers/openstack/scs/1-32/cluster-class/templates/openstack-machine-template-control-plane.yaml b/providers/openstack/scs/1-32/cluster-class/templates/openstack-machine-template-control-plane.yaml index d2acdd7d..703c1b1c 100644 --- a/providers/openstack/scs/1-32/cluster-class/templates/openstack-machine-template-control-plane.yaml +++ b/providers/openstack/scs/1-32/cluster-class/templates/openstack-machine-template-control-plane.yaml @@ -1,3 +1,4 @@ +--- apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate metadata: @@ -5,10 +6,7 @@ metadata: spec: template: spec: - flavor: {{ .Values.controller_flavor }} - identityRef: - cloudName: {{ .Values.identityRef.cloudName }} - name: {{ .Values.identityRef.name }} + flavor: overridden-by-patch image: - filter: - name: {{ .Values.images.controlPlane.name }} + imageRef: + name: overridden-by-patch diff --git a/providers/openstack/scs/1-32/cluster-class/templates/openstack-machine-template-worker.yaml b/providers/openstack/scs/1-32/cluster-class/templates/openstack-machine-template-worker.yaml index 83deb117..920dbb0a 100644 --- a/providers/openstack/scs/1-32/cluster-class/templates/openstack-machine-template-worker.yaml +++ b/providers/openstack/scs/1-32/cluster-class/templates/openstack-machine-template-worker.yaml @@ -1,3 +1,4 @@ +--- apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate metadata: @@ -5,10 +6,7 @@ metadata: spec: template: spec: - flavor: {{ .Values.worker_flavor }} - identityRef: - cloudName: {{ .Values.identityRef.cloudName }} - name: {{ .Values.identityRef.name }} + flavor: overridden-by-patch image: - filter: - name: {{ .Values.images.worker.name }} + imageRef: + name: overridden-by-patch diff --git a/providers/openstack/scs/1-32/cluster-class/values.yaml b/providers/openstack/scs/1-32/cluster-class/values.yaml index afb2139f..1344cf85 100644 --- a/providers/openstack/scs/1-32/cluster-class/values.yaml +++ b/providers/openstack/scs/1-32/cluster-class/values.yaml @@ -1,12 +1,50 @@ -controller_flavor: SCS-2V-4-20s -identityRef: - cloudName: openstack - name: openstack -images: - controlPlane: - name: ubuntu-capi-image-v1.32.5 - worker: - name: ubuntu-capi-image-v1.32.5 -openstack_loadbalancer_apiserver: false -restrict_kubeapi: [] -worker_flavor: SCS-2V-4-20 +# ClusterClass variable defaults +# These are referenced by the ClusterClass template and can be overridden per deployment. +# Variables apply to all nodes by default. Use topology.controlPlane.variables.overrides +# or topology.workers.machineDeployments[].variables.overrides to differentiate. +variables: + # Image configuration + imageName: "ubuntu-capi-image" + imageIsOrc: false + imageAddVersion: true + + # API server + disableAPIServerFloatingIP: false + apiServerLoadBalancer: "octavia-ovn" + certSANs: [] + + # Network + dnsNameservers: ["9.9.9.9", "149.112.112.112"] + nodeCIDR: "10.8.0.0/20" + + # Machine configuration (override per CP/worker via topology) + flavor: "SCS-2V-4" + rootDisk: 50 + serverGroupID: "" + additionalBlockDevices: [] + + # Access management + sshKeyName: "" + securityGroups: [] + securityGroupIDs: [] + + # Cluster-level (control plane only) + controlPlaneAvailabilityZones: [] + controlPlaneOmitAvailabilityZone: false + + # Identity + identityRef: + name: "openstack" + cloudName: "openstack" + + # Container runtime + registryMirrors: [] + + # OIDC + oidcConfig: + clientID: "" + issuerURL: "" + usernameClaim: "preferred_username" + groupsClaim: "groups" + usernamePrefix: "oidc:" + groupsPrefix: "oidc:" diff --git a/providers/openstack/scs/1-32/clusteraddon.yaml b/providers/openstack/scs/1-32/clusteraddon.yaml index d346ba22..bea5fc78 100644 --- a/providers/openstack/scs/1-32/clusteraddon.yaml +++ b/providers/openstack/scs/1-32/clusteraddon.yaml @@ -19,3 +19,12 @@ addonStages: action: apply - name: ccm action: apply + AfterClusterUpgrade: + - name: cni + action: apply + - name: metrics-server + action: apply + - name: csi + action: apply + - name: ccm + action: apply diff --git a/providers/openstack/scs/1-32/stack.yaml b/providers/openstack/scs/1-32/stack.yaml index 4c7690d4..690ec6c5 100644 --- a/providers/openstack/scs/1-32/stack.yaml +++ b/providers/openstack/scs/1-32/stack.yaml @@ -1,7 +1,6 @@ provider: openstack clusterStackName: scs -kubernetesVersion: 1.32 - +kubernetesVersion: 1.32.13 addons: ccm: 2.32.x csi: 2.32.x diff --git a/providers/openstack/scs/1-33/cluster-addon/cni/Chart.yaml b/providers/openstack/scs/1-33/cluster-addon/cni/Chart.yaml index 051b40b3..95516e98 100644 --- a/providers/openstack/scs/1-33/cluster-addon/cni/Chart.yaml +++ b/providers/openstack/scs/1-33/cluster-addon/cni/Chart.yaml @@ -7,4 +7,4 @@ dependencies: - alias: cilium name: cilium repository: https://helm.cilium.io/ - version: 1.19.1 + version: 1.19.2 diff --git a/providers/openstack/scs/1-33/cluster-class/templates/cluster-class.yaml b/providers/openstack/scs/1-33/cluster-class/templates/cluster-class.yaml index d7fbd338..70eab050 100644 --- a/providers/openstack/scs/1-33/cluster-class/templates/cluster-class.yaml +++ b/providers/openstack/scs/1-33/cluster-class/templates/cluster-class.yaml @@ -1,37 +1,36 @@ -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: {{ .Release.Name }}-{{ .Chart.Version }} spec: controlPlane: - ref: - apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + templateRef: + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 kind: KubeadmControlPlaneTemplate name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane machineInfrastructure: - ref: + templateRef: kind: OpenStackMachineTemplate apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane infrastructure: - ref: + templateRef: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackClusterTemplate name: {{ .Release.Name }}-{{ .Chart.Version }}-cluster workers: machineDeployments: - class: default-worker - template: - bootstrap: - ref: - apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 - kind: KubeadmConfigTemplate - name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker - infrastructure: - ref: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate - name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker + bootstrap: + templateRef: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 + kind: KubeadmConfigTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker + infrastructure: + templateRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker variables: # Image variables - name: imageName @@ -44,7 +43,7 @@ spec: If `imageIsOrc` is enabled, this name refers to an ORC image resource. If `imageIsOrc` is disabled, the name is used to filter images available in the OpenStack project. In this case, the specified image must already exist within the project. If `imageAddVersion` is enabled, the Kubernetes version will be appended to form the complete image name (e.g., imageName-v1.32.5) - default: "ubuntu-capi-image" + default: {{ .Values.variables.imageName | quote }} - name: imageIsOrc required: false schema: @@ -54,7 +53,7 @@ spec: Indicates whether the image name refers to an ORC image resource. If set to true (default), the `imageName` is interpreted as a reference to an ORC image. If set to false, the `imageName` is used to filter images in the OpenStack project instead. - default: false + default: {{ .Values.variables.imageIsOrc }} - name: imageAddVersion required: false schema: @@ -62,15 +61,7 @@ spec: type: boolean description: | Add a suffix with the Kubernetes version to the imageName. E.g. imageName-v1.32.5. - default: true - - name: disableAPIServerFloatingIP - required: false - schema: - openAPIV3Schema: - type: boolean - default: false - example: false - description: "DisableAPIServerFloatingIP controls whether a floating IP should be attached to the API server." + default: {{ .Values.variables.imageAddVersion }} # Network variables - name: networkExternalID required: false @@ -79,21 +70,23 @@ spec: type: string example: "ebfe5546-f09f-4f42-ab54-094e457d42ec" format: "uuid4" - description: "networkExternalID is the ID of an external OpenStack Network. This is necessary to get public internet to the VMs in case there are several external networks." + description: |- + ID of an external OpenStack network. Required when multiple + external networks exist and VMs need public internet access. - name: networkMTU required: false schema: openAPIV3Schema: type: integer example: 1500 - description: "networkMTU sets the maximum transmission unit (MTU) value to address fragmentation for the private network ID." + description: "MTU for the private cluster network. Set this to avoid fragmentation issues." - name: dnsNameservers required: false schema: openAPIV3Schema: type: array - description: "dnsNameservers is the list of nameservers for the OpenStack Subnet being created. Set this value when you need to create a new network/subnet which requires access to DNS." - default: ["9.9.9.9", "149.112.112.112"] + description: "DNS nameservers for the cluster subnet. Only used when a new network/subnet is created." + default: {{ .Values.variables.dnsNameservers | toJson }} example: ["9.9.9.9", "149.112.112.112"] items: type: string @@ -103,97 +96,53 @@ spec: openAPIV3Schema: type: string format: "cidr" - default: "10.8.0.0/20" + default: {{ .Values.variables.nodeCIDR | quote }} example: "10.8.0.0/20" description: |- - nodeCIDR is the OpenStack Subnet to be created. - Cluster actuator will create a network, a subnet with nodeCIDR, - and a router connected to this subnet. - If you leave this empty, no network will be created. - # Control plane - - name: controlPlaneFlavor + CIDR for the cluster subnet. CAPO will create a network, subnet, + and router. Leave empty to skip network creation. + # Machine configuration + # These apply to all nodes by default. Use topology.controlPlane.variables.overrides + # or topology.workers.machineDeployments[].variables.overrides to differentiate. + - name: flavor required: false schema: openAPIV3Schema: type: string - default: "SCS-2V-4" - example: "SCS-2V-4-20s" + default: {{ .Values.variables.flavor | quote }} + example: "SCS-2V-4" description: |- - OpenStack instance flavor for control plane nodes. - (Default: SCS-2V-4, replace by SCS-2V-4-20s or specify a controlPlaneRootDisk.) - - name: controlPlaneRootDisk + OpenStack instance flavor. Applies to all nodes by default. + Override per control plane or worker via topology variables overrides. + - name: rootDisk required: false schema: openAPIV3Schema: type: integer minimum: 0 - example: 25 - default: 50 + example: 50 + default: {{ .Values.variables.rootDisk }} description: |- - Root disk size in GiB for control-plane nodes. - OpenStack volume will be created and used instead of an ephemeral disk defined in flavor. - Should only be used for the diskless flavors (>= 20), otherwise set to 0. - - name: controlPlaneServerGroupID + Root disk size in GiB. OpenStack volume will be created and used + instead of an ephemeral disk defined in the flavor. + Set to 0 to use the flavor's ephemeral disk. + - name: serverGroupID required: false schema: openAPIV3Schema: type: string - default: "" + default: {{ .Values.variables.serverGroupID | quote }} example: "3adf4e92-bb33-4e44-8ad3-afda9dfe8ec3" - description: "The server group to assign the control plane nodes to (can be used for anti-affinity)." - - name: controlPlaneAvailabilityZones + description: "Server group for anti-affinity placement. Override per CP/worker via topology." + - name: additionalBlockDevices required: false schema: openAPIV3Schema: type: array - example: ["nova"] - description: "controlPlaneAvailabilityZones is the set of availability zones which control plane machines may be deployed to." - items: - type: string - - name: controlPlaneOmitAvailabilityZone - required: false - schema: - openAPIV3Schema: - type: boolean - example: true + default: {{ .Values.variables.additionalBlockDevices | toJson }} description: |- - controlPlaneOmitAvailabilityZone causes availability zone to be omitted when creating control plane nodes, - allowing the Nova scheduler to make a decision on which availability zone to use based on other scheduling constraints. - # Workers - - name: workerFlavor - required: false - schema: - openAPIV3Schema: - type: string - default: "SCS-4V-8" - example: "SCS-4V-8" - description: "OpenStack instance flavor for worker nodes (default: SCS-4V-8, which requires workerRootDisk)." - - name: workerRootDisk - required: false - schema: - openAPIV3Schema: - type: integer - minimum: 0 - example: 25 - default: 50 - description: |- - Root disk size in GiB for worker nodes. - OpenStack volume will be created and used instead of an ephemeral disk defined in flavor. - Should be used for the diskless flavors (>= 20), otherwise set to 0. - - name: workerServerGroupID - required: false - schema: - openAPIV3Schema: - type: string - default: "" - example: "869fe071-1e56-46a9-9166-47c9f228e297" - description: "The server group to assign the worker nodes to." - - name: workerAdditionalBlockDevices - required: false - schema: - openAPIV3Schema: - type: array - default: [] + Additional block devices (Cinder volumes) to attach to nodes. + Override per CP/worker via topology. items: type: object properties: @@ -205,60 +154,59 @@ spec: type: type: string default: "__DEFAULT__" - required: ["name"] - # Access management - - name: sshKeyName + required: ["name"] + # Cluster-level (control plane only, managed by OpenStackClusterTemplate) + - name: controlPlaneAvailabilityZones required: false schema: openAPIV3Schema: - type: string - default: "" - example: "capi-keypair" - description: "The ssh key name to inject in the nodes (for debugging)." - - name: securityGroups + type: array + default: {{ .Values.variables.controlPlaneAvailabilityZones | toJson }} + example: ["nova"] + description: "Availability zones for control plane nodes (OpenStack cluster-level setting)." + items: + type: string + - name: controlPlaneOmitAvailabilityZone required: false schema: openAPIV3Schema: - type: array - default: [] - example: ["security-group-1"] + type: boolean + default: {{ .Values.variables.controlPlaneOmitAvailabilityZone }} description: |- - The names of extra security groups to assign to worker and control plane nodes. - Will be ignored if `securityGroupIDs` is used. - items: - type: string - - name: securityGroupIDs + Omit availability zone when creating control plane nodes, letting the + Nova scheduler decide based on other scheduling constraints. + # Access management + - name: sshKeyName required: false schema: openAPIV3Schema: - format: "uuid4" - type: array - default: [] - example: ["9ae2f488-30a3-4629-bd51-07acb8eb4278"] - description: "The UUIDs of extra security groups to assign to worker and control plane nodes" - items: - type: string - - name: workerSecurityGroups + type: string + default: {{ .Values.variables.sshKeyName | quote }} + example: "capi-keypair" + description: "SSH key to inject into all nodes (for debugging)." + - name: securityGroups required: false schema: openAPIV3Schema: type: array - default: [] + default: {{ .Values.variables.securityGroups | toJson }} example: ["security-group-1"] description: |- - The names of extra security groups to assign to the worker nodes. - Will be ignored if `workerSecurityGroupIDs` is used. + Extra security groups by name for all nodes. + Ignored if securityGroupIDs is set. Override per CP/worker via topology. items: type: string - - name: workerSecurityGroupIDs + - name: securityGroupIDs required: false schema: openAPIV3Schema: format: "uuid4" type: array - default: [] + default: {{ .Values.variables.securityGroupIDs | toJson }} example: ["9ae2f488-30a3-4629-bd51-07acb8eb4278"] - description: "The UUIDs of extra security groups to assign to the worker nodes" + description: |- + Extra security groups by UUID for all nodes. + Takes precedence over securityGroups. Override per CP/worker via topology. items: type: string - name: identityRef @@ -266,27 +214,39 @@ spec: schema: openAPIV3Schema: type: object - default: {} + default: {{ .Values.variables.identityRef | toJson }} properties: name: type: string example: "openstack" - default: "openstack" + default: {{ .Values.variables.identityRef.name | quote }} description: "The name of the secret that carries the OpenStack clouds.yaml" cloudName: type: string example: "openstack" - default: "openstack" + default: {{ .Values.variables.identityRef.cloudName | quote }} description: "The name of the cloud to use from the clouds.yaml" - # Kubernetes API server + type: + type: string + example: "Secret" + default: {{ .Values.variables.identityRef.type | quote }} + description: "The type of the identityRef" + # API server + - name: disableAPIServerFloatingIP + required: false + schema: + openAPIV3Schema: + type: boolean + default: {{ .Values.variables.disableAPIServerFloatingIP }} + description: "Disable the floating IP on the API server load balancer." - name: certSANs required: false schema: openAPIV3Schema: type: array - default: [] + default: {{ .Values.variables.certSANs | toJson }} example: ["mydomain.example"] - description: "certSANs sets extra Subject Alternative Names for the API Server signing cert." + description: "Extra Subject Alternative Names for the API server TLS certificate." items: type: string - name: apiServerLoadBalancer @@ -294,33 +254,22 @@ spec: schema: openAPIV3Schema: type: string - default: "octavia-ovn" - example: "none, octavia-amphora, octavia-ovn" - description: | - Cluster-API by default places a LoadBalancer in front of the kubernetes API server. - (There are also LBs that the CCM creates for a service type LoadBalancer which are configured independently.) - This setting here is to configure the LoadBalancer that is placed in front of the apiServer. - You can choose from 3 options: - - none: - No LoadBalancer solution will be deployed - - octavia-amphora: - Uses OpenStack's LoadBalancer service Octavia (provider:amphora) - - octavia-ovn: - (default) Uses OpenStack's LoadBalancer service Octavia (provider:ovn) - - name: apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs + default: {{ .Values.variables.apiServerLoadBalancer | quote }} + example: "octavia-ovn" + description: |- + Load balancer in front of the API server. + Options: none, octavia-amphora, octavia-ovn (default). + - name: apiServerAllowedCIDRs required: false schema: openAPIV3Schema: type: array example: ["192.168.10.0/24"] description: |- - apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs restrict access to the Kubernetes API server on a network level. - Ensure that at least the outgoing IP of your Management Cluster is added to the list of allowed CIDRs. - Otherwise CAPO can’t reconcile the target Cluster correctly. - This requires amphora as load balancer provider in version >= v2.12. + Restrict access to the API server to these CIDRs (network-level ACL). + Requires amphora as load balancer provider (CAPO >= v2.12). + Ensure the management cluster's outgoing IP is included, + otherwise CAPO cannot reconcile the workload cluster. items: type: string - name: oidcConfig @@ -344,7 +293,7 @@ spec: usernameClaim: type: string example: "preferred_username" - default: "preferred_username" + default: {{ .Values.variables.oidcConfig.usernameClaim | quote }} description: >- JWT claim to use as the user name. By default sub, which is expected to be a unique identifier of the end user. Admins can choose @@ -354,12 +303,12 @@ spec: groupsClaim: type: string example: "groups" - default: "groups" + default: {{ .Values.variables.oidcConfig.groupsClaim | quote }} description: "JWT claim to use as the user's group. If the claim is present it must be an array of strings." usernamePrefix: type: string example: "oidc:" - default: "oidc:" + default: {{ .Values.variables.oidcConfig.usernamePrefix | quote }} description: >- Prefix prepended to username claims to prevent clashes with existing names (such as system: users). For example, the value @@ -370,11 +319,38 @@ spec: groupsPrefix: type: string example: "oidc:" - default: "oidc:" + default: {{ .Values.variables.oidcConfig.groupsPrefix | quote }} description: >- Prefix prepended to group claims to prevent clashes with existing names (such as system: groups). For example, the value oidc: will create group names like oidc:engineering and oidc:infra. + # Container runtime + - name: registryMirrors + required: false + schema: + openAPIV3Schema: + type: array + default: {{ .Values.variables.registryMirrors | toJson }} + description: "Registry mirrors for upstream container registries. Configures both containerd and CRI-O to pull through a mirror." + items: + type: object + properties: + hostnameUpstream: + type: string + example: "docker.io" + description: "The hostname of the upstream registry." + urlUpstream: + type: string + example: "https://registry-1.docker.io" + description: "The server URL of the upstream registry." + urlMirror: + type: string + example: "https://registry.example.com/v2/dockerhub" + description: "The URL of the mirror registry." + certMirror: + type: string + example: "" + description: "TLS certificate of the mirror in PEM format (optional)." # # Patches # @@ -414,9 +390,9 @@ spec: - op: add path: "/spec/template/spec/apiServerLoadBalancer/provider" value: "ovn" - - name: apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs - description: "Takes care of the patches that should be applied when variable allowedCIDRs is set." - enabledIf: {{ `'{{ and .apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs (eq .apiServerLoadBalancer "octavia-amphora") }}'` }} + - name: apiServerAllowedCIDRs + description: "Restricts API server access to the given CIDRs (requires amphora LB)." + enabledIf: {{ `'{{ and .apiServerAllowedCIDRs (eq .apiServerLoadBalancer "octavia-amphora") }}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 @@ -427,7 +403,7 @@ spec: - op: add path: "/spec/template/spec/apiServerLoadBalancer/allowedCIDRs" valueFrom: - variable: apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs + variable: apiServerAllowedCIDRs - name: networkExternalID description: "Sets the ID of an external OpenStack Network. This is necessary to get public internet to the VMs." enabledIf: {{ `'{{ if .networkExternalID }}true{{end}}'` }} @@ -537,10 +513,13 @@ spec: valueFrom: variable: disableAPIServerFloatingIP # - # Patches for control plane's OpenStackMachineTemplate resources. - # Note: Control plane patches are only applied when the control plane is managed by Kubeadm. + # Patches for OpenStackMachineTemplate resources (image). + # Image patches must stay separate because they use different builtin variables: + # - .builtin.controlPlane.version for CP + # - .builtin.machineDeployment.version for workers + # - name: controlPlaneImage - description: "Sets the OpenStack image name that is used for creating the control plane servers." + description: "Sets the OpenStack image for control plane nodes." definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 @@ -554,120 +533,13 @@ spec: template: | {{ `{{ if .imageIsOrc }}imageRef{{ else }}filter{{ end }}` }}: name: {{ `{{ .imageName }}{{ if .imageAddVersion }}-{{ .builtin.controlPlane.version }}{{ end }}` }} - - name: controlPlaneFlavor - description: "Sets the openstack instance flavor for the KubeadmControlPlane." - enabledIf: {{ `'{{ ne .controlPlaneFlavor "" }}'` }} - definitions: - - selector: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate - matchResources: - controlPlane: true - jsonPatches: - - op: replace - path: "/spec/template/spec/flavor" - valueFrom: - variable: controlPlaneFlavor - - name: controlPlaneRootDisk - description: "Sets the root disk size in GiB for control-plane nodes." - enabledIf: {{ `'{{ if .controlPlaneRootDisk }}true{{end}}'` }} - definitions: - - selector: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate - matchResources: - controlPlane: true - jsonPatches: - - op: add - path: "/spec/template/spec/rootVolume" - valueFrom: - template: | - sizeGiB: {{ `{{ .controlPlaneRootDisk }}` }} - - name: controlPlaneServerGroupID - description: "Sets the server group to assign the control plane nodes to." - enabledIf: {{ `'{{ ne .controlPlaneServerGroupID "" }}'` }} - definitions: - - selector: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate - matchResources: - controlPlane: true - jsonPatches: - - op: add - path: "/spec/template/spec/serverGroup" - valueFrom: - template: | - id: {{ `{{ .controlPlaneServerGroupID }}` }} - # - # Patches for control plane's as well as worker's OpenStackMachineTemplate resources. - # Note: Control plane patches are only applied when the control plane is managed by Kubeadm. - # - # Note: The securityGroups patch must be placed before securityGroupIDs, workerSecurityGroups, and workerSecurityGroupIDs. - # The patch order ensures the last applied patch overwrites previous ones. - - name: securityGroups - description: "Sets the list of the openstack security groups for the worker and the control plane instances." - enabledIf: {{ `'{{ if .securityGroups }}true{{end}}'` }} - definitions: - - selector: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate - matchResources: - controlPlane: true - machineDeploymentClass: - names: - - default-worker - jsonPatches: - - op: add - path: "/spec/template/spec/securityGroups" - valueFrom: - template: {{ `'[ {{ range .securityGroups }} { filter: { name: {{ . }}}}, {{ end }} ]'` }} - # Note: The securityGroupIDs patch must be placed before workerSecurityGroups, workerSecurityGroupIDs and after securityGroupIDs. - # The patch order ensures the last applied patch overwrites previous ones. - - name: securityGroupIDs - description: "Sets the list of the openstack security groups for the worker and the control plane instances by UUID." - enabledIf: {{ `'{{ if .securityGroupIDs }}true{{end}}'` }} - definitions: - - selector: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate - matchResources: - controlPlane: true - machineDeploymentClass: - names: - - default-worker - jsonPatches: - - op: add - path: "/spec/template/spec/securityGroups" - valueFrom: - template: {{ `'[ {{ range .securityGroupIDs }} { id: {{ . }} }, {{ end }} ]'` }} - - name: sshKeyName - description: "Sets the ssh key to inject in the nodes." - enabledIf: {{ `'{{ ne .sshKeyName "" }}'` }} - definitions: - - selector: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate - matchResources: - controlPlane: true - machineDeploymentClass: - names: - - default-worker - jsonPatches: - - op: add - path: "/spec/template/spec/sshKeyName" - valueFrom: - variable: sshKeyName - # - # Patches for worker's OpenStackMachineTemplate resources. - # - name: workerImage - description: "Sets the OpenStack image name that is used for creating the worker servers." + description: "Sets the OpenStack image for worker nodes." definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate matchResources: - controlPlane: false machineDeploymentClass: names: - default-worker @@ -678,15 +550,20 @@ spec: template: | {{ `{{ if .imageIsOrc }}imageRef{{ else }}filter{{ end }}` }}: name: {{ `{{ .imageName }}{{ if .imageAddVersion }}-{{ .builtin.machineDeployment.version }}{{ end }}` }} - - name: workerFlavor - description: "Sets the openstack instance flavor for the worker nodes." - enabledIf: {{ `'{{ ne .workerFlavor "" }}'` }} + # + # Unified machine patches — target both CP and workers. + # Users can override per CP/worker via topology.controlPlane.variables.overrides + # and topology.workers.machineDeployments[].variables.overrides. + # + - name: flavor + description: "Sets the OpenStack instance flavor for all nodes." + enabledIf: {{ `'{{ ne .flavor "" }}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate matchResources: - controlPlane: false + controlPlane: true machineDeploymentClass: names: - default-worker @@ -694,16 +571,16 @@ spec: - op: replace path: "/spec/template/spec/flavor" valueFrom: - variable: workerFlavor - - name: workerRootDisk - description: "Sets the root disk size in GiB for worker nodes." - enabledIf: {{ `'{{ if .workerRootDisk }}true{{end}}'` }} + variable: flavor + - name: rootDisk + description: "Sets the root disk size in GiB. 0 means use ephemeral disk from flavor." + enabledIf: {{ `'{{ if .rootDisk }}true{{end}}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate matchResources: - controlPlane: false + controlPlane: true machineDeploymentClass: names: - default-worker @@ -712,16 +589,16 @@ spec: path: "/spec/template/spec/rootVolume" valueFrom: template: | - sizeGiB: {{ `{{ .workerRootDisk }}` }} - - name: workerServerGroupID - description: "Sets the server group to assign the worker nodes to." - enabledIf: {{ `'{{ ne .workerServerGroupID "" }}'` }} + sizeGiB: {{ `{{ .rootDisk }}` }} + - name: serverGroupID + description: "Sets the server group for anti-affinity." + enabledIf: {{ `'{{ ne .serverGroupID "" }}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate matchResources: - controlPlane: false + controlPlane: true machineDeploymentClass: names: - default-worker @@ -730,15 +607,16 @@ spec: path: "/spec/template/spec/serverGroup" valueFrom: template: | - id: {{ `{{ .workerServerGroupID }}` }} - - name: workerAdditionalBlockDevices - enabledIf: {{ `'{{ if .workerAdditionalBlockDevices }}true{{end}}'` }} + id: {{ `{{ .serverGroupID }}` }} + - name: additionalBlockDevices + description: "Attaches additional Cinder volumes to nodes." + enabledIf: {{ `'{{ if .additionalBlockDevices }}true{{end}}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate matchResources: - controlPlane: false + controlPlane: true machineDeploymentClass: names: - default-worker @@ -747,7 +625,7 @@ spec: path: /spec/template/spec/additionalBlockDevices valueFrom: template: | - {{ `{{- range .workerAdditionalBlockDevices }}` }} + {{ `{{- range .additionalBlockDevices }}` }} - name: {{ `{{ .name }}` }} sizeGiB: {{ `{{ .sizeGiB }}` }} storage: @@ -755,17 +633,19 @@ spec: volume: type: {{ `{{ .type }}` }} {{ `{{- end }}` }} - # Note: The workerSecurityGroups patch must be placed before workerSecurityGroupIDs and after securityGroups and securityGroupIDs. - # The patch order ensures the last applied patch overwrites previous ones. - - name: workerSecurityGroups - description: "Sets the list of the openstack security groups for the worker instances." - enabledIf: {{ `'{{ if .workerSecurityGroups }}true{{end}}'` }} + # + # Access patches — target both CP and workers. + # securityGroupIDs takes precedence over securityGroups (last patch wins). + # + - name: securityGroups + description: "Sets security groups by name for all nodes." + enabledIf: {{ `'{{ if .securityGroups }}true{{end}}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate matchResources: - controlPlane: false + controlPlane: true machineDeploymentClass: names: - default-worker @@ -773,18 +653,16 @@ spec: - op: add path: "/spec/template/spec/securityGroups" valueFrom: - template: {{ `'[ {{ range .workerSecurityGroups }} { filter: { name: {{ . }}}}, {{ end }} ]'` }} - # Note: The workerSecurityGroupIDs patch must be placed after securityGroups, securityGroupIDs and workerSecurityGroupIDs. - # The patch order ensures the last applied patch overwrites previous ones. - - name: workerSecurityGroupIDs - description: "Sets the list of the openstack security groups for the worker instances by UUID." - enabledIf: {{ `'{{ if .workerSecurityGroupIDs }}true{{end}}'` }} + template: {{ `'[ {{ range .securityGroups }} { filter: { name: {{ . }}}}, {{ end }} ]'` }} + - name: securityGroupIDs + description: "Sets security groups by UUID for all nodes. Takes precedence over securityGroups." + enabledIf: {{ `'{{ if .securityGroupIDs }}true{{end}}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate matchResources: - controlPlane: false + controlPlane: true machineDeploymentClass: names: - default-worker @@ -792,14 +670,31 @@ spec: - op: add path: "/spec/template/spec/securityGroups" valueFrom: - template: {{ `'[ {{ range .workerSecurityGroupIDs }} { id: {{ . }} }, {{ end }} ]'` }} + template: {{ `'[ {{ range .securityGroupIDs }} { id: {{ . }} }, {{ end }} ]'` }} + - name: sshKeyName + description: "Sets the SSH key to inject into all nodes." + enabledIf: {{ `'{{ ne .sshKeyName "" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: true + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/sshKeyName" + valueFrom: + variable: sshKeyName # - name: certSANs description: "certSANs sets extra Subject Alternative Names for the API Server signing cert." enabledIf: {{ `'{{ if .certSANs }}true{{end}}'` }} definitions: - selector: - apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 kind: KubeadmControlPlaneTemplate matchResources: controlPlane: true @@ -813,32 +708,161 @@ spec: enabledIf: {{ `'{{ if and .oidcConfig .oidcConfig.clientID .oidcConfig.issuerURL }}true{{end}}'` }} definitions: - selector: - apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 kind: KubeadmControlPlaneTemplate matchResources: controlPlane: true jsonPatches: - op: add - path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-client-id" + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" valueFrom: - variable: oidcConfig.clientID + template: | + name: oidc-client-id + value: {{ `'{{ .oidcConfig.clientID }}'` }} - op: add - path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-issuer-url" + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" valueFrom: - variable: oidcConfig.issuerURL + template: | + name: oidc-issuer-url + value: {{ `'{{ .oidcConfig.issuerURL }}'` }} - op: add - path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-username-claim" + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" valueFrom: - variable: oidcConfig.usernameClaim + template: | + name: oidc-username-claim + value: {{ `'{{ .oidcConfig.usernameClaim }}'` }} - op: add - path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-groups-claim" + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" valueFrom: - variable: oidcConfig.groupsClaim + template: | + name: oidc-groups-claim + value: {{ `'{{ .oidcConfig.groupsClaim }}'` }} - op: add - path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-username-prefix" + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" valueFrom: - variable: oidcConfig.usernamePrefix + template: | + name: oidc-username-prefix + value: {{ `'{{ .oidcConfig.usernamePrefix }}'` }} - op: add - path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-groups-prefix" + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" valueFrom: - variable: oidcConfig.groupsPrefix + template: | + name: oidc-groups-prefix + value: {{ `'{{ .oidcConfig.groupsPrefix }}'` }} + # + # Registry mirror patches + # + - name: registryMirrorsControlPlane + description: "Configure registry mirrors on control plane nodes (containerd + CRI-O)." + enabledIf: {{ `'{{ if .registryMirrors }}true{{end}}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 + kind: KubeadmControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/files" + valueFrom: + template: | + {{ `{{- range $r := .registryMirrors }}` }} + - content: | + server = "{{ `{{ $r.urlUpstream }}` }}" + [host."{{ `{{ $r.urlMirror }}` }}"] + capabilities = ["pull","resolve"] + override_path = true + owner: root:root + path: /etc/containerd/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/hosts.toml + permissions: "0644" + - content: | + [[registry]] + prefix = "{{ `{{ $r.hostnameUpstream }}` }}" + location = "{{ `{{ $r.hostnameUpstream }}` }}" + [[registry.mirror]] + location = "{{ `{{ $r.urlMirror }}` }}" + owner: root:root + path: /etc/containers/registries.conf.d/50-mirror-{{ `{{ $r.hostnameUpstream }}` }}.conf + permissions: "0644" + {{ `{{- if $r.certMirror }}` }} + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containerd/certs/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containers/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + {{ `{{- end }}` }} + {{ `{{- end }}` }} + - path: /etc/kube-proxy-config.yaml + owner: root:root + permissions: "0644" + content: | + --- + apiVersion: kubeproxy.config.k8s.io/v1alpha1 + kind: KubeProxyConfiguration + metricsBindAddress: "0.0.0.0:10249" + + - path: /etc/kube-proxy-patch.sh + owner: root:root + permissions: "0755" + content: | + #!/usr/bin/env bash + set -euo pipefail + dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + kubeadm_file="/etc/kubeadm.yml" + [[ -f ${kubeadm_file} ]] || kubeadm_file="/run/kubeadm/kubeadm.yaml" + [[ -f ${kubeadm_file} ]] || exit 0 + [[ -f ${dir}/kube-proxy-config.yaml ]] || exit 0 + cat "${dir}/kube-proxy-config.yaml" >> "${kubeadm_file}" + rm -f "${dir}/kube-proxy-config.yaml" + echo success > /tmp/kube-proxy-patch + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/preKubeadmCommands/-" + value: "bash /etc/kube-proxy-patch.sh" + - name: registryMirrorsWorker + description: "Configure registry mirrors on worker nodes (containerd + CRI-O)." + enabledIf: {{ `'{{ if .registryMirrors }}true{{end}}'` }} + definitions: + - selector: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 + kind: KubeadmConfigTemplate + matchResources: + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/files" + valueFrom: + template: | + {{ `{{- range $r := .registryMirrors }}` }} + - content: | + server = "{{ `{{ $r.urlUpstream }}` }}" + [host."{{ `{{ $r.urlMirror }}` }}"] + capabilities = ["pull","resolve"] + override_path = true + owner: root:root + path: /etc/containerd/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/hosts.toml + permissions: "0644" + - content: | + [[registry]] + prefix = "{{ `{{ $r.hostnameUpstream }}` }}" + location = "{{ `{{ $r.hostnameUpstream }}` }}" + [[registry.mirror]] + location = "{{ `{{ $r.urlMirror }}` }}" + owner: root:root + path: /etc/containers/registries.conf.d/50-mirror-{{ `{{ $r.hostnameUpstream }}` }}.conf + permissions: "0644" + {{ `{{- if $r.certMirror }}` }} + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containerd/certs/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containers/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + {{ `{{- end }}` }} + {{ `{{- end }}` }} diff --git a/providers/openstack/scs/1-33/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml b/providers/openstack/scs/1-33/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml index 4c1494ed..732d78b3 100644 --- a/providers/openstack/scs/1-33/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml +++ b/providers/openstack/scs/1-33/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml @@ -1,4 +1,4 @@ -apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 kind: KubeadmConfigTemplate metadata: name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker @@ -8,6 +8,8 @@ spec: joinConfiguration: nodeRegistration: kubeletExtraArgs: - cloud-provider: external - provider-id: 'openstack:///{{ `{{ instance_id }}` }}' + - name: cloud-provider + value: external + - name: provider-id + value: 'openstack:///{{ `{{ instance_id }}` }}' name: '{{ `{{ local_hostname }}` }}' diff --git a/providers/openstack/scs/1-33/cluster-class/templates/kubeadm-control-plane-template.yaml b/providers/openstack/scs/1-33/cluster-class/templates/kubeadm-control-plane-template.yaml index 767eac68..b199f36c 100644 --- a/providers/openstack/scs/1-33/cluster-class/templates/kubeadm-control-plane-template.yaml +++ b/providers/openstack/scs/1-33/cluster-class/templates/kubeadm-control-plane-template.yaml @@ -1,4 +1,4 @@ -apiVersion: controlplane.cluster.x-k8s.io/v1beta1 +apiVersion: controlplane.cluster.x-k8s.io/v1beta2 kind: KubeadmControlPlaneTemplate metadata: name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane @@ -7,26 +7,42 @@ spec: spec: kubeadmConfigSpec: clusterConfiguration: - apiServer: {} controllerManager: extraArgs: - cloud-provider: external - bind-address: 0.0.0.0 - secure-port: "10257" + - name: cloud-provider + value: external + - name: bind-address + value: 0.0.0.0 + - name: secure-port + value: "10257" + - name: profiling + value: "false" + - name: terminated-pod-gc-threshold + value: "100" scheduler: extraArgs: - bind-address: 0.0.0.0 - secure-port: "10259" + - name: bind-address + value: 0.0.0.0 + - name: secure-port + value: "10259" + - name: profiling + value: "false" etcd: local: dataDir: /var/lib/etcd extraArgs: - listen-metrics-urls: http://0.0.0.0:2381 - auto-compaction-mode: periodic - auto-compaction-retention: 8h - election-timeout: "2500" - heartbeat-interval: "250" - snapshot-count: "6400" + - name: listen-metrics-urls + value: http://0.0.0.0:2381 + - name: auto-compaction-mode + value: periodic + - name: auto-compaction-retention + value: 8h + - name: election-timeout + value: "2500" + - name: heartbeat-interval + value: "250" + - name: snapshot-count + value: "6400" files: - content: | --- @@ -78,12 +94,16 @@ spec: initConfiguration: nodeRegistration: kubeletExtraArgs: - cloud-provider: external - provider-id: 'openstack:///{{ `{{ instance_id }}` }}' + - name: cloud-provider + value: external + - name: provider-id + value: 'openstack:///{{ `{{ instance_id }}` }}' name: '{{ `{{ local_hostname }}` }}' joinConfiguration: nodeRegistration: kubeletExtraArgs: - cloud-provider: external - provider-id: 'openstack:///{{ `{{ instance_id }}` }}' + - name: cloud-provider + value: external + - name: provider-id + value: 'openstack:///{{ `{{ instance_id }}` }}' name: '{{ `{{ local_hostname }}` }}' diff --git a/providers/openstack/scs/1-33/cluster-class/templates/openstack-cluster-template.yaml b/providers/openstack/scs/1-33/cluster-class/templates/openstack-cluster-template.yaml index 9d03326f..6d96089f 100644 --- a/providers/openstack/scs/1-33/cluster-class/templates/openstack-cluster-template.yaml +++ b/providers/openstack/scs/1-33/cluster-class/templates/openstack-cluster-template.yaml @@ -43,3 +43,49 @@ spec: portRangeMax: 4244 protocol: tcp description: "Allow Hubble traffic for Cilium" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus kube-proxy exporter + portRangeMin: 10249 + portRangeMax: 10249 + protocol: tcp + description: "Allow Prometheus traffic for kube-proxy exporter" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus kube-controller-manager exporter + portRangeMin: 10257 + portRangeMax: 10257 + protocol: tcp + description: "Allow Prometheus traffic for kube-controller-manager exporter" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus kube-scheduler exporter + portRangeMin: 10259 + portRangeMax: 10259 + protocol: tcp + description: "Allow Prometheus traffic for kube-scheduler exporter" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus node exporter + portRangeMin: 9100 + portRangeMax: 9100 + protocol: tcp + description: "Allow Prometheus traffic for scraping node exporter" + controlPlaneNodesSecurityGroupRules: + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus etcd exporter + portRangeMin: 2381 + portRangeMax: 2381 + protocol: tcp + description: "Allow Prometheus traffic for scraping etcd exporter" diff --git a/providers/openstack/scs/1-33/cluster-class/values.yaml b/providers/openstack/scs/1-33/cluster-class/values.yaml index e69de29b..272a7a0a 100644 --- a/providers/openstack/scs/1-33/cluster-class/values.yaml +++ b/providers/openstack/scs/1-33/cluster-class/values.yaml @@ -0,0 +1,51 @@ +# ClusterClass variable defaults +# These are referenced by the ClusterClass template and can be overridden per deployment. +# Variables apply to all nodes by default. Use topology.controlPlane.variables.overrides +# or topology.workers.machineDeployments[].variables.overrides to differentiate. +variables: + # Image configuration + imageName: "ubuntu-capi-image" + imageIsOrc: false + imageAddVersion: true + + # API server + disableAPIServerFloatingIP: false + apiServerLoadBalancer: "octavia-ovn" + certSANs: [] + + # Network + dnsNameservers: ["9.9.9.9", "149.112.112.112"] + nodeCIDR: "10.8.0.0/20" + + # Machine configuration (override per CP/worker via topology) + flavor: "SCS-2V-4" + rootDisk: 50 + serverGroupID: "" + additionalBlockDevices: [] + + # Access management + sshKeyName: "" + securityGroups: [] + securityGroupIDs: [] + + # Cluster-level (control plane only) + controlPlaneAvailabilityZones: [] + controlPlaneOmitAvailabilityZone: false + + # Identity + identityRef: + name: "openstack" + cloudName: "openstack" + type: "Secret" + + # Container runtime + registryMirrors: [] + + # OIDC + oidcConfig: + clientID: "" + issuerURL: "" + usernameClaim: "preferred_username" + groupsClaim: "groups" + usernamePrefix: "oidc:" + groupsPrefix: "oidc:" diff --git a/providers/openstack/scs/1-33/clusteraddon.yaml b/providers/openstack/scs/1-33/clusteraddon.yaml index d346ba22..bea5fc78 100644 --- a/providers/openstack/scs/1-33/clusteraddon.yaml +++ b/providers/openstack/scs/1-33/clusteraddon.yaml @@ -19,3 +19,12 @@ addonStages: action: apply - name: ccm action: apply + AfterClusterUpgrade: + - name: cni + action: apply + - name: metrics-server + action: apply + - name: csi + action: apply + - name: ccm + action: apply diff --git a/providers/openstack/scs/1-33/stack.yaml b/providers/openstack/scs/1-33/stack.yaml index f50583d1..9047a30b 100644 --- a/providers/openstack/scs/1-33/stack.yaml +++ b/providers/openstack/scs/1-33/stack.yaml @@ -1,7 +1,6 @@ provider: openstack clusterStackName: scs -kubernetesVersion: 1.33 - +kubernetesVersion: 1.33.10 addons: ccm: 2.33.x csi: 2.33.x diff --git a/providers/openstack/scs/1-34/cluster-addon/cni/Chart.yaml b/providers/openstack/scs/1-34/cluster-addon/cni/Chart.yaml index 051b40b3..95516e98 100644 --- a/providers/openstack/scs/1-34/cluster-addon/cni/Chart.yaml +++ b/providers/openstack/scs/1-34/cluster-addon/cni/Chart.yaml @@ -7,4 +7,4 @@ dependencies: - alias: cilium name: cilium repository: https://helm.cilium.io/ - version: 1.19.1 + version: 1.19.2 diff --git a/providers/openstack/scs/1-34/cluster-class/templates/cluster-class.yaml b/providers/openstack/scs/1-34/cluster-class/templates/cluster-class.yaml index d7fbd338..c9131e4c 100644 --- a/providers/openstack/scs/1-34/cluster-class/templates/cluster-class.yaml +++ b/providers/openstack/scs/1-34/cluster-class/templates/cluster-class.yaml @@ -1,37 +1,36 @@ -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: {{ .Release.Name }}-{{ .Chart.Version }} spec: controlPlane: - ref: - apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + templateRef: + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 kind: KubeadmControlPlaneTemplate name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane machineInfrastructure: - ref: + templateRef: kind: OpenStackMachineTemplate apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane infrastructure: - ref: + templateRef: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackClusterTemplate name: {{ .Release.Name }}-{{ .Chart.Version }}-cluster workers: machineDeployments: - class: default-worker - template: - bootstrap: - ref: - apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 - kind: KubeadmConfigTemplate - name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker - infrastructure: - ref: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate - name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker + bootstrap: + templateRef: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 + kind: KubeadmConfigTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker + infrastructure: + templateRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker variables: # Image variables - name: imageName @@ -44,7 +43,7 @@ spec: If `imageIsOrc` is enabled, this name refers to an ORC image resource. If `imageIsOrc` is disabled, the name is used to filter images available in the OpenStack project. In this case, the specified image must already exist within the project. If `imageAddVersion` is enabled, the Kubernetes version will be appended to form the complete image name (e.g., imageName-v1.32.5) - default: "ubuntu-capi-image" + default: {{ .Values.variables.imageName | quote }} - name: imageIsOrc required: false schema: @@ -54,7 +53,7 @@ spec: Indicates whether the image name refers to an ORC image resource. If set to true (default), the `imageName` is interpreted as a reference to an ORC image. If set to false, the `imageName` is used to filter images in the OpenStack project instead. - default: false + default: {{ .Values.variables.imageIsOrc }} - name: imageAddVersion required: false schema: @@ -62,15 +61,7 @@ spec: type: boolean description: | Add a suffix with the Kubernetes version to the imageName. E.g. imageName-v1.32.5. - default: true - - name: disableAPIServerFloatingIP - required: false - schema: - openAPIV3Schema: - type: boolean - default: false - example: false - description: "DisableAPIServerFloatingIP controls whether a floating IP should be attached to the API server." + default: {{ .Values.variables.imageAddVersion }} # Network variables - name: networkExternalID required: false @@ -79,21 +70,23 @@ spec: type: string example: "ebfe5546-f09f-4f42-ab54-094e457d42ec" format: "uuid4" - description: "networkExternalID is the ID of an external OpenStack Network. This is necessary to get public internet to the VMs in case there are several external networks." + description: |- + ID of an external OpenStack network. Required when multiple + external networks exist and VMs need public internet access. - name: networkMTU required: false schema: openAPIV3Schema: type: integer example: 1500 - description: "networkMTU sets the maximum transmission unit (MTU) value to address fragmentation for the private network ID." + description: "MTU for the private cluster network. Set this to avoid fragmentation issues." - name: dnsNameservers required: false schema: openAPIV3Schema: type: array - description: "dnsNameservers is the list of nameservers for the OpenStack Subnet being created. Set this value when you need to create a new network/subnet which requires access to DNS." - default: ["9.9.9.9", "149.112.112.112"] + description: "DNS nameservers for the cluster subnet. Only used when a new network/subnet is created." + default: {{ .Values.variables.dnsNameservers | toJson }} example: ["9.9.9.9", "149.112.112.112"] items: type: string @@ -103,97 +96,53 @@ spec: openAPIV3Schema: type: string format: "cidr" - default: "10.8.0.0/20" + default: {{ .Values.variables.nodeCIDR | quote }} example: "10.8.0.0/20" description: |- - nodeCIDR is the OpenStack Subnet to be created. - Cluster actuator will create a network, a subnet with nodeCIDR, - and a router connected to this subnet. - If you leave this empty, no network will be created. - # Control plane - - name: controlPlaneFlavor + CIDR for the cluster subnet. CAPO will create a network, subnet, + and router. Leave empty to skip network creation. + # Machine configuration + # These apply to all nodes by default. Use topology.controlPlane.variables.overrides + # or topology.workers.machineDeployments[].variables.overrides to differentiate. + - name: flavor required: false schema: openAPIV3Schema: type: string - default: "SCS-2V-4" - example: "SCS-2V-4-20s" + default: {{ .Values.variables.flavor | quote }} + example: "SCS-2V-4" description: |- - OpenStack instance flavor for control plane nodes. - (Default: SCS-2V-4, replace by SCS-2V-4-20s or specify a controlPlaneRootDisk.) - - name: controlPlaneRootDisk + OpenStack instance flavor. Applies to all nodes by default. + Override per control plane or worker via topology variables overrides. + - name: rootDisk required: false schema: openAPIV3Schema: type: integer minimum: 0 - example: 25 - default: 50 + example: 50 + default: {{ .Values.variables.rootDisk }} description: |- - Root disk size in GiB for control-plane nodes. - OpenStack volume will be created and used instead of an ephemeral disk defined in flavor. - Should only be used for the diskless flavors (>= 20), otherwise set to 0. - - name: controlPlaneServerGroupID + Root disk size in GiB. OpenStack volume will be created and used + instead of an ephemeral disk defined in the flavor. + Set to 0 to use the flavor's ephemeral disk. + - name: serverGroupID required: false schema: openAPIV3Schema: type: string - default: "" + default: {{ .Values.variables.serverGroupID | quote }} example: "3adf4e92-bb33-4e44-8ad3-afda9dfe8ec3" - description: "The server group to assign the control plane nodes to (can be used for anti-affinity)." - - name: controlPlaneAvailabilityZones + description: "Server group for anti-affinity placement. Override per CP/worker via topology." + - name: additionalBlockDevices required: false schema: openAPIV3Schema: type: array - example: ["nova"] - description: "controlPlaneAvailabilityZones is the set of availability zones which control plane machines may be deployed to." - items: - type: string - - name: controlPlaneOmitAvailabilityZone - required: false - schema: - openAPIV3Schema: - type: boolean - example: true + default: {{ .Values.variables.additionalBlockDevices | toJson }} description: |- - controlPlaneOmitAvailabilityZone causes availability zone to be omitted when creating control plane nodes, - allowing the Nova scheduler to make a decision on which availability zone to use based on other scheduling constraints. - # Workers - - name: workerFlavor - required: false - schema: - openAPIV3Schema: - type: string - default: "SCS-4V-8" - example: "SCS-4V-8" - description: "OpenStack instance flavor for worker nodes (default: SCS-4V-8, which requires workerRootDisk)." - - name: workerRootDisk - required: false - schema: - openAPIV3Schema: - type: integer - minimum: 0 - example: 25 - default: 50 - description: |- - Root disk size in GiB for worker nodes. - OpenStack volume will be created and used instead of an ephemeral disk defined in flavor. - Should be used for the diskless flavors (>= 20), otherwise set to 0. - - name: workerServerGroupID - required: false - schema: - openAPIV3Schema: - type: string - default: "" - example: "869fe071-1e56-46a9-9166-47c9f228e297" - description: "The server group to assign the worker nodes to." - - name: workerAdditionalBlockDevices - required: false - schema: - openAPIV3Schema: - type: array - default: [] + Additional block devices (Cinder volumes) to attach to nodes. + Override per CP/worker via topology. items: type: object properties: @@ -205,60 +154,59 @@ spec: type: type: string default: "__DEFAULT__" - required: ["name"] - # Access management - - name: sshKeyName + required: ["name"] + # Cluster-level (control plane only, managed by OpenStackClusterTemplate) + - name: controlPlaneAvailabilityZones required: false schema: openAPIV3Schema: - type: string - default: "" - example: "capi-keypair" - description: "The ssh key name to inject in the nodes (for debugging)." - - name: securityGroups + type: array + default: {{ .Values.variables.controlPlaneAvailabilityZones | toJson }} + example: ["nova"] + description: "Availability zones for control plane nodes (OpenStack cluster-level setting)." + items: + type: string + - name: controlPlaneOmitAvailabilityZone required: false schema: openAPIV3Schema: - type: array - default: [] - example: ["security-group-1"] + type: boolean + default: {{ .Values.variables.controlPlaneOmitAvailabilityZone }} description: |- - The names of extra security groups to assign to worker and control plane nodes. - Will be ignored if `securityGroupIDs` is used. - items: - type: string - - name: securityGroupIDs + Omit availability zone when creating control plane nodes, letting the + Nova scheduler decide based on other scheduling constraints. + # Access management + - name: sshKeyName required: false schema: openAPIV3Schema: - format: "uuid4" - type: array - default: [] - example: ["9ae2f488-30a3-4629-bd51-07acb8eb4278"] - description: "The UUIDs of extra security groups to assign to worker and control plane nodes" - items: - type: string - - name: workerSecurityGroups + type: string + default: {{ .Values.variables.sshKeyName | quote }} + example: "capi-keypair" + description: "SSH key to inject into all nodes (for debugging)." + - name: securityGroups required: false schema: openAPIV3Schema: type: array - default: [] + default: {{ .Values.variables.securityGroups | toJson }} example: ["security-group-1"] description: |- - The names of extra security groups to assign to the worker nodes. - Will be ignored if `workerSecurityGroupIDs` is used. + Extra security groups by name for all nodes. + Ignored if securityGroupIDs is set. Override per CP/worker via topology. items: type: string - - name: workerSecurityGroupIDs + - name: securityGroupIDs required: false schema: openAPIV3Schema: format: "uuid4" type: array - default: [] + default: {{ .Values.variables.securityGroupIDs | toJson }} example: ["9ae2f488-30a3-4629-bd51-07acb8eb4278"] - description: "The UUIDs of extra security groups to assign to the worker nodes" + description: |- + Extra security groups by UUID for all nodes. + Takes precedence over securityGroups. Override per CP/worker via topology. items: type: string - name: identityRef @@ -266,27 +214,34 @@ spec: schema: openAPIV3Schema: type: object - default: {} + default: {{ .Values.variables.identityRef | toJson }} properties: name: type: string example: "openstack" - default: "openstack" + default: {{ .Values.variables.identityRef.name | quote }} description: "The name of the secret that carries the OpenStack clouds.yaml" cloudName: type: string example: "openstack" - default: "openstack" + default: {{ .Values.variables.identityRef.cloudName | quote }} description: "The name of the cloud to use from the clouds.yaml" - # Kubernetes API server + # API server + - name: disableAPIServerFloatingIP + required: false + schema: + openAPIV3Schema: + type: boolean + default: {{ .Values.variables.disableAPIServerFloatingIP }} + description: "Disable the floating IP on the API server load balancer." - name: certSANs required: false schema: openAPIV3Schema: type: array - default: [] + default: {{ .Values.variables.certSANs | toJson }} example: ["mydomain.example"] - description: "certSANs sets extra Subject Alternative Names for the API Server signing cert." + description: "Extra Subject Alternative Names for the API server TLS certificate." items: type: string - name: apiServerLoadBalancer @@ -294,33 +249,22 @@ spec: schema: openAPIV3Schema: type: string - default: "octavia-ovn" - example: "none, octavia-amphora, octavia-ovn" - description: | - Cluster-API by default places a LoadBalancer in front of the kubernetes API server. - (There are also LBs that the CCM creates for a service type LoadBalancer which are configured independently.) - This setting here is to configure the LoadBalancer that is placed in front of the apiServer. - You can choose from 3 options: - - none: - No LoadBalancer solution will be deployed - - octavia-amphora: - Uses OpenStack's LoadBalancer service Octavia (provider:amphora) - - octavia-ovn: - (default) Uses OpenStack's LoadBalancer service Octavia (provider:ovn) - - name: apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs + default: {{ .Values.variables.apiServerLoadBalancer | quote }} + example: "octavia-ovn" + description: |- + Load balancer in front of the API server. + Options: none, octavia-amphora, octavia-ovn (default). + - name: apiServerAllowedCIDRs required: false schema: openAPIV3Schema: type: array example: ["192.168.10.0/24"] description: |- - apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs restrict access to the Kubernetes API server on a network level. - Ensure that at least the outgoing IP of your Management Cluster is added to the list of allowed CIDRs. - Otherwise CAPO can’t reconcile the target Cluster correctly. - This requires amphora as load balancer provider in version >= v2.12. + Restrict access to the API server to these CIDRs (network-level ACL). + Requires amphora as load balancer provider (CAPO >= v2.12). + Ensure the management cluster's outgoing IP is included, + otherwise CAPO cannot reconcile the workload cluster. items: type: string - name: oidcConfig @@ -344,7 +288,7 @@ spec: usernameClaim: type: string example: "preferred_username" - default: "preferred_username" + default: {{ .Values.variables.oidcConfig.usernameClaim | quote }} description: >- JWT claim to use as the user name. By default sub, which is expected to be a unique identifier of the end user. Admins can choose @@ -354,12 +298,12 @@ spec: groupsClaim: type: string example: "groups" - default: "groups" + default: {{ .Values.variables.oidcConfig.groupsClaim | quote }} description: "JWT claim to use as the user's group. If the claim is present it must be an array of strings." usernamePrefix: type: string example: "oidc:" - default: "oidc:" + default: {{ .Values.variables.oidcConfig.usernamePrefix | quote }} description: >- Prefix prepended to username claims to prevent clashes with existing names (such as system: users). For example, the value @@ -370,11 +314,38 @@ spec: groupsPrefix: type: string example: "oidc:" - default: "oidc:" + default: {{ .Values.variables.oidcConfig.groupsPrefix | quote }} description: >- Prefix prepended to group claims to prevent clashes with existing names (such as system: groups). For example, the value oidc: will create group names like oidc:engineering and oidc:infra. + # Container runtime + - name: registryMirrors + required: false + schema: + openAPIV3Schema: + type: array + default: {{ .Values.variables.registryMirrors | toJson }} + description: "Registry mirrors for upstream container registries. Configures both containerd and CRI-O to pull through a mirror." + items: + type: object + properties: + hostnameUpstream: + type: string + example: "docker.io" + description: "The hostname of the upstream registry." + urlUpstream: + type: string + example: "https://registry-1.docker.io" + description: "The server URL of the upstream registry." + urlMirror: + type: string + example: "https://registry.example.com/v2/dockerhub" + description: "The URL of the mirror registry." + certMirror: + type: string + example: "" + description: "TLS certificate of the mirror in PEM format (optional)." # # Patches # @@ -414,9 +385,9 @@ spec: - op: add path: "/spec/template/spec/apiServerLoadBalancer/provider" value: "ovn" - - name: apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs - description: "Takes care of the patches that should be applied when variable allowedCIDRs is set." - enabledIf: {{ `'{{ and .apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs (eq .apiServerLoadBalancer "octavia-amphora") }}'` }} + - name: apiServerAllowedCIDRs + description: "Restricts API server access to the given CIDRs (requires amphora LB)." + enabledIf: {{ `'{{ and .apiServerAllowedCIDRs (eq .apiServerLoadBalancer "octavia-amphora") }}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 @@ -427,7 +398,7 @@ spec: - op: add path: "/spec/template/spec/apiServerLoadBalancer/allowedCIDRs" valueFrom: - variable: apiServerLoadBalancerOctaviaAmphoraAllowedCIDRs + variable: apiServerAllowedCIDRs - name: networkExternalID description: "Sets the ID of an external OpenStack Network. This is necessary to get public internet to the VMs." enabledIf: {{ `'{{ if .networkExternalID }}true{{end}}'` }} @@ -537,10 +508,13 @@ spec: valueFrom: variable: disableAPIServerFloatingIP # - # Patches for control plane's OpenStackMachineTemplate resources. - # Note: Control plane patches are only applied when the control plane is managed by Kubeadm. + # Patches for OpenStackMachineTemplate resources (image). + # Image patches must stay separate because they use different builtin variables: + # - .builtin.controlPlane.version for CP + # - .builtin.machineDeployment.version for workers + # - name: controlPlaneImage - description: "Sets the OpenStack image name that is used for creating the control plane servers." + description: "Sets the OpenStack image for control plane nodes." definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 @@ -554,120 +528,13 @@ spec: template: | {{ `{{ if .imageIsOrc }}imageRef{{ else }}filter{{ end }}` }}: name: {{ `{{ .imageName }}{{ if .imageAddVersion }}-{{ .builtin.controlPlane.version }}{{ end }}` }} - - name: controlPlaneFlavor - description: "Sets the openstack instance flavor for the KubeadmControlPlane." - enabledIf: {{ `'{{ ne .controlPlaneFlavor "" }}'` }} - definitions: - - selector: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate - matchResources: - controlPlane: true - jsonPatches: - - op: replace - path: "/spec/template/spec/flavor" - valueFrom: - variable: controlPlaneFlavor - - name: controlPlaneRootDisk - description: "Sets the root disk size in GiB for control-plane nodes." - enabledIf: {{ `'{{ if .controlPlaneRootDisk }}true{{end}}'` }} - definitions: - - selector: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate - matchResources: - controlPlane: true - jsonPatches: - - op: add - path: "/spec/template/spec/rootVolume" - valueFrom: - template: | - sizeGiB: {{ `{{ .controlPlaneRootDisk }}` }} - - name: controlPlaneServerGroupID - description: "Sets the server group to assign the control plane nodes to." - enabledIf: {{ `'{{ ne .controlPlaneServerGroupID "" }}'` }} - definitions: - - selector: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate - matchResources: - controlPlane: true - jsonPatches: - - op: add - path: "/spec/template/spec/serverGroup" - valueFrom: - template: | - id: {{ `{{ .controlPlaneServerGroupID }}` }} - # - # Patches for control plane's as well as worker's OpenStackMachineTemplate resources. - # Note: Control plane patches are only applied when the control plane is managed by Kubeadm. - # - # Note: The securityGroups patch must be placed before securityGroupIDs, workerSecurityGroups, and workerSecurityGroupIDs. - # The patch order ensures the last applied patch overwrites previous ones. - - name: securityGroups - description: "Sets the list of the openstack security groups for the worker and the control plane instances." - enabledIf: {{ `'{{ if .securityGroups }}true{{end}}'` }} - definitions: - - selector: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate - matchResources: - controlPlane: true - machineDeploymentClass: - names: - - default-worker - jsonPatches: - - op: add - path: "/spec/template/spec/securityGroups" - valueFrom: - template: {{ `'[ {{ range .securityGroups }} { filter: { name: {{ . }}}}, {{ end }} ]'` }} - # Note: The securityGroupIDs patch must be placed before workerSecurityGroups, workerSecurityGroupIDs and after securityGroupIDs. - # The patch order ensures the last applied patch overwrites previous ones. - - name: securityGroupIDs - description: "Sets the list of the openstack security groups for the worker and the control plane instances by UUID." - enabledIf: {{ `'{{ if .securityGroupIDs }}true{{end}}'` }} - definitions: - - selector: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate - matchResources: - controlPlane: true - machineDeploymentClass: - names: - - default-worker - jsonPatches: - - op: add - path: "/spec/template/spec/securityGroups" - valueFrom: - template: {{ `'[ {{ range .securityGroupIDs }} { id: {{ . }} }, {{ end }} ]'` }} - - name: sshKeyName - description: "Sets the ssh key to inject in the nodes." - enabledIf: {{ `'{{ ne .sshKeyName "" }}'` }} - definitions: - - selector: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: OpenStackMachineTemplate - matchResources: - controlPlane: true - machineDeploymentClass: - names: - - default-worker - jsonPatches: - - op: add - path: "/spec/template/spec/sshKeyName" - valueFrom: - variable: sshKeyName - # - # Patches for worker's OpenStackMachineTemplate resources. - # - name: workerImage - description: "Sets the OpenStack image name that is used for creating the worker servers." + description: "Sets the OpenStack image for worker nodes." definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate matchResources: - controlPlane: false machineDeploymentClass: names: - default-worker @@ -678,15 +545,20 @@ spec: template: | {{ `{{ if .imageIsOrc }}imageRef{{ else }}filter{{ end }}` }}: name: {{ `{{ .imageName }}{{ if .imageAddVersion }}-{{ .builtin.machineDeployment.version }}{{ end }}` }} - - name: workerFlavor - description: "Sets the openstack instance flavor for the worker nodes." - enabledIf: {{ `'{{ ne .workerFlavor "" }}'` }} + # + # Unified machine patches — target both CP and workers. + # Users can override per CP/worker via topology.controlPlane.variables.overrides + # and topology.workers.machineDeployments[].variables.overrides. + # + - name: flavor + description: "Sets the OpenStack instance flavor for all nodes." + enabledIf: {{ `'{{ ne .flavor "" }}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate matchResources: - controlPlane: false + controlPlane: true machineDeploymentClass: names: - default-worker @@ -694,16 +566,16 @@ spec: - op: replace path: "/spec/template/spec/flavor" valueFrom: - variable: workerFlavor - - name: workerRootDisk - description: "Sets the root disk size in GiB for worker nodes." - enabledIf: {{ `'{{ if .workerRootDisk }}true{{end}}'` }} + variable: flavor + - name: rootDisk + description: "Sets the root disk size in GiB. 0 means use ephemeral disk from flavor." + enabledIf: {{ `'{{ if .rootDisk }}true{{end}}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate matchResources: - controlPlane: false + controlPlane: true machineDeploymentClass: names: - default-worker @@ -712,16 +584,16 @@ spec: path: "/spec/template/spec/rootVolume" valueFrom: template: | - sizeGiB: {{ `{{ .workerRootDisk }}` }} - - name: workerServerGroupID - description: "Sets the server group to assign the worker nodes to." - enabledIf: {{ `'{{ ne .workerServerGroupID "" }}'` }} + sizeGiB: {{ `{{ .rootDisk }}` }} + - name: serverGroupID + description: "Sets the server group for anti-affinity." + enabledIf: {{ `'{{ ne .serverGroupID "" }}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate matchResources: - controlPlane: false + controlPlane: true machineDeploymentClass: names: - default-worker @@ -730,15 +602,16 @@ spec: path: "/spec/template/spec/serverGroup" valueFrom: template: | - id: {{ `{{ .workerServerGroupID }}` }} - - name: workerAdditionalBlockDevices - enabledIf: {{ `'{{ if .workerAdditionalBlockDevices }}true{{end}}'` }} + id: {{ `{{ .serverGroupID }}` }} + - name: additionalBlockDevices + description: "Attaches additional Cinder volumes to nodes." + enabledIf: {{ `'{{ if .additionalBlockDevices }}true{{end}}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate matchResources: - controlPlane: false + controlPlane: true machineDeploymentClass: names: - default-worker @@ -747,7 +620,7 @@ spec: path: /spec/template/spec/additionalBlockDevices valueFrom: template: | - {{ `{{- range .workerAdditionalBlockDevices }}` }} + {{ `{{- range .additionalBlockDevices }}` }} - name: {{ `{{ .name }}` }} sizeGiB: {{ `{{ .sizeGiB }}` }} storage: @@ -755,17 +628,19 @@ spec: volume: type: {{ `{{ .type }}` }} {{ `{{- end }}` }} - # Note: The workerSecurityGroups patch must be placed before workerSecurityGroupIDs and after securityGroups and securityGroupIDs. - # The patch order ensures the last applied patch overwrites previous ones. - - name: workerSecurityGroups - description: "Sets the list of the openstack security groups for the worker instances." - enabledIf: {{ `'{{ if .workerSecurityGroups }}true{{end}}'` }} + # + # Access patches — target both CP and workers. + # securityGroupIDs takes precedence over securityGroups (last patch wins). + # + - name: securityGroups + description: "Sets security groups by name for all nodes." + enabledIf: {{ `'{{ if .securityGroups }}true{{end}}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate matchResources: - controlPlane: false + controlPlane: true machineDeploymentClass: names: - default-worker @@ -773,18 +648,16 @@ spec: - op: add path: "/spec/template/spec/securityGroups" valueFrom: - template: {{ `'[ {{ range .workerSecurityGroups }} { filter: { name: {{ . }}}}, {{ end }} ]'` }} - # Note: The workerSecurityGroupIDs patch must be placed after securityGroups, securityGroupIDs and workerSecurityGroupIDs. - # The patch order ensures the last applied patch overwrites previous ones. - - name: workerSecurityGroupIDs - description: "Sets the list of the openstack security groups for the worker instances by UUID." - enabledIf: {{ `'{{ if .workerSecurityGroupIDs }}true{{end}}'` }} + template: {{ `'[ {{ range .securityGroups }} { filter: { name: {{ . }}}}, {{ end }} ]'` }} + - name: securityGroupIDs + description: "Sets security groups by UUID for all nodes. Takes precedence over securityGroups." + enabledIf: {{ `'{{ if .securityGroupIDs }}true{{end}}'` }} definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: OpenStackMachineTemplate matchResources: - controlPlane: false + controlPlane: true machineDeploymentClass: names: - default-worker @@ -792,14 +665,31 @@ spec: - op: add path: "/spec/template/spec/securityGroups" valueFrom: - template: {{ `'[ {{ range .workerSecurityGroupIDs }} { id: {{ . }} }, {{ end }} ]'` }} + template: {{ `'[ {{ range .securityGroupIDs }} { id: {{ . }} }, {{ end }} ]'` }} + - name: sshKeyName + description: "Sets the SSH key to inject into all nodes." + enabledIf: {{ `'{{ ne .sshKeyName "" }}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: true + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/sshKeyName" + valueFrom: + variable: sshKeyName # - name: certSANs description: "certSANs sets extra Subject Alternative Names for the API Server signing cert." enabledIf: {{ `'{{ if .certSANs }}true{{end}}'` }} definitions: - selector: - apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 kind: KubeadmControlPlaneTemplate matchResources: controlPlane: true @@ -813,32 +703,161 @@ spec: enabledIf: {{ `'{{ if and .oidcConfig .oidcConfig.clientID .oidcConfig.issuerURL }}true{{end}}'` }} definitions: - selector: - apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 kind: KubeadmControlPlaneTemplate matchResources: controlPlane: true jsonPatches: - op: add - path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-client-id" + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" valueFrom: - variable: oidcConfig.clientID + template: | + name: oidc-client-id + value: {{ `'{{ .oidcConfig.clientID }}'` }} - op: add - path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-issuer-url" + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" valueFrom: - variable: oidcConfig.issuerURL + template: | + name: oidc-issuer-url + value: {{ `'{{ .oidcConfig.issuerURL }}'` }} - op: add - path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-username-claim" + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" valueFrom: - variable: oidcConfig.usernameClaim + template: | + name: oidc-username-claim + value: {{ `'{{ .oidcConfig.usernameClaim }}'` }} - op: add - path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-groups-claim" + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" valueFrom: - variable: oidcConfig.groupsClaim + template: | + name: oidc-groups-claim + value: {{ `'{{ .oidcConfig.groupsClaim }}'` }} - op: add - path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-username-prefix" + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" valueFrom: - variable: oidcConfig.usernamePrefix + template: | + name: oidc-username-prefix + value: {{ `'{{ .oidcConfig.usernamePrefix }}'` }} - op: add - path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/oidc-groups-prefix" + path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-" valueFrom: - variable: oidcConfig.groupsPrefix + template: | + name: oidc-groups-prefix + value: {{ `'{{ .oidcConfig.groupsPrefix }}'` }} + # + # Registry mirror patches + # + - name: registryMirrorsControlPlane + description: "Configure registry mirrors on control plane nodes (containerd + CRI-O)." + enabledIf: {{ `'{{ if .registryMirrors }}true{{end}}'` }} + definitions: + - selector: + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 + kind: KubeadmControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/files" + valueFrom: + template: | + {{ `{{- range $r := .registryMirrors }}` }} + - content: | + server = "{{ `{{ $r.urlUpstream }}` }}" + [host."{{ `{{ $r.urlMirror }}` }}"] + capabilities = ["pull","resolve"] + override_path = true + owner: root:root + path: /etc/containerd/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/hosts.toml + permissions: "0644" + - content: | + [[registry]] + prefix = "{{ `{{ $r.hostnameUpstream }}` }}" + location = "{{ `{{ $r.hostnameUpstream }}` }}" + [[registry.mirror]] + location = "{{ `{{ $r.urlMirror }}` }}" + owner: root:root + path: /etc/containers/registries.conf.d/50-mirror-{{ `{{ $r.hostnameUpstream }}` }}.conf + permissions: "0644" + {{ `{{- if $r.certMirror }}` }} + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containerd/certs/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containers/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + {{ `{{- end }}` }} + {{ `{{- end }}` }} + - path: /etc/kube-proxy-config.yaml + owner: root:root + permissions: "0644" + content: | + --- + apiVersion: kubeproxy.config.k8s.io/v1alpha1 + kind: KubeProxyConfiguration + metricsBindAddress: "0.0.0.0:10249" + + - path: /etc/kube-proxy-patch.sh + owner: root:root + permissions: "0755" + content: | + #!/usr/bin/env bash + set -euo pipefail + dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + kubeadm_file="/etc/kubeadm.yml" + [[ -f ${kubeadm_file} ]] || kubeadm_file="/run/kubeadm/kubeadm.yaml" + [[ -f ${kubeadm_file} ]] || exit 0 + [[ -f ${dir}/kube-proxy-config.yaml ]] || exit 0 + cat "${dir}/kube-proxy-config.yaml" >> "${kubeadm_file}" + rm -f "${dir}/kube-proxy-config.yaml" + echo success > /tmp/kube-proxy-patch + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/preKubeadmCommands/-" + value: "bash /etc/kube-proxy-patch.sh" + - name: registryMirrorsWorker + description: "Configure registry mirrors on worker nodes (containerd + CRI-O)." + enabledIf: {{ `'{{ if .registryMirrors }}true{{end}}'` }} + definitions: + - selector: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 + kind: KubeadmConfigTemplate + matchResources: + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/files" + valueFrom: + template: | + {{ `{{- range $r := .registryMirrors }}` }} + - content: | + server = "{{ `{{ $r.urlUpstream }}` }}" + [host."{{ `{{ $r.urlMirror }}` }}"] + capabilities = ["pull","resolve"] + override_path = true + owner: root:root + path: /etc/containerd/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/hosts.toml + permissions: "0644" + - content: | + [[registry]] + prefix = "{{ `{{ $r.hostnameUpstream }}` }}" + location = "{{ `{{ $r.hostnameUpstream }}` }}" + [[registry.mirror]] + location = "{{ `{{ $r.urlMirror }}` }}" + owner: root:root + path: /etc/containers/registries.conf.d/50-mirror-{{ `{{ $r.hostnameUpstream }}` }}.conf + permissions: "0644" + {{ `{{- if $r.certMirror }}` }} + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containerd/certs/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + - content: "{{ `{{ $r.certMirror }}` }}" + owner: root:root + path: /etc/containers/certs.d/{{ `{{ $r.hostnameUpstream }}` }}/ca.crt + permissions: "0644" + {{ `{{- end }}` }} + {{ `{{- end }}` }} diff --git a/providers/openstack/scs/1-34/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml b/providers/openstack/scs/1-34/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml index 4c1494ed..732d78b3 100644 --- a/providers/openstack/scs/1-34/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml +++ b/providers/openstack/scs/1-34/cluster-class/templates/kubeadm-config-template-worker-openstack.yaml @@ -1,4 +1,4 @@ -apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 kind: KubeadmConfigTemplate metadata: name: {{ .Release.Name }}-{{ .Chart.Version }}-default-worker @@ -8,6 +8,8 @@ spec: joinConfiguration: nodeRegistration: kubeletExtraArgs: - cloud-provider: external - provider-id: 'openstack:///{{ `{{ instance_id }}` }}' + - name: cloud-provider + value: external + - name: provider-id + value: 'openstack:///{{ `{{ instance_id }}` }}' name: '{{ `{{ local_hostname }}` }}' diff --git a/providers/openstack/scs/1-34/cluster-class/templates/kubeadm-control-plane-template.yaml b/providers/openstack/scs/1-34/cluster-class/templates/kubeadm-control-plane-template.yaml index 767eac68..b199f36c 100644 --- a/providers/openstack/scs/1-34/cluster-class/templates/kubeadm-control-plane-template.yaml +++ b/providers/openstack/scs/1-34/cluster-class/templates/kubeadm-control-plane-template.yaml @@ -1,4 +1,4 @@ -apiVersion: controlplane.cluster.x-k8s.io/v1beta1 +apiVersion: controlplane.cluster.x-k8s.io/v1beta2 kind: KubeadmControlPlaneTemplate metadata: name: {{ .Release.Name }}-{{ .Chart.Version }}-control-plane @@ -7,26 +7,42 @@ spec: spec: kubeadmConfigSpec: clusterConfiguration: - apiServer: {} controllerManager: extraArgs: - cloud-provider: external - bind-address: 0.0.0.0 - secure-port: "10257" + - name: cloud-provider + value: external + - name: bind-address + value: 0.0.0.0 + - name: secure-port + value: "10257" + - name: profiling + value: "false" + - name: terminated-pod-gc-threshold + value: "100" scheduler: extraArgs: - bind-address: 0.0.0.0 - secure-port: "10259" + - name: bind-address + value: 0.0.0.0 + - name: secure-port + value: "10259" + - name: profiling + value: "false" etcd: local: dataDir: /var/lib/etcd extraArgs: - listen-metrics-urls: http://0.0.0.0:2381 - auto-compaction-mode: periodic - auto-compaction-retention: 8h - election-timeout: "2500" - heartbeat-interval: "250" - snapshot-count: "6400" + - name: listen-metrics-urls + value: http://0.0.0.0:2381 + - name: auto-compaction-mode + value: periodic + - name: auto-compaction-retention + value: 8h + - name: election-timeout + value: "2500" + - name: heartbeat-interval + value: "250" + - name: snapshot-count + value: "6400" files: - content: | --- @@ -78,12 +94,16 @@ spec: initConfiguration: nodeRegistration: kubeletExtraArgs: - cloud-provider: external - provider-id: 'openstack:///{{ `{{ instance_id }}` }}' + - name: cloud-provider + value: external + - name: provider-id + value: 'openstack:///{{ `{{ instance_id }}` }}' name: '{{ `{{ local_hostname }}` }}' joinConfiguration: nodeRegistration: kubeletExtraArgs: - cloud-provider: external - provider-id: 'openstack:///{{ `{{ instance_id }}` }}' + - name: cloud-provider + value: external + - name: provider-id + value: 'openstack:///{{ `{{ instance_id }}` }}' name: '{{ `{{ local_hostname }}` }}' diff --git a/providers/openstack/scs/1-34/cluster-class/templates/openstack-cluster-template.yaml b/providers/openstack/scs/1-34/cluster-class/templates/openstack-cluster-template.yaml index 9d03326f..3cc03fbb 100644 --- a/providers/openstack/scs/1-34/cluster-class/templates/openstack-cluster-template.yaml +++ b/providers/openstack/scs/1-34/cluster-class/templates/openstack-cluster-template.yaml @@ -43,3 +43,49 @@ spec: portRangeMax: 4244 protocol: tcp description: "Allow Hubble traffic for Cilium" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus kube-proxy exporter + portRangeMin: 10249 + portRangeMax: 10249 + protocol: tcp + description: "Allow Prometheus traffic for kube-proxy exporter" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus kube-controller-manager exporter + portRangeMin: 10257 + portRangeMax: 10257 + protocol: tcp + description: "Allow Prometheus traffic for kube-controller-manager exporter" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus kube-scheduler exporter + portRangeMin: 10259 + portRangeMax: 10259 + protocol: tcp + description: "Allow Prometheus traffic for kube-scheduler exporter" + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus node exporter + portRangeMin: 9100 + portRangeMax: 9100 + protocol: tcp + description: "Allow Prometheus traffic for scraping node exporter" + controlPlaneNodesSecurityGroupRules: + - remoteManagedGroups: + - worker + direction: ingress + etherType: IPv4 + name: Prometheus etcd exporter + portRangeMin: 2381 + portRangeMax: 2381 + protocol: tcp + description: "Allow Prometheus traffic for scraping etcd exporter" \ No newline at end of file diff --git a/providers/openstack/scs/1-34/cluster-class/values.yaml b/providers/openstack/scs/1-34/cluster-class/values.yaml index e69de29b..1344cf85 100644 --- a/providers/openstack/scs/1-34/cluster-class/values.yaml +++ b/providers/openstack/scs/1-34/cluster-class/values.yaml @@ -0,0 +1,50 @@ +# ClusterClass variable defaults +# These are referenced by the ClusterClass template and can be overridden per deployment. +# Variables apply to all nodes by default. Use topology.controlPlane.variables.overrides +# or topology.workers.machineDeployments[].variables.overrides to differentiate. +variables: + # Image configuration + imageName: "ubuntu-capi-image" + imageIsOrc: false + imageAddVersion: true + + # API server + disableAPIServerFloatingIP: false + apiServerLoadBalancer: "octavia-ovn" + certSANs: [] + + # Network + dnsNameservers: ["9.9.9.9", "149.112.112.112"] + nodeCIDR: "10.8.0.0/20" + + # Machine configuration (override per CP/worker via topology) + flavor: "SCS-2V-4" + rootDisk: 50 + serverGroupID: "" + additionalBlockDevices: [] + + # Access management + sshKeyName: "" + securityGroups: [] + securityGroupIDs: [] + + # Cluster-level (control plane only) + controlPlaneAvailabilityZones: [] + controlPlaneOmitAvailabilityZone: false + + # Identity + identityRef: + name: "openstack" + cloudName: "openstack" + + # Container runtime + registryMirrors: [] + + # OIDC + oidcConfig: + clientID: "" + issuerURL: "" + usernameClaim: "preferred_username" + groupsClaim: "groups" + usernamePrefix: "oidc:" + groupsPrefix: "oidc:" diff --git a/providers/openstack/scs/1-34/clusteraddon.yaml b/providers/openstack/scs/1-34/clusteraddon.yaml index d346ba22..bea5fc78 100644 --- a/providers/openstack/scs/1-34/clusteraddon.yaml +++ b/providers/openstack/scs/1-34/clusteraddon.yaml @@ -19,3 +19,12 @@ addonStages: action: apply - name: ccm action: apply + AfterClusterUpgrade: + - name: cni + action: apply + - name: metrics-server + action: apply + - name: csi + action: apply + - name: ccm + action: apply diff --git a/providers/openstack/scs/1-34/stack.yaml b/providers/openstack/scs/1-34/stack.yaml index 3d219ba0..45e8730d 100644 --- a/providers/openstack/scs/1-34/stack.yaml +++ b/providers/openstack/scs/1-34/stack.yaml @@ -1,7 +1,6 @@ provider: openstack clusterStackName: scs -kubernetesVersion: 1.34 - +kubernetesVersion: 1.34.6 addons: ccm: 2.34.x csi: 2.34.x diff --git a/providers/openstack/scs/1-35/cluster-addon/cni/Chart.yaml b/providers/openstack/scs/1-35/cluster-addon/cni/Chart.yaml index 051b40b3..95516e98 100644 --- a/providers/openstack/scs/1-35/cluster-addon/cni/Chart.yaml +++ b/providers/openstack/scs/1-35/cluster-addon/cni/Chart.yaml @@ -7,4 +7,4 @@ dependencies: - alias: cilium name: cilium repository: https://helm.cilium.io/ - version: 1.19.1 + version: 1.19.2 diff --git a/providers/openstack/scs/1-35/cluster-class/templates/cluster-class.yaml b/providers/openstack/scs/1-35/cluster-class/templates/cluster-class.yaml index 07f42c60..c9131e4c 100644 --- a/providers/openstack/scs/1-35/cluster-class/templates/cluster-class.yaml +++ b/providers/openstack/scs/1-35/cluster-class/templates/cluster-class.yaml @@ -546,7 +546,7 @@ spec: {{ `{{ if .imageIsOrc }}imageRef{{ else }}filter{{ end }}` }}: name: {{ `{{ .imageName }}{{ if .imageAddVersion }}-{{ .builtin.machineDeployment.version }}{{ end }}` }} # - # Unified machine patches -- target both CP and workers. + # Unified machine patches — target both CP and workers. # Users can override per CP/worker via topology.controlPlane.variables.overrides # and topology.workers.machineDeployments[].variables.overrides. # @@ -629,7 +629,7 @@ spec: type: {{ `{{ .type }}` }} {{ `{{- end }}` }} # - # Access patches -- target both CP and workers. + # Access patches — target both CP and workers. # securityGroupIDs takes precedence over securityGroups (last patch wins). # - name: securityGroups @@ -790,6 +790,32 @@ spec: permissions: "0644" {{ `{{- end }}` }} {{ `{{- end }}` }} + - path: /etc/kube-proxy-config.yaml + owner: root:root + permissions: "0644" + content: | + --- + apiVersion: kubeproxy.config.k8s.io/v1alpha1 + kind: KubeProxyConfiguration + metricsBindAddress: "0.0.0.0:10249" + + - path: /etc/kube-proxy-patch.sh + owner: root:root + permissions: "0755" + content: | + #!/usr/bin/env bash + set -euo pipefail + dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + kubeadm_file="/etc/kubeadm.yml" + [[ -f ${kubeadm_file} ]] || kubeadm_file="/run/kubeadm/kubeadm.yaml" + [[ -f ${kubeadm_file} ]] || exit 0 + [[ -f ${dir}/kube-proxy-config.yaml ]] || exit 0 + cat "${dir}/kube-proxy-config.yaml" >> "${kubeadm_file}" + rm -f "${dir}/kube-proxy-config.yaml" + echo success > /tmp/kube-proxy-patch + - op: add + path: "/spec/template/spec/kubeadmConfigSpec/preKubeadmCommands/-" + value: "bash /etc/kube-proxy-patch.sh" - name: registryMirrorsWorker description: "Configure registry mirrors on worker nodes (containerd + CRI-O)." enabledIf: {{ `'{{ if .registryMirrors }}true{{end}}'` }} diff --git a/providers/openstack/scs/1-35/cluster-class/templates/openstack-cluster-template.yaml b/providers/openstack/scs/1-35/cluster-class/templates/openstack-cluster-template.yaml index 6d96089f..3cc03fbb 100644 --- a/providers/openstack/scs/1-35/cluster-class/templates/openstack-cluster-template.yaml +++ b/providers/openstack/scs/1-35/cluster-class/templates/openstack-cluster-template.yaml @@ -88,4 +88,4 @@ spec: portRangeMin: 2381 portRangeMax: 2381 protocol: tcp - description: "Allow Prometheus traffic for scraping etcd exporter" + description: "Allow Prometheus traffic for scraping etcd exporter" \ No newline at end of file diff --git a/providers/openstack/scs/1-35/stack.yaml b/providers/openstack/scs/1-35/stack.yaml index e302c418..0f51ebd8 100644 --- a/providers/openstack/scs/1-35/stack.yaml +++ b/providers/openstack/scs/1-35/stack.yaml @@ -1,7 +1,6 @@ provider: openstack clusterStackName: scs -kubernetesVersion: 1.35 - +kubernetesVersion: 1.35.3 addons: ccm: 2.35.x csi: 2.35.x diff --git a/providers/openstack/scs/image-manager.yaml b/providers/openstack/scs/image-manager.yaml index 7a28a5b5..869d4082 100644 --- a/providers/openstack/scs/image-manager.yaml +++ b/providers/openstack/scs/image-manager.yaml @@ -25,15 +25,15 @@ images: tags: - clusterstacks versions: - - version: 'v1.32.12' - url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2204-kube-v1.32/ubuntu-2204-kube-v1.32.12.qcow2 - checksum: "sha256:cb39992db553b7106c4b127cb3e9cd6418ce830d26aad2b5ab364d1dcc222fa6" - - version: 'v1.33.8' - url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2404-kube-v1.33/ubuntu-2404-kube-v1.33.8.qcow2 - checksum: "sha256:203f5635447f4a59e220bfb649c40eb86f065c051650e4ea1cd11706c0d1f5be" - - version: 'v1.34.4' - url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2404-kube-v1.34/ubuntu-2404-kube-v1.34.4.qcow2 - checksum: "sha256:df3f26b0026a1a9ca3b681df2d8675a7341e138dca6f2326592975bb7c0fe792" - - version: 'v1.35.1' - url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2404-kube-v1.35/ubuntu-2404-kube-v1.35.1.qcow2 - checksum: "sha256:5d424380e2fa85b7a51ad1e955e8efb09ca460b0e80d47cf2f36c02d94dc3f03" + - version: 'v1.32.13' + url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2204-kube-v1.32/ubuntu-2204-kube-v1.32.13.qcow2 + checksum: "sha256:bd1c5664e2bc34cc37a424391a44511fd12becb4c2c51e9f6b102e90d992d5fa" + - version: 'v1.33.10' + url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2404-kube-v1.33/ubuntu-2404-kube-v1.33.10.qcow2 + checksum: "sha256:efc3817b565c407710724b9d7b51cbb433638aad6b613b64843da28d58a777aa" + - version: 'v1.34.6' + url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2404-kube-v1.34/ubuntu-2404-kube-v1.34.6.qcow2 + checksum: "sha256:c59fb893be8320d7112473290358320fad2756e3e31dce4f90ae8bda9d289a3d" + - version: 'v1.35.3' + url: https://nbg1.your-objectstorage.com/osism/openstack-k8s-capi-images/ubuntu-2404-kube-v1.35/ubuntu-2404-kube-v1.35.3.qcow2 + checksum: "sha256:2892b11dcac515f8f2fcb6a1b60f04a45f7d7ec8fdb2a3f3ccc255deb72d69b4" From 0df4ee92a618bea8fa01ed51edac3eaa2d3bc694 Mon Sep 17 00:00:00 2001 From: Nils Arnold Date: Wed, 15 Apr 2026 12:53:13 +0200 Subject: [PATCH 08/11] feat: enable Cilium native routing and kube-proxy replacement Signed-off-by: Nils Arnold --- .../scs/1-35/cluster-addon/cni/overwrite.yaml | 5 ++ .../scs/1-35/cluster-addon/cni/values.yaml | 18 +++++ .../templates/cluster-class.yaml | 79 +++++++++++++------ .../kubeadm-control-plane-template.yaml | 52 +----------- .../templates/openstack-cluster-template.yaml | 38 +++------ ...nstack-machine-template-control-plane.yaml | 1 + .../openstack-machine-template-worker.yaml | 1 + 7 files changed, 95 insertions(+), 99 deletions(-) create mode 100644 providers/openstack/scs/1-35/cluster-addon/cni/overwrite.yaml diff --git a/providers/openstack/scs/1-35/cluster-addon/cni/overwrite.yaml b/providers/openstack/scs/1-35/cluster-addon/cni/overwrite.yaml new file mode 100644 index 00000000..001e1949 --- /dev/null +++ b/providers/openstack/scs/1-35/cluster-addon/cni/overwrite.yaml @@ -0,0 +1,5 @@ +values: | + cilium: + k8sServiceHost: "{{ .Cluster.spec.controlPlaneEndpoint.host }}" + k8sServicePort: "{{ .Cluster.spec.controlPlaneEndpoint.port }}" + ipv4NativeRoutingCIDR: "{{ index .Cluster.spec.clusterNetwork.pods.cidrBlocks 0 }}" \ No newline at end of file diff --git a/providers/openstack/scs/1-35/cluster-addon/cni/values.yaml b/providers/openstack/scs/1-35/cluster-addon/cni/values.yaml index 8a312f0c..03fa1a33 100644 --- a/providers/openstack/scs/1-35/cluster-addon/cni/values.yaml +++ b/providers/openstack/scs/1-35/cluster-addon/cni/values.yaml @@ -6,8 +6,26 @@ cilium: sessionAffinity: true sctp: enabled: true + bpf: + masquerade: true + endpointRoutes: + enabled: true + bandwidthManager: + enabled: true + egressGateway: + enabled: true ipam: mode: "kubernetes" + kubeProxyReplacement: true + kubeProxyReplacementHealthzBindAddr: "0.0.0.0:10256" + routingMode: native + autoDirectNodeRoutes: true + directRoutingSkipUnreachable: true + localRedirectPolicies: + enabled: true + k8s: + requireIPv4PodCIDR: true + policyDenyResponse: icmp gatewayAPI: enabled: true secretsNamespace: diff --git a/providers/openstack/scs/1-35/cluster-class/templates/cluster-class.yaml b/providers/openstack/scs/1-35/cluster-class/templates/cluster-class.yaml index c9131e4c..0f56d662 100644 --- a/providers/openstack/scs/1-35/cluster-class/templates/cluster-class.yaml +++ b/providers/openstack/scs/1-35/cluster-class/templates/cluster-class.yaml @@ -507,6 +507,25 @@ spec: path: "/spec/template/spec/disableAPIServerFloatingIP" valueFrom: variable: disableAPIServerFloatingIP + - name: podCIDROpenStackCluster + description: "Configures pod-network CIDR based security-group rules for native routing." + enabledIf: {{ `'{{- if .builtin.cluster.network.pods -}}true{{- end -}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: "/spec/template/spec/managedSecurityGroups/allNodesSecurityGroupRules/-" + valueFrom: + template: | + remoteIPPrefix: {{ `{{ index .builtin.cluster.network.pods 0 }}` }} + direction: ingress + etherType: IPv4 + name: Pod traffic (Cilium native routing) + description: Allow pod-to-pod traffic for Cilium native routing # # Patches for OpenStackMachineTemplate resources (image). # Image patches must stay separate because they use different builtin variables: @@ -628,6 +647,40 @@ spec: volume: type: {{ `{{ .type }}` }} {{ `{{- end }}` }} + - name: podCIDRAllowedAddressPairsControlPlane + description: "Allows pod-network source addresses on control-plane ports for native routing." + enabledIf: {{ `'{{- if .builtin.cluster.network.pods -}}true{{- end -}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: "/spec/template/spec/ports/-" + valueFrom: + template: | + allowedAddressPairs: + - ipAddress: {{ `{{ index .builtin.cluster.network.pods 0 }}` }} + - name: podCIDRAllowedAddressPairsWorker + description: "Allows pod-network source addresses on worker ports for native routing." + enabledIf: {{ `'{{- if .builtin.cluster.network.pods -}}true{{- end -}}'` }} + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OpenStackMachineTemplate + matchResources: + machineDeploymentClass: + names: + - default-worker + jsonPatches: + - op: add + path: "/spec/template/spec/ports/-" + valueFrom: + template: | + allowedAddressPairs: + - ipAddress: {{ `{{ index .builtin.cluster.network.pods 0 }}` }} # # Access patches — target both CP and workers. # securityGroupIDs takes precedence over securityGroups (last patch wins). @@ -790,32 +843,6 @@ spec: permissions: "0644" {{ `{{- end }}` }} {{ `{{- end }}` }} - - path: /etc/kube-proxy-config.yaml - owner: root:root - permissions: "0644" - content: | - --- - apiVersion: kubeproxy.config.k8s.io/v1alpha1 - kind: KubeProxyConfiguration - metricsBindAddress: "0.0.0.0:10249" - - - path: /etc/kube-proxy-patch.sh - owner: root:root - permissions: "0755" - content: | - #!/usr/bin/env bash - set -euo pipefail - dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) - kubeadm_file="/etc/kubeadm.yml" - [[ -f ${kubeadm_file} ]] || kubeadm_file="/run/kubeadm/kubeadm.yaml" - [[ -f ${kubeadm_file} ]] || exit 0 - [[ -f ${dir}/kube-proxy-config.yaml ]] || exit 0 - cat "${dir}/kube-proxy-config.yaml" >> "${kubeadm_file}" - rm -f "${dir}/kube-proxy-config.yaml" - echo success > /tmp/kube-proxy-patch - - op: add - path: "/spec/template/spec/kubeadmConfigSpec/preKubeadmCommands/-" - value: "bash /etc/kube-proxy-patch.sh" - name: registryMirrorsWorker description: "Configure registry mirrors on worker nodes (containerd + CRI-O)." enabledIf: {{ `'{{ if .registryMirrors }}true{{end}}'` }} diff --git a/providers/openstack/scs/1-35/cluster-class/templates/kubeadm-control-plane-template.yaml b/providers/openstack/scs/1-35/cluster-class/templates/kubeadm-control-plane-template.yaml index b199f36c..9d8aec51 100644 --- a/providers/openstack/scs/1-35/cluster-class/templates/kubeadm-control-plane-template.yaml +++ b/providers/openstack/scs/1-35/cluster-class/templates/kubeadm-control-plane-template.yaml @@ -11,6 +11,8 @@ spec: extraArgs: - name: cloud-provider value: external + - name: allocate-node-cidrs + value: "true" - name: bind-address value: 0.0.0.0 - name: secure-port @@ -43,55 +45,9 @@ spec: value: "250" - name: snapshot-count value: "6400" - files: - - content: | - --- - apiVersion: kubeproxy.config.k8s.io/v1alpha1 - kind: KubeProxyConfiguration - metricsBindAddress: "0.0.0.0:10249" - path: /etc/kube-proxy-config.yaml - - content: | - #!/usr/bin/env bash - - # - # (PK) I couldn't find a better/simpler way to conifgure it. See: - # https://github.com/kubernetes-sigs/cluster-api/issues/4512 - # - - set -o errexit - set -o nounset - set -o pipefail - - dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) - readonly dir - - # Exit fast if already appended. - if [[ ! -f ${dir}/kube-proxy-config.yaml ]]; then - exit 0 - fi - - # kubeadm config is in different directory in Flatcar (/etc) and Ubuntu (/run/kubeadm). - kubeadm_file="/etc/kubeadm.yml" - if [[ ! -f ${kubeadm_file} ]]; then - kubeadm_file="/run/kubeadm/kubeadm.yaml" - fi - - # Run this script only if this is the init node. - if [[ ! -f ${kubeadm_file} ]]; then - exit 0 - fi - - # Append kube-proxy-config.yaml to kubeadm config and delete it - cat "${dir}/kube-proxy-config.yaml" >> "${kubeadm_file}" - rm "${dir}/kube-proxy-config.yaml" - - echo success > /tmp/kube-proxy-patch - owner: root:root - path: /etc/kube-proxy-patch.sh - permissions: "0755" - preKubeadmCommands: - - bash /etc/kube-proxy-patch.sh initConfiguration: + skipPhases: + - addon/kube-proxy nodeRegistration: kubeletExtraArgs: - name: cloud-provider diff --git a/providers/openstack/scs/1-35/cluster-class/templates/openstack-cluster-template.yaml b/providers/openstack/scs/1-35/cluster-class/templates/openstack-cluster-template.yaml index 3cc03fbb..06110054 100644 --- a/providers/openstack/scs/1-35/cluster-class/templates/openstack-cluster-template.yaml +++ b/providers/openstack/scs/1-35/cluster-class/templates/openstack-cluster-template.yaml @@ -13,16 +13,6 @@ spec: enabled: false managedSecurityGroups: allNodesSecurityGroupRules: - - remoteManagedGroups: - - controlplane - - worker - direction: ingress - etherType: IPv4 - name: VXLAN (Cilium) - portRangeMin: 8472 - portRangeMax: 8472 - protocol: udp - description: "Allow VXLAN traffic for Cilium" - remoteManagedGroups: - controlplane - worker @@ -47,11 +37,12 @@ spec: - worker direction: ingress etherType: IPv4 - name: Prometheus kube-proxy exporter - portRangeMin: 10249 - portRangeMax: 10249 + name: Prometheus node exporter + portRangeMin: 9100 + portRangeMax: 9100 protocol: tcp - description: "Allow Prometheus traffic for kube-proxy exporter" + description: "Allow Prometheus traffic for scraping node exporter" + controlPlaneNodesSecurityGroupRules: - remoteManagedGroups: - worker direction: ingress @@ -70,16 +61,6 @@ spec: portRangeMax: 10259 protocol: tcp description: "Allow Prometheus traffic for kube-scheduler exporter" - - remoteManagedGroups: - - worker - direction: ingress - etherType: IPv4 - name: Prometheus node exporter - portRangeMin: 9100 - portRangeMax: 9100 - protocol: tcp - description: "Allow Prometheus traffic for scraping node exporter" - controlPlaneNodesSecurityGroupRules: - remoteManagedGroups: - worker direction: ingress @@ -88,4 +69,11 @@ spec: portRangeMin: 2381 portRangeMax: 2381 protocol: tcp - description: "Allow Prometheus traffic for scraping etcd exporter" \ No newline at end of file + description: "Allow Prometheus traffic for scraping etcd exporter" + workerNodesSecurityGroupRules: + - remoteManagedGroups: + - controlplane + direction: ingress + etherType: IPv4 + name: Control plane to workers + description: "Allow traffic from control-plane nodes to worker nodes (kube-apiserver to admission webhooks)" diff --git a/providers/openstack/scs/1-35/cluster-class/templates/openstack-machine-template-control-plane.yaml b/providers/openstack/scs/1-35/cluster-class/templates/openstack-machine-template-control-plane.yaml index 703c1b1c..fb304712 100644 --- a/providers/openstack/scs/1-35/cluster-class/templates/openstack-machine-template-control-plane.yaml +++ b/providers/openstack/scs/1-35/cluster-class/templates/openstack-machine-template-control-plane.yaml @@ -7,6 +7,7 @@ spec: template: spec: flavor: overridden-by-patch + ports: [] image: imageRef: name: overridden-by-patch diff --git a/providers/openstack/scs/1-35/cluster-class/templates/openstack-machine-template-worker.yaml b/providers/openstack/scs/1-35/cluster-class/templates/openstack-machine-template-worker.yaml index 920dbb0a..96dd4462 100644 --- a/providers/openstack/scs/1-35/cluster-class/templates/openstack-machine-template-worker.yaml +++ b/providers/openstack/scs/1-35/cluster-class/templates/openstack-machine-template-worker.yaml @@ -7,6 +7,7 @@ spec: template: spec: flavor: overridden-by-patch + ports: [] image: imageRef: name: overridden-by-patch From d98d28daad4602c8c21529b7c83cd47c859ca48a Mon Sep 17 00:00:00 2001 From: Nils Arnold Date: Wed, 15 Apr 2026 12:53:13 +0200 Subject: [PATCH 09/11] Update docs Signed-off-by: Nils Arnold --- README.md | 4 +- docs/overview.md | 39 +++++------ docs/providers/openstack/hcp.md | 70 +++++++++---------- docs/providers/openstack/scs-configuration.md | 66 ++++++++--------- 4 files changed, 89 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 5c3f06f6..dffebbc2 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ See [docs/quickstart.md](docs/quickstart.md) for a complete walkthrough. ## Repository structure -``` +```text providers/ / / @@ -41,7 +41,7 @@ providers/ stack.yaml # stack metadata and addon version pins cluster-class/ # Helm chart: ClusterClass + infrastructure templates cluster-addon/ # Helm chart: CNI, CCM, CSI, metrics-server - node-images/ # image build instructions (OpenStack only) + image-manager.yaml # OpenStack only: aggregated image references ``` Each `1-XX/` directory is self-contained: it carries its own `stack.yaml`, diff --git a/docs/overview.md b/docs/overview.md index 3cb3c9e6..b3b4448a 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -19,7 +19,7 @@ the `ClusterClass` available for creating workload clusters. ## Repository structure -``` +```text providers/ / / @@ -27,7 +27,6 @@ providers/ stack.yaml # metadata: provider, name, k8s version, addon pins cluster-class/ # Helm chart producing the ClusterClass cluster-addon/ # Helm chart with CNI, CCM, CSI, metrics-server - node-images/ # image build definitions (provider-specific) image-manager.yaml # OpenStack only: aggregated image references ``` @@ -39,8 +38,7 @@ or sharing between minor versions -- changes to one version never affect another This design makes it straightforward to: -- Support different CAPI API versions across minors (e.g. `v1beta1` for 1-32, - `v1beta2` for 1-35). +- Support provider-specific API and implementation differences across minors. - Pin addons to version ranges that match the Kubernetes minor (e.g. CCM `2.34.x` for K8s 1.34). - Drop old versions by simply removing their directory. @@ -71,9 +69,8 @@ updates the `Chart.yaml` dependencies in the addon chart. The standard SCS cluster stack. Creates dedicated VMs for both control plane and worker nodes on OpenStack. Supports Kubernetes 1.32 through 1.35. -- Versions 1-32 and below use CAPI `v1beta1` with role-prefixed variables - (`controlPlaneFlavor`, `workerFlavor`). -- Version 1-35 uses CAPI `v1beta2` with unified variables (`flavor`) and +- Versions 1-32 through 1-35 use a `ClusterClass`-based configuration model. +- Version 1-35 uses unified variables (`flavor`) and per-role overrides via `topology.controlPlane.variables.overrides`. ### OpenStack / hcp @@ -98,7 +95,7 @@ ClusterClass, addons, or node images produces a new version. The full identifier of a cluster stack release is: -``` +```text ----v ``` @@ -115,19 +112,19 @@ rolling update of nodes. The build system uses [`just`](https://just.systems) as a task runner and a set of bash scripts in `hack/`: -| Command | Description | -|------------------------------------|------------------------------------------------| -| `just build --version 1.35` | Build locally to `.release/` | -| `just publish --version 1.35` | Build and push to OCI registry | -| `just dev --version 1.35` | Publish and print `ClusterStack` resource YAML | -| `just dev --install-cso --version 1.35` | Also install/upgrade CSO via Helm | -| `just install-cso` | Install CSO standalone | -| `just matrix` | Show version and addon matrix | -| `just update versions` | Update Kubernetes patch versions | -| `just update addons` | Update addon charts from upstream | -| `just generate-resources` | Generate `ClusterStack` + `Cluster` YAML | -| `just generate-docs` | Regenerate configuration documentation | -| `just clean` | Remove `.release/` build artifacts | +| Command | Description | +| --------------------------------------- | ---------------------------------------------- | +| `just build --version 1.35` | Build locally to `.release/` | +| `just publish --version 1.35` | Build and push to OCI registry | +| `just dev --version 1.35` | Publish and print `ClusterStack` resource YAML | +| `just dev --install-cso --version 1.35` | Also install/upgrade CSO via Helm | +| `just install-cso` | Install CSO standalone | +| `just matrix` | Show version and addon matrix | +| `just update versions` | Update Kubernetes patch versions | +| `just update addons` | Update addon charts from upstream | +| `just generate-resources` | Generate `ClusterStack` + `Cluster` YAML | +| `just generate-docs` | Regenerate configuration documentation | +| `just clean` | Remove `.release/` build artifacts | ### OCI workflow diff --git a/docs/providers/openstack/hcp.md b/docs/providers/openstack/hcp.md index a1cf7238..3b433f3c 100644 --- a/docs/providers/openstack/hcp.md +++ b/docs/providers/openstack/hcp.md @@ -7,20 +7,20 @@ Only worker nodes are created as OpenStack VMs. ## How it differs from the `scs` stack -| Aspect | scs | hcp | -|----------------------------|-------------------------------------------|--------------------------------------------| -| Control plane | Dedicated OpenStack VMs | Pods in the management cluster | -| Control plane template | `KubeadmControlPlaneTemplate` | `HostedControlPlaneTemplate` (v1alpha1) | -| CAPI API version | `v1beta2` (1-35) | `v1beta1` | -| CP machine infrastructure | `OpenStackMachineTemplate` | None (no CP VMs) | -| API server load balancer | Configurable (Octavia / none) | `none` by default (uses Gateway API) | -| API server floating IP | Configurable | Disabled by default | -| Variable model | Unified (`flavor`, `rootDisk`) | Worker-prefixed (`workerFlavor`, etc.) | -| Unique variables | `oidcConfig`, `registryMirrors` | `gatewayName`, `gatewayNamespace`, `controlPlaneReplicas` | +| Aspect | scs | hcp | +| ------ | --- | --- | +| Control plane | Dedicated OpenStack VMs | Pods in the management cluster | +| Control plane template | `KubeadmControlPlaneTemplate` | `HostedControlPlaneTemplate` (v1alpha1) | +| CAPI API version | `v1beta2` (1-35) | `v1beta1` | +| CP machine infrastructure | `OpenStackMachineTemplate` | None (no CP VMs) | +| API server load balancer | Configurable (Octavia / none) | `none` by default (uses Gateway API) | +| API server floating IP | Configurable | Disabled by default | +| Variable model | Unified (`flavor`, `rootDisk`) | Worker-prefixed (`workerFlavor`, etc.) | +| Unique variables | `oidcConfig`, `registryMirrors` | `gatewayName`, `gatewayNamespace`, `controlPlaneReplicas` | ## Architecture -``` +```text Management Cluster +-- CP pods (etcd, apiserver, controller-manager, scheduler) +-- HostedControlPlane resource @@ -121,33 +121,33 @@ management cluster and reference it: ### HCP-specific variables -| Variable | Type | Default | Description | -|--------------------------|---------|-------------|--------------------------------------------------| -| `controlPlaneReplicas` | integer | 3 | Number of hosted control plane replicas | -| `gatewayName` | string | "" | Gateway API resource name for API server ingress | -| `gatewayNamespace` | string | "default" | Namespace of the Gateway resource | -| `disableAPIServerFloatingIP` | boolean | true | Disable floating IP for API server | -| `apiServerLoadBalancer` | string | "none" | Load balancer type (typically `none` for HCP) | +| Variable | Type | Default | Description | +| -------- | ---- | ------- | ----------- | +| `controlPlaneReplicas` | integer | 3 | Number of hosted control plane replicas | +| `gatewayName` | string | "" | Gateway API resource name for API server ingress | +| `gatewayNamespace` | string | "default" | Namespace of the Gateway resource | +| `disableAPIServerFloatingIP` | boolean | true | Disable floating IP for API server | +| `apiServerLoadBalancer` | string | "none" | Load balancer type (typically `none` for HCP) | ### Worker variables -| Variable | Type | Default | Description | -|------------------------------|---------|-----------------|--------------------------------------------| -| `workerFlavor` | string | "SCS-2V-4" | OpenStack flavor for worker nodes | -| `workerRootDisk` | integer | 25 | Root disk size in GiB | -| `workerServerGroupID` | string | "" | Anti-affinity server group for workers | -| `workerAdditionalBlockDevices` | array | [] | Extra Cinder volumes for workers | -| `workerSecurityGroups` | array | [] | Security group names for workers | -| `workerSecurityGroupIDs` | array | [] | Security group UUIDs for workers | +| Variable | Type | Default | Description | +| -------- | ---- | ------- | ----------- | +| `workerFlavor` | string | "SCS-2V-4" | OpenStack flavor for worker nodes | +| `workerRootDisk` | integer | 25 | Root disk size in GiB | +| `workerServerGroupID` | string | "" | Anti-affinity server group for workers | +| `workerAdditionalBlockDevices` | array | [] | Extra Cinder volumes for workers | +| `workerSecurityGroups` | array | [] | Security group names for workers | +| `workerSecurityGroupIDs` | array | [] | Security group UUIDs for workers | ### Shared variables (same as scs) -| Variable | Type | Default | Description | -|---------------------|---------|---------------------------------|------------------------------------------| -| `imageName` | string | "ubuntu-capi-image" | Base OS image name | -| `networkExternalID` | string | "" | External network UUID | -| `networkMTU` | integer | (provider default) | MTU for cluster network | -| `dnsNameservers` | array | ["5.1.66.255", "185.150.99.255"]| DNS nameservers | -| `nodeCIDR` | string | "10.8.0.0/20" | Subnet CIDR for cluster nodes | -| `sshKey` | string | "" | SSH key to inject into nodes | -| `certSANs` | array | [] | Extra SANs for API server certificate | +| Variable | Type | Default | Description | +| -------- | ---- | ------- | ----------- | +| `imageName` | string | "ubuntu-capi-image" | Base OS image name | +| `networkExternalID` | string | "" | External network UUID | +| `networkMTU` | integer | (provider default) | MTU for cluster network | +| `dnsNameservers` | array | ["9.9.9.9", "149.112.112.112"] | DNS nameservers | +| `nodeCIDR` | string | "10.8.0.0/20" | Subnet CIDR for cluster nodes | +| `sshKeyName` | string | "" | SSH key to inject into nodes | +| `certSANs` | array | [] | Extra SANs for API server certificate | diff --git a/docs/providers/openstack/scs-configuration.md b/docs/providers/openstack/scs-configuration.md index 55819bf2..bdbeaea1 100644 --- a/docs/providers/openstack/scs-configuration.md +++ b/docs/providers/openstack/scs-configuration.md @@ -5,7 +5,7 @@ This page lists the custom configuration options available, including their defa ## Version matrix | Version | K8s | CS Version | cilium | metrics-server | os-csi | os-ccm | -|---------|-----|----------|---------|---------|---------|---------| +| ------- | --- | ---------- | ------ | -------------- | ------ | ------ | | 1-32 | 1.32 | - | 1.19.1 | 3.13.0 | 2.32.x | 2.32.x | | 1-33 | 1.33 | - | 1.19.1 | 3.13.0 | 2.33.x | 2.33.x | | 1-34 | 1.34 | - | 1.19.1 | 3.13.0 | 2.34.x | 2.34.x | @@ -69,35 +69,37 @@ In v1beta2, per-role overrides (e.g. different flavors for control plane and wor ## Available variables > **Note:** This table documents the **1-35** (v1beta2) variable set with unified -> variable names. Older versions (1-32, 1-33) use role-prefixed names like -> `controlPlaneFlavor` / `workerFlavor` instead of the unified `flavor`. +> variable names. The other currently maintained SCS minors in this repo use the +> same unified variable model and `ClusterClass`-based configuration pattern. -|Name|Type|Default|Example|Description|Required| -|----|----|-------|-------|-----------|--------| -|`imageName`|string|"ubuntu-capi-image"|"ubuntu-capi-image"|Base name of the OpenStack image for cluster nodes.|False| -|`imageIsOrc`|boolean|false|true|Whether the image name refers to an ORC (OpenStack Resource Controller) image resource instead of a Glance image.|False| -|`imageAddVersion`|boolean|true|false|Append the Kubernetes version suffix to the image name (e.g. `ubuntu-capi-image-v1.35`).|False| -|`networkExternalID`|string|""|"ebfe5546-f09f-4f42-ab54-094e457d42ec"|ID of the external OpenStack network for public internet access.|False| -|`networkMTU`|integer||1500|Maximum transmission unit (MTU) for the private cluster network.|False| -|`dnsNameservers`|array|["5.1.66.255", "185.150.99.255"]|["8.8.8.8"]|DNS nameservers for the cluster subnet.|False| -|`nodeCIDR`|string|"10.8.0.0/20"|"10.8.0.0/20"|CIDR for the cluster subnet. A network, subnet, and router will be created.|False| -|`flavor`|string|"SCS-2V-4-20s"|"SCS-4V-8-20"|OpenStack instance flavor for all nodes. Override per role using topology variable overrides.|False| -|`rootDisk`|integer|0|50|Root disk size in GiB. When set, an OpenStack volume is used instead of the ephemeral disk from the flavor.|False| -|`serverGroupID`|string|""|"3adf4e92-bb33-4e44-8ad3-afda9dfe8ec3"|Server group for anti-affinity placement. Override per role using topology variable overrides.|False| -|`additionalBlockDevices`|array|[]|[{"name": "data", "sizeGiB": 100, "type": "Volume"}]|Additional Cinder volumes to attach to nodes.|False| -|`sshKey`|string|""|"capi-keypair"|SSH key pair name to inject into nodes.|False| -|`apiServerLoadBalancer`|string|"octavia-ovn"|"none"|Load balancer for the API server. Options: `none`, `octavia-amphora`, `octavia-ovn`.|False| -|`apiServerAllowedCIDRs`|array|[]|["10.0.0.0/8"]|CIDRs allowed to access the API server load balancer.|False| -|`disableAPIServerFloatingIP`|boolean|false|true|Disable floating IP for the API server.|False| -|`certSANs`|array|[]|["mydomain.example"]|Extra Subject Alternative Names for the API server certificate.|False| -|`controlPlaneAvailabilityZones`|array|[]|["nova"]|Availability zones for control plane nodes.|False| -|`controlPlaneOmitAvailabilityZone`|boolean|false|true|Omit availability zone when creating control plane nodes, letting Nova schedule freely.|False| -|`identityRef.name`|string|"openstack"|"openstack"|Name of the Secret containing OpenStack credentials.|False| -|`identityRef.cloudName`|string|"openstack"|"openstack"|Cloud name within the credentials Secret.|False| -|`oidcConfig.clientID`|string||"kubectl"|OIDC client ID for API server authentication.|| -|`oidcConfig.issuerURL`|string||"https://dex.example.com"|OIDC provider discovery URL (must be HTTPS).|| -|`oidcConfig.usernameClaim`|string|"preferred_username"|"email"|JWT claim to use as the username.|| -|`oidcConfig.groupsClaim`|string|"groups"|"groups"|JWT claim to use as groups.|| -|`oidcConfig.usernamePrefix`|string|"oidc:"|"oidc:"|Prefix for OIDC usernames.|| -|`oidcConfig.groupsPrefix`|string|"oidc:"|"oidc:"|Prefix for OIDC group names.|| -|`registryMirrors`|array|[]|[{"hostnameUpstream": "docker.io", "urlMirror": "https://mirror.example.com"}]|Container registry mirrors for node containerd configuration.|| +| Name | Type | Default | Example | Description | Required | +| ---- | ---- | ------- | ------- | ----------- | -------- | +| `imageName` | string | "ubuntu-capi-image" | "ubuntu-capi-image" | Base name of the OpenStack image for cluster nodes. | False | +| `imageIsOrc` | boolean | false | true | Whether the image name refers to an ORC (OpenStack Resource Controller) image resource instead of a Glance image. | False | +| `imageAddVersion` | boolean | true | false | Append the Kubernetes version suffix to the image name (e.g. `ubuntu-capi-image-v1.35`). | False | +| `networkExternalID` | string | "" | "ebfe5546-f09f-4f42-ab54-094e457d42ec" | ID of the external OpenStack network for public internet access. | False | +| `networkMTU` | integer | (provider default) | 1500 | Maximum transmission unit (MTU) for the private cluster network. | False | +| `dnsNameservers` | array | ["9.9.9.9", "149.112.112.112"] | ["8.8.8.8"] | DNS nameservers for the cluster subnet. | False | +| `nodeCIDR` | string | "10.8.0.0/20" | "10.8.0.0/20" | CIDR for the cluster subnet. A network, subnet, and router will be created. | False | +| `flavor` | string | "SCS-2V-4" | "SCS-4V-8-20" | OpenStack instance flavor for all nodes. Override per role using topology variable overrides. | False | +| `rootDisk` | integer | 50 | 50 | Root disk size in GiB. When set, an OpenStack volume is used instead of the ephemeral disk from the flavor. | False | +| `serverGroupID` | string | "" | "3adf4e92-bb33-4e44-8ad3-afda9dfe8ec3" | Server group for anti-affinity placement. Override per role using topology variable overrides. | False | +| `additionalBlockDevices` | array | [] | [{"name": "data", "sizeGiB": 100, "type": "Volume"}] | Additional Cinder volumes to attach to nodes. | False | +| `sshKeyName` | string | "" | "capi-keypair" | SSH key pair name to inject into nodes. | False | +| `securityGroups` | array | [] | ["security-group-1"] | Extra security groups by name for all nodes. Ignored if `securityGroupIDs` is set. | False | +| `securityGroupIDs` | array | [] | ["9ae2f488-30a3-4629-bd51-07acb8eb4278"] | Extra security groups by UUID for all nodes. Takes precedence over `securityGroups`. | False | +| `apiServerLoadBalancer` | string | "octavia-ovn" | "none" | Load balancer for the API server. Options: `none`, `octavia-amphora`, `octavia-ovn`. | False | +| `apiServerAllowedCIDRs` | array | [] | ["10.0.0.0/8"] | CIDRs allowed to access the API server load balancer. | False | +| `disableAPIServerFloatingIP` | boolean | false | true | Disable floating IP for the API server. | False | +| `certSANs` | array | [] | ["mydomain.example"] | Extra Subject Alternative Names for the API server certificate. | False | +| `controlPlaneAvailabilityZones` | array | [] | ["nova"] | Availability zones for control plane nodes. | False | +| `controlPlaneOmitAvailabilityZone` | boolean | false | true | Omit availability zone when creating control plane nodes, letting Nova schedule freely. | False | +| `identityRef.name` | string | "openstack" | "openstack" | Name of the Secret containing OpenStack credentials. | False | +| `identityRef.cloudName` | string | "openstack" | "openstack" | Cloud name within the credentials Secret. | False | +| `oidcConfig.clientID` | string | "" | "kubectl" | OIDC client ID for API server authentication. | False | +| `oidcConfig.issuerURL` | string | "" | `"https://dex.example.com"` | OIDC provider discovery URL (must be HTTPS). | False | +| `oidcConfig.usernameClaim` | string | "preferred_username" | "email" | JWT claim to use as the username. | False | +| `oidcConfig.groupsClaim` | string | "groups" | "groups" | JWT claim to use as groups. | False | +| `oidcConfig.usernamePrefix` | string | "oidc:" | "oidc:" | Prefix for OIDC usernames. | False | +| `oidcConfig.groupsPrefix` | string | "oidc:" | "oidc:" | Prefix for OIDC group names. | False | +| `registryMirrors` | array | [] | [{"hostnameUpstream": "docker.io", "urlMirror": "https://mirror.example.com"}] | Container registry mirrors for node containerd configuration. | False | From 59001d29e53aad7503633af21996df9c96bc95e8 Mon Sep 17 00:00:00 2001 From: Nils Arnold Date: Wed, 15 Apr 2026 12:42:27 +0200 Subject: [PATCH 10/11] Update .envrc.example for compatibility and clarity on legacy variables Signed-off-by: Nils Arnold --- .envrc.example | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.envrc.example b/.envrc.example index e464c745..1d236559 100644 --- a/.envrc.example +++ b/.envrc.example @@ -2,6 +2,8 @@ export KUBECONFIG=$PWD/.mgt-cluster-kubeconfig.yaml export PATH=$PWD/hack/tools/bin:$PATH +# Legacy variables from the previous workflow. They are currently not consumed by hack scripts, but are kept here for compatibility/reference. + export PROVIDER=docker # Versions @@ -21,3 +23,18 @@ export DISABLE_VERSIONCHECK="true" # Release export RELEASE_CLUSTER_CLASS=$CLUSTER_CLASS_NAME export RELEASE_KUBERNETES_VERSION=1-27 + +# Current workflow: the build scripts derive the stack from PROVIDER and + +export PROVIDER=openstack +export CLUSTER_STACK=scs + +# OCI registry (leave unset for auto-configured ttl.sh during development) +# export OCI_REGISTRY=registry.example.com +# export OCI_REPOSITORY=kaas/cluster-stacks +# export OCI_USERNAME=user +# export OCI_PASSWORD=password +# export OCI_ACCESS_TOKEN=token + +# Optional: GitHub token for higher API rate limits during version resolution +# export GITHUB_TOKEN=ghp_... \ No newline at end of file From 77c6ae8f2d3b3a556d00d609f4ca624b9de5ee5a Mon Sep 17 00:00:00 2001 From: Nils Arnold Date: Wed, 15 Apr 2026 13:06:23 +0200 Subject: [PATCH 11/11] fix: ensure newline at end of file in overwrite.yaml Signed-off-by: Nils Arnold --- providers/openstack/scs/1-35/cluster-addon/cni/overwrite.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/openstack/scs/1-35/cluster-addon/cni/overwrite.yaml b/providers/openstack/scs/1-35/cluster-addon/cni/overwrite.yaml index 001e1949..8f86f6a2 100644 --- a/providers/openstack/scs/1-35/cluster-addon/cni/overwrite.yaml +++ b/providers/openstack/scs/1-35/cluster-addon/cni/overwrite.yaml @@ -2,4 +2,4 @@ values: | cilium: k8sServiceHost: "{{ .Cluster.spec.controlPlaneEndpoint.host }}" k8sServicePort: "{{ .Cluster.spec.controlPlaneEndpoint.port }}" - ipv4NativeRoutingCIDR: "{{ index .Cluster.spec.clusterNetwork.pods.cidrBlocks 0 }}" \ No newline at end of file + ipv4NativeRoutingCIDR: "{{ index .Cluster.spec.clusterNetwork.pods.cidrBlocks 0 }}"