diff --git a/.envrc b/.envrc index f77b861..59ae13e 100644 --- a/.envrc +++ b/.envrc @@ -32,4 +32,4 @@ export PYRIGHT_PYTHON_FORCE_VERSION=latest #export PULUMI_BACKEND_URL=${PULUMI_BACKEND_URL:-file://$PWD/.pulumi} #export PULUMI_CONFIG_PASSPHRASE=${PULUMI_CONFIG_PASSPHRASE:-foobarbaz} #export PULUMI_HOME=$PWD/.pulumi -#export PULUMI_K8S_DELETE_UNREACHABLE=true +export PULUMI_K8S_DELETE_UNREACHABLE=true diff --git a/.github/bin/nuke_namespace b/.github/bin/nuke_namespace new file mode 100755 index 0000000..ef35c52 --- /dev/null +++ b/.github/bin/nuke_namespace @@ -0,0 +1,202 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Uncomment for debug +#set -x + +# Dependency checks +if ! command -v kubectl &>/dev/null; then + echo "ERROR: 'kubectl' not found in PATH. Please install or adjust PATH." + exit 1 +fi + +if ! command -v jq &>/dev/null; then + echo "ERROR: 'jq' not found in PATH. Please install jq." + exit 1 +fi + +if ! command -v mapfile &>/dev/null; then + echo "ERROR: 'mapfile' command not found. Please run this script with Bash 4+." + exit 1 +fi + +LAST_RESORT=false +KUBECTL_OPTS=() +NAMESPACES=() + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --last-resort) + LAST_RESORT=true + shift + ;; + -*) + KUBECTL_OPTS+=("$1") + shift + if [[ $# -gt 0 && ! "$1" =~ ^- ]]; then + KUBECTL_OPTS+=("$1") + shift + fi + ;; + *) + NAMESPACES+=("$1") + shift + ;; + esac +done + +if [ ${#NAMESPACES[@]} -eq 0 ]; then + echo "Usage: $0 [--last-resort] [kubectl-flags...] [namespace2 ...]" + exit 1 +fi + +timestamp() { + date +"%Y-%m-%d %H:%M:%S" +} + +discover_namespace_resources() { + local namespace=$1 + local results=() + + # Get all namespaced resource types + local resources + resources=$(kubectl "${KUBECTL_OPTS[@]}" api-resources --verbs=list --namespaced -o name || true) + while read -r resource; do + [ -z "$resource" ] && continue + local items + items=$(kubectl "${KUBECTL_OPTS[@]}" get "$resource" -n "$namespace" --no-headers 2>/dev/null || true) + if [ -n "$items" ]; then + while read -r line; do + [ -z "$line" ] && continue + local name + name=$(echo "$line" | awk '{print $1}') + results+=("$resource $name") + done <<<"$items" + fi + done <<<"$resources" + + for r in "${results[@]}"; do + echo "$r" + done +} + +remove_finalizers_from_resource() { + local namespace=$1 + local resource=$2 + local name=$3 + + local json + json=$(kubectl "${KUBECTL_OPTS[@]}" get "$resource" "$name" -n "$namespace" -o json 2>/dev/null || true) + if [ -n "$json" ]; then + local patched + patched=$(echo "$json" | jq 'del(.metadata.finalizers)') + echo "$patched" | kubectl "${KUBECTL_OPTS[@]}" replace -n "$namespace" -f - &>/dev/null || true + fi +} + +force_delete_resource() { + local namespace=$1 + local resource=$2 + local name=$3 + # Force delete immediately + kubectl "${KUBECTL_OPTS[@]}" delete "$resource" "$name" -n "$namespace" --force --grace-period=0 --ignore-not-found &>/dev/null || true +} + +remove_namespace_finalizers() { + local namespace=$1 + local json + json=$(kubectl "${KUBECTL_OPTS[@]}" get namespace "$namespace" -o json 2>/dev/null || true) + if [ -n "$json" ]; then + local patched + patched=$(echo "$json" | jq 'del(.spec.finalizers)') + echo "$patched" | kubectl "${KUBECTL_OPTS[@]}" replace --raw "/api/v1/namespaces/$namespace/finalize" -f - &>/dev/null || true + fi +} + +force_delete_namespace() { + local namespace=$1 + + remove_namespace_finalizers "$namespace" + kubectl "${KUBECTL_OPTS[@]}" delete namespace "$namespace" --ignore-not-found --wait=false &>/dev/null || true + + # Attempt a few times if still present + for i in {1..5}; do + if ! kubectl "${KUBECTL_OPTS[@]}" get namespace "$namespace" &>/dev/null; then + echo "$(timestamp) INFO: Namespace $namespace deleted." + break + else + echo "$(timestamp) WARNING: Namespace $namespace still present, removing finalizers again..." + remove_namespace_finalizers "$namespace" + kubectl "${KUBECTL_OPTS[@]}" delete namespace "$namespace" --ignore-not-found --wait=false &>/dev/null || true + sleep 2 + fi + done +} + +confirm_deletion() { + echo + echo "WARNING: This is a NUCLEAR OPTION. It will forcibly remove finalizers and destroy all resources/namespaces listed." + echo "Are you sure you want to proceed? (y/n)" + read -r answer + if [[ "$answer" != "y" && "$answer" != "Y" ]]; then + echo "Aborting." + exit 1 + fi +} + +echo "$(timestamp) INFO: Starting nuclear cleanup..." + +all_resources=() +for namespace in "${NAMESPACES[@]}"; do + echo "$(timestamp) INFO: Discovering resources in namespace: $namespace..." + namespace_resources=$(discover_namespace_resources "$namespace") + if [ -n "$namespace_resources" ]; then + echo "$(timestamp) INFO: The following resources will be deleted in namespace $namespace:" + echo "$namespace_resources" | awk '{print "- " $1 ": " $2}' + all_resources+=("$namespace_resources") + else + echo "$(timestamp) INFO: No resources found in $namespace. We will just force delete the namespace." + fi +done + +confirm_deletion + +for namespace in "${NAMESPACES[@]}"; do + echo "$(timestamp) INFO: Cleaning up resources in $namespace..." + namespace_resources=$(discover_namespace_resources "$namespace") + + if [ -n "$namespace_resources" ]; then + while read -r line; do + resource=$(echo "$line" | awk '{print $1}') + name=$(echo "$line" | awk '{print $2}') + echo "$(timestamp) INFO: Removing finalizers and force deleting $resource/$name in $namespace..." + remove_finalizers_from_resource "$namespace" "$resource" "$name" + force_delete_resource "$namespace" "$resource" "$name" + done <<<"$namespace_resources" + else + echo "$(timestamp) INFO: No resources in $namespace." + fi + + echo "$(timestamp) INFO: Force deleting namespace $namespace..." + force_delete_namespace "$namespace" +done + +echo "$(timestamp) INFO: Nuclear cleanup completed." + +# Post-check: If LAST_RESORT is true and there's still stuck resources +if $LAST_RESORT; then + echo "$(timestamp) INFO: Last resort mode enabled. Checking if any stuck resources remain..." + # Attempt to detect stuck CRDs or APIs that won't go away. + stuck_crds=$(kubectl "${KUBECTL_OPTS[@]}" api-resources 2>/dev/null | grep fluxcd.controlplane.io || true) + if [ -n "$stuck_crds" ]; then + echo "$(timestamp) WARNING: The following resources still appear to be present:" + echo "$stuck_crds" + echo "You may need to:" + echo "1. Recreate their CRDs and namespaces." + echo "2. Remove finalizers from them manually (using 'kubectl patch' or 'jq')." + echo "3. Delete them again." + echo "If this fails, consider restarting the API server or contacting support." + fi +fi diff --git a/modules/aws/eks.py b/modules/aws/eks.py index 554fa4c..5dfafd2 100644 --- a/modules/aws/eks.py +++ b/modules/aws/eks.py @@ -20,7 +20,7 @@ from typing import Dict, List, Optional, Any, TYPE_CHECKING import pulumi_aws as aws -from pulumi import ResourceOptions, log +from pulumi import ResourceOptions, log, CustomTimeouts import pulumi_kubernetes as k8s import pulumi import json @@ -321,7 +321,9 @@ def create_cluster( opts=ResourceOptions( provider=self.provider.provider, depends_on=depends_on, - parent=vpc # Make cluster a child of VPC + parent=vpc, + # Allowed: 20 minute create, 15 minute update, 10 minute delete + custom_timeouts=CustomTimeouts(create="1200s", update="900s", delete="600s") ), ) @@ -373,8 +375,9 @@ def create_node_group( tags=merged_tags, opts=ResourceOptions( provider=self.provider.provider, - parent=cluster, # Make node group child of cluster + parent=cluster, depends_on=depends_on or [], + custom_timeouts=CustomTimeouts(create="5m", update="5m", delete="10s"), ), ) diff --git a/modules/kubernetes/crossplane/deployment.py b/modules/kubernetes/crossplane/deployment.py index cb95176..2434d2b 100644 --- a/modules/kubernetes/crossplane/deployment.py +++ b/modules/kubernetes/crossplane/deployment.py @@ -1,6 +1,6 @@ -# modules/kubernetes/crossplane/deployment.py +# ./modules/kubernetes/crossplane/deployment.py from typing import Dict, Any, List -from pulumi import log, ResourceOptions +from pulumi import log, ResourceOptions, CustomTimeouts import pulumi_kubernetes as k8s from ..deployment import KubernetesModule @@ -39,31 +39,35 @@ def deploy(self, config: Dict[str, Any]) -> ModuleDeploymentResult: labels={ "app.kubernetes.io/name": "crossplane", "app.kubernetes.io/part-of": "crossplane", - } + }, + ), + opts=ResourceOptions( + custom_timeouts=CustomTimeouts(create="5m", update="5m", delete="10s"), + provider=self.provider.provider, + parent=self.provider.provider, ), - opts=ResourceOptions(provider=self.provider.provider), ) # Configure Helm values helm_values = { "metrics": { - "enabled": True, + "enabled": crossplane_config.metrics_enabled, + "service": {"annotations": {"prometheus.io/scrape": "true", "prometheus.io/port": "8080"}}, }, "resourcesCrossplane": crossplane_config.resource_limits, - "serviceAccount": { - "create": True, - "name": "crossplane" - }, - "provider": { - "packages": [] - } + "serviceAccount": {"create": True, "name": "crossplane"}, + "deploymentStrategy": "RollingUpdate", + "rbacManager": {"deploy": True, "deploymentStrategy": "RollingUpdate"}, + "provider": {"packages": []}, + # Initialize args so it's ready for appending + "args": [], } # Add feature flags if enabled if crossplane_config.enable_external_secret_stores: - helm_values["args"] = ["--enable-external-secret-stores"] + helm_values["args"].append("--enable-external-secret-stores") if crossplane_config.enable_composition_revisions: - helm_values["args"] = helm_values.get("args", []) + ["--enable-composition-revisions"] + helm_values["args"].append("--enable-composition-revisions") # Deploy Helm release log.info(f"Deploying Crossplane Helm release to namespace {crossplane_config.namespace}") @@ -71,18 +75,20 @@ def deploy(self, config: Dict[str, Any]) -> ModuleDeploymentResult: f"{self.name}-system", k8s.helm.v3.ReleaseArgs( chart="crossplane", - repository_opts=k8s.helm.v3.RepositoryOptsArgs( - repo="https://charts.crossplane.io/stable" - ), + repository_opts=k8s.helm.v3.RepositoryOptsArgs(repo="https://charts.crossplane.io/stable"), version=crossplane_config.version, namespace=crossplane_config.namespace, values=helm_values, wait_for_jobs=True, + timeout=600, # 10 minutes + cleanup_on_fail=True, + atomic=True, ), opts=ResourceOptions( provider=self.provider.provider, parent=namespace, depends_on=[namespace], + custom_timeouts=CustomTimeouts(create="1200s", update="600s", delete="30s"), ), ) @@ -96,13 +102,9 @@ def deploy(self, config: Dict[str, Any]) -> ModuleDeploymentResult: metadata={ "namespace": crossplane_config.namespace, "version": crossplane_config.version, - } + }, ) except Exception as e: log.error(f"Failed to deploy Crossplane: {str(e)}") - return ModuleDeploymentResult( - success=False, - version="", - errors=[str(e)] - ) + return ModuleDeploymentResult(success=False, version="", errors=[str(e)]) diff --git a/modules/kubernetes/crossplane/helm_values_defaults_docs.yaml b/modules/kubernetes/crossplane/helm_values_defaults_docs.yaml new file mode 100644 index 0000000..8ca9c34 --- /dev/null +++ b/modules/kubernetes/crossplane/helm_values_defaults_docs.yaml @@ -0,0 +1,195 @@ +# helm-docs renders these comments into markdown. Use markdown formatting where +# appropiate. +# +# -- The number of Crossplane pod `replicas` to deploy. +replicas: 1 + +# -- The number of Crossplane ReplicaSets to retain. +revisionHistoryLimit: null + +# -- The deployment strategy for the Crossplane and RBAC Manager pods. +deploymentStrategy: RollingUpdate + +image: + # -- Repository for the Crossplane pod image. + repository: xpkg.upbound.io/crossplane/crossplane + # -- The Crossplane image tag. Defaults to the value of `appVersion` in `Chart.yaml`. + tag: "" + # -- The image pull policy used for Crossplane and RBAC Manager pods. + pullPolicy: IfNotPresent + +# -- Add `nodeSelectors` to the Crossplane pod deployment. +nodeSelector: {} +# -- Add `tolerations` to the Crossplane pod deployment. +tolerations: [] +# -- Add `affinities` to the Crossplane pod deployment. +affinity: {} +# -- Add `topologySpreadConstraints` to the Crossplane pod deployment. +topologySpreadConstraints: [] + +# -- Enable `hostNetwork` for the Crossplane deployment. Caution: enabling `hostNetwork` grants the Crossplane Pod access to the host network namespace. Consider setting `dnsPolicy` to `ClusterFirstWithHostNet`. +hostNetwork: false + +# -- Specify the `dnsPolicy` to be used by the Crossplane pod. +dnsPolicy: "" + +# -- Add custom `labels` to the Crossplane pod deployment. +customLabels: {} + +# -- Add custom `annotations` to the Crossplane pod deployment. +customAnnotations: {} + +serviceAccount: + # -- Add custom `annotations` to the Crossplane ServiceAccount. + customAnnotations: {} + +# -- Enable [leader election](https://docs.crossplane.io/latest/concepts/pods/#leader-election) for the Crossplane pod. +leaderElection: true +# -- Add custom arguments to the Crossplane pod. +args: [] + +provider: + # -- A list of Provider packages to install. + packages: [] + +configuration: + # -- A list of Configuration packages to install. + packages: [] + +function: + # -- A list of Function packages to install + packages: [] + +# -- The imagePullSecret names to add to the Crossplane ServiceAccount. +imagePullSecrets: [] + +registryCaBundleConfig: + # -- The ConfigMap name containing a custom CA bundle to enable fetching packages from registries with unknown or untrusted certificates. + name: "" + # -- The ConfigMap key containing a custom CA bundle to enable fetching packages from registries with unknown or untrusted certificates. + key: "" + +service: + # -- Configure annotations on the service object. Only enabled when webhooks.enabled = true + customAnnotations: {} + +webhooks: + # -- Enable webhooks for Crossplane and installed Provider packages. + enabled: true + +rbacManager: + # -- Deploy the RBAC Manager pod and its required roles. + deploy: true + # -- Don't install aggregated Crossplane ClusterRoles. + skipAggregatedClusterRoles: false + # -- The number of RBAC Manager pod `replicas` to deploy. + replicas: 1 + # -- The number of RBAC Manager ReplicaSets to retain. + revisionHistoryLimit: null + # -- Enable [leader election](https://docs.crossplane.io/latest/concepts/pods/#leader-election) for the RBAC Manager pod. + leaderElection: true + # -- Add custom arguments to the RBAC Manager pod. + args: [] + # -- Add `nodeSelectors` to the RBAC Manager pod deployment. + nodeSelector: {} + # -- Add `tolerations` to the RBAC Manager pod deployment. + tolerations: [] + # -- Add `affinities` to the RBAC Manager pod deployment. + affinity: {} + # -- Add `topologySpreadConstraints` to the RBAC Manager pod deployment. + topologySpreadConstraints: [] + +# -- The PriorityClass name to apply to the Crossplane and RBAC Manager pods. +priorityClassName: "" + +resourcesCrossplane: + limits: + # -- CPU resource limits for the Crossplane pod. + cpu: 500m + # -- Memory resource limits for the Crossplane pod. + memory: 1024Mi + requests: + # -- CPU resource requests for the Crossplane pod. + cpu: 100m + # -- Memory resource requests for the Crossplane pod. + memory: 256Mi + +securityContextCrossplane: + # -- The user ID used by the Crossplane pod. + runAsUser: 65532 + # -- The group ID used by the Crossplane pod. + runAsGroup: 65532 + # -- Enable `allowPrivilegeEscalation` for the Crossplane pod. + allowPrivilegeEscalation: false + # -- Set the Crossplane pod root file system as read-only. + readOnlyRootFilesystem: true + +packageCache: + # -- Set to `Memory` to hold the package cache in a RAM backed file system. Useful for Crossplane development. + medium: "" + # -- The size limit for the package cache. If medium is `Memory` the `sizeLimit` can't exceed Node memory. + sizeLimit: 20Mi + # -- The name of a PersistentVolumeClaim to use as the package cache. Disables the default package cache `emptyDir` Volume. + pvc: "" + # -- The name of a ConfigMap to use as the package cache. Disables the default package cache `emptyDir` Volume. + configMap: "" + +resourcesRBACManager: + limits: + # -- CPU resource limits for the RBAC Manager pod. + cpu: 100m + # -- Memory resource limits for the RBAC Manager pod. + memory: 512Mi + requests: + # -- CPU resource requests for the RBAC Manager pod. + cpu: 100m + # -- Memory resource requests for the RBAC Manager pod. + memory: 256Mi + +securityContextRBACManager: + # -- The user ID used by the RBAC Manager pod. + runAsUser: 65532 + # -- The group ID used by the RBAC Manager pod. + runAsGroup: 65532 + # -- Enable `allowPrivilegeEscalation` for the RBAC Manager pod. + allowPrivilegeEscalation: false + # -- Set the RBAC Manager pod root file system as read-only. + readOnlyRootFilesystem: true + +metrics: + # -- Enable Prometheus path, port and scrape annotations and expose port 8080 for both the Crossplane and RBAC Manager pods. + enabled: false + +# -- Add custom environmental variables to the Crossplane pod deployment. +# Replaces any `.` in a variable name with `_`. For example, `SAMPLE.KEY=value1` becomes `SAMPLE_KEY=value1`. +extraEnvVarsCrossplane: {} + +# -- Add custom environmental variables to the RBAC Manager pod deployment. +# Replaces any `.` in a variable name with `_`. For example, `SAMPLE.KEY=value1` becomes `SAMPLE_KEY=value1`. +extraEnvVarsRBACManager: {} + +# -- Add a custom `securityContext` to the Crossplane pod. +podSecurityContextCrossplane: {} + +# -- Add a custom `securityContext` to the RBAC Manager pod. +podSecurityContextRBACManager: {} + +# -- Add custom `volumes` to the Crossplane pod. +extraVolumesCrossplane: {} + +# -- Add custom `volumeMounts` to the Crossplane pod. +extraVolumeMountsCrossplane: {} + +# -- To add arbitrary Kubernetes Objects during a Helm Install +extraObjects: [] + # - apiVersion: pkg.crossplane.io/v1alpha1 + # kind: ControllerConfig + # metadata: + # name: aws-config + # annotations: + # eks.amazonaws.com/role-arn: arn:aws:iam::123456789101:role/example + # helm.sh/hook: post-install + # spec: + # podSecurityContext: + # fsGroup: 2000 + diff --git a/modules/kubernetes/flux/deployment.py b/modules/kubernetes/flux/deployment.py index 857d728..b9c0e64 100644 --- a/modules/kubernetes/flux/deployment.py +++ b/modules/kubernetes/flux/deployment.py @@ -1,8 +1,9 @@ -# modules/kubernetes/flux/deployment.py from typing import Dict, Any, List from pulumi import log, ResourceOptions, CustomTimeouts import pulumi_kubernetes as k8s +from pulumi_kubernetes.helm.v4 import Chart + from ..deployment import KubernetesModule from .types import FluxConfig from modules.core.interfaces import ModuleDeploymentResult @@ -16,7 +17,6 @@ def __init__(self, init_config): self.name = "flux" def validate_config(self, config: Dict[str, Any]) -> List[str]: - """Validate Flux configuration.""" try: if config is None: config = {} @@ -25,126 +25,127 @@ def validate_config(self, config: Dict[str, Any]) -> List[str]: except Exception as e: return [str(e)] + def sanitize_version(self, version: str) -> str: + return version.lstrip("v") if version and version.startswith("v") else version + def deploy(self, config: Dict[str, Any]) -> ModuleDeploymentResult: - """Deploy Flux system.""" try: - # Parse config flux_config = FluxConfig(**config) + operator_version = self.sanitize_version(flux_config.operator_version) + + log.info(f"Deploying Flux Operator {operator_version} from OCI registry...") - # Deploy namespace + # Deploy namespace (no strict parent, so it doesn't block teardown) namespace = k8s.core.v1.Namespace( - f"{self.name}-namespace", + f"{self.name}-system", metadata=k8s.meta.v1.ObjectMetaArgs( name=flux_config.namespace, - labels=flux_config.labels, - annotations=flux_config.annotations, + labels={ + "app.kubernetes.io/name": "flux", + "app.kubernetes.io/part-of": "flux", + }, + annotations={ + "fluxcd.controlplane.io/reconcile": "enabled", + "fluxcd.controlplane.io/reconcileEvery": "1h", + "fluxcd.controlplane.io/reconcileTimeout": "3m", + }, + ), + opts=ResourceOptions( + provider=self.provider.provider, + parent=self.provider.provider ), - opts=ResourceOptions(provider=self.provider.provider) ) - # Deploy Flux components using Helm - chart_name = "flux2" - chart_url = "https://fluxcd-community.github.io/helm-charts" + # Deploy storage PVC (child of namespace) + storage_pvc = k8s.core.v1.PersistentVolumeClaim( + f"{self.name}-storage", + metadata=k8s.meta.v1.ObjectMetaArgs(name=f"{self.name}-storage", namespace=flux_config.namespace), + spec=k8s.core.v1.PersistentVolumeClaimSpecArgs( + access_modes=["ReadWriteOnce"], + resources=k8s.core.v1.ResourceRequirementsArgs(requests={"storage": flux_config.storage_size}), + storage_class_name=flux_config.storage_class, + ), + opts=ResourceOptions(provider=self.provider.provider, parent=namespace), + ) - # Configure Helm values - helm_values = { - "git": { - "url": flux_config.git_repository, - "branch": flux_config.git_branch, - "path": flux_config.git_path, + # Deploy Flux Operator chart with minimal deletion wait times + operator_chart = Chart( + "flux-operator", + chart="oci://ghcr.io/controlplaneio-fluxcd/charts/flux-operator", + version=operator_version, + namespace=flux_config.namespace, + values={ + "operator": { + "create": True, + "resources": { + "limits": {"cpu": "200m", "memory": "256Mi"}, + "requests": {"cpu": "100m", "memory": "128Mi"}, + }, + } }, - "interval": flux_config.interval, - "components": { - component: {"enabled": component in flux_config.components} - for component in [ - "source-controller", - "kustomize-controller", - "helm-controller", - "notification-controller", - ] - }, - } + opts=ResourceOptions( + provider=self.provider.provider, + parent=storage_pvc, + custom_timeouts=CustomTimeouts(create="5m", update="5m", delete="10s"), + ), + ) - # Deploy Helm release - release = k8s.helm.v3.Release( - f"{self.name}-system", - k8s.helm.v3.ReleaseArgs( - chart=chart_name, - version=flux_config.version, + # Deploy FluxInstance CR with short delete timeout + flux_instance = k8s.apiextensions.CustomResource( + f"{self.name}-instance", + api_version="fluxcd.controlplane.io/v1", + kind="FluxInstance", + metadata=k8s.meta.v1.ObjectMetaArgs( + name="flux", namespace=flux_config.namespace, - repository_opts=k8s.helm.v3.RepositoryOptsArgs(repo=chart_url), - values=helm_values, + annotations={ + "fluxcd.controlplane.io/reconcile": "enabled", + "fluxcd.controlplane.io/reconcileEvery": flux_config.reconcile_interval, + "fluxcd.controlplane.io/reconcileTimeout": flux_config.reconcile_timeout, + }, ), + spec={ + "distribution": {"version": flux_config.version, "registry": "ghcr.io/fluxcd"}, + "components": flux_config.components, + "cluster": { + "type": "kubernetes", + "multitenant": flux_config.multitenant, + "networkPolicy": flux_config.network_policy, + "domain": flux_config.cluster_domain, + }, + "storage": {"class": flux_config.storage_class, "size": flux_config.storage_size}, + "kustomize": { + "patches": [ + { + "target": {"kind": "Deployment", "name": "(kustomize-controller|helm-controller)"}, + "patch": f""" + - op: add + path: /spec/template/spec/containers/0/args/- + value: --concurrent={flux_config.concurrent_reconciles} + - op: add + path: /spec/template/spec/containers/0/args/- + value: --requeue-dependency={flux_config.requeue_dependency_interval} + """, + } + ] + }, + }, opts=ResourceOptions( provider=self.provider.provider, - parent=namespace, - depends_on=[namespace], - custom_timeouts=CustomTimeouts( - create="2m", - update="2m", - delete="2m" - ), + parent=operator_chart, + custom_timeouts=CustomTimeouts(create="5m", update="5m", delete="10s"), ), ) - # TODO: Uncomment this when we have a git repository - ## Create GitRepository resource - #git_repo = k8s.apiextensions.CustomResource( - # f"{self.name}-gitrepository", - # api_version="source.toolkit.fluxcd.io/v1", - # kind="GitRepository", - # metadata=k8s.meta.v1.ObjectMetaArgs( - # name="flux-system", - # namespace=flux_config.namespace, - # ), - # spec={ - # "interval": flux_config.interval, - # "ref": { - # "branch": flux_config.git_branch, - # }, - # "url": flux_config.git_repository, - # }, - # opts=ResourceOptions(provider=self.provider.provider, parent=release, depends_on=[release]), - #) - - ## Create Kustomization resource - #kustomization = k8s.apiextensions.CustomResource( - # f"{self.name}-kustomization", - # api_version="kustomize.toolkit.fluxcd.io/v1", - # kind="Kustomization", - # metadata=k8s.meta.v1.ObjectMetaArgs( - # name="flux-system", - # namespace=flux_config.namespace, - # ), - # spec={ - # "interval": flux_config.interval, - # "path": flux_config.git_path, - # "prune": True, - # "sourceRef": { - # "kind": "GitRepository", - # "name": "flux-system", - # }, - # }, - # opts=ResourceOptions(provider=self.provider.provider, parent=git_repo, depends_on=[git_repo]), - #) - - # Return deployment result - resource_ids = [ - f"{self.name}-namespace", - f"{self.name}-system", - f"{self.name}-gitrepository", - f"{self.name}-kustomization", - ] - return ModuleDeploymentResult( success=True, - version=flux_config.version or "latest", - resources=resource_ids, + version=flux_config.version, + resources=[f"{self.name}-namespace", f"{self.name}-storage", "flux-operator", f"{self.name}-instance"], metadata={ "namespace": flux_config.namespace, - "git_repository": flux_config.git_repository, - "git_branch": flux_config.git_branch, + "version": flux_config.version, "components": flux_config.components, + "storage_pvc": storage_pvc.metadata.name, }, ) diff --git a/modules/kubernetes/flux/types.py b/modules/kubernetes/flux/types.py index 162f16f..0349e48 100644 --- a/modules/kubernetes/flux/types.py +++ b/modules/kubernetes/flux/types.py @@ -10,14 +10,25 @@ class FluxConfig(KubernetesConfig): """Flux module configuration.""" namespace: str = Field(default="flux-system") - version: Optional[str] = Field(default=None) - git_repository: str = Field(...) # Required - git_branch: str = Field(default="main") - git_path: str = Field(default="./") - interval: str = Field(default="1m") + version: Optional[str] = Field(default="2.x") + operator_version: Optional[str] = Field(default="latest") components: List[str] = Field( - default=["source-controller", "kustomize-controller", "helm-controller", "notification-controller"] + default=[ + "source-controller", + "kustomize-controller", + "helm-controller", + "notification-controller", + ] ) + storage_class: str = Field(default="standard") + storage_size: str = Field(default="10Gi") + network_policy: bool = Field(default=True) + multitenant: bool = Field(default=False) + cluster_domain: str = Field(default="cluster.local") + reconcile_interval: str = Field(default="1h") + reconcile_timeout: str = Field(default="3m") + concurrent_reconciles: int = Field(default=10) + requeue_dependency_interval: str = Field(default="5s") class Config: arbitrary_types_allowed = True diff --git a/modules/kubernetes/prometheus/deployment.py b/modules/kubernetes/prometheus/deployment.py index 76edefd..5dd4b37 100644 --- a/modules/kubernetes/prometheus/deployment.py +++ b/modules/kubernetes/prometheus/deployment.py @@ -5,7 +5,7 @@ """ from typing import Dict, Any, List -from pulumi import log, ResourceOptions +from pulumi import log, ResourceOptions, CustomTimeouts import pulumi_kubernetes as k8s from ..deployment import KubernetesModule @@ -66,7 +66,11 @@ def deploy(self, config: Dict[str, Any]) -> ModuleDeploymentResult: labels=prometheus_config.labels, annotations=prometheus_config.annotations, ), - opts=ResourceOptions(provider=self.provider.provider) + opts=ResourceOptions( + custom_timeouts=CustomTimeouts(create="5m", update="5m", delete="10s"), + provider=self.provider.provider, + parent=self.provider.provider + ) ) # Deploy Prometheus stack @@ -105,6 +109,7 @@ def deploy(self, config: Dict[str, Any]) -> ModuleDeploymentResult: values=helm_values, ), opts=ResourceOptions( + custom_timeouts=CustomTimeouts(create="5m", update="5m", delete="10s"), provider=self.provider.provider, parent=namespace, depends_on=[namespace] @@ -192,6 +197,7 @@ def _create_services( selector={svc_def["selector"]: svc_def["name"]}, ), opts=ResourceOptions( + custom_timeouts=CustomTimeouts(create="5m", update="5m", delete="10s"), provider=self.provider.provider, parent=namespace, depends_on=[release]