From 43a2a8616a509dd4057f517e922a7415ebd5fbbc Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Fri, 13 Sep 2024 22:03:40 +0000 Subject: [PATCH 01/43] implement type safety, config encapsulation, accelerate code, module one of many --- pulumi/__main__.py | 1051 +++++++++++---------- pulumi/src/cert_manager/deploy.py | 388 ++++++-- pulumi/src/cert_manager/types.py | 32 + pulumi/src/lib/helm_chart_versions.py | 6 - pulumi/src/lib/kubernetes_api_endpoint.py | 30 - pulumi/src/lib/namespace.py | 74 +- pulumi/src/lib/types.py | 15 + pulumi/src/vm/talos.py | 2 +- 8 files changed, 969 insertions(+), 629 deletions(-) create mode 100644 pulumi/src/cert_manager/types.py delete mode 100644 pulumi/src/lib/kubernetes_api_endpoint.py create mode 100644 pulumi/src/lib/types.py diff --git a/pulumi/__main__.py b/pulumi/__main__.py index 1866716..419f4a8 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -1,529 +1,590 @@ import os -import requests +from typing import Any, Dict, List, Optional, Tuple + import pulumi import pulumi_kubernetes as k8s from pulumi_kubernetes import Provider -from src.lib.kubernetes_api_endpoint import KubernetesApiEndpointIp -from src.cilium.deploy import deploy_cilium -from src.cert_manager.deploy import deploy_cert_manager -from src.kubevirt.deploy import deploy_kubevirt -from src.containerized_data_importer.deploy import deploy_cdi -from src.cluster_network_addons.deploy import deploy_cnao -from src.multus.deploy import deploy_multus -from src.hostpath_provisioner.deploy import deploy as deploy_hostpath_provisioner -from src.openunison.deploy import deploy_openunison -from src.prometheus.deploy import deploy_prometheus -from src.kubernetes_dashboard.deploy import deploy_kubernetes_dashboard -from src.kv_manager.deploy import deploy_ui_for_kubevirt -from src.ceph.deploy import deploy_rook_operator -from src.vm.ubuntu import deploy_ubuntu_vm -from src.vm.talos import deploy_talos_cluster - -################################################################################## -# Load the Pulumi Config +########################################## +# Load Pulumi Config config = pulumi.Config() - -# Get pulumi stack name stack_name = pulumi.get_stack() - -# Get the pulumi project name project_name = pulumi.get_project() -################################################################################## -# Get the Kubernetes configuration +########################################## +# Kubernetes Configuration kubernetes_config = config.get_object("kubernetes") or {} - -# Get Kubeconfig from Pulumi ESC Config kubeconfig = kubernetes_config.get("kubeconfig") - -# Require Kubernetes context set explicitly kubernetes_context = kubernetes_config.get("context") -# Get the Kubernetes distribution (supports: kind, talos) -kubernetes_distribution = kubernetes_config.get("distribution") or "talos" +# Assume Talos Kubernetes distribution +kubernetes_distribution = "talos" # Create a Kubernetes provider instance k8s_provider = Provider( "k8sProvider", kubeconfig=kubeconfig, - context=kubernetes_context + context=kubernetes_context, ) -versions = {} - -################################################################################## -## Enable/Disable Kargo Kubevirt PaaS Infrastructure Modules -################################################################################## +# Initialize versions dictionary to track deployed component versions +versions: Dict[str, Dict[str, Any]] = {} -# Utility function to handle config with default "enabled" flag -def get_module_config(module_name): +########################################## +# Module Configuration and Enable Flags +def get_module_config(module_name: str) -> Tuple[Dict[str, Any], bool]: module_config = config.get_object(module_name) or {"enabled": "false"} - module_enabled = str(module_config.get('enabled')).lower() == "true" - return module_config, module_enabled - -# Get configurations and enabled flags -config_cilium, cilium_enabled = get_module_config('cilium') -config_cert_manager, cert_manager_enabled = get_module_config('cert_manager') -config_kubevirt, kubevirt_enabled = get_module_config('kubevirt') -config_cdi, cdi_enabled = get_module_config('cdi') -config_multus, multus_enabled = get_module_config('multus') -config_prometheus, prometheus_enabled = get_module_config('prometheus') -config_openunison, openunison_enabled = get_module_config('openunison') -config_hostpath_provisioner, hostpath_provisioner_enabled = get_module_config('hostpath_provisioner') -config_cnao, cnao_enabled = get_module_config('cnao') -config_kubernetes_dashboard, kubernetes_dashboard_enabled = get_module_config('kubernetes_dashboard') -config_kubevirt_manager, kubevirt_manager_enabled = get_module_config('kubevirt_manager') -config_vm, vm_enabled = get_module_config('vm') -config_talos, talos_cluster_enabled = get_module_config('talos') - -################################################################################## -## Core Kargo Kubevirt PaaS Infrastructure -################################################################################## - -depends = [] - -def safe_append(depends, resource): - if resource: - depends.append(resource) - -################################################################################## -# Fetch the Cilium Version -# Deploy Cilium -def run_cilium(): - if cilium_enabled: - namespace = "kube-system" - l2announcements = config_cilium.get('l2announcements') or "192.168.1.70/28" - l2_bridge_name = config_cilium.get('l2_bridge_name') or "br0" - cilium_version = config_cilium.get('version') # or "1.14.7" - - cilium = deploy_cilium( - "cilium-cni", - k8s_provider, - kubernetes_distribution, - project_name, - kubernetes_endpoint_service_address, - namespace, - cilium_version, - l2_bridge_name, - l2announcements, - ) - cilium_version = cilium[0] - cilium_release = cilium[1] - - safe_append(depends, cilium_release) - - versions["cilium"] = {"enabled": cilium_enabled, "version": cilium_version} - - return cilium_version, cilium_release - - return None, None - -cilium_version, cilium_release = run_cilium() - -################################################################################## -# Fetch the Cert Manager Version -# Deploy Cert Manager -def run_cert_manager(): - if cert_manager_enabled: - ns_name = "cert-manager" - cert_manager_version = config_cert_manager.get('version') or None - - cert_manager = deploy_cert_manager( - ns_name, - cert_manager_version, - kubernetes_distribution, - depends, - k8s_provider - ) - - versions["cert_manager"] = {"enabled": cert_manager_enabled, "version": cert_manager[0]} - cert_manager_release = cert_manager[1] - cert_manager_selfsigned_cert = cert_manager[2] - - pulumi.export("cert_manager_selfsigned_cert", cert_manager_selfsigned_cert) - - safe_append(depends, cert_manager_release) - - return cert_manager, cert_manager_release, cert_manager_selfsigned_cert - return None, None, None - -cert_manager, cert_manager_release, cert_manager_selfsigned_cert = run_cert_manager() - -################################################################################## -# Deploy KubeVirt -def run_kubevirt(): - if kubevirt_enabled: - ns_name = "kubevirt" - kubevirt_version = config_kubevirt.get('version') or None - kubevirt_emulation = config_kubevirt.get('emulation') or False - - custom_depends = [] - safe_append(custom_depends, cilium_release) - safe_append(custom_depends, cert_manager_release) - - kubevirt = deploy_kubevirt( - custom_depends, - ns_name, - kubevirt_version, - kubevirt_emulation, - k8s_provider, - kubernetes_distribution, - ) - - versions["kubevirt"] = {"enabled": kubevirt_enabled, "version": kubevirt[0]} - kubevirt_operator = kubevirt[1] - - safe_append(depends, kubevirt_operator) - - return kubevirt, kubevirt_operator - return None, None - -kubevirt, kubevirt_operator = run_kubevirt() - -################################################################################## -# Deploy Multus -def run_multus(): - if multus_enabled: - ns_name = "multus" - multus_version = config_multus.get('version') or "master" - bridge_name = config_multus.get('bridge_name') or "br0" - - custom_depends = [] - - if cilium_enabled: - safe_append(custom_depends, cilium_release) - if cert_manager_enabled: - safe_append(custom_depends, cert_manager_release) - - multus = deploy_multus( - custom_depends, - multus_version, - bridge_name, - k8s_provider - ) - - versions["multus"] = {"enabled": multus_enabled, "version": multus[0]} - multus_release = multus[1] - - safe_append(depends, multus_release) - - return multus, multus_release - return None, None - -multus, multus_release = run_multus() - -################################################################################## -# Deploy Cluster Network Addons Operator (CNAO) -def run_cnao(): - if cnao_enabled: - ns_name = "cluster-network-addons" - cnao_version = config_cnao.get('version') or None - - custom_depends = [] - - if cilium_enabled: - safe_append(custom_depends, cilium_release) - - if cert_manager_enabled: - safe_append(custom_depends, cert_manager_release) - - cnao = deploy_cnao( - custom_depends, - cnao_version, - k8s_provider - ) - - versions["cnao"] = {"enabled": cnao_enabled, "version": cnao[0]} - cnao_release = cnao[1] - - safe_append(depends, cnao_release) - - return cnao, cnao_release - return None, None - -cnao, cnao_release = run_cnao() + module_enabled = str(module_config.get('enabled', 'false')).lower() == "true" -################################################################################## -# Deploy Hostpath Provisioner -def run_hostpath_provisioner(): - if hostpath_provisioner_enabled: - if not cert_manager_enabled: - msg = "HPP requires Cert Manager. Please enable Cert Manager and try again." - pulumi.log.error(msg) - return None, None + # Remove 'enabled' key from module_config dictionary as modules do not need this key + module_config.pop('enabled', None) - hostpath_default_path = config_hostpath_provisioner.get('default_path') or "/var/mnt/hostpath-provisioner" - hostpath_default_storage_class = config_hostpath_provisioner.get('default_storage_class') or False - ns_name = "hostpath-provisioner" - hostpath_provisioner_version = config_hostpath_provisioner.get('version') or None - - custom_depends = [] - - if cilium_enabled: - safe_append(custom_depends, cilium_release) - if cert_manager_enabled: - safe_append(custom_depends, cert_manager_release) - if kubevirt_enabled: - safe_append(custom_depends, kubevirt_operator) - - hostpath_provisioner = deploy_hostpath_provisioner( - custom_depends, - hostpath_provisioner_version, - ns_name, - hostpath_default_path, - hostpath_default_storage_class, - k8s_provider, - ) - - versions["hostpath_provisioner"] = {"enabled": hostpath_provisioner_enabled, "version": hostpath_provisioner[0]} - hostpath_provisioner_release = hostpath_provisioner[1] - - safe_append(depends, hostpath_provisioner_release) - - return hostpath_provisioner, hostpath_provisioner_release - return None, None - -hostpath_provisioner, hostpath_provisioner_release = run_hostpath_provisioner() - -################################################################################## -# Deploy Containerized Data Importer (CDI) -def run_cdi(): - if cdi_enabled: - ns_name = "cdi" - cdi_version = config_cdi.get('version') or None - - cdi = deploy_cdi( - depends, - cdi_version, - k8s_provider - ) - - versions["cdi"] = {"enabled": cdi_enabled, "version": cdi[0]} - cdi_release = cdi[1] - - safe_append(depends, cdi_release) - - return cdi, cdi_release - return None, None - -cdi, cdi_release = run_cdi() - -################################################################################## -# Deploy Prometheus -def run_prometheus(): - if prometheus_enabled: - ns_name = "monitoring" - prometheus_version = config_prometheus.get('version') or None - - prometheus = deploy_prometheus( - depends, - ns_name, - prometheus_version, - k8s_provider, - openunison_enabled - ) - - versions["prometheus"] = {"enabled": prometheus_enabled, "version": prometheus[0]} - prometheus_release = prometheus[1] - - safe_append(depends, prometheus_release) - - return prometheus, prometheus_release - return None, None - -prometheus, prometheus_release = run_prometheus() - -################################################################################## -# Deploy Kubernetes Dashboard -def run_kubernetes_dashboard(): - if kubernetes_dashboard_enabled: - ns_name = "kubernetes-dashboard" - kubernetes_dashboard_version = config_kubernetes_dashboard.get('version') or None - - if cilium_enabled: - safe_append(depends, cilium_release) - - kubernetes_dashboard = deploy_kubernetes_dashboard( - depends, - ns_name, - kubernetes_dashboard_version, - k8s_provider - ) - - versions["kubernetes_dashboard"] = {"enabled": kubernetes_dashboard_enabled, "version": kubernetes_dashboard[0]} - kubernetes_dashboard_release = kubernetes_dashboard[1] - - safe_append(depends, kubernetes_dashboard_release) - - return kubernetes_dashboard, kubernetes_dashboard_release - return None, None - -kubernetes_dashboard, kubernetes_dashboard_release = run_kubernetes_dashboard() - -################################################################################## -def run_openunison(): - if openunison_enabled: - ns_name = "openunison" - openunison_version = config_openunison.get('version') or None - domain_suffix = config_openunison.get('dns_suffix') or "kargo.arpa" - cluster_issuer = config_openunison.get('cluster_issuer') or "cluster-selfsigned-issuer-ca" - - config_openunison_github = config_openunison.get_object('github') or {} - openunison_github_teams = config_openunison_github.get('teams') - openunison_github_client_id = config_openunison_github.get('client_id') - openunison_github_client_secret = config_openunison_github.get('client_secret') - - enabled = {} - - if kubevirt_enabled: - enabled["kubevirt"] = {"enabled": kubevirt_enabled} - - if prometheus_enabled: - enabled["prometheus"] = {"enabled": prometheus_enabled} - - pulumi.export("enabled", enabled) - - openunison = deploy_openunison( - depends, - ns_name, - openunison_version, - k8s_provider, - domain_suffix, - cluster_issuer, - cert_manager_selfsigned_cert, - kubernetes_dashboard_release, - openunison_github_client_id, - openunison_github_client_secret, - openunison_github_teams, - enabled, - ) - - versions["openunison"] = {"enabled": openunison_enabled, "version": openunison[0]} - openunison_release = openunison[1] - - safe_append(depends, openunison_release) - - return openunison, openunison_release - return None, None - -openunison, openunison_release = run_openunison() - -################################################################################## -# Deploy Rook Ceph -def run_rook_ceph(): - deploy_ceph = config.get_bool('ceph.enabled') or False - if deploy_ceph: - rook_operator = deploy_rook_operator( - "kargo", - k8s_provider, - kubernetes_distribution, - "kargo", - "rook-ceph" - ) - return rook_operator - return None - -rook_operator = run_rook_ceph() - -################################################################################## -# Deploy Kubevirt Manager -def run_kubevirt_manager(): - kubevirt_manager_enabled = config.get_bool('kubevirt_manager.enabled') or False - if kubevirt_manager_enabled: - kubevirt_manager = deploy_ui_for_kubevirt( - "kargo", - k8s_provider, - kubernetes_distribution, - "kargo", - "kubevirt_manager" - ) - pulumi.export('kubevirt_manager', kubevirt_manager) - return kubevirt_manager - return None - -kubevirt_manager = run_kubevirt_manager() - -################################################################################## -# Deploy Ubuntu VM -def run_ubuntu_vm(): - if vm_enabled: - # Get the SSH Public Key string from Pulumi Config if it exists - ssh_pub_key = config.get("ssh_pub_key") - if not ssh_pub_key: - # Get the SSH public key from the local filesystem - with open(f"{os.environ['HOME']}/.ssh/id_rsa.pub", "r") as f: - ssh_pub_key = f.read().strip() - - # Define the default values - default_vm_config = { - "namespace": "default", - "instance_name": "ubuntu", - "image_name": "docker.io/containercraft/ubuntu:22.04", - "node_port": 30590, - "ssh_user": "kc2", - "ssh_password": "kc2", - "ssh_pub_key": ssh_pub_key - } - - # Merge the default values with the existing config_vm values - config_vm_merged = {**default_vm_config, **{k: v for k, v in config_vm.items() if v is not None}} - - # Pass the merged configuration to the deploy_ubuntu_vm function - ubuntu_vm, ubuntu_ssh_service = deploy_ubuntu_vm( - config_vm_merged, - k8s_provider, - depends - ) - - versions["ubuntu_vm"] = { - "enabled": vm_enabled, - "name": ubuntu_vm.metadata["name"] - } - - safe_append(depends, ubuntu_ssh_service) + return module_config, module_enabled - return ubuntu_vm, ubuntu_ssh_service - else: - return None, None +########################################## +# Dependency Management -ubuntu_vm, ubuntu_ssh_service = run_ubuntu_vm() +# Initialize a list to keep track of dependencies between resources +global_depends_on: List[pulumi.Resource] = [] -################################################################################## -# Deploy Kargo-on-Kargo Development Cluster (Controlplane + Worker VirtualMachinePools) -def run_talos_cluster(): - if talos_cluster_enabled: - # Append the resources to the `depends` list - custom_depends = [] +########################################## +# Deploy Modules - # depends on cert manager, multus - safe_append(custom_depends, cert_manager_release) - safe_append(custom_depends, multus_release) - if cdi_enabled: - safe_append(custom_depends, cdi_release) +# Cert Manager Module +config_cert_manager_dict, cert_manager_enabled = get_module_config('cert_manager') - # Deploy the Talos cluster (controlplane and workers) - controlplane_vm_pool, worker_vm_pool = deploy_talos_cluster( - config_talos=config_talos, - k8s_provider=k8s_provider, - depends_on=custom_depends, - parent=kubevirt_operator, - ) +if cert_manager_enabled: + from src.cert_manager.types import CertManagerConfig + config_cert_manager = CertManagerConfig.merge(config_cert_manager_dict) - # Export the Talos configuration and versions - versions["talos_cluster"] = { - "enabled": talos_cluster_enabled, - "running": config_talos.get("running", True), - "controlplane": config_talos.get("controlplane", {}), - "workers": config_talos.get("workers", {}) - } + from src.cert_manager.deploy import deploy_cert_manager_module - return controlplane_vm_pool, worker_vm_pool - else: - return None, None + cert_manager_version, cert_manager_release, cert_manager_selfsigned_cert = deploy_cert_manager_module( + cert_manager_enabled=cert_manager_enabled, + config_cert_manager=config_cert_manager, + global_depends_on=global_depends_on, + k8s_provider=k8s_provider, + versions=versions, + ) + pulumi.export("cert_manager_selfsigned_cert", cert_manager_selfsigned_cert) +else: + cert_manager_selfsigned_cert = None -# Run the Talos cluster deployment -talos_controlplane_vm_pool, talos_worker_vm_pool = run_talos_cluster() +########################################## +# Export Component Versions -# Export the component versions pulumi.export("versions", versions) + +#import os +#from typing import Any, Dict, List, Optional, Tuple +# +#import pulumi +#import pulumi_kubernetes as k8s +#from pulumi_kubernetes import Provider +# +#from src.cilium.deploy import deploy_cilium +#from src.cert_manager.deploy import deploy_cert_manager +#from src.kubevirt.deploy import deploy_kubevirt +#from src.containerized_data_importer.deploy import deploy_cdi +#from src.cluster_network_addons.deploy import deploy_cnao +#from src.multus.deploy import deploy_multus +#from src.hostpath_provisioner.deploy import deploy as deploy_hostpath_provisioner +#from src.openunison.deploy import deploy_openunison +#from src.prometheus.deploy import deploy_prometheus +#from src.kubernetes_dashboard.deploy import deploy_kubernetes_dashboard +#from src.kv_manager.deploy import deploy_ui_for_kubevirt +#from src.ceph.deploy import deploy_rook_operator +#from src.vm.ubuntu import deploy_ubuntu_vm +#from src.vm.talos import deploy_talos_cluster +# +########################################### +## Load Pulumi Config +#config = pulumi.Config() +#stack_name = pulumi.get_stack() +#project_name = pulumi.get_project() +# +########################################### +## Kubernetes Configuration +#kubernetes_config = config.get_object("kubernetes") or {} +#kubeconfig = kubernetes_config.get("kubeconfig") +#kubernetes_context = kubernetes_config.get("context") +# +## Create a Kubernetes provider instance +#k8s_provider = Provider( +# "k8sProvider", +# kubeconfig=kubeconfig, +# context=kubernetes_context, +#) +# +## Initialize versions dictionary to track deployed component versions +#versions: Dict[str, Dict[str, Any]] = {} +# +########################################### +## Module Configuration and Enable Flags +#def get_module_config(module_name: str) -> Tuple[Dict[str, Any], bool]: +# """ +# Retrieves the configuration for a module and determines if it is enabled. +# +# Args: +# module_name: The name of the module. +# +# Returns: +# A tuple containing the module configuration dictionary and a boolean indicating if the module is enabled. +# """ +# module_config = config.get_object(module_name) or {"enabled": "false"} +# module_enabled = str(module_config.get('enabled', 'false')).lower() == "true" +# return module_config, module_enabled +# +## Retrieve configurations and enable flags for all modules +#config_cilium, cilium_enabled = get_module_config('cilium') +#config_cert_manager, cert_manager_enabled = get_module_config('cert_manager') +#config_kubevirt, kubevirt_enabled = get_module_config('kubevirt') +#config_cdi, cdi_enabled = get_module_config('cdi') +#config_multus, multus_enabled = get_module_config('multus') +#config_prometheus, prometheus_enabled = get_module_config('prometheus') +#config_openunison, openunison_enabled = get_module_config('openunison') +#config_hostpath_provisioner, hostpath_provisioner_enabled = get_module_config('hostpath_provisioner') +#config_cnao, cnao_enabled = get_module_config('cnao') +#config_kubernetes_dashboard, kubernetes_dashboard_enabled = get_module_config('kubernetes_dashboard') +#config_kubevirt_manager, kubevirt_manager_enabled = get_module_config('kubevirt_manager') +#config_vm, vm_enabled = get_module_config('vm') +#config_talos, talos_cluster_enabled = get_module_config('talos') +# +########################################### +## Dependency Management +# +## Initialize a list to keep track of dependencies between resources +#global_depends_on: List[pulumi.Resource] = [] +# +########################################### +## Module Deployment Functions +# +#def deploy_cert_manager_module() -> Tuple[Optional[str], Optional[pulumi.Resource], Optional[str]]: +# """ +# Deploys the Cert Manager module if enabled. +# +# Returns: +# A tuple containing the version, Helm release, and CA certificate data. +# """ +# if not cert_manager_enabled: +# pulumi.log.info("Cert Manager module is disabled. Skipping deployment.") +# return None, None, None +# +# namespace = "cert-manager" +# cert_manager_version = config_cert_manager.get('version') +# +# cert_manager_version, release, ca_cert_b64, _ = deploy_cert_manager( +# namespace=namespace, +# version=cert_manager_version, +# depends_on=global_depends_on, +# k8s_provider=k8s_provider, +# ) +# +# versions["cert_manager"] = {"enabled": cert_manager_enabled, "version": cert_manager_version} +# +# if release: +# global_depends_on.append(release) +# +# # Export the CA certificate +# pulumi.export("cert_manager_selfsigned_cert", ca_cert_b64) +# +# return cert_manager_version, release, ca_cert_b64 +# +#cert_manager_version, cert_manager_release, cert_manager_selfsigned_cert = deploy_cert_manager_module() +# +########################################### +## Export Component Versions +# +#pulumi.export("versions", versions) + +#def deploy_kubevirt_module() -> Tuple[Optional[str], Optional[pulumi.Resource]]: +# """ +# Deploys the KubeVirt module if enabled. +# +# Returns: +# A tuple containing the version and the KubeVirt operator resource. +# """ +# if not kubevirt_enabled: +# pulumi.log.info("KubeVirt module is disabled. Skipping deployment.") +# return None, None +# +# namespace = "kubevirt" +# kubevirt_version = config_kubevirt.get('version') +# kubevirt_emulation = config_kubevirt.get('emulation', False) +# +# depends_on = [] +# if cilium_enabled: +# depends_on.append(cilium_release) +# if cert_manager_enabled: +# depends_on.append(cert_manager_release) +# +# kubevirt_version, kubevirt_operator = deploy_kubevirt( +# depends_on=depends_on, +# namespace=namespace, +# version=kubevirt_version, +# emulation=kubevirt_emulation, +# k8s_provider=k8s_provider, +# ) +# +# versions["kubevirt"] = {"enabled": kubevirt_enabled, "version": kubevirt_version} +# +# if kubevirt_operator: +# global_depends_on.append(kubevirt_operator) +# +# return kubevirt_version, kubevirt_operator +# +#def deploy_multus_module() -> Tuple[Optional[str], Optional[pulumi.Resource]]: +# """ +# Deploys the Multus module if enabled. +# +# Returns: +# A tuple containing the version and the Multus Helm release resource. +# """ +# if not multus_enabled: +# pulumi.log.info("Multus module is disabled. Skipping deployment.") +# return None, None +# +# namespace = "multus" +# multus_version = config_multus.get('version', "master") +# bridge_name = config_multus.get('bridge_name', "br0") +# +# depends_on = [] +# if cilium_enabled: +# depends_on.append(cilium_release) +# if cert_manager_enabled: +# depends_on.append(cert_manager_release) +# +# multus_version, multus_release = deploy_multus( +# depends_on=depends_on, +# version=multus_version, +# bridge_name=bridge_name, +# k8s_provider=k8s_provider, +# ) +# +# versions["multus"] = {"enabled": multus_enabled, "version": multus_version} +# +# if multus_release: +# global_depends_on.append(multus_release) +# +# return multus_version, multus_release +# +#def deploy_hostpath_provisioner_module() -> Tuple[Optional[str], Optional[pulumi.Resource]]: +# """ +# Deploys the HostPath Provisioner module if enabled. +# +# Returns: +# A tuple containing the version and the Helm release resource. +# """ +# if not hostpath_provisioner_enabled: +# pulumi.log.info("HostPath Provisioner module is disabled. Skipping deployment.") +# return None, None +# +# if not cert_manager_enabled: +# error_msg = "HostPath Provisioner requires Cert Manager. Please enable Cert Manager and try again." +# pulumi.log.error(error_msg) +# raise Exception(error_msg) +# +# namespace = "hostpath-provisioner" +# hostpath_version = config_hostpath_provisioner.get('version') +# default_path = config_hostpath_provisioner.get('default_path', "/var/mnt/hostpath-provisioner") +# default_storage_class = config_hostpath_provisioner.get('default_storage_class', False) +# +# depends_on = [] +# if cilium_enabled: +# depends_on.append(cilium_release) +# if cert_manager_enabled: +# depends_on.append(cert_manager_release) +# if kubevirt_enabled: +# depends_on.append(kubevirt_operator) +# +# hostpath_version, hostpath_release = deploy_hostpath_provisioner( +# depends_on=depends_on, +# version=hostpath_version, +# namespace=namespace, +# default_path=default_path, +# default_storage_class=default_storage_class, +# k8s_provider=k8s_provider, +# ) +# +# versions["hostpath_provisioner"] = {"enabled": hostpath_provisioner_enabled, "version": hostpath_version} +# +# if hostpath_release: +# global_depends_on.append(hostpath_release) +# +# return hostpath_version, hostpath_release +# +#def deploy_cdi_module() -> Tuple[Optional[str], Optional[pulumi.Resource]]: +# """ +# Deploys the Containerized Data Importer (CDI) module if enabled. +# +# Returns: +# A tuple containing the version and the CDI Helm release resource. +# """ +# if not cdi_enabled: +# pulumi.log.info("CDI module is disabled. Skipping deployment.") +# return None, None +# +# namespace = "cdi" +# cdi_version = config_cdi.get('version') +# +# cdi_version, cdi_release = deploy_cdi( +# depends_on=global_depends_on, +# version=cdi_version, +# k8s_provider=k8s_provider, +# ) +# +# versions["cdi"] = {"enabled": cdi_enabled, "version": cdi_version} +# +# if cdi_release: +# global_depends_on.append(cdi_release) +# +# return cdi_version, cdi_release +# +#def deploy_prometheus_module() -> Tuple[Optional[str], Optional[pulumi.Resource]]: +# """ +# Deploys the Prometheus module if enabled. +# +# Returns: +# A tuple containing the version and the Prometheus Helm release resource. +# """ +# if not prometheus_enabled: +# pulumi.log.info("Prometheus module is disabled. Skipping deployment.") +# return None, None +# +# namespace = "monitoring" +# prometheus_version = config_prometheus.get('version') +# +# prometheus_version, prometheus_release = deploy_prometheus( +# depends_on=global_depends_on, +# namespace=namespace, +# version=prometheus_version, +# k8s_provider=k8s_provider, +# openunison_enabled=openunison_enabled, +# ) +# +# versions["prometheus"] = {"enabled": prometheus_enabled, "version": prometheus_version} +# +# if prometheus_release: +# global_depends_on.append(prometheus_release) +# +# return prometheus_version, prometheus_release +# +#def deploy_kubernetes_dashboard_module() -> Tuple[Optional[str], Optional[pulumi.Resource]]: +# """ +# Deploys the Kubernetes Dashboard module if enabled. +# +# Returns: +# A tuple containing the version and the Dashboard Helm release resource. +# """ +# if not kubernetes_dashboard_enabled: +# pulumi.log.info("Kubernetes Dashboard module is disabled. Skipping deployment.") +# return None, None +# +# namespace = "kubernetes-dashboard" +# dashboard_version = config_kubernetes_dashboard.get('version') +# +# depends_on = global_depends_on.copy() +# if cilium_enabled: +# depends_on.append(cilium_release) +# +# dashboard_version, dashboard_release = deploy_kubernetes_dashboard( +# depends_on=depends_on, +# namespace=namespace, +# version=dashboard_version, +# k8s_provider=k8s_provider, +# ) +# +# versions["kubernetes_dashboard"] = {"enabled": kubernetes_dashboard_enabled, "version": dashboard_version} +# +# if dashboard_release: +# global_depends_on.append(dashboard_release) +# +# return dashboard_version, dashboard_release +# +#def deploy_openunison_module() -> Tuple[Optional[str], Optional[pulumi.Resource]]: +# """ +# Deploys the OpenUnison module if enabled. +# +# Returns: +# A tuple containing the version and the OpenUnison Helm release resource. +# """ +# if not openunison_enabled: +# pulumi.log.info("OpenUnison module is disabled. Skipping deployment.") +# return None, None +# +# namespace = "openunison" +# openunison_version = config_openunison.get('version') +# domain_suffix = config_openunison.get('dns_suffix', "kargo.arpa") +# cluster_issuer = config_openunison.get('cluster_issuer', "cluster-selfsigned-issuer-ca") +# +# github_config = config_openunison.get('github', {}) +# github_client_id = github_config.get('client_id') +# github_client_secret = github_config.get('client_secret') +# github_teams = github_config.get('teams') +# +# if not github_client_id or not github_client_secret: +# error_msg = "OpenUnison requires GitHub OAuth credentials. Please provide 'client_id' and 'client_secret' in the configuration." +# pulumi.log.error(error_msg) +# raise Exception(error_msg) +# +# enabled_components = { +# "kubevirt": {"enabled": kubevirt_enabled}, +# "prometheus": {"enabled": prometheus_enabled}, +# } +# +# pulumi.export("enabled_components", enabled_components) +# +# openunison_version, openunison_release = deploy_openunison( +# depends_on=global_depends_on, +# namespace=namespace, +# version=openunison_version, +# k8s_provider=k8s_provider, +# domain_suffix=domain_suffix, +# cluster_issuer=cluster_issuer, +# ca_cert_b64=cert_manager_selfsigned_cert, +# kubernetes_dashboard_release=kubernetes_dashboard_release, +# github_client_id=github_client_id, +# github_client_secret=github_client_secret, +# github_teams=github_teams, +# enabled_components=enabled_components, +# ) +# +# versions["openunison"] = {"enabled": openunison_enabled, "version": openunison_version} +# +# if openunison_release: +# global_depends_on.append(openunison_release) +# +# return openunison_version, openunison_release +# +#def deploy_ubuntu_vm_module() -> Tuple[Optional[Any], Optional[Any]]: +# """ +# Deploys an Ubuntu VM using KubeVirt if enabled. +# +# Returns: +# A tuple containing the VM resource and the SSH service resource. +# """ +# if not vm_enabled: +# pulumi.log.info("Ubuntu VM module is disabled. Skipping deployment.") +# return None, None +# +# # Get the SSH public key from Pulumi Config or local filesystem +# ssh_pub_key = config.get("ssh_pub_key") +# if not ssh_pub_key: +# ssh_key_path = os.path.expanduser("~/.ssh/id_rsa.pub") +# try: +# with open(ssh_key_path, "r") as f: +# ssh_pub_key = f.read().strip() +# except FileNotFoundError: +# error_msg = f"SSH public key not found at {ssh_key_path}. Please provide 'ssh_pub_key' in the configuration." +# pulumi.log.error(error_msg) +# raise Exception(error_msg) +# +# # Merge default VM configuration with user-provided configuration +# default_vm_config = { +# "namespace": "default", +# "instance_name": "ubuntu", +# "image_name": "docker.io/containercraft/ubuntu:22.04", +# "node_port": 30590, +# "ssh_user": "kc2", +# "ssh_password": "kc2", +# "ssh_pub_key": ssh_pub_key, +# } +# +# vm_config = {**default_vm_config, **config_vm} +# +# # Deploy the Ubuntu VM +# ubuntu_vm, ssh_service = deploy_ubuntu_vm( +# vm_config=vm_config, +# k8s_provider=k8s_provider, +# depends_on=global_depends_on, +# ) +# +# versions["ubuntu_vm"] = {"enabled": vm_enabled, "name": ubuntu_vm.metadata["name"]} +# +# if ssh_service: +# global_depends_on.append(ssh_service) +# +# return ubuntu_vm, ssh_service +# +#def deploy_talos_cluster_module() -> Tuple[Optional[Any], Optional[Any]]: +# """ +# Deploys a Talos cluster using KubeVirt if enabled. +# +# Returns: +# A tuple containing the control plane VM pool and the worker VM pool resources. +# """ +# if not talos_cluster_enabled: +# pulumi.log.info("Talos cluster module is disabled. Skipping deployment.") +# return None, None +# +# depends_on = [] +# if cert_manager_enabled: +# depends_on.append(cert_manager_release) +# if multus_enabled: +# depends_on.append(multus_release) +# if cdi_enabled: +# depends_on.append(cdi_release) +# +# controlplane_vm_pool, worker_vm_pool = deploy_talos_cluster( +# config_talos=config_talos, +# k8s_provider=k8s_provider, +# parent=kubevirt_operator, +# depends_on=depends_on, +# ) +# +# versions["talos_cluster"] = { +# "enabled": talos_cluster_enabled, +# "running": config_talos.get("running", True), +# "controlplane": config_talos.get("controlplane", {}), +# "workers": config_talos.get("workers", {}), +# } +# +# return controlplane_vm_pool, worker_vm_pool + +# Deactivating Cilium module deployment until Talos 1.8 releases with cni optimizations +#def deploy_cilium_module() -> Tuple[Optional[str], Optional[pulumi.Resource]]: +# """ +# Deploys the Cilium CNI module if enabled. +# +# Returns: +# A tuple containing the Cilium version and the Helm release resource. +# """ +# if not cilium_enabled: +# pulumi.log.info("Cilium module is disabled. Skipping deployment.") +# return None, None +# +# namespace = "kube-system" +# l2_announcements = config_cilium.get('l2announcements', "192.168.1.70/28") +# l2_bridge_name = config_cilium.get('l2_bridge_name', "br0") +# cilium_version = config_cilium.get('version') +# +# # Deploy Cilium using the provided parameters +# cilium_version, cilium_release = deploy_cilium( +# name="cilium-cni", +# provider=k8s_provider, +# project_name=project_name, +# kubernetes_endpoint_service_address=None, # Replace with actual endpoint if needed +# namespace=namespace, +# version=cilium_version, +# l2_bridge_name=l2_bridge_name, +# l2announcements=l2_announcements, +# ) +# +# versions["cilium"] = {"enabled": cilium_enabled, "version": cilium_version} +# +# # Add the release to the global dependencies +# if cilium_release: +# global_depends_on.append(cilium_release) +# +# return cilium_version, cilium_release + +#kubevirt_version, kubevirt_operator = deploy_kubevirt_module() +#multus_version, multus_release = deploy_multus_module() +#hostpath_version, hostpath_release = deploy_hostpath_provisioner_module() +#cdi_version, cdi_release = deploy_cdi_module() +#prometheus_version, prometheus_release = deploy_prometheus_module() +#dashboard_version, kubernetes_dashboard_release = deploy_kubernetes_dashboard_module() +#openunison_version, openunison_release = deploy_openunison_module() +#ubuntu_vm, ubuntu_ssh_service = deploy_ubuntu_vm_module() +#talos_controlplane_vm_pool, talos_worker_vm_pool = deploy_talos_cluster_module() +#cilium_version, cilium_release = deploy_cilium_module() diff --git a/pulumi/src/cert_manager/deploy.py b/pulumi/src/cert_manager/deploy.py index b4cf261..436f670 100644 --- a/pulumi/src/cert_manager/deploy.py +++ b/pulumi/src/cert_manager/deploy.py @@ -1,48 +1,98 @@ +# src/cert_manager/deploy.py + import pulumi import pulumi_kubernetes as k8s from pulumi_kubernetes.apiextensions.CustomResource import CustomResource +from typing import Optional, List, Dict, Any, Tuple + from src.lib.namespace import create_namespace from src.lib.helm_chart_versions import get_latest_helm_chart_version +from src.lib.types import NamespaceConfig +from .types import CertManagerConfig + +def deploy_cert_manager_module( + cert_manager_enabled: bool, + config_cert_manager: CertManagerConfig, + global_depends_on: List[pulumi.Resource], + k8s_provider: k8s.Provider, + versions: Dict[str, Dict[str, Any]], + ) -> Tuple[Optional[str], Optional[pulumi.Resource], Optional[str]]: + """ + Deploys the Cert Manager module if enabled. + + Returns: + A tuple containing the version, Helm release, and CA certificate data. + """ + if not cert_manager_enabled: + pulumi.log.info("Cert Manager module is disabled. Skipping deployment.") + return None, None, None + + cert_manager_version, release, ca_cert_b64, _ = deploy_cert_manager( + config_cert_manager=config_cert_manager, + depends_on=global_depends_on, + k8s_provider=k8s_provider, + ) + + versions["cert_manager"] = {"enabled": cert_manager_enabled, "version": cert_manager_version} + + if release: + global_depends_on.append(release) + + # Export the CA certificate + pulumi.export("cert_manager_selfsigned_cert", ca_cert_b64) + + return cert_manager_version, release, ca_cert_b64 def deploy_cert_manager( - ns_name: str, - version: str, - kubernetes_distribution: str, - depends: pulumi.Resource, + config_cert_manager: CertManagerConfig, + depends_on: Optional[List[pulumi.Resource]], k8s_provider: k8s.Provider - ): - - # Create namespace - ns_retain = False - ns_protect = False - ns_annotations = {} - ns_labels = {} - namespace = create_namespace( - depends, - ns_name, - ns_retain, - ns_protect, - k8s_provider, - custom_labels=ns_labels, - custom_annotations=ns_annotations + ) -> Tuple[str, k8s.helm.v3.Release, str, k8s.core.v1.Secret]: + """ + Deploys Cert Manager using Helm. + + Args: + config_cert_manager: Configuration object for Cert Manager. + depends_on: List of dependencies. + k8s_provider: Kubernetes provider. + + Returns: + A tuple containing the version, Helm release, CA certificate data, and the secret resource. + """ + # Extract configuration values + namespace = config_cert_manager.namespace + version = config_cert_manager.version + cluster_issuer_name = config_cert_manager.cluster_issuer + install_crds = config_cert_manager.install_crds + + # Validate required fields + if not namespace: + raise ValueError("Namespace must be specified in the cert_manager configuration.") + + # Create NamespaceConfig instance + namespace_config = NamespaceConfig(name=namespace) + + # Create namespace using the new create_namespace function + namespace_resource = create_namespace( + config=namespace_config, + k8s_provider=k8s_provider, + depends_on=depends_on, ) chart_name = "cert-manager" - chart_index_path = "index.yaml" chart_url = "https://charts.jetstack.io" - chart_index_url = f"{chart_url}/{chart_index_path}" # Fetch the latest version from the helm chart index if version is None: - version = get_latest_helm_chart_version(chart_index_url, chart_name) + version = get_latest_helm_chart_version(f"{chart_url}/index.yaml", chart_name) version = version.lstrip("v") pulumi.log.info(f"Setting helm release version to latest: {chart_name}/{version}") else: # Log the version override pulumi.log.info(f"Using helm release version: {chart_name}/{version}") - # Deploy cert-manager using the Helm release with updated custom values - helm_values = gen_helm_values(kubernetes_distribution) + # Generate Helm values + helm_values = gen_helm_values(config_cert_manager) # Deploy cert-manager using the Helm release with custom values release = k8s.helm.v3.Release( @@ -50,17 +100,17 @@ def deploy_cert_manager( k8s.helm.v3.ReleaseArgs( chart=chart_name, version=version, - namespace=ns_name, + namespace=namespace, skip_await=False, - repository_opts= k8s.helm.v3.RepositoryOptsArgs( + repository_opts=k8s.helm.v3.RepositoryOptsArgs( repo=chart_url ), values=helm_values, ), opts=pulumi.ResourceOptions( - provider = k8s_provider, - parent=namespace, - depends_on=[], + provider=k8s_provider, + parent=namespace_resource, + depends_on=depends_on, custom_timeouts=pulumi.CustomTimeouts( create="8m", update="4m", @@ -76,15 +126,15 @@ def deploy_cert_manager( kind="ClusterIssuer", metadata={ "name": "cluster-selfsigned-issuer-root", - "namespace": ns_name + "namespace": namespace }, spec={ "selfSigned": {} }, opts=pulumi.ResourceOptions( - provider = k8s_provider, + provider=k8s_provider, parent=release, - depends_on=[namespace], + depends_on=[namespace_resource], custom_timeouts=pulumi.CustomTimeouts( create="5m", update="10m", @@ -99,7 +149,7 @@ def deploy_cert_manager( kind="Certificate", metadata={ "name": "cluster-selfsigned-issuer-ca", - "namespace": ns_name + "namespace": namespace }, spec={ "commonName": "cluster-selfsigned-issuer-ca", @@ -118,9 +168,9 @@ def deploy_cert_manager( "secretName": "cluster-selfsigned-issuer-ca" }, opts=pulumi.ResourceOptions( - provider = k8s_provider, + provider=k8s_provider, parent=cluster_issuer_root, - depends_on=[namespace], + depends_on=[namespace_resource], custom_timeouts=pulumi.CustomTimeouts( create="5m", update="10m", @@ -130,12 +180,12 @@ def deploy_cert_manager( ) cluster_issuer = CustomResource( - "cluster-selfsigned-issuer", + cluster_issuer_name, api_version="cert-manager.io/v1", kind="ClusterIssuer", metadata={ - "name": "cluster-selfsigned-issuer", - "namespace": ns_name + "name": cluster_issuer_name, + "namespace": namespace }, spec={ "ca": { @@ -145,7 +195,7 @@ def deploy_cert_manager( opts=pulumi.ResourceOptions( provider=k8s_provider, parent=cluster_issuer_ca_certificate, - depends_on=[namespace], + depends_on=[namespace_resource], custom_timeouts=pulumi.CustomTimeouts( create="4m", update="4m", @@ -158,13 +208,13 @@ def deploy_cert_manager( ca_secret = k8s.core.v1.Secret( "cluster-selfsigned-issuer-ca-secret", metadata={ - "namespace": ns_name, + "namespace": namespace, "name": cluster_issuer_ca_certificate.spec["secretName"] }, opts=pulumi.ResourceOptions( provider=k8s_provider, parent=cluster_issuer, - depends_on=[cluster_issuer] + depends_on=[cluster_issuer], ) ) @@ -173,12 +223,22 @@ def deploy_cert_manager( return version, release, ca_data_tls_crt_b64, ca_secret -def gen_helm_values(kubernetes_distribution: str): +def gen_helm_values( + config_cert_manager: CertManagerConfig + ) -> Dict[str, Any]: + """ + Generates custom Helm values for the cert-manager Helm chart. + + Args: + config_cert_manager: Configuration object for Cert Manager. + Returns: + A dictionary of Helm values. + """ # Define custom values for the cert-manager Helm chart - common_values = { + helm_values = { 'replicaCount': 1, - 'installCRDs': True, + 'installCRDs': config_cert_manager.install_crds, 'resources': { 'limits': { 'cpu': '500m', @@ -190,16 +250,232 @@ def gen_helm_values(kubernetes_distribution: str): } } } + return helm_values - if kubernetes_distribution == 'kind': - # Kind-specific Helm values - return { - **common_values, - } - elif kubernetes_distribution == 'talos': - # Talos-specific Helm values per the Talos Docs - return { - **common_values, - } - else: - raise ValueError(f"Unsupported Kubernetes distribution: {kubernetes_distribution}") +#import pulumi +#import pulumi_kubernetes as k8s +#from pulumi_kubernetes.apiextensions.CustomResource import CustomResource +#from typing import Optional, List, Dict, Any, Tuple +# +#from src.lib.namespace import create_namespace +#from src.lib.helm_chart_versions import get_latest_helm_chart_version +# +#def deploy_cert_manager( +# namespace: str, +# version: Optional[str], +# kubernetes_distribution: str, +# depends_on: Optional[List[pulumi.Resource]], +# k8s_provider: k8s.Provider +# ) -> Tuple[str, k8s.helm.v3.Release, str, k8s.core.v1.Secret]: +# """ +# Deploys Cert Manager using Helm. +# +# Args: +# namespace: Namespace name for Cert Manager. +# version: Version of the Cert Manager Helm chart. +# kubernetes_distribution: The Kubernetes distribution in use. +# depends_on: List of dependencies. +# k8s_provider: Kubernetes provider. +# +# Returns: +# A tuple containing the version, Helm release, CA certificate data, and the secret resource. +# """ +# # Create namespace +# ns_retain = False +# ns_protect = False +# ns_annotations = {} +# ns_labels = {} +# namespace_resource = create_namespace( +# depends_on, +# namespace, +# ns_retain, +# ns_protect, +# k8s_provider, +# custom_labels=ns_labels, +# custom_annotations=ns_annotations +# ) +# +# chart_name = "cert-manager" +# chart_index_path = "index.yaml" +# chart_url = "https://charts.jetstack.io" +# chart_index_url = f"{chart_url}/{chart_index_path}" +# +# # Fetch the latest version from the helm chart index +# if version is None: +# version = get_latest_helm_chart_version(chart_index_url, chart_name) +# version = version.lstrip("v") +# pulumi.log.info(f"Setting helm release version to latest: {chart_name}/{version}") +# else: +# # Log the version override +# pulumi.log.info(f"Using helm release version: {chart_name}/{version}") +# +# # Deploy cert-manager using the Helm release with updated custom values +# helm_values = gen_helm_values(kubernetes_distribution) +# +# # Deploy cert-manager using the Helm release with custom values +# release = k8s.helm.v3.Release( +# chart_name, +# k8s.helm.v3.ReleaseArgs( +# chart=chart_name, +# version=version, +# namespace=namespace, +# skip_await=False, +# repository_opts= k8s.helm.v3.RepositoryOptsArgs( +# repo=chart_url +# ), +# values=helm_values, +# ), +# opts=pulumi.ResourceOptions( +# provider = k8s_provider, +# parent=namespace_resource, +# depends_on=depends_on, +# custom_timeouts=pulumi.CustomTimeouts( +# create="8m", +# update="4m", +# delete="4m" +# ) +# ) +# ) +# +# # Create a self-signed ClusterIssuer resource +# cluster_issuer_root = CustomResource( +# "cluster-selfsigned-issuer-root", +# api_version="cert-manager.io/v1", +# kind="ClusterIssuer", +# metadata={ +# "name": "cluster-selfsigned-issuer-root", +# "namespace": namespace +# }, +# spec={ +# "selfSigned": {} +# }, +# opts=pulumi.ResourceOptions( +# provider = k8s_provider, +# parent=release, +# depends_on=[namespace_resource], +# custom_timeouts=pulumi.CustomTimeouts( +# create="5m", +# update="10m", +# delete="10m" +# ) +# ) +# ) +# +# cluster_issuer_ca_certificate = CustomResource( +# "cluster-selfsigned-issuer-ca", +# api_version="cert-manager.io/v1", +# kind="Certificate", +# metadata={ +# "name": "cluster-selfsigned-issuer-ca", +# "namespace": namespace +# }, +# spec={ +# "commonName": "cluster-selfsigned-issuer-ca", +# "duration": "2160h0m0s", +# "isCA": True, +# "issuerRef": { +# "group": "cert-manager.io", +# "kind": "ClusterIssuer", +# "name": cluster_issuer_root.metadata["name"], +# }, +# "privateKey": { +# "algorithm": "ECDSA", +# "size": 256 +# }, +# "renewBefore": "360h0m0s", +# "secretName": "cluster-selfsigned-issuer-ca" +# }, +# opts=pulumi.ResourceOptions( +# provider = k8s_provider, +# parent=cluster_issuer_root, +# depends_on=[namespace_resource], +# custom_timeouts=pulumi.CustomTimeouts( +# create="5m", +# update="10m", +# delete="10m" +# ) +# ) +# ) +# +# cluster_issuer = CustomResource( +# "cluster-selfsigned-issuer", +# api_version="cert-manager.io/v1", +# kind="ClusterIssuer", +# metadata={ +# "name": "cluster-selfsigned-issuer", +# "namespace": namespace +# }, +# spec={ +# "ca": { +# "secretName": cluster_issuer_ca_certificate.spec["secretName"], +# } +# }, +# opts=pulumi.ResourceOptions( +# provider=k8s_provider, +# parent=cluster_issuer_ca_certificate, +# depends_on=[namespace_resource], +# custom_timeouts=pulumi.CustomTimeouts( +# create="4m", +# update="4m", +# delete="4m" +# ) +# ) +# ) +# +# # Retrieve the CA certificate secret +# ca_secret = k8s.core.v1.Secret( +# "cluster-selfsigned-issuer-ca-secret", +# metadata={ +# "namespace": namespace, +# "name": cluster_issuer_ca_certificate.spec["secretName"] +# }, +# opts=pulumi.ResourceOptions( +# provider=k8s_provider, +# parent=cluster_issuer, +# depends_on=[cluster_issuer], +# ) +# ) +# +# # Extract the tls.crt value from the secret +# ca_data_tls_crt_b64 = ca_secret.data.apply(lambda data: data["tls.crt"]) +# +# return version, release, ca_data_tls_crt_b64, ca_secret +# +#def gen_helm_values(kubernetes_distribution: str) -> Dict[str, Any]: +# """ +# Generates custom Helm values for the cert-manager Helm chart based on the Kubernetes distribution. +# +# Args: +# kubernetes_distribution: The Kubernetes distribution in use. +# +# Returns: +# A dictionary of Helm values. +# """ +# # Define custom values for the cert-manager Helm chart +# common_values = { +# 'replicaCount': 1, +# 'installCRDs': True, +# 'resources': { +# 'limits': { +# 'cpu': '500m', +# 'memory': '1024Mi' +# }, +# 'requests': { +# 'cpu': '250m', +# 'memory': '512Mi' +# } +# } +# } +# +# if kubernetes_distribution == 'kind': +# # Kind-specific Helm values +# return { +# **common_values, +# } +# elif kubernetes_distribution == 'talos': +# # Talos-specific Helm values per the Talos Docs +# return { +# **common_values, +# } +# else: +# raise ValueError(f"Unsupported Kubernetes distribution: {kubernetes_distribution}") diff --git a/pulumi/src/cert_manager/types.py b/pulumi/src/cert_manager/types.py new file mode 100644 index 0000000..2ddd602 --- /dev/null +++ b/pulumi/src/cert_manager/types.py @@ -0,0 +1,32 @@ +# src/cert_manager/types.py + +from dataclasses import dataclass, field +from typing import Optional, Dict, Any +import pulumi + +@dataclass +class CertManagerConfig: + namespace: str = "cert-manager" + version: Optional[str] = None + cluster_issuer: str = "cluster-selfsigned-issuer" + install_crds: bool = True + + @staticmethod + def merge(user_config: Dict[str, Any]) -> 'CertManagerConfig': + """ + Merges user-provided configuration with default values for CertManagerConfig. + + Args: + user_config: The user-provided configuration dictionary. + + Returns: + A CertManagerConfig object with defaults applied. + """ + default_config = CertManagerConfig() + merged_config = default_config.__dict__.copy() + for key, value in user_config.items(): + if hasattr(default_config, key): + merged_config[key] = value + else: + pulumi.log.warn(f"Unknown configuration key '{key}' in cert_manager config.") + return CertManagerConfig(**merged_config) diff --git a/pulumi/src/lib/helm_chart_versions.py b/pulumi/src/lib/helm_chart_versions.py index 1aa006b..ab3dc82 100644 --- a/pulumi/src/lib/helm_chart_versions.py +++ b/pulumi/src/lib/helm_chart_versions.py @@ -53,9 +53,3 @@ def get_latest_helm_chart_version(url, chart_name): except requests.RequestException as e: logging.error(f"Error fetching data: {e}") return f"Error fetching data: {e}" - -## Example usage -#url = "https://raw.githubusercontent.com/cilium/charts/master/index.yaml" -#chart = "cilium" -#latest_version = get_latest_helm_chart_version(url, chart) -#print(f"The latest version of {chart} is: {latest_version}") diff --git a/pulumi/src/lib/kubernetes_api_endpoint.py b/pulumi/src/lib/kubernetes_api_endpoint.py deleted file mode 100644 index e165039..0000000 --- a/pulumi/src/lib/kubernetes_api_endpoint.py +++ /dev/null @@ -1,30 +0,0 @@ -import pulumi -from pulumi_kubernetes import core, meta, Provider - -class KubernetesApiEndpointIp(pulumi.ComponentResource): - """ - Represents a Kubernetes API endpoint IP address. - - Args: - name (str): The name of the resource. - k8s_provider (Provider): The Kubernetes provider. - - Attributes: - endpoint (core.v1.Endpoints): The Kubernetes endpoint. - ips (pulumi.Output[str]): The comma-separated string of IP addresses. - - """ - def __init__(self, name: str, k8s_provider: Provider): - super().__init__('custom:x:KubernetesApiEndpointIp', name, {}, opts=pulumi.ResourceOptions(provider=k8s_provider)) - - self.endpoint = core.v1.Endpoints.get( - "kubernetes", - "kubernetes", - opts=pulumi.ResourceOptions(provider=k8s_provider) - ) - - self.ips = self.endpoint.subsets.apply( - lambda subsets: ','.join([address.ip for subset in subsets for address in subset.addresses]) if subsets else '' - ) - - self.register_outputs({"ip_string": self.ips}) diff --git a/pulumi/src/lib/namespace.py b/pulumi/src/lib/namespace.py index f4838aa..f1ab0da 100644 --- a/pulumi/src/lib/namespace.py +++ b/pulumi/src/lib/namespace.py @@ -1,61 +1,53 @@ +# src/lib/namespace.py + import pulumi import pulumi_kubernetes as k8s +from typing import List, Optional +from .types import NamespaceConfig def create_namespace( - depends, - ns_name: str, - ns_retain, - ns_protect, + config: NamespaceConfig, k8s_provider: k8s.Provider, - custom_labels=None, - custom_annotations=None, - finalizers=None - ): - - # Default labels and annotations - default_labels = { - "ccio.v1/app": "kargo" - } - - default_annotations = {} + depends_on: Optional[List[pulumi.Resource]] = None, +) -> k8s.core.v1.Namespace: + """ + Creates a Kubernetes Namespace based on the provided configuration. - # Merge custom labels and annotations with defaults - complete_labels = {**default_labels, **(custom_labels or {})} - complete_annotations = {**default_annotations, **(custom_annotations or {})} + Args: + config: NamespaceConfig object containing namespace configurations. + k8s_provider: The Kubernetes provider. + depends_on: Optional list of resources to depend on. - # Use default finalizers if none are provided - if finalizers is None: - finalizers = ["kubernetes"] + Returns: + The created Namespace resource. + """ - # If `depends` is None, set it to an empty list - if depends == (None, None): - depends = [] + # Ensure depends_on is a list + if depends_on is None: + depends_on = [] - # Create the namespace with merged labels, annotations, and finalizers + # Create the namespace namespace_resource = k8s.core.v1.Namespace( - ns_name, + config.name, metadata=k8s.meta.v1.ObjectMetaArgs( - name=ns_name, - annotations=complete_annotations, - labels=complete_labels + name=config.name, + labels=config.labels, + annotations=config.annotations, ), spec=k8s.core.v1.NamespaceSpecArgs( - finalizers=finalizers, + finalizers=config.finalizers, ), opts=pulumi.ResourceOptions( - protect=ns_protect, - retain_on_delete=ns_retain, + protect=config.protect, + retain_on_delete=config.retain_on_delete, provider=k8s_provider, - depends_on=depends, - ignore_changes=[ - "metadata", - "spec" - ], + depends_on=depends_on, + ignore_changes=config.ignore_changes, custom_timeouts=pulumi.CustomTimeouts( - create="5m", - update="10m", - delete="10m" - ) + create=config.custom_timeouts.get("create", "5m"), + update=config.custom_timeouts.get("update", "10m"), + delete=config.custom_timeouts.get("delete", "10m"), + ), ) ) diff --git a/pulumi/src/lib/types.py b/pulumi/src/lib/types.py new file mode 100644 index 0000000..a146417 --- /dev/null +++ b/pulumi/src/lib/types.py @@ -0,0 +1,15 @@ +# src/lib/types.py + +from dataclasses import dataclass, field +from typing import Optional, Dict, List + +@dataclass +class NamespaceConfig: + name: str + labels: Dict[str, str] = field(default_factory=lambda: {"ccio.v1/app": "kargo"}) + annotations: Dict[str, str] = field(default_factory=dict) + finalizers: List[str] = field(default_factory=lambda: ["kubernetes"]) + protect: bool = False + retain_on_delete: bool = False + ignore_changes: List[str] = field(default_factory=lambda: ["metadata", "spec"]) + custom_timeouts: Dict[str, str] = field(default_factory=lambda: {"create": "5m", "update": "10m", "delete": "10m"}) diff --git a/pulumi/src/vm/talos.py b/pulumi/src/vm/talos.py index a99ed84..cd4ffb5 100644 --- a/pulumi/src/vm/talos.py +++ b/pulumi/src/vm/talos.py @@ -4,8 +4,8 @@ def deploy_talos_cluster( config_talos: dict, k8s_provider: k8s.Provider, + parent, depends_on: pulumi.Output[list], - parent ): """ Deploy the Talos controlplane and worker VirtualMachinePools based on the provided configuration. From 9c3431fccba83c1866049ffebf5a76bf7d1a6f6c Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sat, 14 Sep 2024 06:17:18 +0000 Subject: [PATCH 02/43] operationalize version locking, customization, prescedence, distribution, and reporting --- pulumi/__main__.py | 60 +++++-- pulumi/default_versions.json | 3 + pulumi/src/cert_manager/deploy.py | 254 +----------------------------- pulumi/src/lib/versions.py | 86 ++++++++++ 4 files changed, 140 insertions(+), 263 deletions(-) create mode 100644 pulumi/default_versions.json create mode 100644 pulumi/src/lib/versions.py diff --git a/pulumi/__main__.py b/pulumi/__main__.py index 419f4a8..737f712 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -1,3 +1,5 @@ +# __main__.py + import os from typing import Any, Dict, List, Optional, Tuple @@ -5,20 +7,28 @@ import pulumi_kubernetes as k8s from pulumi_kubernetes import Provider +from src.lib.versions import load_default_versions + ########################################## # Load Pulumi Config config = pulumi.Config() stack_name = pulumi.get_stack() project_name = pulumi.get_project() +# Load Default Versions +default_versions = load_default_versions(config) + +# Initialize versions dictionary to track deployed component versions +versions: Dict[str, str] = {} + ########################################## # Kubernetes Configuration kubernetes_config = config.get_object("kubernetes") or {} -kubeconfig = kubernetes_config.get("kubeconfig") +kubeconfig = kubernetes_config.get("kubeconfig") or os.getenv('KUBECONFIG') kubernetes_context = kubernetes_config.get("context") -# Assume Talos Kubernetes distribution -kubernetes_distribution = "talos" +pulumi.log.info(f"kubeconfig: {kubeconfig}") +pulumi.log.info(f"kubernetes_context: {kubernetes_context}") # Create a Kubernetes provider instance k8s_provider = Provider( @@ -27,31 +37,46 @@ context=kubernetes_context, ) -# Initialize versions dictionary to track deployed component versions -versions: Dict[str, Dict[str, Any]] = {} +# Initialize configurations dictionary to track module configurations +configurations: Dict[str, Dict[str, Any]] = {} ########################################## # Module Configuration and Enable Flags -def get_module_config(module_name: str) -> Tuple[Dict[str, Any], bool]: +def get_module_config( + module_name: str, + config: pulumi.Config, + default_versions: Dict[str, Any] +) -> Tuple[Dict[str, Any], bool]: module_config = config.get_object(module_name) or {"enabled": "false"} module_enabled = str(module_config.get('enabled', 'false')).lower() == "true" # Remove 'enabled' key from module_config dictionary as modules do not need this key module_config.pop('enabled', None) + # Handle version injection + module_version = module_config.get('version') + if not module_version: + # No version specified in module config, use default + module_version = default_versions.get(module_name) + if module_version: + module_config['version'] = module_version + else: + # No default version, set to None (will be handled in module) + module_config['version'] = None + else: + # Version specified in module config; keep as is (could be 'latest') + pass + return module_config, module_enabled ########################################## -# Dependency Management +# Deploy Modules # Initialize a list to keep track of dependencies between resources global_depends_on: List[pulumi.Resource] = [] -########################################## -# Deploy Modules - # Cert Manager Module -config_cert_manager_dict, cert_manager_enabled = get_module_config('cert_manager') +config_cert_manager_dict, cert_manager_enabled = get_module_config('cert_manager', config, default_versions) if cert_manager_enabled: from src.cert_manager.types import CertManagerConfig @@ -60,12 +85,19 @@ def get_module_config(module_name: str) -> Tuple[Dict[str, Any], bool]: from src.cert_manager.deploy import deploy_cert_manager_module cert_manager_version, cert_manager_release, cert_manager_selfsigned_cert = deploy_cert_manager_module( - cert_manager_enabled=cert_manager_enabled, config_cert_manager=config_cert_manager, global_depends_on=global_depends_on, k8s_provider=k8s_provider, - versions=versions, ) + + # Add Cert Manager version to versions dictionary + versions["cert_manager"] = cert_manager_version + + # Add Cert Manager configuration to configurations dictionary + configurations["cert_manager"] = { + "enabled": cert_manager_enabled, + } + pulumi.export("cert_manager_selfsigned_cert", cert_manager_selfsigned_cert) else: cert_manager_selfsigned_cert = None @@ -74,6 +106,8 @@ def get_module_config(module_name: str) -> Tuple[Dict[str, Any], bool]: # Export Component Versions pulumi.export("versions", versions) +pulumi.export("configuration", configurations) + #import os #from typing import Any, Dict, List, Optional, Tuple diff --git a/pulumi/default_versions.json b/pulumi/default_versions.json new file mode 100644 index 0000000..565513e --- /dev/null +++ b/pulumi/default_versions.json @@ -0,0 +1,3 @@ +{ + "cert_manager": "1.15.3" +} diff --git a/pulumi/src/cert_manager/deploy.py b/pulumi/src/cert_manager/deploy.py index 436f670..fbc6b6e 100644 --- a/pulumi/src/cert_manager/deploy.py +++ b/pulumi/src/cert_manager/deploy.py @@ -11,11 +11,9 @@ from .types import CertManagerConfig def deploy_cert_manager_module( - cert_manager_enabled: bool, config_cert_manager: CertManagerConfig, global_depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, - versions: Dict[str, Dict[str, Any]], ) -> Tuple[Optional[str], Optional[pulumi.Resource], Optional[str]]: """ Deploys the Cert Manager module if enabled. @@ -23,18 +21,12 @@ def deploy_cert_manager_module( Returns: A tuple containing the version, Helm release, and CA certificate data. """ - if not cert_manager_enabled: - pulumi.log.info("Cert Manager module is disabled. Skipping deployment.") - return None, None, None - cert_manager_version, release, ca_cert_b64, _ = deploy_cert_manager( config_cert_manager=config_cert_manager, depends_on=global_depends_on, k8s_provider=k8s_provider, ) - versions["cert_manager"] = {"enabled": cert_manager_enabled, "version": cert_manager_version} - if release: global_depends_on.append(release) @@ -48,17 +40,6 @@ def deploy_cert_manager( depends_on: Optional[List[pulumi.Resource]], k8s_provider: k8s.Provider ) -> Tuple[str, k8s.helm.v3.Release, str, k8s.core.v1.Secret]: - """ - Deploys Cert Manager using Helm. - - Args: - config_cert_manager: Configuration object for Cert Manager. - depends_on: List of dependencies. - k8s_provider: Kubernetes provider. - - Returns: - A tuple containing the version, Helm release, CA certificate data, and the secret resource. - """ # Extract configuration values namespace = config_cert_manager.namespace version = config_cert_manager.version @@ -82,13 +63,14 @@ def deploy_cert_manager( chart_name = "cert-manager" chart_url = "https://charts.jetstack.io" - # Fetch the latest version from the helm chart index - if version is None: + # Handle 'latest' or None version + if version == 'latest' or version is None: + # Fetch the latest version from the helm chart index version = get_latest_helm_chart_version(f"{chart_url}/index.yaml", chart_name) version = version.lstrip("v") pulumi.log.info(f"Setting helm release version to latest: {chart_name}/{version}") else: - # Log the version override + # Log the version being used pulumi.log.info(f"Using helm release version: {chart_name}/{version}") # Generate Helm values @@ -251,231 +233,3 @@ def gen_helm_values( } } return helm_values - -#import pulumi -#import pulumi_kubernetes as k8s -#from pulumi_kubernetes.apiextensions.CustomResource import CustomResource -#from typing import Optional, List, Dict, Any, Tuple -# -#from src.lib.namespace import create_namespace -#from src.lib.helm_chart_versions import get_latest_helm_chart_version -# -#def deploy_cert_manager( -# namespace: str, -# version: Optional[str], -# kubernetes_distribution: str, -# depends_on: Optional[List[pulumi.Resource]], -# k8s_provider: k8s.Provider -# ) -> Tuple[str, k8s.helm.v3.Release, str, k8s.core.v1.Secret]: -# """ -# Deploys Cert Manager using Helm. -# -# Args: -# namespace: Namespace name for Cert Manager. -# version: Version of the Cert Manager Helm chart. -# kubernetes_distribution: The Kubernetes distribution in use. -# depends_on: List of dependencies. -# k8s_provider: Kubernetes provider. -# -# Returns: -# A tuple containing the version, Helm release, CA certificate data, and the secret resource. -# """ -# # Create namespace -# ns_retain = False -# ns_protect = False -# ns_annotations = {} -# ns_labels = {} -# namespace_resource = create_namespace( -# depends_on, -# namespace, -# ns_retain, -# ns_protect, -# k8s_provider, -# custom_labels=ns_labels, -# custom_annotations=ns_annotations -# ) -# -# chart_name = "cert-manager" -# chart_index_path = "index.yaml" -# chart_url = "https://charts.jetstack.io" -# chart_index_url = f"{chart_url}/{chart_index_path}" -# -# # Fetch the latest version from the helm chart index -# if version is None: -# version = get_latest_helm_chart_version(chart_index_url, chart_name) -# version = version.lstrip("v") -# pulumi.log.info(f"Setting helm release version to latest: {chart_name}/{version}") -# else: -# # Log the version override -# pulumi.log.info(f"Using helm release version: {chart_name}/{version}") -# -# # Deploy cert-manager using the Helm release with updated custom values -# helm_values = gen_helm_values(kubernetes_distribution) -# -# # Deploy cert-manager using the Helm release with custom values -# release = k8s.helm.v3.Release( -# chart_name, -# k8s.helm.v3.ReleaseArgs( -# chart=chart_name, -# version=version, -# namespace=namespace, -# skip_await=False, -# repository_opts= k8s.helm.v3.RepositoryOptsArgs( -# repo=chart_url -# ), -# values=helm_values, -# ), -# opts=pulumi.ResourceOptions( -# provider = k8s_provider, -# parent=namespace_resource, -# depends_on=depends_on, -# custom_timeouts=pulumi.CustomTimeouts( -# create="8m", -# update="4m", -# delete="4m" -# ) -# ) -# ) -# -# # Create a self-signed ClusterIssuer resource -# cluster_issuer_root = CustomResource( -# "cluster-selfsigned-issuer-root", -# api_version="cert-manager.io/v1", -# kind="ClusterIssuer", -# metadata={ -# "name": "cluster-selfsigned-issuer-root", -# "namespace": namespace -# }, -# spec={ -# "selfSigned": {} -# }, -# opts=pulumi.ResourceOptions( -# provider = k8s_provider, -# parent=release, -# depends_on=[namespace_resource], -# custom_timeouts=pulumi.CustomTimeouts( -# create="5m", -# update="10m", -# delete="10m" -# ) -# ) -# ) -# -# cluster_issuer_ca_certificate = CustomResource( -# "cluster-selfsigned-issuer-ca", -# api_version="cert-manager.io/v1", -# kind="Certificate", -# metadata={ -# "name": "cluster-selfsigned-issuer-ca", -# "namespace": namespace -# }, -# spec={ -# "commonName": "cluster-selfsigned-issuer-ca", -# "duration": "2160h0m0s", -# "isCA": True, -# "issuerRef": { -# "group": "cert-manager.io", -# "kind": "ClusterIssuer", -# "name": cluster_issuer_root.metadata["name"], -# }, -# "privateKey": { -# "algorithm": "ECDSA", -# "size": 256 -# }, -# "renewBefore": "360h0m0s", -# "secretName": "cluster-selfsigned-issuer-ca" -# }, -# opts=pulumi.ResourceOptions( -# provider = k8s_provider, -# parent=cluster_issuer_root, -# depends_on=[namespace_resource], -# custom_timeouts=pulumi.CustomTimeouts( -# create="5m", -# update="10m", -# delete="10m" -# ) -# ) -# ) -# -# cluster_issuer = CustomResource( -# "cluster-selfsigned-issuer", -# api_version="cert-manager.io/v1", -# kind="ClusterIssuer", -# metadata={ -# "name": "cluster-selfsigned-issuer", -# "namespace": namespace -# }, -# spec={ -# "ca": { -# "secretName": cluster_issuer_ca_certificate.spec["secretName"], -# } -# }, -# opts=pulumi.ResourceOptions( -# provider=k8s_provider, -# parent=cluster_issuer_ca_certificate, -# depends_on=[namespace_resource], -# custom_timeouts=pulumi.CustomTimeouts( -# create="4m", -# update="4m", -# delete="4m" -# ) -# ) -# ) -# -# # Retrieve the CA certificate secret -# ca_secret = k8s.core.v1.Secret( -# "cluster-selfsigned-issuer-ca-secret", -# metadata={ -# "namespace": namespace, -# "name": cluster_issuer_ca_certificate.spec["secretName"] -# }, -# opts=pulumi.ResourceOptions( -# provider=k8s_provider, -# parent=cluster_issuer, -# depends_on=[cluster_issuer], -# ) -# ) -# -# # Extract the tls.crt value from the secret -# ca_data_tls_crt_b64 = ca_secret.data.apply(lambda data: data["tls.crt"]) -# -# return version, release, ca_data_tls_crt_b64, ca_secret -# -#def gen_helm_values(kubernetes_distribution: str) -> Dict[str, Any]: -# """ -# Generates custom Helm values for the cert-manager Helm chart based on the Kubernetes distribution. -# -# Args: -# kubernetes_distribution: The Kubernetes distribution in use. -# -# Returns: -# A dictionary of Helm values. -# """ -# # Define custom values for the cert-manager Helm chart -# common_values = { -# 'replicaCount': 1, -# 'installCRDs': True, -# 'resources': { -# 'limits': { -# 'cpu': '500m', -# 'memory': '1024Mi' -# }, -# 'requests': { -# 'cpu': '250m', -# 'memory': '512Mi' -# } -# } -# } -# -# if kubernetes_distribution == 'kind': -# # Kind-specific Helm values -# return { -# **common_values, -# } -# elif kubernetes_distribution == 'talos': -# # Talos-specific Helm values per the Talos Docs -# return { -# **common_values, -# } -# else: -# raise ValueError(f"Unsupported Kubernetes distribution: {kubernetes_distribution}") diff --git a/pulumi/src/lib/versions.py b/pulumi/src/lib/versions.py new file mode 100644 index 0000000..2368044 --- /dev/null +++ b/pulumi/src/lib/versions.py @@ -0,0 +1,86 @@ +# src/lib/versions.py + +import json +import os +import pulumi +import requests + +def load_default_versions(config: pulumi.Config) -> dict: + # Get stack name + stack_name = pulumi.get_stack() + + # Get configuration settings + default_versions_source = config.get('default_versions.source') + versions_channel = config.get('versions.channel') or 'stable' + versions_stack_name = config.get_bool('versions.stack_name') or False + + default_versions = {} + + def load_versions_from_file(file_path): + try: + with open(file_path, 'r') as f: + versions = json.load(f) + pulumi.log.info(f"Loaded default versions from file: {file_path}") + return versions + except FileNotFoundError: + pulumi.log.warn(f"Default versions file not found at {file_path}.") + except json.JSONDecodeError as e: + pulumi.log.error(f"Error decoding JSON from {file_path}: {e}") + return None + + def load_versions_from_url(url): + try: + response = requests.get(url) + response.raise_for_status() + versions = response.json() + pulumi.log.info(f"Loaded default versions from URL: {url}") + return versions + except requests.RequestException as e: + pulumi.log.warn(f"Failed to fetch default versions from {url}: {e}") + except json.JSONDecodeError as e: + pulumi.log.error(f"Error decoding JSON from {url}: {e}") + return None + + # Determine the precedence of version sources + if default_versions_source: + # User-specified source via Pulumi config + if default_versions_source.startswith(('http://', 'https://')): + # It's a URL + default_versions = load_versions_from_url(default_versions_source) + else: + # It's a file path + default_versions = load_versions_from_file(default_versions_source) + + if not default_versions: + pulumi.log.error(f"Failed to load default versions from specified source: {default_versions_source}") + raise Exception("Cannot proceed without default versions.") + + else: + # Check if versions.stack_name is set to true + if versions_stack_name: + # Attempt to load versions from ./versions/$STACK_NAME.json + current_dir = os.path.dirname(os.path.abspath(__file__)) + versions_dir = os.path.join(current_dir, '..', '..', 'versions') + stack_versions_path = os.path.join(versions_dir, f'{stack_name}.json') + default_versions = load_versions_from_file(stack_versions_path) + + # If versions are still not loaded, attempt to load from local default_versions.json + if not default_versions: + current_dir = os.path.dirname(os.path.abspath(__file__)) + default_versions_path = os.path.join(current_dir, '..', '..', 'default_versions.json') + default_versions = load_versions_from_file(default_versions_path) + + # If still not loaded, attempt to fetch from remote URL based on channel + if not default_versions: + # Construct the URL based on the channel + base_url = 'https://github.com/containercraft/kargo/releases/latest/download/' + versions_filename = f'{versions_channel}_versions.json' + versions_url = f'{base_url}{versions_filename}' + default_versions = load_versions_from_url(versions_url) + + # If versions are still not loaded, log an error and raise an exception + if not default_versions: + pulumi.log.error("Could not load default versions from any source. Cannot proceed.") + raise Exception("Cannot proceed without default versions.") + + return default_versions From 1e1dd48a2d3560e37e37f2442613d5c792041257 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sat, 14 Sep 2024 14:50:38 +0000 Subject: [PATCH 03/43] add documentation --- pulumi/src/README.md | 246 ++++++++++++++++++++++++++++++ pulumi/src/cert_manager/README.md | 193 +++++++++++++++++++++++ 2 files changed, 439 insertions(+) create mode 100644 pulumi/src/README.md create mode 100644 pulumi/src/cert_manager/README.md diff --git a/pulumi/src/README.md b/pulumi/src/README.md new file mode 100644 index 0000000..0c82b19 --- /dev/null +++ b/pulumi/src/README.md @@ -0,0 +1,246 @@ +# Kargo Modules Development Guide + +Welcome to the Kargo modules development guide! This document provides an overview of the design principles, code structure, and best practices for developing and maintaining modules within the Kargo codebase. It is intended for developers and AI language models (like ChatGPT) to quickly understand and contribute to the project. + +## Table of Contents + +- [Introduction](#introduction) +- [Design Principles](#design-principles) +- [Code Structure](#code-structure) +- [Version Management](#version-management) +- [Module Development Guide](#module-development-guide) + - [1. Module Configuration](#1-module-configuration) + - [2. Defining Configuration Types](#2-defining-configuration-types) + - [3. Module Deployment Logic](#3-module-deployment-logic) + - [4. Updating `__main__.py`](#4-updating-__main__py) +- [Best Practices](#best-practices) +- [Example Module: Cert Manager](#example-module-cert-manager) +- [Conclusion](#conclusion) + +--- + +## Introduction + +Kargo is a Kubernetes deployment framework that leverages Pulumi for infrastructure as code (IaC). This guide aims to standardize module development by centralizing version handling, simplifying module code, and promoting consistency across the codebase. + +--- + +## Design Principles + +- **Centralization of Common Logic**: Shared functionality, such as version handling, is centralized to reduce duplication and simplify maintenance. +- **Simplification of Module Code**: Modules focus solely on their specific deployment logic, relying on centralized utilities for configuration and version management. +- **Consistency**: Establish clear patterns and standards for module development to ensure uniformity across the codebase. +- **Maintainability**: Write clean, readable code with proper documentation and type annotations to facilitate ease of maintenance and contribution. +- **Flexibility**: Allow users to override configurations and versions as needed, while providing sensible defaults. + +--- + +## Code Structure + +- **`__main__.py`**: The entry point of the Pulumi program. Handles global configurations, Kubernetes provider setup, version loading, and module deployments. +- **`src/lib/`**: Contains shared utilities and libraries, such as version management (`versions.py`) and shared types (`types.py`). +- **`src//`**: Each module resides in its own directory under `src/`, containing its specific types (`types.py`) and deployment logic (`deploy.py`). +- **`default_versions.json`**: A JSON file containing default versions for modules. Can be overridden by user configurations. + +--- + +## Version Management + +### Centralized Version Handling + +Version management is centralized in `src/lib/versions.py`. The `load_default_versions` function loads versions based on the following precedence: + +1. **User-Specified Source**: Via Pulumi config `default_versions.source`. +2. **Stack-Specific Versions**: If `versions.stack_name` is `true`, loads from `./versions/$STACK_NAME.json`. +3. **Local Default Versions**: Loads from `./default_versions.json`. +4. **Remote Versions**: Fetches from a remote URL based on the specified `versions.channel`. + +### Injecting Versions into Modules + +In `__main__.py`, the `get_module_config` function handles module configuration loading and version injection. Modules receive configurations with versions already set, eliminating the need for individual modules to handle version logic. + +--- + +## Module Development Guide + +Follow these steps to develop or enhance a module in the Kargo codebase. + +### 1. Module Configuration + +- **Purpose**: Retrieve and prepare the module's configuration, including version information. +- **Implementation**: Use the `get_module_config` function in `__main__.py`. + +```python +# __main__.py + +config_module_dict, module_enabled = get_module_config('module_name', config, default_versions) +``` + +- **Parameters**: + - `module_name`: The name of the module as defined in Pulumi config. + - `config`: The global Pulumi config object. + - `default_versions`: The dictionary containing default versions. + +### 2. Defining Configuration Types + +- **Purpose**: Define a data class for the module's configuration with default values. +- **Implementation**: Create a `types.py` in the module's directory. + +```python +# src/module_name/types.py + +from dataclasses import dataclass +from typing import Optional, Dict, Any +import pulumi + +@dataclass +class ModuleNameConfig: + version: Optional[str] = None # Version will be injected + # ... other configuration fields ... + + @staticmethod + def merge(user_config: Dict[str, Any]) -> 'ModuleNameConfig': + default_config = ModuleNameConfig() + merged_config = default_config.__dict__.copy() + for key, value in user_config.items(): + if hasattr(default_config, key): + merged_config[key] = value + else: + pulumi.log.warn(f"Unknown configuration key '{key}' in module_name config.") + return ModuleNameConfig(**merged_config) +``` + +### 3. Module Deployment Logic + +- **Purpose**: Implement the module's deployment logic using the merged configuration. +- **Implementation**: Create a `deploy.py` in the module's directory. + +```python +# src/module_name/deploy.py + +def deploy_module_name( + config_module_name: ModuleNameConfig, + global_depends_on: List[pulumi.Resource], + k8s_provider: k8s.Provider, +) -> Tuple[Optional[str], Optional[pulumi.Resource]]: + # Module-specific deployment logic + # Use config_module_name.version as needed + # Return version and any relevant resources +``` + +### 4. Updating `__main__.py` + +- **Purpose**: Integrate the module into the main Pulumi program. +- **Implementation**: + +```python +# __main__.py + +if module_enabled: + from src.module_name.types import ModuleNameConfig + config_module_name = ModuleNameConfig.merge(config_module_dict) + + from src.module_name.deploy import deploy_module_name + + module_version, module_resource = deploy_module_name( + config_module_name=config_module_name, + global_depends_on=global_depends_on, + k8s_provider=k8s_provider, + ) + + # Update versions and configurations dictionaries + versions["module_name"] = module_version + configurations["module_name"] = { + "enabled": module_enabled, + } +``` + +--- + +## Best Practices + +- **Centralize Common Logic**: Use shared utilities from `src/lib/` to avoid duplication. +- **Type Annotations**: Use type hints throughout the code for better readability and tooling support. +- **Documentation**: Include docstrings and comments to explain complex logic. +- **Consistent Coding Style**: Follow the project's coding conventions for formatting and naming. +- **Error Handling**: Implement robust error handling and logging for easier debugging. +- **Avoid Global Variables**: Pass necessary objects as arguments to functions and methods. + +--- + +## Example Module: Cert Manager + +### Configuration + +```python +# Configurable by either of: +# - Pulumi Stack Config `Pulumi.stack.yaml` +# - Pulumi CLI `--config` flag +# - example: pulumi config set --path cert_manager.enabled true +# - example: pulumi config set --path cert_manager.version "1.15.3" +# - example: pulumi config set --path cert_manager.version "latest" +config: + cert_manager: + enabled: true + version: "1.15.3" +``` + +### Types Definition + +```python +# src/cert_manager/types.py + +from dataclasses import dataclass +from typing import Optional, Dict, Any +import pulumi + +@dataclass +class CertManagerConfig: + version: Optional[str] = None + # ... other fields ... + + @staticmethod + def merge(user_config: Dict[str, Any]) -> 'CertManagerConfig': + # Merging logic as shown above +``` + +### Deployment Logic + +```python +# src/cert_manager/deploy.py + +def deploy_cert_manager_module( + config_cert_manager: CertManagerConfig, + global_depends_on: List[pulumi.Resource], + k8s_provider: k8s.Provider, +) -> Tuple[Optional[str], Optional[pulumi.Resource], Optional[str]]: + # Deployment logic for Cert Manager + # Return version, release resource, and any additional outputs +``` + +### Integration in `__main__.py` + +```python +# __main__.py + +config_cert_manager_dict, cert_manager_enabled = get_module_config('cert_manager', config, default_versions) + +if cert_manager_enabled: + from src.cert_manager.types import CertManagerConfig + config_cert_manager = CertManagerConfig.merge(config_cert_manager_dict) + + from src.cert_manager.deploy import deploy_cert_manager_module + + cert_manager_version, cert_manager_release, cert_manager_selfsigned_cert = deploy_cert_manager_module( + config_cert_manager=config_cert_manager, + global_depends_on=global_depends_on, + k8s_provider=k8s_provider, + ) + + versions["cert_manager"] = cert_manager_version + configurations["cert_manager"] = { + "enabled": cert_manager_enabled, + } + + pulumi.export("cert_manager_selfsigned_cert", cert_manager_selfsigned_cert) +``` diff --git a/pulumi/src/cert_manager/README.md b/pulumi/src/cert_manager/README.md new file mode 100644 index 0000000..4929af3 --- /dev/null +++ b/pulumi/src/cert_manager/README.md @@ -0,0 +1,193 @@ +# Cert Manager Module for Kargo PaaS + +Welcome to the Cert Manager module for the ContainerCraft Kargo PaaS platform! This module automates the deployment and management of certificates in your Kubernetes cluster using [cert-manager](https://cert-manager.io/). It simplifies the process of issuing and renewing SSL/TLS certificates, enhancing the security of your applications. + +This guide provides an overview of the Cert Manager module, instructions on how to enable and configure it, and explanations of its functionality within the Kargo PaaS platform. Whether you're new to Kubernetes, cert-manager, or ContainerCraft Kargo PaaS, this guide will help you get started. + +--- + +## Table of Contents + +- [Introduction](#introduction) +- [Prerequisites](#prerequisites) +- [Features](#features) +- [Enabling the Module](#enabling-the-module) +- [Configuration](#configuration) + - [Default Configuration](#default-configuration) + - [Custom Configuration](#custom-configuration) +- [Module Components](#module-components) + - [Namespace Creation](#namespace-creation) + - [Helm Chart Deployment](#helm-chart-deployment) + - [Self-Signed Cluster Issuer](#self-signed-cluster-issuer) +- [Integration with Kargo PaaS](#integration-with-kargo-paas) +- [Best Practices](#best-practices) +- [Troubleshooting](#troubleshooting) +- [Additional Resources](#additional-resources) +- [Conclusion](#conclusion) + +--- + +## Introduction + +The Cert Manager module automates the installation and configuration of cert-manager in your Kubernetes cluster using Pulumi and the Kargo PaaS platform. By integrating cert-manager, you can easily manage SSL/TLS certificates for your applications, enabling secure communication and compliance with industry standards. + +--- + +## Prerequisites + +- **Kubernetes Cluster**: A running Kubernetes cluster accessible via `kubeconfig`. +- **Pulumi Installed**: Ensure Pulumi is installed and configured. +- **Kargo PaaS Setup**: Basic setup of the Kargo PaaS platform. +- **Access to Helm Charts**: Ability to pull Helm charts from `https://charts.jetstack.io`. + +--- + +## Features + +- **Automated Deployment**: Installs cert-manager using Helm charts. +- **Version Management**: Supports specific versions or defaults to the latest version. +- **Namespace Isolation**: Deploys cert-manager in a dedicated namespace. +- **Self-Signed Certificates**: Sets up a self-signed ClusterIssuer for certificate management. +- **Customizable Configuration**: Allows overriding default settings to fit your needs. + +--- + +## Enabling the Module + +To enable the Cert Manager module, update your Pulumi configuration to include the `cert_manager` section with `enabled: true`. + +### Example Pulumi Configuration + +```yaml +# Pulumi..yaml + +config: + cert_manager: + enabled: true # (default: true) +``` + +Alternatively, you can set the configuration via the Pulumi CLI: + +```bash +pulumi config set --path cert_manager.enabled true +``` + +--- + +## Configuration + +### Default Configuration + +The module comes with sensible defaults to simplify deployment: + +- **Namespace**: `cert-manager` +- **Version**: Uses the default version specified in `default_versions.json` +- **Cluster Issuer Name**: `cluster-selfsigned-issuer` +- **Install CRDs**: `true` + +### Custom Configuration + +You can customize the module's behavior by providing additional configuration options. + +#### Available Configuration Options + +- **namespace** *(string)*: The namespace where cert-manager will be deployed. +- **version** *(string)*: The version of the cert-manager Helm chart to deploy. Use `'latest'` to fetch the latest version. +- **cluster_issuer** *(string)*: The name of the ClusterIssuer to create. +- **install_crds** *(bool)*: Whether to install Custom Resource Definitions (CRDs). + +#### Example Custom Configuration + +```yaml +# Pulumi..yaml + +config: + cert_manager: + enabled: true + version: "1.15.3" + cluster_issuer: "my-cluster-issuer" + namespace: "custom-cert-manager" + install_crds: true # (default: true) +``` + +--- + +## Module Components + +### Namespace Creation + +The module creates a dedicated namespace for cert-manager to ensure isolation and better management. + +- **Namespace**: Configurable via the `namespace` parameter. +- **Labels and Annotations**: Applied as per best practices for identification. + +### Helm Chart Deployment + +The module deploys cert-manager using the official Helm chart from Jetstack. + +- **Repository**: `https://charts.jetstack.io` +- **Chart Name**: `cert-manager` +- **Version**: Configurable; defaults to the version specified in `default_versions.json`. +- **Custom Values**: Resources and replica counts are configured for optimal performance. + +### Self-Signed Cluster Issuer + +To enable certificate issuance, the module sets up a self-signed ClusterIssuer. + +- **Root ClusterIssuer**: `cluster-selfsigned-issuer-root` +- **CA Certificate**: Generated and stored in a Kubernetes Secret. +- **Primary ClusterIssuer**: Uses the CA certificate to issue certificates for your applications. +- **Secret Export**: The CA certificate data is exported and available for other modules or applications. + +--- + +## Integration with Kargo PaaS + +The Cert Manager module integrates seamlessly with the Kargo PaaS platform, leveraging shared configurations and best practices. + +- **Version Handling**: Uses centralized version management from `src/lib/versions.py`. +- **Configuration Merging**: Combines user-provided configurations with defaults for consistency. +- **Resource Dependencies**: Manages dependencies between resources to ensure correct deployment order. +- **Exported Outputs**: Provides necessary outputs for other modules or services to consume. + +--- + +## Best Practices + +- **Version Specification**: While the module can use the latest version, specifying a version ensures consistency across deployments. +- **CRD Management**: Set `install_crds` to `true` unless CRDs are managed externally. +- **ClusterIssuer Naming**: Use meaningful names for ClusterIssuers to avoid conflicts. +- **Resource Monitoring**: Keep an eye on resource usage, especially if adjusting the default resource requests and limits. + +--- + +## Troubleshooting + +### Common Issues + +- **Connection Errors**: Ensure your `kubeconfig` and Kubernetes context are correctly configured. +- **Version Conflicts**: If deployment fails due to version issues, verify the specified version is available in the Helm repository. +- **CRD Issues**: If CRDs are not installed, cert-manager components may fail to function properly. + +### Debugging Steps + +1. **Check Pulumi Logs**: Look for error messages during deployment. +2. **Verify Kubernetes Resources**: Use `kubectl` to inspect the cert-manager namespace and resources. +3. **Review Configuration**: Ensure all configuration options are correctly set in your Pulumi config. + +--- + +## Additional Resources + +- **cert-manager Documentation**: [https://cert-manager.io/docs/](https://cert-manager.io/docs/) +- **Kargo PaaS Documentation**: Refer to the main [Kargo README](../README.md) for developer guidelines. +- **Pulumi Kubernetes Provider**: [https://www.pulumi.com/docs/reference/pkg/kubernetes/](https://www.pulumi.com/docs/reference/pkg/kubernetes/) +- **Helm Charts**: [https://artifacthub.io/packages/helm/cert-manager/cert-manager](https://artifacthub.io/packages/helm/cert-manager/cert-manager) + +--- + +## Conclusion + +The Cert Manager module simplifies the integration of cert-manager into your Kubernetes cluster within the Kargo PaaS platform. By following this guide, you can easily enable and configure the module to enhance the security and reliability of your applications. + +**Need Help?** If you have questions or need assistance, feel free to reach out to the community or maintainers. From acbaf28e61a5bdd9c18065f2c8c3bc884cb1d63a Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sat, 14 Sep 2024 15:12:23 +0000 Subject: [PATCH 04/43] nit documentation --- pulumi/src/README.md | 11 +++-- pulumi/src/cert_manager/README.md | 75 ++++++++----------------------- 2 files changed, 26 insertions(+), 60 deletions(-) diff --git a/pulumi/src/README.md b/pulumi/src/README.md index 0c82b19..a0c91f8 100644 --- a/pulumi/src/README.md +++ b/pulumi/src/README.md @@ -1,6 +1,6 @@ # Kargo Modules Development Guide -Welcome to the Kargo modules development guide! This document provides an overview of the design principles, code structure, and best practices for developing and maintaining modules within the Kargo codebase. It is intended for developers and AI language models (like ChatGPT) to quickly understand and contribute to the project. +Welcome to the Kargo Kubevirt PaaS IaC module developer guide. This document provides an overview of the design principles, code structure, and best practices for developing and maintaining modules within the Kargo IaC codebase. It is intended for developers and AI language models (like ChatGPT) to quickly understand and contribute to the project. ## Table of Contents @@ -21,7 +21,7 @@ Welcome to the Kargo modules development guide! This document provides an overvi ## Introduction -Kargo is a Kubernetes deployment framework that leverages Pulumi for infrastructure as code (IaC). This guide aims to standardize module development by centralizing version handling, simplifying module code, and promoting consistency across the codebase. +Kargo is a Kubernetes & Kubevirt based Platform Engineering IaC development & deployment framework that leverages Pulumi for infrastructure as code (IaC). This guide aims to standardize module development by centralizing version handling, simplifying module code, and promoting consistency across the codebase. --- @@ -35,12 +35,15 @@ Kargo is a Kubernetes deployment framework that leverages Pulumi for infrastruct --- -## Code Structure +## IaC Module Structure - **`__main__.py`**: The entry point of the Pulumi program. Handles global configurations, Kubernetes provider setup, version loading, and module deployments. - **`src/lib/`**: Contains shared utilities and libraries, such as version management (`versions.py`) and shared types (`types.py`). - **`src//`**: Each module resides in its own directory under `src/`, containing its specific types (`types.py`) and deployment logic (`deploy.py`). -- **`default_versions.json`**: A JSON file containing default versions for modules. Can be overridden by user configurations. +- **`src//types.py`**: Defines data classes for module configurations with default values and merging logic. +- **`src//deploy.py`**: Contains the module-specific deployment logic, taking in the merged configuration and returning relevant outputs. +- **`src//README.md`**: Module-specific documentation with configuration options, features, and usage instructions. +- **`src//*.py`**: Additional utility files or scripts specific to the module. --- diff --git a/pulumi/src/cert_manager/README.md b/pulumi/src/cert_manager/README.md index 4929af3..22b3fff 100644 --- a/pulumi/src/cert_manager/README.md +++ b/pulumi/src/cert_manager/README.md @@ -1,8 +1,8 @@ -# Cert Manager Module for Kargo PaaS +# Cert Manager Module -Welcome to the Cert Manager module for the ContainerCraft Kargo PaaS platform! This module automates the deployment and management of certificates in your Kubernetes cluster using [cert-manager](https://cert-manager.io/). It simplifies the process of issuing and renewing SSL/TLS certificates, enhancing the security of your applications. +ContainerCraft Kargo Kubevirt PaaS Cert Manager module. -This guide provides an overview of the Cert Manager module, instructions on how to enable and configure it, and explanations of its functionality within the Kargo PaaS platform. Whether you're new to Kubernetes, cert-manager, or ContainerCraft Kargo PaaS, this guide will help you get started. +The `cert_manager` module automates SSL certificates and certificate issuers in your platform using [cert-manager](https://cert-manager.io/). Cert Manager is a dependency of many components in Kargo Kubevirt PaaS including Containerized Data Importer, Kubevirt, Hostpath Provisioner, and more. Cert Manager improves the simplicity of issuing and renewing SSL/TLS certificates making PKI (Private Key Infrastructure) an integrated and automated feature of the platform. --- @@ -29,32 +29,23 @@ This guide provides an overview of the Cert Manager module, instructions on how ## Introduction -The Cert Manager module automates the installation and configuration of cert-manager in your Kubernetes cluster using Pulumi and the Kargo PaaS platform. By integrating cert-manager, you can easily manage SSL/TLS certificates for your applications, enabling secure communication and compliance with industry standards. - ---- - -## Prerequisites - -- **Kubernetes Cluster**: A running Kubernetes cluster accessible via `kubeconfig`. -- **Pulumi Installed**: Ensure Pulumi is installed and configured. -- **Kargo PaaS Setup**: Basic setup of the Kargo PaaS platform. -- **Access to Helm Charts**: Ability to pull Helm charts from `https://charts.jetstack.io`. +This guide provides an overview of the Cert Manager module, instructions on how to enable and configure it, and explanations of its functionality within the Kargo PaaS platform. Whether you're new to Kubernetes, cert-manager, or ContainerCraft Kargo PaaS, this guide will help you get started. --- ## Features - **Automated Deployment**: Installs cert-manager using Helm charts. -- **Version Management**: Supports specific versions or defaults to the latest version. +- **Version Management**: Supports explicit version pinning or accepts `latest`. - **Namespace Isolation**: Deploys cert-manager in a dedicated namespace. - **Self-Signed Certificates**: Sets up a self-signed ClusterIssuer for certificate management. -- **Customizable Configuration**: Allows overriding default settings to fit your needs. +- **Customizable Configuration**: Allows setting overrides for customization. --- ## Enabling the Module -To enable the Cert Manager module, update your Pulumi configuration to include the `cert_manager` section with `enabled: true`. +The Cert Manager module is enabled by default and executes with sane defaults. Customization can be configured in the Pulumi Stack Configuration. ### Example Pulumi Configuration @@ -66,10 +57,10 @@ config: enabled: true # (default: true) ``` -Alternatively, you can set the configuration via the Pulumi CLI: +Alternatively, you can set configuration values via the Pulumi CLI: ```bash -pulumi config set --path cert_manager.enabled true +pulumi config set --path cert_manager.key value ``` --- @@ -100,6 +91,7 @@ You can customize the module's behavior by providing additional configuration op ```yaml # Pulumi..yaml +# Default values are shown for reference config: cert_manager: @@ -107,7 +99,7 @@ config: version: "1.15.3" cluster_issuer: "my-cluster-issuer" namespace: "custom-cert-manager" - install_crds: true # (default: true) + install_crds: true ``` --- @@ -125,69 +117,40 @@ The module creates a dedicated namespace for cert-manager to ensure isolation an The module deploys cert-manager using the official Helm chart from Jetstack. -- **Repository**: `https://charts.jetstack.io` - **Chart Name**: `cert-manager` -- **Version**: Configurable; defaults to the version specified in `default_versions.json`. +- **Repository**: `https://charts.jetstack.io` - **Custom Values**: Resources and replica counts are configured for optimal performance. +- **Version**: Configurable; defaults to the version specified in `default_versions.json`. ### Self-Signed Cluster Issuer -To enable certificate issuance, the module sets up a self-signed ClusterIssuer. +To enable self signed local certificates, the module includes a self-signed ClusterIssuer chain of trust. - **Root ClusterIssuer**: `cluster-selfsigned-issuer-root` - **CA Certificate**: Generated and stored in a Kubernetes Secret. - **Primary ClusterIssuer**: Uses the CA certificate to issue certificates for your applications. - **Secret Export**: The CA certificate data is exported and available for other modules or applications. ---- - -## Integration with Kargo PaaS - -The Cert Manager module integrates seamlessly with the Kargo PaaS platform, leveraging shared configurations and best practices. - -- **Version Handling**: Uses centralized version management from `src/lib/versions.py`. -- **Configuration Merging**: Combines user-provided configurations with defaults for consistency. -- **Resource Dependencies**: Manages dependencies between resources to ensure correct deployment order. -- **Exported Outputs**: Provides necessary outputs for other modules or services to consume. - ---- - -## Best Practices - -- **Version Specification**: While the module can use the latest version, specifying a version ensures consistency across deployments. -- **CRD Management**: Set `install_crds` to `true` unless CRDs are managed externally. -- **ClusterIssuer Naming**: Use meaningful names for ClusterIssuers to avoid conflicts. -- **Resource Monitoring**: Keep an eye on resource usage, especially if adjusting the default resource requests and limits. - ---- - ## Troubleshooting ### Common Issues - **Connection Errors**: Ensure your `kubeconfig` and Kubernetes context are correctly configured. -- **Version Conflicts**: If deployment fails due to version issues, verify the specified version is available in the Helm repository. -- **CRD Issues**: If CRDs are not installed, cert-manager components may fail to function properly. +- **Version Conflicts**: If deployment fails due to version issues, verify the specified version is available in the Helm repository. Alternatively use `'latest'` and Kargo will fetch the latest version. +- **CRD Issues**: If `install_crds` is set to false and are not otherwise installed, cert-manager components may fail install or function properly. ### Debugging Steps 1. **Check Pulumi Logs**: Look for error messages during deployment. 2. **Verify Kubernetes Resources**: Use `kubectl` to inspect the cert-manager namespace and resources. -3. **Review Configuration**: Ensure all configuration options are correctly set in your Pulumi config. +3. **Review Configuration**: Ensure all configuration options are correctly set in your Pulumi config or remove configuration to use defaults. --- ## Additional Resources - **cert-manager Documentation**: [https://cert-manager.io/docs/](https://cert-manager.io/docs/) -- **Kargo PaaS Documentation**: Refer to the main [Kargo README](../README.md) for developer guidelines. +- **Kargo Kubevirt PaaS IaC Documentation**: Refer to the main [Kargo README](../README.md) for developer guidelines. - **Pulumi Kubernetes Provider**: [https://www.pulumi.com/docs/reference/pkg/kubernetes/](https://www.pulumi.com/docs/reference/pkg/kubernetes/) - **Helm Charts**: [https://artifacthub.io/packages/helm/cert-manager/cert-manager](https://artifacthub.io/packages/helm/cert-manager/cert-manager) - ---- - -## Conclusion - -The Cert Manager module simplifies the integration of cert-manager into your Kubernetes cluster within the Kargo PaaS platform. By following this guide, you can easily enable and configure the module to enhance the security and reliability of your applications. - -**Need Help?** If you have questions or need assistance, feel free to reach out to the community or maintainers. +- **Need Help?** If you have questions or need assistance, feel free to reach out to the community or maintainers on GitHub, Discord, or Twitter. From 1b79f35268796a3007f1785b9c53c659f804ff55 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sat, 14 Sep 2024 15:32:05 +0000 Subject: [PATCH 05/43] refactor code for docs and docstrings --- pulumi/__main__.py | 65 +++++++++++++----- pulumi/src/cert_manager/deploy.py | 105 +++++++++++++++++++++--------- pulumi/src/cert_manager/types.py | 50 +++++++++++++- pulumi/src/lib/namespace.py | 43 ++++++++++-- pulumi/src/lib/types.py | 35 +++++++++- pulumi/src/lib/versions.py | 66 +++++++++++++++---- 6 files changed, 293 insertions(+), 71 deletions(-) diff --git a/pulumi/__main__.py b/pulumi/__main__.py index 737f712..91871b9 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -1,5 +1,8 @@ # __main__.py +# Main entry point for the Kargo Pulumi IaC program. +# This script is responsible for deploying the Kargo platform components located in the respective src/ directories. + import os from typing import Any, Dict, List, Optional, Tuple @@ -10,61 +13,81 @@ from src.lib.versions import load_default_versions ########################################## -# Load Pulumi Config +# Load Pulumi Config and Initialize Variables + +# Load the Pulumi configuration settings config = pulumi.Config() stack_name = pulumi.get_stack() project_name = pulumi.get_project() -# Load Default Versions +# Load default versions for modules default_versions = load_default_versions(config) -# Initialize versions dictionary to track deployed component versions +# Initialize a dictionary to keep track of deployed component versions versions: Dict[str, str] = {} ########################################## # Kubernetes Configuration + +# Retrieve Kubernetes settings from Pulumi config or environment variables kubernetes_config = config.get_object("kubernetes") or {} kubeconfig = kubernetes_config.get("kubeconfig") or os.getenv('KUBECONFIG') kubernetes_context = kubernetes_config.get("context") -pulumi.log.info(f"kubeconfig: {kubeconfig}") -pulumi.log.info(f"kubernetes_context: {kubernetes_context}") - -# Create a Kubernetes provider instance +# Create a Kubernetes provider instance to interact with the cluster k8s_provider = Provider( "k8sProvider", kubeconfig=kubeconfig, context=kubernetes_context, ) -# Initialize configurations dictionary to track module configurations +# Initialize a dictionary to keep track of module configurations configurations: Dict[str, Dict[str, Any]] = {} +# Log the Kubernetes configuration details +pulumi.log.info(f"kubeconfig: {kubeconfig}") +pulumi.log.info(f"kubernetes_context: {kubernetes_context}") + +# TODO: log the git repository URL and branch/commit hash using standard python libraries and pulumi logging functions + ########################################## # Module Configuration and Enable Flags + def get_module_config( module_name: str, config: pulumi.Config, default_versions: Dict[str, Any] ) -> Tuple[Dict[str, Any], bool]: + """ + Retrieves and prepares the configuration for a module. + + Args: + module_name: The name of the module to configure. + config: The Pulumi configuration object. + default_versions: A dictionary of default versions for modules. + + Returns: + A tuple containing the module's configuration dictionary and a boolean indicating if the module is enabled. + """ + # Get the module's configuration from Pulumi config, default to {"enabled": "false"} if not set module_config = config.get_object(module_name) or {"enabled": "false"} module_enabled = str(module_config.get('enabled', 'false')).lower() == "true" - # Remove 'enabled' key from module_config dictionary as modules do not need this key + # Remove 'enabled' key from the module configuration as modules do not need this key beyond this point module_config.pop('enabled', None) - # Handle version injection + # Handle version injection into the module configuration module_version = module_config.get('version') if not module_version: - # No version specified in module config, use default + # No version specified in module config; use default version module_version = default_versions.get(module_name) if module_version: module_config['version'] = module_version else: - # No default version, set to None (will be handled in module) + # No default version available; set to None (module will handle this case) module_config['version'] = None else: - # Version specified in module config; keep as is (could be 'latest') + # Version is specified in module config; keep as is (could be 'latest' or a specific version) pass return module_config, module_enabled @@ -72,40 +95,48 @@ def get_module_config( ########################################## # Deploy Modules -# Initialize a list to keep track of dependencies between resources +# Initialize a list to manage dependencies between resources globally global_depends_on: List[pulumi.Resource] = [] # Cert Manager Module +# Retrieve configuration and enable flag for the cert_manager module config_cert_manager_dict, cert_manager_enabled = get_module_config('cert_manager', config, default_versions) if cert_manager_enabled: + # Import the CertManagerConfig data class and merge the user config with defaults from src.cert_manager.types import CertManagerConfig config_cert_manager = CertManagerConfig.merge(config_cert_manager_dict) + # Import the deployment function for the cert_manager module from src.cert_manager.deploy import deploy_cert_manager_module + # Deploy the cert_manager module cert_manager_version, cert_manager_release, cert_manager_selfsigned_cert = deploy_cert_manager_module( config_cert_manager=config_cert_manager, global_depends_on=global_depends_on, k8s_provider=k8s_provider, ) - # Add Cert Manager version to versions dictionary + # Record the deployed version of cert_manager versions["cert_manager"] = cert_manager_version - # Add Cert Manager configuration to configurations dictionary + # Record the configuration state of cert_manager configurations["cert_manager"] = { "enabled": cert_manager_enabled, } + # Export the self-signed certificate data from cert_manager pulumi.export("cert_manager_selfsigned_cert", cert_manager_selfsigned_cert) else: cert_manager_selfsigned_cert = None ########################################## -# Export Component Versions +# Export Component Versions and Configurations +# Export the versions of deployed components pulumi.export("versions", versions) + +# Export the configurations of deployed modules pulumi.export("configuration", configurations) diff --git a/pulumi/src/cert_manager/deploy.py b/pulumi/src/cert_manager/deploy.py index fbc6b6e..968e97c 100644 --- a/pulumi/src/cert_manager/deploy.py +++ b/pulumi/src/cert_manager/deploy.py @@ -1,5 +1,13 @@ # src/cert_manager/deploy.py +""" +Deployment logic for the Cert Manager module in Kubernetes. + +This module defines functions that deploy cert-manager into a Kubernetes cluster using Pulumi +and Helm. It handles the setup of necessary resources including namespaces, Helm releases, +and ClusterIssuers, and it manages the issuance of certificates via cert-manager. +""" + import pulumi import pulumi_kubernetes as k8s from pulumi_kubernetes.apiextensions.CustomResource import CustomResource @@ -16,11 +24,24 @@ def deploy_cert_manager_module( k8s_provider: k8s.Provider, ) -> Tuple[Optional[str], Optional[pulumi.Resource], Optional[str]]: """ - Deploys the Cert Manager module if enabled. + Deploys the Cert Manager module using Helm and manages certificate resources. + + This function handles the orchestration of deploying cert-manager into the Kubernetes + cluster. It ensures that the appropriate version is deployed using Helm and configures + necessary resources like the ClusterIssuer for certificate management. + + Args: + config_cert_manager (CertManagerConfig): Configuration object for cert-manager deployment. + global_depends_on (List[pulumi.Resource]): A list of resources that the deployment depends on. + k8s_provider (k8s.Provider): The Kubernetes provider for this deployment. Returns: - A tuple containing the version, Helm release, and CA certificate data. + Tuple[Optional[str], Optional[pulumi.Resource], Optional[str]]: + - The deployed cert-manager version. + - The Helm release resource for cert-manager. + - The base64-encoded CA certificate from the self-signed issuer. """ + # Deploy cert-manager and retrieve the version, Helm release, and CA certificate cert_manager_version, release, ca_cert_b64, _ = deploy_cert_manager( config_cert_manager=config_cert_manager, depends_on=global_depends_on, @@ -28,55 +49,74 @@ def deploy_cert_manager_module( ) if release: + # Append the release to global dependencies to maintain resource ordering global_depends_on.append(release) - # Export the CA certificate + # Export the CA certificate for use by other modules or consumers pulumi.export("cert_manager_selfsigned_cert", ca_cert_b64) return cert_manager_version, release, ca_cert_b64 + def deploy_cert_manager( config_cert_manager: CertManagerConfig, depends_on: Optional[List[pulumi.Resource]], k8s_provider: k8s.Provider ) -> Tuple[str, k8s.helm.v3.Release, str, k8s.core.v1.Secret]: - # Extract configuration values + """ + Deploys cert-manager into the Kubernetes cluster using a Helm chart. + + This function installs cert-manager via Helm, creates necessary resources such as + namespaces and self-signed ClusterIssuers, and manages the associated certificate data. + + Args: + config_cert_manager (CertManagerConfig): Configuration settings for cert-manager. + depends_on (Optional[List[pulumi.Resource]]): Resources that the deployment should depend on. + k8s_provider (k8s.Provider): The Kubernetes provider instance. + + Returns: + Tuple[str, k8s.helm.v3.Release, str, k8s.core.v1.Secret]: + - The version of cert-manager deployed. + - The Helm release resource for cert-manager. + - The base64-encoded CA certificate. + - The Kubernetes Secret resource containing the CA certificate. + """ + # Extract configuration details from the CertManagerConfig object namespace = config_cert_manager.namespace version = config_cert_manager.version cluster_issuer_name = config_cert_manager.cluster_issuer install_crds = config_cert_manager.install_crds - # Validate required fields + # Validate that a namespace has been provided if not namespace: raise ValueError("Namespace must be specified in the cert_manager configuration.") - # Create NamespaceConfig instance + # Create the namespace for cert-manager based on the provided configuration namespace_config = NamespaceConfig(name=namespace) - - # Create namespace using the new create_namespace function namespace_resource = create_namespace( config=namespace_config, k8s_provider=k8s_provider, depends_on=depends_on, ) + # Define Helm chart details for cert-manager chart_name = "cert-manager" chart_url = "https://charts.jetstack.io" - # Handle 'latest' or None version + # Determine which version of cert-manager to deploy if version == 'latest' or version is None: - # Fetch the latest version from the helm chart index + # Fetch the latest cert-manager version from the Helm chart repository version = get_latest_helm_chart_version(f"{chart_url}/index.yaml", chart_name) - version = version.lstrip("v") - pulumi.log.info(f"Setting helm release version to latest: {chart_name}/{version}") + version = version.lstrip("v") # Remove 'v' prefix if present + pulumi.log.info(f"Setting Helm release version to latest: {chart_name}/{version}") else: - # Log the version being used - pulumi.log.info(f"Using helm release version: {chart_name}/{version}") + # Use the provided version + pulumi.log.info(f"Using Helm release version: {chart_name}/{version}") - # Generate Helm values + # Generate the values to pass into the Helm chart deployment helm_values = gen_helm_values(config_cert_manager) - # Deploy cert-manager using the Helm release with custom values + # Deploy cert-manager using Helm and return the release resource release = k8s.helm.v3.Release( chart_name, k8s.helm.v3.ReleaseArgs( @@ -101,7 +141,7 @@ def deploy_cert_manager( ) ) - # Create a self-signed ClusterIssuer resource + # Create a self-signed ClusterIssuer in the cluster cluster_issuer_root = CustomResource( "cluster-selfsigned-issuer-root", api_version="cert-manager.io/v1", @@ -125,6 +165,7 @@ def deploy_cert_manager( ) ) + # Create a certificate signed by the self-signed ClusterIssuer cluster_issuer_ca_certificate = CustomResource( "cluster-selfsigned-issuer-ca", api_version="cert-manager.io/v1", @@ -135,7 +176,7 @@ def deploy_cert_manager( }, spec={ "commonName": "cluster-selfsigned-issuer-ca", - "duration": "2160h0m0s", + "duration": "2160h0m0s", # 90 days "isCA": True, "issuerRef": { "group": "cert-manager.io", @@ -146,7 +187,7 @@ def deploy_cert_manager( "algorithm": "ECDSA", "size": 256 }, - "renewBefore": "360h0m0s", + "renewBefore": "360h0m0s", # Renew 15 days before expiration "secretName": "cluster-selfsigned-issuer-ca" }, opts=pulumi.ResourceOptions( @@ -161,6 +202,7 @@ def deploy_cert_manager( ) ) + # Create the primary ClusterIssuer resource using the CA certificate cluster_issuer = CustomResource( cluster_issuer_name, api_version="cert-manager.io/v1", @@ -186,7 +228,7 @@ def deploy_cert_manager( ) ) - # Retrieve the CA certificate secret + # Retrieve the CA certificate secret generated by cert-manager ca_secret = k8s.core.v1.Secret( "cluster-selfsigned-issuer-ca-secret", metadata={ @@ -200,35 +242,40 @@ def deploy_cert_manager( ) ) - # Extract the tls.crt value from the secret + # Extract the base64-encoded CA certificate from the secret ca_data_tls_crt_b64 = ca_secret.data.apply(lambda data: data["tls.crt"]) return version, release, ca_data_tls_crt_b64, ca_secret + def gen_helm_values( config_cert_manager: CertManagerConfig ) -> Dict[str, Any]: """ Generates custom Helm values for the cert-manager Helm chart. + This function generates specific Helm chart values based on the provided + cert-manager configuration. These values are used during Helm deployment + to customize the resources, including replica counts and resource requests/limits. + Args: - config_cert_manager: Configuration object for Cert Manager. + config_cert_manager (CertManagerConfig): The configuration for cert-manager. Returns: - A dictionary of Helm values. + Dict[str, Any]: A dictionary of Helm chart values. """ # Define custom values for the cert-manager Helm chart helm_values = { - 'replicaCount': 1, - 'installCRDs': config_cert_manager.install_crds, + 'replicaCount': 1, # Set the number of cert-manager replicas + 'installCRDs': config_cert_manager.install_crds, # Whether to install CRDs 'resources': { 'limits': { - 'cpu': '500m', - 'memory': '1024Mi' + 'cpu': '500m', # CPU resource limit + 'memory': '1024Mi' # Memory resource limit }, 'requests': { - 'cpu': '250m', - 'memory': '512Mi' + 'cpu': '250m', # CPU resource request + 'memory': '512Mi' # Memory resource request } } } diff --git a/pulumi/src/cert_manager/types.py b/pulumi/src/cert_manager/types.py index 2ddd602..d389806 100644 --- a/pulumi/src/cert_manager/types.py +++ b/pulumi/src/cert_manager/types.py @@ -1,11 +1,33 @@ # src/cert_manager/types.py +""" +Types and data structures specific to the Cert Manager module. + +This module defines the configuration data class for the Cert Manager module, +which is used to manage SSL/TLS certificates within the Kubernetes cluster. +It standardizes the configuration options and provides methods to merge user +configurations with default settings. +""" + from dataclasses import dataclass, field from typing import Optional, Dict, Any import pulumi @dataclass class CertManagerConfig: + """ + Configuration for deploying the Cert Manager module. + + Attributes: + namespace (str): The Kubernetes namespace where cert-manager will be deployed. + Defaults to "cert-manager". + version (Optional[str]): The version of the cert-manager Helm chart to deploy. + If None, the default version from the versions management system will be used. + cluster_issuer (str): The name of the ClusterIssuer to create for certificate issuance. + Defaults to "cluster-selfsigned-issuer". + install_crds (bool): Whether to install Custom Resource Definitions (CRDs) required by cert-manager. + Defaults to True. + """ namespace: str = "cert-manager" version: Optional[str] = None cluster_issuer: str = "cluster-selfsigned-issuer" @@ -14,19 +36,41 @@ class CertManagerConfig: @staticmethod def merge(user_config: Dict[str, Any]) -> 'CertManagerConfig': """ - Merges user-provided configuration with default values for CertManagerConfig. + Merges user-provided configuration with default values. + + This method creates a new CertManagerConfig instance by combining the + default configuration with any user-specified settings. It ensures that + all required fields are set and warns the user about any unknown configuration keys. Args: - user_config: The user-provided configuration dictionary. + user_config (Dict[str, Any]): A dictionary containing user-provided configuration options. Returns: - A CertManagerConfig object with defaults applied. + CertManagerConfig: An instance of CertManagerConfig with merged settings. + + Example: + ```python + # User-provided configuration + user_config = { + "namespace": "my-cert-manager", + "install_crds": False + } + + # Merge with defaults + config = CertManagerConfig.merge(user_config) + ``` """ + # Create a default configuration instance default_config = CertManagerConfig() + # Copy default configuration into a dictionary merged_config = default_config.__dict__.copy() + # Iterate over user-provided configuration and update the merged_config for key, value in user_config.items(): if hasattr(default_config, key): + # If the key exists in the default configuration, update the value merged_config[key] = value else: + # Warn the user about any unknown configuration keys pulumi.log.warn(f"Unknown configuration key '{key}' in cert_manager config.") + # Return a new CertManagerConfig instance with merged settings return CertManagerConfig(**merged_config) diff --git a/pulumi/src/lib/namespace.py b/pulumi/src/lib/namespace.py index f1ab0da..5bb77fe 100644 --- a/pulumi/src/lib/namespace.py +++ b/pulumi/src/lib/namespace.py @@ -1,5 +1,13 @@ # src/lib/namespace.py +""" +Utility functions for managing Kubernetes namespaces within the Kargo PaaS platform. + +This module provides functions to create and manage Kubernetes namespaces +using Pulumi and the Kubernetes provider. It leverages the NamespaceConfig +data class to standardize configurations and ensure consistency across deployments. +""" + import pulumi import pulumi_kubernetes as k8s from typing import List, Optional @@ -13,20 +21,41 @@ def create_namespace( """ Creates a Kubernetes Namespace based on the provided configuration. + This function simplifies the creation of a Kubernetes namespace by applying + settings specified in a NamespaceConfig object. It ensures namespaces are + created consistently and according to best practices within the Kargo PaaS platform. + Args: - config: NamespaceConfig object containing namespace configurations. - k8s_provider: The Kubernetes provider. - depends_on: Optional list of resources to depend on. + config (NamespaceConfig): An object containing namespace configuration settings. + k8s_provider (k8s.Provider): The Kubernetes provider instance to interact with the cluster. + depends_on (Optional[List[pulumi.Resource]]): An optional list of resources that + this namespace depends on, ensuring proper resource creation order. Returns: - The created Namespace resource. - """ + k8s.core.v1.Namespace: The created Namespace resource. - # Ensure depends_on is a list + Example: + ```python + # Define namespace configuration + namespace_config = NamespaceConfig( + name="my-namespace", + labels={"app": "my-app"}, + protect=True + ) + + # Create the namespace + namespace_resource = create_namespace( + config=namespace_config, + k8s_provider=k8s_provider, + depends_on=[other_resource], + ) + ``` + """ + # Ensure depends_on is initialized as a list if not provided if depends_on is None: depends_on = [] - # Create the namespace + # Create the Kubernetes Namespace resource with the specified configuration namespace_resource = k8s.core.v1.Namespace( config.name, metadata=k8s.meta.v1.ObjectMetaArgs( diff --git a/pulumi/src/lib/types.py b/pulumi/src/lib/types.py index a146417..faa3a3e 100644 --- a/pulumi/src/lib/types.py +++ b/pulumi/src/lib/types.py @@ -1,10 +1,39 @@ # src/lib/types.py +""" +Types and data structures used across Kargo modules. + +This module defines shared configuration types that are utilized by various modules +within the Kargo PaaS platform. These data classes help standardize configurations +and simplify the management of Kubernetes resources. +""" + from dataclasses import dataclass, field from typing import Optional, Dict, List @dataclass class NamespaceConfig: + """ + Configuration for creating or managing a Kubernetes namespace. + + Attributes: + name (str): The name of the Kubernetes namespace. + labels (Dict[str, str]): Labels to apply to the namespace for identification and grouping. + Defaults to {"ccio.v1/app": "kargo"}. + annotations (Dict[str, str]): Annotations to add metadata to the namespace. + Defaults to an empty dictionary. + finalizers (List[str]): List of finalizers to prevent accidental deletion of the namespace. + Defaults to ["kubernetes"]. + protect (bool): If True, protects the namespace from deletion. + Defaults to False. + retain_on_delete (bool): If True, retains the namespace when the Pulumi stack is destroyed. + Defaults to False. + ignore_changes (List[str]): Fields to ignore during updates, useful for fields managed externally. + Defaults to ["metadata", "spec"]. + custom_timeouts (Dict[str, str]): Custom timeouts for create, update, and delete operations. + Helps manage operations that may take longer than default timeouts. + Defaults to {"create": "5m", "update": "10m", "delete": "10m"}. + """ name: str labels: Dict[str, str] = field(default_factory=lambda: {"ccio.v1/app": "kargo"}) annotations: Dict[str, str] = field(default_factory=dict) @@ -12,4 +41,8 @@ class NamespaceConfig: protect: bool = False retain_on_delete: bool = False ignore_changes: List[str] = field(default_factory=lambda: ["metadata", "spec"]) - custom_timeouts: Dict[str, str] = field(default_factory=lambda: {"create": "5m", "update": "10m", "delete": "10m"}) + custom_timeouts: Dict[str, str] = field(default_factory=lambda: { + "create": "5m", + "update": "10m", + "delete": "10m" + }) diff --git a/pulumi/src/lib/versions.py b/pulumi/src/lib/versions.py index 2368044..aaa2ca1 100644 --- a/pulumi/src/lib/versions.py +++ b/pulumi/src/lib/versions.py @@ -6,17 +6,44 @@ import requests def load_default_versions(config: pulumi.Config) -> dict: - # Get stack name + """ + Loads the default versions for modules based on the specified configuration settings. + + This function attempts to load version information from multiple sources in order of precedence: + 1. User-specified source via Pulumi config (`default_versions.source`). + 2. Stack-specific versions file (`./versions/$STACK_NAME.json`) if `versions.stack_name` is set to true. + 3. Local default versions file (`./default_versions.json`). + 4. Remote versions based on the specified channel (`versions.channel`). + + Args: + config: The Pulumi configuration object. + + Returns: + A dictionary containing the default versions for modules. + + Raises: + Exception: If default versions cannot be loaded from any source. + """ + # Get the current stack name (e.g., 'dev', 'prod') stack_name = pulumi.get_stack() - # Get configuration settings - default_versions_source = config.get('default_versions.source') - versions_channel = config.get('versions.channel') or 'stable' - versions_stack_name = config.get_bool('versions.stack_name') or False + # Retrieve configuration settings from Pulumi config + default_versions_source = config.get('default_versions.source') # User-specified source for versions + versions_channel = config.get('versions.channel') or 'stable' # Channel to use ('stable' by default) + versions_stack_name = config.get_bool('versions.stack_name') or False # Use stack-specific versions if true - default_versions = {} + default_versions = {} # Initialize the default versions dictionary def load_versions_from_file(file_path): + """ + Loads versions from a local JSON file. + + Args: + file_path: The path to the JSON file containing versions. + + Returns: + A dictionary of versions if successful, otherwise None. + """ try: with open(file_path, 'r') as f: versions = json.load(f) @@ -29,6 +56,15 @@ def load_versions_from_file(file_path): return None def load_versions_from_url(url): + """ + Loads versions from a remote URL. + + Args: + url: The URL pointing to the JSON file containing versions. + + Returns: + A dictionary of versions if successful, otherwise None. + """ try: response = requests.get(url) response.raise_for_status() @@ -43,22 +79,24 @@ def load_versions_from_url(url): # Determine the precedence of version sources if default_versions_source: - # User-specified source via Pulumi config + # User has specified a source for default versions via Pulumi config if default_versions_source.startswith(('http://', 'https://')): - # It's a URL + # The source is a URL default_versions = load_versions_from_url(default_versions_source) else: - # It's a file path + # The source is assumed to be a file path default_versions = load_versions_from_file(default_versions_source) if not default_versions: + # Failed to load versions from the specified source pulumi.log.error(f"Failed to load default versions from specified source: {default_versions_source}") raise Exception("Cannot proceed without default versions.") else: - # Check if versions.stack_name is set to true + # No user-specified source; proceed with other options in order of precedence + if versions_stack_name: - # Attempt to load versions from ./versions/$STACK_NAME.json + # Attempt to load versions from a stack-specific file (e.g., ./versions/dev.json) current_dir = os.path.dirname(os.path.abspath(__file__)) versions_dir = os.path.join(current_dir, '..', '..', 'versions') stack_versions_path = os.path.join(versions_dir, f'{stack_name}.json') @@ -70,15 +108,15 @@ def load_versions_from_url(url): default_versions_path = os.path.join(current_dir, '..', '..', 'default_versions.json') default_versions = load_versions_from_file(default_versions_path) - # If still not loaded, attempt to fetch from remote URL based on channel + # If still not loaded, attempt to fetch from a remote URL based on the specified channel if not default_versions: - # Construct the URL based on the channel + # Construct the URL based on the channel (e.g., 'stable' or 'latest') base_url = 'https://github.com/containercraft/kargo/releases/latest/download/' versions_filename = f'{versions_channel}_versions.json' versions_url = f'{base_url}{versions_filename}' default_versions = load_versions_from_url(versions_url) - # If versions are still not loaded, log an error and raise an exception + # If versions are still not loaded after all attempts, log an error and raise an exception if not default_versions: pulumi.log.error("Could not load default versions from any source. Cannot proceed.") raise Exception("Cannot proceed without default versions.") From f0c651b2e19ad09c8b25351181fab48b27cf0fa7 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sat, 14 Sep 2024 18:20:24 +0000 Subject: [PATCH 06/43] improve developer experience --- pulumi/DEVELOPER.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 pulumi/DEVELOPER.md diff --git a/pulumi/DEVELOPER.md b/pulumi/DEVELOPER.md new file mode 100644 index 0000000..557d430 --- /dev/null +++ b/pulumi/DEVELOPER.md @@ -0,0 +1,44 @@ +## **Developer & Architecture Ethos** + +Kargo Kubevirt Kubernetes PaaS - Pulumi Python Infrastructure as Code (IaC) + +> Prime Directive: "Features are nice. Quality is paramount." +> +> Quality is not just about the product or code. Enjoyable developer experience is imperative to the survival of FOSS. + +#### **Developer Directives** +- **Improve Code Maintainability:** Enhance structure and organization. Prioritize readable, reusable, and extensible code. +- **Optimize Performance:** Code execution performance. honor configuration. Do not execute inactive code. +- **Establish Standard Practices:** Develop a consistent approach to configuration handling, module deployment, and code organization to guide future development. + +--- + +### **Developer Imperatives** + +ContainerCraft is a Developer Experience (DX) and User Experience (UX) obsessed project. As part of the CCIO Open Source education and skill development ecosystem, Kargo project's survival is dependent on the happiness of community developers and users. + +The following guidelines promote happiness as a means of promoting growth and sustainability. + +| **Imperative** | **Explanation** | +|--------------------------|-------------------------------------------------------------------------------------------------------| +| **User Experience (UX)** | Provide clear error messages, and logging to improve intuitive learning, development, and debugging. | +| **Developer Experience (DX)** | Optimize DX with clear documentation, examples, and architecture. | +| **Configurable Modules** | Support user module customization via the pulumi stack configuration pattern. | +| **Module Data Classes** | Utilize typed dataclasses to safely encapsulate module configuration. | +| **Sane Defaults in Data Classes** | Include sensible default values for module configurations. | +| **User Configuration Handling** | Merge user-provided configurations with defaults. Allow minimal input for module configuration. | +| **Simple Function Signatures** | Reduce parameter count. Encapsulate configuration objects in function signatures. | +| **Type Annotations** | Enhance readability and maintainability with type annotations. | +| **Safe Function Signatures** | Enforce type safety. Raise unknown configuration keys gracefully. | +| **Maintain a streamlined entrypoint** | Minimize top-level code. Encapsulate module logic within the module directory and code. | +| **Reuse + Dedupe code** | Refactor common patterns and logic into shared utilities in `src/lib/`. Adopt shared utilities when possible. | +| **Version Control Dependencies** | Utilize `versions.py` to manage component versions within `__main__.py`. | +| **Transparency** | Return informative version and configuration values to `version` and `configuration` dictionaries. | +| **Conditional Execution** | Use conditional imports and execution. Prevent unnecessary code execution when modules are disabled. | +| **Remove Deprecated Code** | Eliminate deprecated feature code. | + +--- + +### **Detailed Breakdown** + +TODO: Add detailed breakdown of each directive and imperative with code snippet examples and links to exemplary code. From edcc1de46fab9f2f417ddac7d9e49f724b1f1b6c Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sat, 14 Sep 2024 18:20:56 +0000 Subject: [PATCH 07/43] improve developer experience --- pulumi/{DEVELOPER.md => README.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pulumi/{DEVELOPER.md => README.md} (100%) diff --git a/pulumi/DEVELOPER.md b/pulumi/README.md similarity index 100% rename from pulumi/DEVELOPER.md rename to pulumi/README.md From 4b1159bac7e5ab2216ac6223d2bbd3182cd37c5e Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sat, 14 Sep 2024 18:21:47 +0000 Subject: [PATCH 08/43] improve developer experience --- pulumi/DEVELOPER.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 pulumi/DEVELOPER.md diff --git a/pulumi/DEVELOPER.md b/pulumi/DEVELOPER.md new file mode 100644 index 0000000..98e4901 --- /dev/null +++ b/pulumi/DEVELOPER.md @@ -0,0 +1,44 @@ +## **Developer & Architecture Ethos** + +Kargo Kubevirt Kubernetes PaaS - Pulumi Python Infrastructure as Code (IaC) + +> Prime Directive: "Features are nice. Quality is paramount." +> +> Quality is not just about the product or code. Enjoyable developer experience is imperative to the survival of FOSS. + +#### **Developer Directives** +- **Improve Code Maintainability:** Enhance structure and organization. Prioritize readable, reusable, and extensible code. +- **Optimize Performance:** Code execution performance. honor configuration. Do not execute inactive code. +- **Establish Standard Practices:** Develop a consistent approach to configuration handling, module deployment, and code organization to guide future development. + +--- + +### **Developer Imperatives** + +ContainerCraft is a Developer Experience (DX) and User Experience (UX) obsessed project. As part of the CCIO Open Source education and skill development ecosystem, Kargo project's survival is dependent on the happiness of community developers and users. + +The following guidelines promote happiness as a means of promoting growth and sustainability. + +| **Imperative** | **Explanation** | +|--------------------------|-------------------------------------------------------------------------------------------------------| +| **User Experience (UX)** | Provide clear error messages, and logging to improve intuitive learning, development, and debugging. | +| **Developer Experience (DX)** | Optimize DX with clear documentation, examples, and architecture. | +| **Configurable Modules** | Support user module customization via the pulumi stack configuration pattern. | +| **Module Data Classes** | Utilize typed dataclasses to safely encapsulate module configuration. | +| **Sane Defaults in Data Classes** | Include sensible default values for module configurations. | +| **User Configuration Handling** | Merge user-provided configurations with defaults. Allow minimal input for module configuration. | +| **Simple Function Signatures** | Reduce parameter count. Encapsulate configuration objects in function signatures. | +| **Type Annotations** | Enhance readability and maintainability with type annotations. | +| **Safe Function Signatures** | Enforce type safety. Raise unknown configuration keys gracefully. | +| **Maintain a streamlined entrypoint** | Minimize top-level code. Encapsulate module logic within the module directory and code. | +| **Reuse + Dedupe code** | Refactor common patterns and logic into shared utilities in `src/lib/`. Adopt shared utilities when possible. | +| **Version Control Dependencies** | Utilize `versions.py` to manage component versions within `__main__.py`. | +| **Transparency** | Return informative version and configuration values to `version` and `configuration` dictionaries. | +| **Conditional Execution** | Use conditional imports and execution. Prevent unnecessary code execution when modules are disabled. | +| **Remove Deprecated Code** | Eliminate deprecated feature code. | + +--- + +### **Detailed Breakdown** + +**TODO**: Add detailed breakdown of each imperative and explanation with code snippet examples and links to exemplary code. From edeb00c55e102f1e5ed76b52dce5e7088c63bbce Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sat, 14 Sep 2024 18:24:38 +0000 Subject: [PATCH 09/43] improve developer experience --- pulumi/DEVELOPER.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pulumi/DEVELOPER.md b/pulumi/DEVELOPER.md index 98e4901..b66f3d8 100644 --- a/pulumi/DEVELOPER.md +++ b/pulumi/DEVELOPER.md @@ -6,6 +6,8 @@ Kargo Kubevirt Kubernetes PaaS - Pulumi Python Infrastructure as Code (IaC) > > Quality is not just about the product or code. Enjoyable developer experience is imperative to the survival of FOSS. +ContainerCraft is a Developer Experience (DX) and User Experience (UX) obsessed project. As part of the CCIO Open Source education and skill development ecosystem, Kargo project's survival is dependent on the happiness of community developers and users. + #### **Developer Directives** - **Improve Code Maintainability:** Enhance structure and organization. Prioritize readable, reusable, and extensible code. - **Optimize Performance:** Code execution performance. honor configuration. Do not execute inactive code. @@ -15,8 +17,6 @@ Kargo Kubevirt Kubernetes PaaS - Pulumi Python Infrastructure as Code (IaC) ### **Developer Imperatives** -ContainerCraft is a Developer Experience (DX) and User Experience (UX) obsessed project. As part of the CCIO Open Source education and skill development ecosystem, Kargo project's survival is dependent on the happiness of community developers and users. - The following guidelines promote happiness as a means of promoting growth and sustainability. | **Imperative** | **Explanation** | From c01ad84fcc418c86254bc9d817904ff2ed832a2d Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sat, 14 Sep 2024 18:26:55 +0000 Subject: [PATCH 10/43] improve developer experience --- pulumi/DEVELOPER.md | 44 -------------------------------------------- pulumi/README.md | 6 +++--- 2 files changed, 3 insertions(+), 47 deletions(-) delete mode 100644 pulumi/DEVELOPER.md diff --git a/pulumi/DEVELOPER.md b/pulumi/DEVELOPER.md deleted file mode 100644 index b66f3d8..0000000 --- a/pulumi/DEVELOPER.md +++ /dev/null @@ -1,44 +0,0 @@ -## **Developer & Architecture Ethos** - -Kargo Kubevirt Kubernetes PaaS - Pulumi Python Infrastructure as Code (IaC) - -> Prime Directive: "Features are nice. Quality is paramount." -> -> Quality is not just about the product or code. Enjoyable developer experience is imperative to the survival of FOSS. - -ContainerCraft is a Developer Experience (DX) and User Experience (UX) obsessed project. As part of the CCIO Open Source education and skill development ecosystem, Kargo project's survival is dependent on the happiness of community developers and users. - -#### **Developer Directives** -- **Improve Code Maintainability:** Enhance structure and organization. Prioritize readable, reusable, and extensible code. -- **Optimize Performance:** Code execution performance. honor configuration. Do not execute inactive code. -- **Establish Standard Practices:** Develop a consistent approach to configuration handling, module deployment, and code organization to guide future development. - ---- - -### **Developer Imperatives** - -The following guidelines promote happiness as a means of promoting growth and sustainability. - -| **Imperative** | **Explanation** | -|--------------------------|-------------------------------------------------------------------------------------------------------| -| **User Experience (UX)** | Provide clear error messages, and logging to improve intuitive learning, development, and debugging. | -| **Developer Experience (DX)** | Optimize DX with clear documentation, examples, and architecture. | -| **Configurable Modules** | Support user module customization via the pulumi stack configuration pattern. | -| **Module Data Classes** | Utilize typed dataclasses to safely encapsulate module configuration. | -| **Sane Defaults in Data Classes** | Include sensible default values for module configurations. | -| **User Configuration Handling** | Merge user-provided configurations with defaults. Allow minimal input for module configuration. | -| **Simple Function Signatures** | Reduce parameter count. Encapsulate configuration objects in function signatures. | -| **Type Annotations** | Enhance readability and maintainability with type annotations. | -| **Safe Function Signatures** | Enforce type safety. Raise unknown configuration keys gracefully. | -| **Maintain a streamlined entrypoint** | Minimize top-level code. Encapsulate module logic within the module directory and code. | -| **Reuse + Dedupe code** | Refactor common patterns and logic into shared utilities in `src/lib/`. Adopt shared utilities when possible. | -| **Version Control Dependencies** | Utilize `versions.py` to manage component versions within `__main__.py`. | -| **Transparency** | Return informative version and configuration values to `version` and `configuration` dictionaries. | -| **Conditional Execution** | Use conditional imports and execution. Prevent unnecessary code execution when modules are disabled. | -| **Remove Deprecated Code** | Eliminate deprecated feature code. | - ---- - -### **Detailed Breakdown** - -**TODO**: Add detailed breakdown of each imperative and explanation with code snippet examples and links to exemplary code. diff --git a/pulumi/README.md b/pulumi/README.md index 557d430..b66f3d8 100644 --- a/pulumi/README.md +++ b/pulumi/README.md @@ -6,6 +6,8 @@ Kargo Kubevirt Kubernetes PaaS - Pulumi Python Infrastructure as Code (IaC) > > Quality is not just about the product or code. Enjoyable developer experience is imperative to the survival of FOSS. +ContainerCraft is a Developer Experience (DX) and User Experience (UX) obsessed project. As part of the CCIO Open Source education and skill development ecosystem, Kargo project's survival is dependent on the happiness of community developers and users. + #### **Developer Directives** - **Improve Code Maintainability:** Enhance structure and organization. Prioritize readable, reusable, and extensible code. - **Optimize Performance:** Code execution performance. honor configuration. Do not execute inactive code. @@ -15,8 +17,6 @@ Kargo Kubevirt Kubernetes PaaS - Pulumi Python Infrastructure as Code (IaC) ### **Developer Imperatives** -ContainerCraft is a Developer Experience (DX) and User Experience (UX) obsessed project. As part of the CCIO Open Source education and skill development ecosystem, Kargo project's survival is dependent on the happiness of community developers and users. - The following guidelines promote happiness as a means of promoting growth and sustainability. | **Imperative** | **Explanation** | @@ -41,4 +41,4 @@ The following guidelines promote happiness as a means of promoting growth and su ### **Detailed Breakdown** -TODO: Add detailed breakdown of each directive and imperative with code snippet examples and links to exemplary code. +**TODO**: Add detailed breakdown of each imperative and explanation with code snippet examples and links to exemplary code. From db3c16e64c3b04ae413183c7f2cb6900dcf2d9c2 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sun, 15 Sep 2024 05:19:47 +0000 Subject: [PATCH 11/43] streamline iac entrypoint main.py --- pulumi/__main__.py | 176 ++++-------------------------- pulumi/src/cert_manager/deploy.py | 9 -- pulumi/src/lib/config.py | 58 ++++++++++ pulumi/src/lib/deploy_module.py | 77 +++++++++++++ pulumi/src/lib/init.py | 79 ++++++++++++++ pulumi/src/lib/metadata.py | 45 ++++++++ pulumi/src/lib/versions.py | 9 -- 7 files changed, 282 insertions(+), 171 deletions(-) create mode 100644 pulumi/src/lib/config.py create mode 100644 pulumi/src/lib/deploy_module.py create mode 100644 pulumi/src/lib/init.py create mode 100644 pulumi/src/lib/metadata.py diff --git a/pulumi/__main__.py b/pulumi/__main__.py index 91871b9..e4d1a55 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -1,107 +1,45 @@ # __main__.py - # Main entry point for the Kargo Pulumi IaC program. -# This script is responsible for deploying the Kargo platform components located in the respective src/ directories. +# This script is responsible for deploying the Kargo platform components. +# Components are located in their respective src/ directories. +# Generic Python library imports import os from typing import Any, Dict, List, Optional, Tuple +# Pulumi imports import pulumi import pulumi_kubernetes as k8s from pulumi_kubernetes import Provider -from src.lib.versions import load_default_versions - -########################################## -# Load Pulumi Config and Initialize Variables - -# Load the Pulumi configuration settings -config = pulumi.Config() -stack_name = pulumi.get_stack() -project_name = pulumi.get_project() +# Local imports +from src.lib.init import initialize_pulumi +from src.lib.config import get_module_config, export_results -# Load default versions for modules -default_versions = load_default_versions(config) - -# Initialize a dictionary to keep track of deployed component versions -versions: Dict[str, str] = {} - -########################################## -# Kubernetes Configuration +# Execute Pulumi & Custom Initialization +init = initialize_pulumi() -# Retrieve Kubernetes settings from Pulumi config or environment variables -kubernetes_config = config.get_object("kubernetes") or {} -kubeconfig = kubernetes_config.get("kubeconfig") or os.getenv('KUBECONFIG') -kubernetes_context = kubernetes_config.get("context") +# Pulumi Configuration +config = init["config"] -# Create a Kubernetes provider instance to interact with the cluster -k8s_provider = Provider( - "k8sProvider", - kubeconfig=kubeconfig, - context=kubernetes_context, -) +# Kubernetes Provider +k8s_provider = init["k8s_provider"] -# Initialize a dictionary to keep track of module configurations -configurations: Dict[str, Dict[str, Any]] = {} +# Default Module Versions +versions = init["versions"] -# Log the Kubernetes configuration details -pulumi.log.info(f"kubeconfig: {kubeconfig}") -pulumi.log.info(f"kubernetes_context: {kubernetes_context}") - -# TODO: log the git repository URL and branch/commit hash using standard python libraries and pulumi logging functions +# Initialize empty version and configuration dictionaries +configurations = init["configurations"] +default_versions = init["default_versions"] +global_depends_on = init["global_depends_on"] ########################################## -# Module Configuration and Enable Flags - -def get_module_config( - module_name: str, - config: pulumi.Config, - default_versions: Dict[str, Any] -) -> Tuple[Dict[str, Any], bool]: - """ - Retrieves and prepares the configuration for a module. - - Args: - module_name: The name of the module to configure. - config: The Pulumi configuration object. - default_versions: A dictionary of default versions for modules. - - Returns: - A tuple containing the module's configuration dictionary and a boolean indicating if the module is enabled. - """ - # Get the module's configuration from Pulumi config, default to {"enabled": "false"} if not set - module_config = config.get_object(module_name) or {"enabled": "false"} - module_enabled = str(module_config.get('enabled', 'false')).lower() == "true" - - # Remove 'enabled' key from the module configuration as modules do not need this key beyond this point - module_config.pop('enabled', None) - - # Handle version injection into the module configuration - module_version = module_config.get('version') - if not module_version: - # No version specified in module config; use default version - module_version = default_versions.get(module_name) - if module_version: - module_config['version'] = module_version - else: - # No default version available; set to None (module will handle this case) - module_config['version'] = None - else: - # Version is specified in module config; keep as is (could be 'latest' or a specific version) - pass +# Deploy Cert Manager Module - return module_config, module_enabled - -########################################## -# Deploy Modules - -# Initialize a list to manage dependencies between resources globally -global_depends_on: List[pulumi.Resource] = [] - -# Cert Manager Module # Retrieve configuration and enable flag for the cert_manager module config_cert_manager_dict, cert_manager_enabled = get_module_config('cert_manager', config, default_versions) +# Check if the cert_manager module is enabled & execute deployment logic if true if cert_manager_enabled: # Import the CertManagerConfig data class and merge the user config with defaults from src.cert_manager.types import CertManagerConfig @@ -132,23 +70,9 @@ def get_module_config( ########################################## # Export Component Versions and Configurations - -# Export the versions of deployed components -pulumi.export("versions", versions) - -# Export the configurations of deployed modules -pulumi.export("configuration", configurations) +export_results(versions, configurations) -#import os -#from typing import Any, Dict, List, Optional, Tuple -# -#import pulumi -#import pulumi_kubernetes as k8s -#from pulumi_kubernetes import Provider -# -#from src.cilium.deploy import deploy_cilium -#from src.cert_manager.deploy import deploy_cert_manager #from src.kubevirt.deploy import deploy_kubevirt #from src.containerized_data_importer.deploy import deploy_cdi #from src.cluster_network_addons.deploy import deploy_cnao @@ -161,12 +85,7 @@ def get_module_config( #from src.ceph.deploy import deploy_rook_operator #from src.vm.ubuntu import deploy_ubuntu_vm #from src.vm.talos import deploy_talos_cluster -# -########################################### -## Load Pulumi Config -#config = pulumi.Config() -#stack_name = pulumi.get_stack() -#project_name = pulumi.get_project() +#from src.cilium.deploy import deploy_cilium # ########################################### ## Kubernetes Configuration @@ -186,19 +105,6 @@ def get_module_config( # ########################################### ## Module Configuration and Enable Flags -#def get_module_config(module_name: str) -> Tuple[Dict[str, Any], bool]: -# """ -# Retrieves the configuration for a module and determines if it is enabled. -# -# Args: -# module_name: The name of the module. -# -# Returns: -# A tuple containing the module configuration dictionary and a boolean indicating if the module is enabled. -# """ -# module_config = config.get_object(module_name) or {"enabled": "false"} -# module_enabled = str(module_config.get('enabled', 'false')).lower() == "true" -# return module_config, module_enabled # ## Retrieve configurations and enable flags for all modules #config_cilium, cilium_enabled = get_module_config('cilium') @@ -222,42 +128,6 @@ def get_module_config( #global_depends_on: List[pulumi.Resource] = [] # ########################################### -## Module Deployment Functions -# -#def deploy_cert_manager_module() -> Tuple[Optional[str], Optional[pulumi.Resource], Optional[str]]: -# """ -# Deploys the Cert Manager module if enabled. -# -# Returns: -# A tuple containing the version, Helm release, and CA certificate data. -# """ -# if not cert_manager_enabled: -# pulumi.log.info("Cert Manager module is disabled. Skipping deployment.") -# return None, None, None -# -# namespace = "cert-manager" -# cert_manager_version = config_cert_manager.get('version') -# -# cert_manager_version, release, ca_cert_b64, _ = deploy_cert_manager( -# namespace=namespace, -# version=cert_manager_version, -# depends_on=global_depends_on, -# k8s_provider=k8s_provider, -# ) -# -# versions["cert_manager"] = {"enabled": cert_manager_enabled, "version": cert_manager_version} -# -# if release: -# global_depends_on.append(release) -# -# # Export the CA certificate -# pulumi.export("cert_manager_selfsigned_cert", ca_cert_b64) -# -# return cert_manager_version, release, ca_cert_b64 -# -#cert_manager_version, cert_manager_release, cert_manager_selfsigned_cert = deploy_cert_manager_module() -# -########################################### ## Export Component Versions # #pulumi.export("versions", versions) diff --git a/pulumi/src/cert_manager/deploy.py b/pulumi/src/cert_manager/deploy.py index 968e97c..e40324d 100644 --- a/pulumi/src/cert_manager/deploy.py +++ b/pulumi/src/cert_manager/deploy.py @@ -1,13 +1,4 @@ # src/cert_manager/deploy.py - -""" -Deployment logic for the Cert Manager module in Kubernetes. - -This module defines functions that deploy cert-manager into a Kubernetes cluster using Pulumi -and Helm. It handles the setup of necessary resources including namespaces, Helm releases, -and ClusterIssuers, and it manages the issuance of certificates via cert-manager. -""" - import pulumi import pulumi_kubernetes as k8s from pulumi_kubernetes.apiextensions.CustomResource import CustomResource diff --git a/pulumi/src/lib/config.py b/pulumi/src/lib/config.py new file mode 100644 index 0000000..c3c9cb1 --- /dev/null +++ b/pulumi/src/lib/config.py @@ -0,0 +1,58 @@ +# pulumi/src/lib/config.py + +""" +Configuration Management Module + +This module handles the retrieval and preparation of configurations for different modules +within the Kargo Pulumi IaC program. It centralizes configuration logic to promote reuse +and maintainability. +""" + +from typing import Any, Dict, Tuple +import pulumi + +def get_module_config( + module_name: str, + config: pulumi.Config, + default_versions: Dict[str, Any] +) -> Tuple[Dict[str, Any], bool]: + """ + Retrieves and prepares the configuration for a module. + + Args: + module_name (str): The name of the module to configure. + config (pulumi.Config): The Pulumi configuration object. + default_versions (Dict[str, Any]): A dictionary of default versions for modules. + + Returns: + Tuple[Dict[str, Any], bool]: + + A tuple containing the module's configuration dictionary + and a boolean indicating if the module is enabled. + """ + # Get the module's configuration from Pulumi config, default to {"enabled": "false"} if not set + module_config = config.get_object(module_name) or {"enabled": "false"} + module_enabled = str(module_config.get('enabled', 'false')).lower() == "true" + + # Remove 'enabled' key from the module configuration as modules do not need this key beyond this point + module_config.pop('enabled', None) + + # Handle version injection into the module configuration + module_version = module_config.get('version') + if not module_version: + # No version specified in module config; use default version + module_version = default_versions.get(module_name) + if module_version: + module_config['version'] = module_version + else: + # No default version available; set to None (module will handle this case) + module_config['version'] = None + else: + # Version is specified in module config; keep as is (could be 'latest' or a specific version) + pass + + return module_config, module_enabled + +def export_results(versions: Dict[str, str], configurations: Dict[str, Dict[str, Any]]): + pulumi.export("versions", versions) + pulumi.export("configuration", configurations) diff --git a/pulumi/src/lib/deploy_module.py b/pulumi/src/lib/deploy_module.py new file mode 100644 index 0000000..e4bae17 --- /dev/null +++ b/pulumi/src/lib/deploy_module.py @@ -0,0 +1,77 @@ +# pulumi/src/lib/deploy_module.py + +# TODO: Implement the `deploy_module` function in __main__.py +# Proceed with objective after all modules have been implemented. + +from typing import Any, Dict, List, Tuple +import pulumi + +def deploy_module( + module_name: str, + config: pulumi.Config, + default_versions: Dict[str, Any], + global_depends_on: List[pulumi.Resource], + k8s_provider: Any, + deploy_function: Any, + export_name: str +) -> None: + """ + Helper function to deploy a module based on configuration. + + Args: + module_name (str): Name of the module. + config (pulumi.Config): Pulumi configuration object. + default_versions (Dict[str, Any]): Default versions for modules. + global_depends_on (List[pulumi.Resource]): Global dependencies. + k8s_provider (Any): Kubernetes provider. + deploy_function (Callable): Function to deploy the module. + export_name (str): Name of the export variable. + """ + module_config_dict, module_enabled = get_module_config(module_name, config, default_versions) + + if module_enabled: + # Dynamically import the module's types and deploy function + module_types = __import__(f"src.{module_name}.types", fromlist=['']) + ModuleConfigClass = getattr(module_types, f"{module_name.capitalize()}Config") + config_obj = ModuleConfigClass.merge(module_config_dict) + + module_deploy = __import__(f"src.{module_name}.deploy", fromlist=['']) + deploy_func = getattr(module_deploy, f"deploy_{module_name}_module") + + version, release, exported_value = deploy_func( + config_obj=config_obj, + global_depends_on=global_depends_on, + k8s_provider=k8s_provider, + ) + + versions[module_name] = version + configurations[module_name] = { + "enabled": module_enabled, + } + + pulumi.export(export_name, exported_value) + +# # Example Implementation +# from src.lib.deploy_helper import deploy_module + +# # Deploy Cert Manager +# deploy_module( +# module_name='cert_manager', +# config=config, +# default_versions=default_versions, +# global_depends_on=global_depends_on, +# k8s_provider=k8s_provider, +# deploy_function=deploy_cert_manager_module, +# export_name='cert_manager_selfsigned_cert' +# ) +# +# # Deploy Ceph +# deploy_module( +# module_name='ceph', +# config=config, +# default_versions=default_versions, +# global_depends_on=global_depends_on, +# k8s_provider=k8s_provider, +# deploy_function=deploy_ceph_module, +# export_name='ceph_release' +# ) diff --git a/pulumi/src/lib/init.py b/pulumi/src/lib/init.py new file mode 100644 index 0000000..9587905 --- /dev/null +++ b/pulumi/src/lib/init.py @@ -0,0 +1,79 @@ +# src/lib/init.py + +import os +import pulumi +import pulumi_kubernetes as k8s +from pulumi_kubernetes import Provider +from typing import Dict, List, Any + +from src.lib.versions import load_default_versions +from src.lib.metadata import collect_git_info + +def initialize_pulumi() -> Dict[str, Any]: + """ + Initializes the Pulumi configuration, Kubernetes provider, and global resources. + + Returns: + A dictionary containing initialized resources like: + - config: Pulumi Config object + - stack_name: The Pulumi stack name + - project_name: The Pulumi project name + - versions: Dictionary to track deployed component versions + - configurations: Dictionary to track module configurations + - global_depends_on: List to manage dependencies globally + - k8s_provider: Kubernetes provider instance + """ + # Load Pulumi config + config = pulumi.Config() + stack_name = pulumi.get_stack() + project_name = pulumi.get_project() + + # Load default versions for modules + default_versions = load_default_versions(config) + + # Initialize dictionaries for versions and configurations + versions: Dict[str, str] = {} + configurations: Dict[str, Dict[str, Any]] = {} + + # Initialize a list to manage dependencies globally + global_depends_on: List[pulumi.Resource] = [] + + # Retrieve Kubernetes settings from Pulumi config or environment variables + kubernetes_config = config.get_object("kubernetes") or {} + kubeconfig = kubernetes_config.get("kubeconfig") or os.getenv('KUBECONFIG') + kubernetes_context = kubernetes_config.get("context") + + # Log the Kubernetes configuration details + pulumi.log.info(f"kubeconfig: {kubeconfig}") + pulumi.log.info(f"kubernetes_context: {kubernetes_context}") + + # Create a Kubernetes provider instance + k8s_provider = Provider( + "k8sProvider", + kubeconfig=kubeconfig, + context=kubernetes_context, + ) + + # Retrieve the Git repository source, branch, and commit metadata + git_info = collect_git_info() + + # Append Git metadata to the configurations dictionary under 'source_repository' + # TODO: populate global resource tags / labels / annotations with Git metadata + configurations["source_repository"] = { + "remote": git_info["remote"], + "branch": git_info["branch"], + "commit": git_info["commit"] + } + + # Return all initialized resources + return { + "config": config, + "stack_name": stack_name, + "project_name": project_name, + "default_versions": default_versions, + "versions": versions, + "configurations": configurations, + "global_depends_on": global_depends_on, + "k8s_provider": k8s_provider, + "git_info": git_info + } diff --git a/pulumi/src/lib/metadata.py b/pulumi/src/lib/metadata.py new file mode 100644 index 0000000..e0371df --- /dev/null +++ b/pulumi/src/lib/metadata.py @@ -0,0 +1,45 @@ +# src/lib/metadata.py +# Description: This module contains utility functions for retrieving metadata about the current Pulumi project source code repository. + +import subprocess +from typing import Dict + +def collect_git_info() -> Dict[str, str]: + """ + Retrieves the current Git repository's remote URL, branch, and commit hash. + + Returns: + Dict[str, str]: A dictionary containing the 'remote', 'branch', and 'commit' information. + """ + try: + # Get the remote URL + remote = subprocess.check_output( + ['git', 'config', '--get', 'remote.origin.url'], + stderr=subprocess.STDOUT + ).strip().decode('utf-8') + + # Get the current branch + branch = subprocess.check_output( + ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], + stderr=subprocess.STDOUT + ).strip().decode('utf-8') + + # Get the latest commit hash + commit = subprocess.check_output( + ['git', 'rev-parse', 'HEAD'], + stderr=subprocess.STDOUT + ).strip().decode('utf-8') + + return { + 'remote': remote, + 'branch': branch, + 'commit': commit + } + + except subprocess.CalledProcessError as e: + pulumi.log.error(f"Error fetching git information: {e.output.decode('utf-8')}") + return { + 'remote': 'N/A', + 'branch': 'N/A', + 'commit': 'N/A' + } diff --git a/pulumi/src/lib/versions.py b/pulumi/src/lib/versions.py index aaa2ca1..20fec0e 100644 --- a/pulumi/src/lib/versions.py +++ b/pulumi/src/lib/versions.py @@ -56,15 +56,6 @@ def load_versions_from_file(file_path): return None def load_versions_from_url(url): - """ - Loads versions from a remote URL. - - Args: - url: The URL pointing to the JSON file containing versions. - - Returns: - A dictionary of versions if successful, otherwise None. - """ try: response = requests.get(url) response.raise_for_status() From aa9764c4dc013e0091a921cf4f501c2d351ca04c Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sun, 15 Sep 2024 06:40:10 +0000 Subject: [PATCH 12/43] refactor kubevirt module --- .github/bin/delete-namespace | 15 +++ pulumi/__main__.py | 35 ++++- pulumi/default_versions.json | 3 +- pulumi/src/kubevirt/deploy.py | 240 +++++++++++++++++++++++----------- pulumi/src/kubevirt/types.py | 42 ++++++ 5 files changed, 258 insertions(+), 77 deletions(-) create mode 100644 pulumi/src/kubevirt/types.py diff --git a/.github/bin/delete-namespace b/.github/bin/delete-namespace index 2a88060..b6cb589 100755 --- a/.github/bin/delete-namespace +++ b/.github/bin/delete-namespace @@ -23,6 +23,18 @@ delete_namespace() { echo "Namespace $namespace has been deleted." } +# Delete CustomResourceDefinitions (CRDs) associated with cert-manager and kubevirt +delete_crds() { + local crds=("certificaterequests.cert-manager.io" "certificates.cert-manager.io" "challenges.acme.cert-manager.io" \ + "orders.acme.cert-manager.io" "clusterissuers.cert-manager.io" "issuers.cert-manager.io" \ + "kubevirts.kubevirt.io") + + for crd in "${crds[@]}"; do + echo "Deleting CRD: $crd" + kubectl delete crd "$crd" --ignore-not-found --wait + done +} + # Check for at least one namespace provided as argument if [ $# -eq 0 ]; then echo "Usage: $0 ... " @@ -34,3 +46,6 @@ for namespace in "$@"; do delete_resources_in_namespace "$namespace" delete_namespace "$namespace" done + +# Delete CRDs +delete_crds diff --git a/pulumi/__main__.py b/pulumi/__main__.py index e4d1a55..a1a9968 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -55,10 +55,8 @@ k8s_provider=k8s_provider, ) - # Record the deployed version of cert_manager + # Record the deployed version of cert_manager and configuration versions["cert_manager"] = cert_manager_version - - # Record the configuration state of cert_manager configurations["cert_manager"] = { "enabled": cert_manager_enabled, } @@ -68,6 +66,37 @@ else: cert_manager_selfsigned_cert = None + +########################################## +# Deploy Cert Manager Module + +# KubeVirt Module Configuration and Deployment +config_kubevirt_dict, kubevirt_enabled = get_module_config('kubevirt', config, default_versions) + +if kubevirt_enabled: + # Import the KubeVirtConfig data class and merge the user config with defaults + from src.kubevirt.types import KubeVirtConfig + config_kubevirt = KubeVirtConfig.merge(config_kubevirt_dict) + + # Import the deployment function for the KubeVirt module + from src.kubevirt.deploy import deploy_kubevirt_module + + # Deploy the KubeVirt module + kubevirt_version, kubevirt_operator = deploy_kubevirt_module( + config_kubevirt=config_kubevirt, + global_depends_on=global_depends_on, + k8s_provider=k8s_provider, + ) + + # Record the deployed version & configuration + versions["kubevirt"] = kubevirt_version + configurations["kubevirt"] = { + "enabled": kubevirt_enabled + } +else: + kubevirt_operator = None + + ########################################## # Export Component Versions and Configurations export_results(versions, configurations) diff --git a/pulumi/default_versions.json b/pulumi/default_versions.json index 565513e..317c436 100644 --- a/pulumi/default_versions.json +++ b/pulumi/default_versions.json @@ -1,3 +1,4 @@ { - "cert_manager": "1.15.3" + "cert_manager": "1.15.3", + "kubevirt": "1.3.1" } diff --git a/pulumi/src/kubevirt/deploy.py b/pulumi/src/kubevirt/deploy.py index fc2f0a3..b5b1758 100644 --- a/pulumi/src/kubevirt/deploy.py +++ b/pulumi/src/kubevirt/deploy.py @@ -1,110 +1,132 @@ +# src/kubevirt/deploy.py + import requests import yaml import tempfile import os +from typing import Optional, List, Tuple, Dict + import pulumi import pulumi_kubernetes as k8s from pulumi_kubernetes.apiextensions.CustomResource import CustomResource from pulumi_kubernetes.meta.v1 import ObjectMetaArgs + from src.lib.namespace import create_namespace +from src.lib.types import NamespaceConfig +from .types import KubeVirtConfig -def deploy_kubevirt( - depends, - ns_name: str, - version: str, - use_emulation: bool, +def deploy_kubevirt_module( + config_kubevirt: KubeVirtConfig, + global_depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, - kubernetes_distribution: str - ): - - # Create namespace - ns_retain = True - ns_protect = False - ns_annotations = {} - ns_labels = { - "kubevirt.io": "", - "kubernetes.io/metadata.name": ns_name, - "openshift.io/cluster-monitoring": "true", - "pod-security.kubernetes.io/enforce": "privileged" - } - namespace = create_namespace( - depends, - ns_name, - ns_retain, - ns_protect, - k8s_provider, - custom_labels=ns_labels, - custom_annotations=ns_annotations + cert_manager_release: Optional[pulumi.Resource] = None + ) -> Tuple[Optional[str], Optional[pulumi.Resource]]: + """ + Deploys the KubeVirt module using YAML and custom resources. + + Args: + config_kubevirt (KubeVirtConfig): Configuration object for KubeVirt deployment. + global_depends_on (List[pulumi.Resource]): A list of resources that the deployment depends on. + k8s_provider (k8s.Provider): The Kubernetes provider for this deployment. + cert_manager_release (Optional[pulumi.Resource]): The cert-manager resource, if deployed. + + Returns: + Tuple[Optional[str], Optional[pulumi.Resource]]: + - The deployed KubeVirt version. + - The KubeVirt operator resource. + """ + # Create the namespace for KubeVirt first, independently + namespace_config = NamespaceConfig(name=config_kubevirt.namespace) + namespace_resource = create_namespace( + config=namespace_config, + k8s_provider=k8s_provider, + depends_on=global_depends_on # Ensure it waits for any global dependencies like cert-manager ) - # Fetch the latest stable version of KubeVirt - if version is None: + # Now deploy KubeVirt, ensuring it depends on the namespace creation + kubevirt_version, kubevirt_operator = deploy_kubevirt( + config_kubevirt=config_kubevirt, + depends_on=[namespace_resource] + global_depends_on, + k8s_provider=k8s_provider + ) + + if kubevirt_operator: + # Optionally add KubeVirt to global dependencies + global_depends_on.append(kubevirt_operator) + + return kubevirt_version, kubevirt_operator + + +def deploy_kubevirt( + config_kubevirt: KubeVirtConfig, + depends_on: List[pulumi.Resource], + k8s_provider: k8s.Provider + ) -> Tuple[str, k8s.yaml.ConfigFile]: + """ + Deploys KubeVirt into the Kubernetes cluster using its YAML manifests. + + Args: + config_kubevirt (KubeVirtConfig): Configuration settings for KubeVirt. + depends_on (List[pulumi.Resource]): Resources that the deployment should depend on. + k8s_provider (k8s.Provider): The Kubernetes provider instance. + + Returns: + Tuple[str, k8s.yaml.ConfigFile]: + - The version of KubeVirt deployed. + - The KubeVirt operator resource. + """ + # Extract configuration details from the KubeVirtConfig object + namespace = config_kubevirt.namespace + version = config_kubevirt.version + use_emulation = config_kubevirt.use_emulation + + # Fetch or use the specified KubeVirt version + if version == 'latest' or version is None: kubevirt_stable_version_url = 'https://storage.googleapis.com/kubevirt-prow/release/kubevirt/kubevirt/stable.txt' - version = requests.get(kubevirt_stable_version_url).text.strip() - version = version.lstrip("v") - pulumi.log.info(f"Setting version to latest stable: kubevirt/{version}") + version = requests.get(kubevirt_stable_version_url).text.strip().lstrip("v") + pulumi.log.info(f"Setting KubeVirt release version to latest: {version}") else: - # Log the version override - pulumi.log.info(f"Using helm release version: kubevirt/{version}") + pulumi.log.info(f"Using KubeVirt version: {version}") # Download the KubeVirt operator YAML kubevirt_operator_url = f'https://github.com/kubevirt/kubevirt/releases/download/v{version}/kubevirt-operator.yaml' response = requests.get(kubevirt_operator_url) kubevirt_yaml = yaml.safe_load_all(response.text) - # Edit the YAML in memory to remove the Namespace and adjust other resources - transformed_yaml = [] - for resource in kubevirt_yaml: - if resource and resource.get('kind') == 'Namespace': - pulumi.log.debug(f"Transforming Namespace resource: {resource['metadata']['name']}") - continue # Skip adding this namespace to the edited YAML - if resource and 'metadata' in resource: - resource['metadata']['namespace'] = ns_name - pulumi.log.debug(f"Setting namespace for {resource['kind']} to {ns_name}") - transformed_yaml.append(resource) - - # Write the edited YAML to a temporary file + # Transform the YAML and set the correct namespace + transformed_yaml = _transform_yaml(kubevirt_yaml, namespace) + + # Write the transformed YAML to a temporary file with tempfile.NamedTemporaryFile(delete=False, mode='w') as temp_file: yaml.dump_all(transformed_yaml, temp_file) temp_file_path = temp_file.name - #pulumi.log.info(f"Deploying KubeVirt from local file path: {temp_file_path}") - - # Ensure the tempfile is closed before passing it to ConfigFile - temp_file.close() - # Pass the edited YAML directly to ConfigFile + # Deploy the KubeVirt operator operator = k8s.yaml.ConfigFile( 'kubevirt-operator', file=temp_file_path, opts=pulumi.ResourceOptions( - parent=namespace, - depends_on=depends, provider=k8s_provider, + parent=None, + depends_on=depends_on, + custom_timeouts=pulumi.CustomTimeouts( + create="10m", + update="5m", + delete="5m" + ) ) ) - # Ensure the temporary file is deleted after Pulumi uses it + # Clean up the temporary file after Pulumi has used it pulumi.Output.all().apply(lambda _: os.unlink(temp_file_path)) - # Determine useEmulation based on the kubernetes_distribution - use_emulation = True if kubernetes_distribution == "kind" else use_emulation + # Deploy the KubeVirt custom resource if use_emulation: - pulumi.log.info("KVM Emulation configured for KubeVirt in development.") + pulumi.log.info("KVM Emulation enabled for KubeVirt.") # Create the KubeVirt custom resource object kubevirt_custom_resource_spec = { - "customizeComponents": {}, - "workloadUpdateStrategy": {}, - "certificateRotateStrategy": {}, - "imagePullPolicy": "IfNotPresent", "configuration": { - "smbios": { - "sku": "kargo-kc2", - "version": version, - "manufacturer": "ContainerCraft", - "product": "Kargo", - "family": "CCIO" - }, "developerConfiguration": { "useEmulation": use_emulation, "featureGates": [ @@ -113,9 +135,12 @@ def deploy_kubevirt( "AutoResourceLimitsGate" ] }, - "permittedHostDevices": { - "pciHostDevices": [ - ] + "smbios": { + "sku": "kargo-kc2", + "version": version, + "manufacturer": "ContainerCraft", + "product": "Kargo", + "family": "CCIO" } } } @@ -127,14 +152,83 @@ def deploy_kubevirt( kind="KubeVirt", metadata=ObjectMetaArgs( name="kubevirt", - namespace=ns_name, + namespace=namespace, ), spec=kubevirt_custom_resource_spec, opts=pulumi.ResourceOptions( provider=k8s_provider, - parent=operator, - depends_on=depends + parent=None, + depends_on=[operator], ) ) return version, operator + + +def _transform_yaml(yaml_data, namespace: str) -> List[Dict]: + """ + Helper function to transform YAML to set namespace and modify resources. + + Args: + yaml_data: The YAML data to transform. + namespace: The namespace to set for the resources. + + Returns: + List[Dict]: The transformed YAML with the appropriate namespace. + """ + transformed = [] + for resource in yaml_data: + if resource.get('kind') == 'Namespace': + continue # Skip the Namespace resource + if 'metadata' in resource: + resource['metadata']['namespace'] = namespace + transformed.append(resource) + return transformed + + + ## Create the KubeVirt custom resource object + #kubevirt_custom_resource_spec = { + # "customizeComponents": {}, + # "workloadUpdateStrategy": {}, + # "certificateRotateStrategy": {}, + # "imagePullPolicy": "IfNotPresent", + # "configuration": { + # "smbios": { + # "sku": "kargo-kc2", + # "version": version, + # "manufacturer": "ContainerCraft", + # "product": "Kargo", + # "family": "CCIO" + # }, + # "developerConfiguration": { + # "useEmulation": use_emulation, + # "featureGates": [ + # "HostDevices", + # "ExpandDisks", + # "AutoResourceLimitsGate" + # ] + # }, + # "permittedHostDevices": { + # "pciHostDevices": [] + # } + # } + #} + + ## Create the KubeVirt custom resource + #kubevirt = CustomResource( + # "kubevirt", + # api_version="kubevirt.io/v1", + # kind="KubeVirt", + # metadata=ObjectMetaArgs( + # name="kubevirt", + # namespace=namespace, + # ), + # spec=kubevirt_custom_resource_spec, + # opts=pulumi.ResourceOptions( + # provider=k8s_provider, + # parent=operator, + # depends_on=[operator], + # ) + #) + + #return version, operator diff --git a/pulumi/src/kubevirt/types.py b/pulumi/src/kubevirt/types.py new file mode 100644 index 0000000..779ea9d --- /dev/null +++ b/pulumi/src/kubevirt/types.py @@ -0,0 +1,42 @@ +# src/kubevirt/types.py + +import pulumi +from dataclasses import dataclass +from typing import Optional, Dict, Any + +@dataclass +class KubeVirtConfig: + """ + Configuration for deploying the KubeVirt module. + + Attributes: + namespace (str): The Kubernetes namespace where KubeVirt will be deployed. + Defaults to "kubevirt". + version (Optional[str]): The version of KubeVirt to deploy. + Defaults to None, which fetches the latest version. + use_emulation (bool): Whether to use emulation for KVM. + Defaults to False. + """ + namespace: str = "kubevirt" + version: Optional[str] = None + use_emulation: bool = False + + @staticmethod + def merge(user_config: Dict[str, Any]) -> 'KubeVirtConfig': + """ + Merges user-provided configuration with default values. + + Args: + user_config (Dict[str, Any]): A dictionary containing user-provided configuration options. + + Returns: + KubeVirtConfig: An instance of KubeVirtConfig with merged settings. + """ + default_config = KubeVirtConfig() + merged_config = default_config.__dict__.copy() + for key, value in user_config.items(): + if hasattr(default_config, key): + merged_config[key] = value + else: + pulumi.log.warn(f"Unknown configuration key '{key}' in kubevirt config.") + return KubeVirtConfig(**merged_config) From 713cd1d343442596527744dcdcf5d4f77644baf6 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sun, 15 Sep 2024 06:40:36 +0000 Subject: [PATCH 13/43] refactor kubevirt module --- pulumi/src/kubevirt/deploy.py | 48 ----------------------------------- 1 file changed, 48 deletions(-) diff --git a/pulumi/src/kubevirt/deploy.py b/pulumi/src/kubevirt/deploy.py index b5b1758..ab4020e 100644 --- a/pulumi/src/kubevirt/deploy.py +++ b/pulumi/src/kubevirt/deploy.py @@ -184,51 +184,3 @@ def _transform_yaml(yaml_data, namespace: str) -> List[Dict]: resource['metadata']['namespace'] = namespace transformed.append(resource) return transformed - - - ## Create the KubeVirt custom resource object - #kubevirt_custom_resource_spec = { - # "customizeComponents": {}, - # "workloadUpdateStrategy": {}, - # "certificateRotateStrategy": {}, - # "imagePullPolicy": "IfNotPresent", - # "configuration": { - # "smbios": { - # "sku": "kargo-kc2", - # "version": version, - # "manufacturer": "ContainerCraft", - # "product": "Kargo", - # "family": "CCIO" - # }, - # "developerConfiguration": { - # "useEmulation": use_emulation, - # "featureGates": [ - # "HostDevices", - # "ExpandDisks", - # "AutoResourceLimitsGate" - # ] - # }, - # "permittedHostDevices": { - # "pciHostDevices": [] - # } - # } - #} - - ## Create the KubeVirt custom resource - #kubevirt = CustomResource( - # "kubevirt", - # api_version="kubevirt.io/v1", - # kind="KubeVirt", - # metadata=ObjectMetaArgs( - # name="kubevirt", - # namespace=namespace, - # ), - # spec=kubevirt_custom_resource_spec, - # opts=pulumi.ResourceOptions( - # provider=k8s_provider, - # parent=operator, - # depends_on=[operator], - # ) - #) - - #return version, operator From ed888329a15da5c145411af297091490d7ad29e4 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Mon, 16 Sep 2024 05:46:18 +0000 Subject: [PATCH 14/43] working transform --- docs/deploy-on-kind-in-codespaces.md | 67 ------------ pulumi/__main__.py | 98 +++++++++++------ pulumi/src/aws/__init__.py | 0 pulumi/src/aws/deploy.py | 131 ++++++++++++++++++++++ pulumi/src/azure/__init__.py | 0 pulumi/src/cert_manager/deploy.py | 155 ++++++++++++--------------- pulumi/src/cert_manager/types.py | 62 +---------- pulumi/src/kubernetes/__init__.py | 0 pulumi/src/kubevirt/deploy.py | 107 +++++++++++------- pulumi/src/kubevirt/types.py | 24 ++++- pulumi/src/lib/compliance.py | 41 +++++++ pulumi/src/lib/config.py | 25 ++--- pulumi/src/lib/init.py | 51 ++++++--- pulumi/src/lib/metadata.py | 45 ++++++-- pulumi/src/lib/namespace.py | 51 ++------- pulumi/src/lib/types.py | 65 +++++++---- pulumi/src/lib/utils.py | 55 ++++++++++ 17 files changed, 592 insertions(+), 385 deletions(-) delete mode 100644 docs/deploy-on-kind-in-codespaces.md create mode 100644 pulumi/src/aws/__init__.py create mode 100644 pulumi/src/aws/deploy.py create mode 100644 pulumi/src/azure/__init__.py create mode 100644 pulumi/src/kubernetes/__init__.py create mode 100644 pulumi/src/lib/compliance.py create mode 100644 pulumi/src/lib/utils.py diff --git a/docs/deploy-on-kind-in-codespaces.md b/docs/deploy-on-kind-in-codespaces.md deleted file mode 100644 index a8b7482..0000000 --- a/docs/deploy-on-kind-in-codespaces.md +++ /dev/null @@ -1,67 +0,0 @@ -# How To: deploy on Kind in Codespaces - -1. launch codespaces from Kargo repository -2. open a terminal -3. run the following commands for minimum viable deployment of Kargo IaC: - -```bash -# Login to pulumi -pulumi login - -# Create Kind K8s Cluster -make kind - -# Configure Kargo -pulumi config set kubernetes.kubeconfig $PATH -pulumi config set cilium.enabled true - -# Deploy Kargo -pulumi up -``` - ---- - -## Additional Modules - -### Cilium - -```bash -# Enable Cilium -pulumi config set cilium.enabled true - -# Set Cilium version -pulumi config set cilium.version 1.14.7 -``` - -### Cert Manager - -```bash -# Enable Cert Manager -pulumi config set cert_manager.enabled true - -# Set Cert Manager version -pulumi config set cert-manager.version 1.15.1 -``` - -### Kubevirt - -```bash -# Enable Kubevirt -pulumi config set --path kubevirt.enabled true - -# Set Kubevirt version -pulumi config set --path kubevirt.version 1.2.2 -``` - -### Multus - -```bash -# Enable Multus -pulumi config set --path multus.enabled true - -# Set Multus version -pulumi config set --path multus.version master - -# Set Multus Default Bridge Name for Network Attachment Definition -pulumi config set --path multus.default_bridge br0 -``` diff --git a/pulumi/__main__.py b/pulumi/__main__.py index a1a9968..2703743 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -1,34 +1,25 @@ # __main__.py # Main entry point for the Kargo Pulumi IaC program. # This script is responsible for deploying the Kargo platform components. -# Components are located in their respective src/ directories. -# Generic Python library imports -import os -from typing import Any, Dict, List, Optional, Tuple - -# Pulumi imports +# Import Pulumi Libraries import pulumi import pulumi_kubernetes as k8s -from pulumi_kubernetes import Provider + +# Import Python Standard Libraries +from typing import Any, Dict, List, Optional, Tuple # Local imports from src.lib.init import initialize_pulumi from src.lib.config import get_module_config, export_results -# Execute Pulumi & Custom Initialization +# Pulumi Initialization init = initialize_pulumi() -# Pulumi Configuration +# Retrieve initialized resources config = init["config"] - -# Kubernetes Provider k8s_provider = init["k8s_provider"] - -# Default Module Versions versions = init["versions"] - -# Initialize empty version and configuration dictionaries configurations = init["configurations"] default_versions = init["default_versions"] global_depends_on = init["global_depends_on"] @@ -37,71 +28,112 @@ # Deploy Cert Manager Module # Retrieve configuration and enable flag for the cert_manager module -config_cert_manager_dict, cert_manager_enabled = get_module_config('cert_manager', config, default_versions) +config_cert_manager_dict, cert_manager_enabled = get_module_config( + 'cert_manager', + config, + default_versions, +) # Check if the cert_manager module is enabled & execute deployment logic if true if cert_manager_enabled: - # Import the CertManagerConfig data class and merge the user config with defaults from src.cert_manager.types import CertManagerConfig config_cert_manager = CertManagerConfig.merge(config_cert_manager_dict) - # Import the deployment function for the cert_manager module from src.cert_manager.deploy import deploy_cert_manager_module - # Deploy the cert_manager module cert_manager_version, cert_manager_release, cert_manager_selfsigned_cert = deploy_cert_manager_module( config_cert_manager=config_cert_manager, global_depends_on=global_depends_on, k8s_provider=k8s_provider, ) - # Record the deployed version of cert_manager and configuration + # Record the deployed version and configuration versions["cert_manager"] = cert_manager_version configurations["cert_manager"] = { "enabled": cert_manager_enabled, } - # Export the self-signed certificate data from cert_manager pulumi.export("cert_manager_selfsigned_cert", cert_manager_selfsigned_cert) else: cert_manager_selfsigned_cert = None - ########################################## -# Deploy Cert Manager Module +# Deploy KubeVirt Module -# KubeVirt Module Configuration and Deployment -config_kubevirt_dict, kubevirt_enabled = get_module_config('kubevirt', config, default_versions) +# Retrieve configuration and enable flag for the kubevirt module +config_kubevirt_dict, kubevirt_enabled = get_module_config( + 'kubevirt', + config, + default_versions, +) +# Check if the kubevirt module is enabled & execute deployment logic if true if kubevirt_enabled: - # Import the KubeVirtConfig data class and merge the user config with defaults from src.kubevirt.types import KubeVirtConfig config_kubevirt = KubeVirtConfig.merge(config_kubevirt_dict) - # Import the deployment function for the KubeVirt module from src.kubevirt.deploy import deploy_kubevirt_module - # Deploy the KubeVirt module + # Pass cert_manager_release as a dependency if cert_manager is enabled kubevirt_version, kubevirt_operator = deploy_kubevirt_module( config_kubevirt=config_kubevirt, global_depends_on=global_depends_on, k8s_provider=k8s_provider, + cert_manager_release=cert_manager_release if cert_manager_enabled else None ) - # Record the deployed version & configuration + # Record the deployed version and configuration versions["kubevirt"] = kubevirt_version configurations["kubevirt"] = { - "enabled": kubevirt_enabled + "enabled": kubevirt_enabled, } -else: - kubevirt_operator = None + pulumi.export("kubevirt_version", kubevirt_version) +else: + kubevirt_version = None ########################################## -# Export Component Versions and Configurations +# Export Component Metadata Outputs: +# - Versions +# - Configurations export_results(versions, configurations) + +############################################ +## Deploy Kubevirt Module +# +## KubeVirt Module Configuration and Deployment +#config_kubevirt_dict, kubevirt_enabled = get_module_config('kubevirt', config, default_versions) +# +#if kubevirt_enabled: +# # Import the KubeVirtConfig data class and merge the user config with defaults +# from src.kubevirt.types import KubeVirtConfig +# config_kubevirt = KubeVirtConfig.merge(config_kubevirt_dict) +# +# # Import the deployment function for the KubeVirt module +# from src.kubevirt.deploy import deploy_kubevirt_module +# +# # Deploy the KubeVirt module +# kubevirt_version, kubevirt_operator = deploy_kubevirt_module( +# config_kubevirt=config_kubevirt, +# global_depends_on=global_depends_on, +# k8s_provider=k8s_provider, +# ) +# +# # Apply Git and compliance metadata to KubeVirt +# apply_git_metadata(kubevirt_operator, git_info) +# apply_compliance_metadata(kubevirt_operator, compliance_config.__dict__) +# +# # Record the deployed version & configuration +# versions["kubevirt"] = kubevirt_version +# configurations["kubevirt"] = { +# "enabled": kubevirt_enabled +# } +#else: +# kubevirt_operator = None + + #from src.kubevirt.deploy import deploy_kubevirt #from src.containerized_data_importer.deploy import deploy_cdi #from src.cluster_network_addons.deploy import deploy_cnao diff --git a/pulumi/src/aws/__init__.py b/pulumi/src/aws/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pulumi/src/aws/deploy.py b/pulumi/src/aws/deploy.py new file mode 100644 index 0000000..03a987f --- /dev/null +++ b/pulumi/src/aws/deploy.py @@ -0,0 +1,131 @@ +# src/aws/deploy.py +# Description: generic boilerplate code not currently active in the project. +# This code is provided as a reference for future implementation. +# Key features include utilization of generate_compliance_tags, generate_compliance_labels, and generate_compliance_annotations functions from src/compliance/utils.py. + +import pulumi +from pulumi import ResourceOptions +import pulumi_aws as aws +import pulumi_eks as eks +import pulumi_kubernetes as k8s + +# Global Pulumi settings +stack_tags = { + "project": pulumi.get_project(), + "stack": pulumi.get_stack(), + "owner": "pulumi-user", +} + +stack_labels = { + "environment": "testing", +} + +pulumi.runtime.set_all_project_tags(stack_tags) +pulumi.runtime.set_all_project_labels(stack_labels) + +# AWS S3 Bucket with global tags +s3_bucket = aws.s3.Bucket("nginxStorageBucket", + tags={ + **stack_tags, + "Name": "nginxStorageBucket", + "Environment": "Dev", + } +) + +# AWS EKS Cluster with global tags +eks_cluster = eks.Cluster("exampleCluster", + tags={ + **stack_tags, + "Name": "exampleCluster", + "Environment": "Dev", + } +) + +# Kubernetes Persistent Volume +persistent_volume = k8s.core.v1.PersistentVolume("nginxPv", + metadata=k8s.meta.v1.ObjectMetaArgs( + name="nginx-pv", + labels={ + **stack_labels, + "type": "local", + } + ), + spec=k8s.core.v1.PersistentVolumeSpecArgs( + capacity={"storage": "1Gi"}, + access_modes=["ReadWriteOnce"], + aws_elastic_block_store=k8s.core.v1.AWSElasticBlockStoreVolumeSourceArgs( + volume_id=s3_bucket.id.apply(lambda id: f"aws://{aws.region}/{id}"), + fs_type="ext4", + ), + ), + opts=ResourceOptions(parent=eks_cluster) +) + +# Kubernetes Persistent Volume Claim +persistent_volume_claim = k8s.core.v1.PersistentVolumeClaim("nginxPvc", + metadata=k8s.meta.v1.ObjectMetaArgs( + name="nginx-pvc", + labels=stack_labels, + ), + spec=k8s.core.v1.PersistentVolumeClaimSpecArgs( + access_modes=["ReadWriteOnce"], + resources=k8s.core.v1.ResourceRequirementsArgs( + requests={"storage": "1Gi"}, + ), + ), + opts=ResourceOptions(parent=eks_cluster) +) + +# Kubernetes Nginx Deployment with Persistent Storage +nginx_deployment = k8s.apps.v1.Deployment("nginxDeployment", + metadata=k8s.meta.v1.ObjectMetaArgs( + name="nginx-deployment", + labels={ + **stack_labels, + "app": "nginx", + } + ), + spec=k8s.apps.v1.DeploymentSpecArgs( + replicas=1, + selector=k8s.meta.v1.LabelSelectorArgs( + match_labels={ + "app": "nginx", + } + ), + template=k8s.core.v1.PodTemplateSpecArgs( + metadata=k8s.meta.v1.ObjectMetaArgs( + labels={ + **stack_labels, + "app": "nginx", + } + ), + spec=k8s.core.v1.PodSpecArgs( + containers=[ + k8s.core.v1.ContainerArgs( + name="nginx", + image="nginx:1.14.2", + ports=[k8s.core.v1.ContainerPortArgs(container_port=80)], + volume_mounts=[ + k8s.core.v1.VolumeMountArgs( + name="nginx-storage", + mount_path="/usr/share/nginx/html", + ) + ], + ) + ], + volumes=[ + k8s.core.v1.VolumeArgs( + name="nginx-storage", + persistent_volume_claim=k8s.core.v1.PersistentVolumeClaimVolumeSourceArgs( + claim_name=persistent_volume_claim.metadata.name, + ) + ) + ] + ) + ), + ), + opts=ResourceOptions(parent=eks_cluster) +) + +pulumi.export("s3BucketName", s3_bucket.bucket) +pulumi.export("eksClusterName", eks_cluster.core.apply(lambda core: core.endpoint)) diff --git a/pulumi/src/azure/__init__.py b/pulumi/src/azure/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pulumi/src/cert_manager/deploy.py b/pulumi/src/cert_manager/deploy.py index e40324d..32eeb86 100644 --- a/pulumi/src/cert_manager/deploy.py +++ b/pulumi/src/cert_manager/deploy.py @@ -1,4 +1,5 @@ # src/cert_manager/deploy.py + import pulumi import pulumi_kubernetes as k8s from pulumi_kubernetes.apiextensions.CustomResource import CustomResource @@ -6,108 +7,100 @@ from src.lib.namespace import create_namespace from src.lib.helm_chart_versions import get_latest_helm_chart_version +from src.cert_manager.types import CertManagerConfig from src.lib.types import NamespaceConfig -from .types import CertManagerConfig +from src.lib.metadata import get_global_annotations, get_global_labels def deploy_cert_manager_module( config_cert_manager: CertManagerConfig, global_depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, - ) -> Tuple[Optional[str], Optional[pulumi.Resource], Optional[str]]: - """ - Deploys the Cert Manager module using Helm and manages certificate resources. - - This function handles the orchestration of deploying cert-manager into the Kubernetes - cluster. It ensures that the appropriate version is deployed using Helm and configures - necessary resources like the ClusterIssuer for certificate management. + ) -> Tuple[str, pulumi.Resource, str]: - Args: - config_cert_manager (CertManagerConfig): Configuration object for cert-manager deployment. - global_depends_on (List[pulumi.Resource]): A list of resources that the deployment depends on. - k8s_provider (k8s.Provider): The Kubernetes provider for this deployment. - - Returns: - Tuple[Optional[str], Optional[pulumi.Resource], Optional[str]]: - - The deployed cert-manager version. - - The Helm release resource for cert-manager. - - The base64-encoded CA certificate from the self-signed issuer. - """ # Deploy cert-manager and retrieve the version, Helm release, and CA certificate - cert_manager_version, release, ca_cert_b64, _ = deploy_cert_manager( + cert_manager_version, release, ca_cert_b64 = deploy_cert_manager( config_cert_manager=config_cert_manager, depends_on=global_depends_on, k8s_provider=k8s_provider, ) - if release: - # Append the release to global dependencies to maintain resource ordering - global_depends_on.append(release) + # Append the release to global dependencies to maintain resource ordering + global_depends_on.append(release) # Export the CA certificate for use by other modules or consumers pulumi.export("cert_manager_selfsigned_cert", ca_cert_b64) return cert_manager_version, release, ca_cert_b64 - def deploy_cert_manager( config_cert_manager: CertManagerConfig, depends_on: Optional[List[pulumi.Resource]], k8s_provider: k8s.Provider - ) -> Tuple[str, k8s.helm.v3.Release, str, k8s.core.v1.Secret]: - """ - Deploys cert-manager into the Kubernetes cluster using a Helm chart. - - This function installs cert-manager via Helm, creates necessary resources such as - namespaces and self-signed ClusterIssuers, and manages the associated certificate data. - - Args: - config_cert_manager (CertManagerConfig): Configuration settings for cert-manager. - depends_on (Optional[List[pulumi.Resource]]): Resources that the deployment should depend on. - k8s_provider (k8s.Provider): The Kubernetes provider instance. + ) -> Tuple[str, k8s.helm.v3.Release, str]: - Returns: - Tuple[str, k8s.helm.v3.Release, str, k8s.core.v1.Secret]: - - The version of cert-manager deployed. - - The Helm release resource for cert-manager. - - The base64-encoded CA certificate. - - The Kubernetes Secret resource containing the CA certificate. - """ - # Extract configuration details from the CertManagerConfig object + # Extract configuration details namespace = config_cert_manager.namespace version = config_cert_manager.version cluster_issuer_name = config_cert_manager.cluster_issuer install_crds = config_cert_manager.install_crds - # Validate that a namespace has been provided - if not namespace: - raise ValueError("Namespace must be specified in the cert_manager configuration.") - - # Create the namespace for cert-manager based on the provided configuration - namespace_config = NamespaceConfig(name=namespace) + # Create the namespace for cert-manager + namespace_config = NamespaceConfig( + name=namespace, + ) namespace_resource = create_namespace( config=namespace_config, k8s_provider=k8s_provider, depends_on=depends_on, ) - # Define Helm chart details for cert-manager + # Define Helm chart details chart_name = "cert-manager" chart_url = "https://charts.jetstack.io" - # Determine which version of cert-manager to deploy + # Determine version if version == 'latest' or version is None: - # Fetch the latest cert-manager version from the Helm chart repository version = get_latest_helm_chart_version(f"{chart_url}/index.yaml", chart_name) - version = version.lstrip("v") # Remove 'v' prefix if present + version = version.lstrip("v") pulumi.log.info(f"Setting Helm release version to latest: {chart_name}/{version}") else: - # Use the provided version pulumi.log.info(f"Using Helm release version: {chart_name}/{version}") - # Generate the values to pass into the Helm chart deployment + # Generate Helm values helm_values = gen_helm_values(config_cert_manager) - # Deploy cert-manager using Helm and return the release resource + # Access global labels and annotations + global_labels = get_global_labels() + global_annotations = get_global_annotations() + + # Define transformation for Helm resources + def helm_transform(args: pulumi.ResourceTransformationArgs): + if args.resource and args.resource.__class__.__name__.startswith("k8s"): + props = args.props + if 'metadata' in props: + metadata = props['metadata'] + if isinstance(metadata, dict): + metadata.setdefault('labels', {}) + metadata['labels'].update(global_labels) + metadata.setdefault('annotations', {}) + metadata['annotations'].update(global_annotations) + elif isinstance(metadata, k8s.meta.v1.ObjectMetaArgs): + if metadata.labels is None: + metadata.labels = {} + metadata.labels = {**metadata.labels, **global_labels} + if metadata.annotations is None: + metadata.annotations = {} + metadata.annotations = {**metadata.annotations, **global_annotations} + props['metadata'] = metadata + else: + props['metadata'] = { + 'labels': global_labels, + 'annotations': global_annotations + } + return pulumi.ResourceTransformationResult(props, args.opts) + return None + + # Deploy cert-manager using Helm release = k8s.helm.v3.Release( chart_name, k8s.helm.v3.ReleaseArgs( @@ -123,7 +116,8 @@ def deploy_cert_manager( opts=pulumi.ResourceOptions( provider=k8s_provider, parent=namespace_resource, - depends_on=depends_on, + depends_on=[namespace_resource] + (depends_on or []), + transformations=[helm_transform], custom_timeouts=pulumi.CustomTimeouts( create="8m", update="4m", @@ -132,14 +126,13 @@ def deploy_cert_manager( ) ) - # Create a self-signed ClusterIssuer in the cluster + # Create ClusterIssuer resources cluster_issuer_root = CustomResource( "cluster-selfsigned-issuer-root", api_version="cert-manager.io/v1", kind="ClusterIssuer", metadata={ "name": "cluster-selfsigned-issuer-root", - "namespace": namespace }, spec={ "selfSigned": {} @@ -147,7 +140,7 @@ def deploy_cert_manager( opts=pulumi.ResourceOptions( provider=k8s_provider, parent=release, - depends_on=[namespace_resource], + depends_on=[release], custom_timeouts=pulumi.CustomTimeouts( create="5m", update="10m", @@ -156,35 +149,34 @@ def deploy_cert_manager( ) ) - # Create a certificate signed by the self-signed ClusterIssuer cluster_issuer_ca_certificate = CustomResource( "cluster-selfsigned-issuer-ca", api_version="cert-manager.io/v1", kind="Certificate", metadata={ "name": "cluster-selfsigned-issuer-ca", - "namespace": namespace + "namespace": namespace, }, spec={ "commonName": "cluster-selfsigned-issuer-ca", - "duration": "2160h0m0s", # 90 days + "duration": "2160h0m0s", "isCA": True, "issuerRef": { "group": "cert-manager.io", "kind": "ClusterIssuer", - "name": cluster_issuer_root.metadata["name"], + "name": "cluster-selfsigned-issuer-root", }, "privateKey": { "algorithm": "ECDSA", "size": 256 }, - "renewBefore": "360h0m0s", # Renew 15 days before expiration + "renewBefore": "360h0m0s", "secretName": "cluster-selfsigned-issuer-ca" }, opts=pulumi.ResourceOptions( provider=k8s_provider, parent=cluster_issuer_root, - depends_on=[namespace_resource], + depends_on=[cluster_issuer_root], custom_timeouts=pulumi.CustomTimeouts( create="5m", update="10m", @@ -193,24 +185,22 @@ def deploy_cert_manager( ) ) - # Create the primary ClusterIssuer resource using the CA certificate cluster_issuer = CustomResource( cluster_issuer_name, api_version="cert-manager.io/v1", kind="ClusterIssuer", metadata={ "name": cluster_issuer_name, - "namespace": namespace }, spec={ "ca": { - "secretName": cluster_issuer_ca_certificate.spec["secretName"], + "secretName": "cluster-selfsigned-issuer-ca", } }, opts=pulumi.ResourceOptions( provider=k8s_provider, parent=cluster_issuer_ca_certificate, - depends_on=[namespace_resource], + depends_on=[cluster_issuer_ca_certificate], custom_timeouts=pulumi.CustomTimeouts( create="4m", update="4m", @@ -219,12 +209,12 @@ def deploy_cert_manager( ) ) - # Retrieve the CA certificate secret generated by cert-manager + # Retrieve the CA certificate secret ca_secret = k8s.core.v1.Secret( "cluster-selfsigned-issuer-ca-secret", metadata={ + "name": "cluster-selfsigned-issuer-ca", "namespace": namespace, - "name": cluster_issuer_ca_certificate.spec["secretName"] }, opts=pulumi.ResourceOptions( provider=k8s_provider, @@ -233,11 +223,10 @@ def deploy_cert_manager( ) ) - # Extract the base64-encoded CA certificate from the secret + # Extract the base64-encoded CA certificate ca_data_tls_crt_b64 = ca_secret.data.apply(lambda data: data["tls.crt"]) - return version, release, ca_data_tls_crt_b64, ca_secret - + return version, release, ca_data_tls_crt_b64 def gen_helm_values( config_cert_manager: CertManagerConfig @@ -245,10 +234,6 @@ def gen_helm_values( """ Generates custom Helm values for the cert-manager Helm chart. - This function generates specific Helm chart values based on the provided - cert-manager configuration. These values are used during Helm deployment - to customize the resources, including replica counts and resource requests/limits. - Args: config_cert_manager (CertManagerConfig): The configuration for cert-manager. @@ -257,16 +242,16 @@ def gen_helm_values( """ # Define custom values for the cert-manager Helm chart helm_values = { - 'replicaCount': 1, # Set the number of cert-manager replicas - 'installCRDs': config_cert_manager.install_crds, # Whether to install CRDs + 'replicaCount': 1, + 'installCRDs': config_cert_manager.install_crds, 'resources': { 'limits': { - 'cpu': '500m', # CPU resource limit - 'memory': '1024Mi' # Memory resource limit + 'cpu': '500m', + 'memory': '1024Mi' }, 'requests': { - 'cpu': '250m', # CPU resource request - 'memory': '512Mi' # Memory resource request + 'cpu': '250m', + 'memory': '512Mi' } } } diff --git a/pulumi/src/cert_manager/types.py b/pulumi/src/cert_manager/types.py index d389806..b0c0bcb 100644 --- a/pulumi/src/cert_manager/types.py +++ b/pulumi/src/cert_manager/types.py @@ -1,76 +1,22 @@ # src/cert_manager/types.py -""" -Types and data structures specific to the Cert Manager module. - -This module defines the configuration data class for the Cert Manager module, -which is used to manage SSL/TLS certificates within the Kubernetes cluster. -It standardizes the configuration options and provides methods to merge user -configurations with default settings. -""" - -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Optional, Dict, Any import pulumi @dataclass class CertManagerConfig: - """ - Configuration for deploying the Cert Manager module. - - Attributes: - namespace (str): The Kubernetes namespace where cert-manager will be deployed. - Defaults to "cert-manager". - version (Optional[str]): The version of the cert-manager Helm chart to deploy. - If None, the default version from the versions management system will be used. - cluster_issuer (str): The name of the ClusterIssuer to create for certificate issuance. - Defaults to "cluster-selfsigned-issuer". - install_crds (bool): Whether to install Custom Resource Definitions (CRDs) required by cert-manager. - Defaults to True. - """ - namespace: str = "cert-manager" version: Optional[str] = None + namespace: str = "cert-manager" cluster_issuer: str = "cluster-selfsigned-issuer" install_crds: bool = True @staticmethod def merge(user_config: Dict[str, Any]) -> 'CertManagerConfig': - """ - Merges user-provided configuration with default values. - - This method creates a new CertManagerConfig instance by combining the - default configuration with any user-specified settings. It ensures that - all required fields are set and warns the user about any unknown configuration keys. - - Args: - user_config (Dict[str, Any]): A dictionary containing user-provided configuration options. - - Returns: - CertManagerConfig: An instance of CertManagerConfig with merged settings. - - Example: - ```python - # User-provided configuration - user_config = { - "namespace": "my-cert-manager", - "install_crds": False - } - - # Merge with defaults - config = CertManagerConfig.merge(user_config) - ``` - """ - # Create a default configuration instance default_config = CertManagerConfig() - # Copy default configuration into a dictionary - merged_config = default_config.__dict__.copy() - # Iterate over user-provided configuration and update the merged_config for key, value in user_config.items(): if hasattr(default_config, key): - # If the key exists in the default configuration, update the value - merged_config[key] = value + setattr(default_config, key, value) else: - # Warn the user about any unknown configuration keys pulumi.log.warn(f"Unknown configuration key '{key}' in cert_manager config.") - # Return a new CertManagerConfig instance with merged settings - return CertManagerConfig(**merged_config) + return default_config diff --git a/pulumi/src/kubernetes/__init__.py b/pulumi/src/kubernetes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pulumi/src/kubevirt/deploy.py b/pulumi/src/kubevirt/deploy.py index ab4020e..b1f0cc3 100644 --- a/pulumi/src/kubevirt/deploy.py +++ b/pulumi/src/kubevirt/deploy.py @@ -14,6 +14,7 @@ from src.lib.namespace import create_namespace from src.lib.types import NamespaceConfig from .types import KubeVirtConfig +from src.lib.metadata import get_global_labels, get_global_annotations def deploy_kubevirt_module( config_kubevirt: KubeVirtConfig, @@ -22,7 +23,7 @@ def deploy_kubevirt_module( cert_manager_release: Optional[pulumi.Resource] = None ) -> Tuple[Optional[str], Optional[pulumi.Resource]]: """ - Deploys the KubeVirt module using YAML and custom resources. + Deploys the KubeVirt module with labels and annotations. Args: config_kubevirt (KubeVirtConfig): Configuration object for KubeVirt deployment. @@ -40,19 +41,19 @@ def deploy_kubevirt_module( namespace_resource = create_namespace( config=namespace_config, k8s_provider=k8s_provider, - depends_on=global_depends_on # Ensure it waits for any global dependencies like cert-manager + depends_on=global_depends_on ) # Now deploy KubeVirt, ensuring it depends on the namespace creation kubevirt_version, kubevirt_operator = deploy_kubevirt( config_kubevirt=config_kubevirt, - depends_on=[namespace_resource] + global_depends_on, - k8s_provider=k8s_provider + depends_on=namespace_resource, + k8s_provider=k8s_provider, + namespace_resource=namespace_resource ) - if kubevirt_operator: - # Optionally add KubeVirt to global dependencies - global_depends_on.append(kubevirt_operator) + # Optionally add KubeVirt to global dependencies + global_depends_on.append(kubevirt_operator) return kubevirt_version, kubevirt_operator @@ -60,30 +61,26 @@ def deploy_kubevirt_module( def deploy_kubevirt( config_kubevirt: KubeVirtConfig, depends_on: List[pulumi.Resource], - k8s_provider: k8s.Provider + k8s_provider: k8s.Provider, + namespace_resource: pulumi.Resource ) -> Tuple[str, k8s.yaml.ConfigFile]: """ - Deploys KubeVirt into the Kubernetes cluster using its YAML manifests. - - Args: - config_kubevirt (KubeVirtConfig): Configuration settings for KubeVirt. - depends_on (List[pulumi.Resource]): Resources that the deployment should depend on. - k8s_provider (k8s.Provider): The Kubernetes provider instance. - - Returns: - Tuple[str, k8s.yaml.ConfigFile]: - - The version of KubeVirt deployed. - - The KubeVirt operator resource. + Deploys KubeVirt into the Kubernetes cluster using its YAML manifests and applies labels and annotations. """ # Extract configuration details from the KubeVirtConfig object namespace = config_kubevirt.namespace version = config_kubevirt.version use_emulation = config_kubevirt.use_emulation + labels = config_kubevirt.labels + annotations = config_kubevirt.annotations # Fetch or use the specified KubeVirt version if version == 'latest' or version is None: kubevirt_stable_version_url = 'https://storage.googleapis.com/kubevirt-prow/release/kubevirt/kubevirt/stable.txt' - version = requests.get(kubevirt_stable_version_url).text.strip().lstrip("v") + response = requests.get(kubevirt_stable_version_url) + if response.status_code != 200: + raise Exception(f"Failed to fetch latest KubeVirt version from {kubevirt_stable_version_url}") + version = response.text.strip().lstrip("v") pulumi.log.info(f"Setting KubeVirt release version to latest: {version}") else: pulumi.log.info(f"Using KubeVirt version: {version}") @@ -91,34 +88,65 @@ def deploy_kubevirt( # Download the KubeVirt operator YAML kubevirt_operator_url = f'https://github.com/kubevirt/kubevirt/releases/download/v{version}/kubevirt-operator.yaml' response = requests.get(kubevirt_operator_url) + if response.status_code != 200: + raise Exception(f"Failed to download KubeVirt operator YAML from {kubevirt_operator_url}") kubevirt_yaml = yaml.safe_load_all(response.text) # Transform the YAML and set the correct namespace transformed_yaml = _transform_yaml(kubevirt_yaml, namespace) + def kubevirt_transform(obj: dict, opts: pulumi.ResourceOptions): + """ + Transformation function to add labels and annotations to Kubernetes objects + in the KubeVirt operator YAML manifests. + """ + # Ensure the 'metadata' field exists in the object + obj.setdefault("metadata", {}) + + # Apply labels + obj["metadata"].setdefault("labels", {}) + obj["metadata"]["labels"].update(labels) + + # Apply annotations + obj["metadata"].setdefault("annotations", {}) + obj["metadata"]["annotations"].update(annotations) + + # If this is a Pod or a Deployment, ensure the template also has metadata + if "spec" in obj and "template" in obj["spec"]: + obj["spec"]["template"].setdefault("metadata", {}) + obj["spec"]["template"]["metadata"].setdefault("labels", {}) + obj["spec"]["template"]["metadata"]["labels"].update(labels) + obj["spec"]["template"]["metadata"].setdefault("annotations", {}) + obj["spec"]["template"]["metadata"]["annotations"].update(annotations) + + # Debugging: print the transformed object + #print(f"Transformed object: {obj['metadata']}") + # Write the transformed YAML to a temporary file with tempfile.NamedTemporaryFile(delete=False, mode='w') as temp_file: yaml.dump_all(transformed_yaml, temp_file) temp_file_path = temp_file.name - # Deploy the KubeVirt operator - operator = k8s.yaml.ConfigFile( - 'kubevirt-operator', - file=temp_file_path, - opts=pulumi.ResourceOptions( - provider=k8s_provider, - parent=None, - depends_on=depends_on, - custom_timeouts=pulumi.CustomTimeouts( - create="10m", - update="5m", - delete="5m" + try: + # Deploy the KubeVirt operator with transformation + operator = k8s.yaml.ConfigFile( + 'kubevirt-operator', + file=temp_file_path, + transformations=[kubevirt_transform], + opts=pulumi.ResourceOptions( + provider=k8s_provider, + parent=None, + depends_on=depends_on, + custom_timeouts=pulumi.CustomTimeouts( + create="10m", + update="5m", + delete="5m" + ) ) ) - ) - - # Clean up the temporary file after Pulumi has used it - pulumi.Output.all().apply(lambda _: os.unlink(temp_file_path)) + finally: + # Clean up the temporary file after Pulumi has used it + pulumi.Output.all().apply(lambda _: os.unlink(temp_file_path)) # Deploy the KubeVirt custom resource if use_emulation: @@ -145,19 +173,20 @@ def deploy_kubevirt( } } - # Create the KubeVirt custom resource kubevirt = CustomResource( "kubevirt", api_version="kubevirt.io/v1", kind="KubeVirt", metadata=ObjectMetaArgs( name="kubevirt", + labels=labels, namespace=namespace, + annotations=annotations ), spec=kubevirt_custom_resource_spec, opts=pulumi.ResourceOptions( provider=k8s_provider, - parent=None, + parent=namespace_resource, depends_on=[operator], ) ) @@ -179,7 +208,7 @@ def _transform_yaml(yaml_data, namespace: str) -> List[Dict]: transformed = [] for resource in yaml_data: if resource.get('kind') == 'Namespace': - continue # Skip the Namespace resource + continue if 'metadata' in resource: resource['metadata']['namespace'] = namespace transformed.append(resource) diff --git a/pulumi/src/kubevirt/types.py b/pulumi/src/kubevirt/types.py index 779ea9d..90cc7d0 100644 --- a/pulumi/src/kubevirt/types.py +++ b/pulumi/src/kubevirt/types.py @@ -1,8 +1,9 @@ # src/kubevirt/types.py -import pulumi -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Optional, Dict, Any +import pulumi +from src.lib.metadata import get_global_labels, get_global_annotations @dataclass class KubeVirtConfig: @@ -16,15 +17,21 @@ class KubeVirtConfig: Defaults to None, which fetches the latest version. use_emulation (bool): Whether to use emulation for KVM. Defaults to False. + labels (Dict[str, str]): Labels to apply to KubeVirt resources. + Defaults to an empty dict. + annotations (Dict[str, Any]): Annotations to apply to KubeVirt resources. + Defaults to an empty dict. """ namespace: str = "kubevirt" version: Optional[str] = None use_emulation: bool = False + labels: Dict[str, str] = field(default_factory=dict) + annotations: Dict[str, Any] = field(default_factory=dict) @staticmethod def merge(user_config: Dict[str, Any]) -> 'KubeVirtConfig': """ - Merges user-provided configuration with default values. + Merges user-provided configuration with default values and global labels and annotations. Args: user_config (Dict[str, Any]): A dictionary containing user-provided configuration options. @@ -34,9 +41,20 @@ def merge(user_config: Dict[str, Any]) -> 'KubeVirtConfig': """ default_config = KubeVirtConfig() merged_config = default_config.__dict__.copy() + + # Merge user-provided config for key, value in user_config.items(): if hasattr(default_config, key): merged_config[key] = value else: pulumi.log.warn(f"Unknown configuration key '{key}' in kubevirt config.") + + # Retrieve global labels and annotations + global_labels = get_global_labels() + global_annotations = get_global_annotations() + + # Merge global labels and annotations into the config + merged_config['labels'].update(global_labels) + merged_config['annotations'].update(global_annotations) + return KubeVirtConfig(**merged_config) diff --git a/pulumi/src/lib/compliance.py b/pulumi/src/lib/compliance.py new file mode 100644 index 0000000..f0e00c5 --- /dev/null +++ b/pulumi/src/lib/compliance.py @@ -0,0 +1,41 @@ +# src/lib/compliance.py + +import json +from typing import Dict +from .types import ComplianceConfig +from .utils import sanitize_label_value + +# src/lib/compliance.py + +import json +from typing import Dict +from .types import ComplianceConfig +from .utils import sanitize_label_value + +def generate_compliance_labels(compliance_config: ComplianceConfig) -> Dict[str, str]: + labels = {} + # Only essential selection flags should be kept in labels + if compliance_config.fisma.enabled: + labels['compliance.fisma.enabled'] = 'true' + if compliance_config.nist.enabled: + labels['compliance.nist.enabled'] = 'true' + if compliance_config.scip.environment: + labels['compliance.scip.environment'] = sanitize_label_value(compliance_config.scip.environment) + return labels + +def generate_compliance_annotations(compliance_config: ComplianceConfig) -> Dict[str, str]: + annotations = {} + # Serialize the more complex metadata into JSON objects or strings + if compliance_config.fisma.level: + annotations['compliance.fisma.level'] = compliance_config.fisma.level + if compliance_config.fisma.ato: + annotations['compliance.fisma.ato'] = json.dumps(compliance_config.fisma.ato) # Store as JSON + + if compliance_config.nist.controls: + annotations['compliance.nist.controls'] = json.dumps(compliance_config.nist.controls) # Store as JSON array + if compliance_config.nist.auxiliary: + annotations['compliance.nist.auxiliary'] = json.dumps(compliance_config.nist.auxiliary) + if compliance_config.nist.exceptions: + annotations['compliance.nist.exceptions'] = json.dumps(compliance_config.nist.exceptions) + + return annotations diff --git a/pulumi/src/lib/config.py b/pulumi/src/lib/config.py index c3c9cb1..07938bf 100644 --- a/pulumi/src/lib/config.py +++ b/pulumi/src/lib/config.py @@ -1,4 +1,5 @@ -# pulumi/src/lib/config.py +# src/lib/config.py +# Description: Module Configuration Parsing & Loading """ Configuration Management Module @@ -14,7 +15,7 @@ def get_module_config( module_name: str, config: pulumi.Config, - default_versions: Dict[str, Any] + default_versions: Dict[str, Any], ) -> Tuple[Dict[str, Any], bool]: """ Retrieves and prepares the configuration for a module. @@ -26,15 +27,14 @@ def get_module_config( Returns: Tuple[Dict[str, Any], bool]: - - A tuple containing the module's configuration dictionary - and a boolean indicating if the module is enabled. + A tuple containing the module's configuration dictionary + and a boolean indicating if the module is enabled. """ - # Get the module's configuration from Pulumi config, default to {"enabled": "false"} if not set - module_config = config.get_object(module_name) or {"enabled": "false"} + # Get the module's configuration from Pulumi config, default to an empty dict + module_config = config.get_object(module_name) or {} module_enabled = str(module_config.get('enabled', 'false')).lower() == "true" - # Remove 'enabled' key from the module configuration as modules do not need this key beyond this point + # Remove 'enabled' key from the module configuration module_config.pop('enabled', None) # Handle version injection into the module configuration @@ -42,14 +42,7 @@ def get_module_config( if not module_version: # No version specified in module config; use default version module_version = default_versions.get(module_name) - if module_version: - module_config['version'] = module_version - else: - # No default version available; set to None (module will handle this case) - module_config['version'] = None - else: - # Version is specified in module config; keep as is (could be 'latest' or a specific version) - pass + module_config['version'] = module_version return module_config, module_enabled diff --git a/pulumi/src/lib/init.py b/pulumi/src/lib/init.py index 9587905..42eba8e 100644 --- a/pulumi/src/lib/init.py +++ b/pulumi/src/lib/init.py @@ -1,4 +1,5 @@ # src/lib/init.py +# Description: Initializes Pulumi configuration, Kubernetes provider, and global resources. import os import pulumi @@ -7,21 +8,20 @@ from typing import Dict, List, Any from src.lib.versions import load_default_versions -from src.lib.metadata import collect_git_info +from src.lib.metadata import ( + collect_git_info, + generate_git_labels, + generate_git_annotations, + set_global_labels, + set_global_annotations +) +from src.lib.compliance import generate_compliance_labels, generate_compliance_annotations +from src.lib.types import ComplianceConfig +from src.lib.utils import generate_global_transformations def initialize_pulumi() -> Dict[str, Any]: """ Initializes the Pulumi configuration, Kubernetes provider, and global resources. - - Returns: - A dictionary containing initialized resources like: - - config: Pulumi Config object - - stack_name: The Pulumi stack name - - project_name: The Pulumi project name - - versions: Dictionary to track deployed component versions - - configurations: Dictionary to track module configurations - - global_depends_on: List to manage dependencies globally - - k8s_provider: Kubernetes provider instance """ # Load Pulumi config config = pulumi.Config() @@ -58,13 +58,35 @@ def initialize_pulumi() -> Dict[str, Any]: git_info = collect_git_info() # Append Git metadata to the configurations dictionary under 'source_repository' - # TODO: populate global resource tags / labels / annotations with Git metadata configurations["source_repository"] = { "remote": git_info["remote"], "branch": git_info["branch"], "commit": git_info["commit"] } + # Collect compliance configuration + compliance_config_dict = config.get_object('compliance') or {} + compliance_config = ComplianceConfig.merge(compliance_config_dict) + + # Generate compliance labels and annotations + compliance_labels = generate_compliance_labels(compliance_config) + compliance_annotations = generate_compliance_annotations(compliance_config) + + # Generate Git labels and annotations + git_labels = generate_git_labels(git_info) + git_annotations = generate_git_annotations(git_info) + + # Combine labels and annotations + global_labels = {**compliance_labels, **git_labels} + global_annotations = {**compliance_annotations, **git_annotations} + + # Store global labels and annotations for access in other modules + set_global_labels(global_labels) + set_global_annotations(global_annotations) + + # Apply global transformations to all resources + generate_global_transformations(global_labels, global_annotations) + # Return all initialized resources return { "config": config, @@ -75,5 +97,8 @@ def initialize_pulumi() -> Dict[str, Any]: "configurations": configurations, "global_depends_on": global_depends_on, "k8s_provider": k8s_provider, - "git_info": git_info + "git_info": git_info, + "compliance_config": compliance_config, + "global_labels": global_labels, + "global_annotations": global_annotations, } diff --git a/pulumi/src/lib/metadata.py b/pulumi/src/lib/metadata.py index e0371df..83e8f02 100644 --- a/pulumi/src/lib/metadata.py +++ b/pulumi/src/lib/metadata.py @@ -1,30 +1,24 @@ # src/lib/metadata.py -# Description: This module contains utility functions for retrieving metadata about the current Pulumi project source code repository. import subprocess from typing import Dict +from .utils import sanitize_label_value, extract_repo_name def collect_git_info() -> Dict[str, str]: """ Retrieves the current Git repository's remote URL, branch, and commit hash. - - Returns: - Dict[str, str]: A dictionary containing the 'remote', 'branch', and 'commit' information. """ try: - # Get the remote URL remote = subprocess.check_output( ['git', 'config', '--get', 'remote.origin.url'], stderr=subprocess.STDOUT ).strip().decode('utf-8') - # Get the current branch branch = subprocess.check_output( ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], stderr=subprocess.STDOUT ).strip().decode('utf-8') - # Get the latest commit hash commit = subprocess.check_output( ['git', 'rev-parse', 'HEAD'], stderr=subprocess.STDOUT @@ -43,3 +37,40 @@ def collect_git_info() -> Dict[str, str]: 'branch': 'N/A', 'commit': 'N/A' } + +def generate_git_labels(git_info: Dict[str, str]) -> Dict[str, str]: + # Only essential metadata should be in labels (e.g., branch) + labels = { + "git.branch": sanitize_label_value(git_info.get("branch", "")), + "git.commit": git_info.get("commit", "")[:7], # Shorten commit hash + } + return {k: v for k, v in labels.items() if v} + +def generate_git_annotations(git_info: Dict[str, str]) -> Dict[str, str]: + # Store more detailed information in annotations + annotations = { + "git.remote": git_info.get("remote", ""), + "git.commit.full": git_info.get("commit", ""), + "git.branch": git_info.get("branch", "") + } + return {k: v for k, v in annotations.items() if v} + +########################################## +# Global labels and annotations +# Singleton to store global labels and annotations +_global_labels = {} +_global_annotations = {} + +def set_global_labels(labels): + global _global_labels + _global_labels = labels + +def set_global_annotations(annotations): + global _global_annotations + _global_annotations = annotations + +def get_global_labels(): + return _global_labels + +def get_global_annotations(): + return _global_annotations diff --git a/pulumi/src/lib/namespace.py b/pulumi/src/lib/namespace.py index 5bb77fe..6ba3fcc 100644 --- a/pulumi/src/lib/namespace.py +++ b/pulumi/src/lib/namespace.py @@ -18,54 +18,19 @@ def create_namespace( k8s_provider: k8s.Provider, depends_on: Optional[List[pulumi.Resource]] = None, ) -> k8s.core.v1.Namespace: - """ - Creates a Kubernetes Namespace based on the provided configuration. - - This function simplifies the creation of a Kubernetes namespace by applying - settings specified in a NamespaceConfig object. It ensures namespaces are - created consistently and according to best practices within the Kargo PaaS platform. - - Args: - config (NamespaceConfig): An object containing namespace configuration settings. - k8s_provider (k8s.Provider): The Kubernetes provider instance to interact with the cluster. - depends_on (Optional[List[pulumi.Resource]]): An optional list of resources that - this namespace depends on, ensuring proper resource creation order. - - Returns: - k8s.core.v1.Namespace: The created Namespace resource. - - Example: - ```python - # Define namespace configuration - namespace_config = NamespaceConfig( - name="my-namespace", - labels={"app": "my-app"}, - protect=True - ) - - # Create the namespace - namespace_resource = create_namespace( - config=namespace_config, - k8s_provider=k8s_provider, - depends_on=[other_resource], - ) - ``` - """ - # Ensure depends_on is initialized as a list if not provided if depends_on is None: depends_on = [] - # Create the Kubernetes Namespace resource with the specified configuration namespace_resource = k8s.core.v1.Namespace( config.name, - metadata=k8s.meta.v1.ObjectMetaArgs( - name=config.name, - labels=config.labels, - annotations=config.annotations, - ), - spec=k8s.core.v1.NamespaceSpecArgs( - finalizers=config.finalizers, - ), + metadata={ + "name": config.name, + "labels": config.labels, + "annotations": config.annotations, + }, + spec={ + "finalizers": config.finalizers, + }, opts=pulumi.ResourceOptions( protect=config.protect, retain_on_delete=config.retain_on_delete, diff --git a/pulumi/src/lib/types.py b/pulumi/src/lib/types.py index faa3a3e..3114f73 100644 --- a/pulumi/src/lib/types.py +++ b/pulumi/src/lib/types.py @@ -4,35 +4,17 @@ Types and data structures used across Kargo modules. This module defines shared configuration types that are utilized by various modules -within the Kargo PaaS platform. These data classes help standardize configurations -and simplify the management of Kubernetes resources. +within the Kargo PaaS platform. """ from dataclasses import dataclass, field -from typing import Optional, Dict, List +from typing import Optional, List, Dict, Any +import pulumi @dataclass class NamespaceConfig: """ Configuration for creating or managing a Kubernetes namespace. - - Attributes: - name (str): The name of the Kubernetes namespace. - labels (Dict[str, str]): Labels to apply to the namespace for identification and grouping. - Defaults to {"ccio.v1/app": "kargo"}. - annotations (Dict[str, str]): Annotations to add metadata to the namespace. - Defaults to an empty dictionary. - finalizers (List[str]): List of finalizers to prevent accidental deletion of the namespace. - Defaults to ["kubernetes"]. - protect (bool): If True, protects the namespace from deletion. - Defaults to False. - retain_on_delete (bool): If True, retains the namespace when the Pulumi stack is destroyed. - Defaults to False. - ignore_changes (List[str]): Fields to ignore during updates, useful for fields managed externally. - Defaults to ["metadata", "spec"]. - custom_timeouts (Dict[str, str]): Custom timeouts for create, update, and delete operations. - Helps manage operations that may take longer than default timeouts. - Defaults to {"create": "5m", "update": "10m", "delete": "10m"}. """ name: str labels: Dict[str, str] = field(default_factory=lambda: {"ccio.v1/app": "kargo"}) @@ -46,3 +28,44 @@ class NamespaceConfig: "update": "10m", "delete": "10m" }) + +@dataclass +class FismaConfig: + enabled: bool = False + level: Optional[str] = None + ato: Dict[str, str] = field(default_factory=dict) + +@dataclass +class NistConfig: + enabled: bool = False + controls: List[str] = field(default_factory=list) + auxiliary: List[str] = field(default_factory=list) + exceptions: List[str] = field(default_factory=list) + +@dataclass +class ScipConfig: + environment: Optional[str] = None + ownership: Dict[str, Any] = field(default_factory=dict) + provider: Dict[str, Any] = field(default_factory=dict) + +@dataclass +class ComplianceConfig: + fisma: FismaConfig = field(default_factory=FismaConfig) + nist: NistConfig = field(default_factory=NistConfig) + scip: ScipConfig = field(default_factory=ScipConfig) + + @staticmethod + def merge(user_config: Dict[str, Any]) -> 'ComplianceConfig': + default_config = ComplianceConfig() + for key, value in user_config.items(): + if hasattr(default_config, key): + # Recursively merge nested configurations + nested_config = getattr(default_config, key) + for nested_key, nested_value in value.items(): + if hasattr(nested_config, nested_key): + setattr(nested_config, nested_key, nested_value) + else: + pulumi.log.warn(f"Unknown key '{nested_key}' in compliance.{key}") + else: + pulumi.log.warn(f"Unknown compliance configuration key: {key}") + return default_config diff --git a/pulumi/src/lib/utils.py b/pulumi/src/lib/utils.py new file mode 100644 index 0000000..641b627 --- /dev/null +++ b/pulumi/src/lib/utils.py @@ -0,0 +1,55 @@ +# src/lib/utils.py +# Description: Utility functions for sanitizing compliance metadata labels. + +import re +import pulumi +import pulumi_kubernetes as k8s +from typing import Optional + +def generate_global_transformations(global_labels: dict, global_annotations: dict): + """ + Registers a global transformation function that applies the provided labels and annotations to all resources. + """ + def global_transform(args: pulumi.ResourceTransformationArgs) -> Optional[pulumi.ResourceTransformationResult]: + props = args.props + if 'metadata' in props: + metadata = props['metadata'] + if isinstance(metadata, dict): + metadata.setdefault('labels', {}) + metadata['labels'].update(global_labels) + metadata.setdefault('annotations', {}) + metadata['annotations'].update(global_annotations) + elif isinstance(metadata, k8s.meta.v1.ObjectMetaArgs): + if metadata.labels is None: + metadata.labels = {} + metadata.labels = {**metadata.labels, **global_labels} + if metadata.annotations is None: + metadata.annotations = {} + metadata.annotations = {**metadata.annotations, **global_annotations} + props['metadata'] = metadata + else: + props['metadata'] = { + 'labels': global_labels, + 'annotations': global_annotations + } + return pulumi.ResourceTransformationResult(props, args.opts) + + pulumi.runtime.register_stack_transformation(global_transform) + +def sanitize_label_value(value: str) -> str: + value = value.lower() + # Replace invalid characters with '-' + sanitized = re.sub(r'[^a-z0-9_.-]', '-', value) + # Remove leading and trailing non-alphanumeric characters + sanitized = re.sub(r'^[^a-z0-9]+', '', sanitized) + sanitized = re.sub(r'[^a-z0-9]+$', '', sanitized) + # Truncate to 63 characters + return sanitized[:63] + +def extract_repo_name(remote_url: str) -> str: + # Extract the repository name from the remote URL + # Example: 'https://github.com/ContainerCraft/Kargo.git' -> 'ContainerCraft/Kargo' + match = re.search(r'[:/]([^/:]+/[^/\.]+)(\.git)?$', remote_url) + if match: + return match.group(1) + return remote_url From c5107df0d887d02fe0b8481e3f0f7066270be7fe Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Mon, 16 Sep 2024 05:57:36 +0000 Subject: [PATCH 15/43] return compliance configuration --- pulumi/__main__.py | 3 ++- pulumi/src/lib/config.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pulumi/__main__.py b/pulumi/__main__.py index 2703743..17f039e 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -23,6 +23,7 @@ configurations = init["configurations"] default_versions = init["default_versions"] global_depends_on = init["global_depends_on"] +compliance_config = init["compliance_config"] ########################################## # Deploy Cert Manager Module @@ -96,7 +97,7 @@ # Export Component Metadata Outputs: # - Versions # - Configurations -export_results(versions, configurations) +export_results(versions, configurations, compliance_config) diff --git a/pulumi/src/lib/config.py b/pulumi/src/lib/config.py index 07938bf..23d4e7e 100644 --- a/pulumi/src/lib/config.py +++ b/pulumi/src/lib/config.py @@ -46,6 +46,7 @@ def get_module_config( return module_config, module_enabled -def export_results(versions: Dict[str, str], configurations: Dict[str, Dict[str, Any]]): +def export_results(versions: Dict[str, str], configurations: Dict[str, Dict[str, Any]], compliance: Dict[str, Any]): pulumi.export("versions", versions) pulumi.export("configuration", configurations) + pulumi.export("compliance", compliance) From 461217f8b4d706530dde56254c27d8adfe4bff4a Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Mon, 16 Sep 2024 17:39:05 +0000 Subject: [PATCH 16/43] add sample config --- pulumi/stacks/Pulumi.Kargo.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 pulumi/stacks/Pulumi.Kargo.yaml diff --git a/pulumi/stacks/Pulumi.Kargo.yaml b/pulumi/stacks/Pulumi.Kargo.yaml new file mode 100644 index 0000000..8e937b8 --- /dev/null +++ b/pulumi/stacks/Pulumi.Kargo.yaml @@ -0,0 +1,6 @@ +config: + kargo:cert_manager: + enabled: true + kargo:kubernetes: + context: admin@talos-kargo-docker + kubeconfig: /workspaces/Kargo/.kube/config From 171478861b9ba712bdc5d90e00cc1d919a3eb8a6 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Mon, 16 Sep 2024 17:39:35 +0000 Subject: [PATCH 17/43] add sample config --- pulumi/stacks/Pulumi.ci.yaml | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 pulumi/stacks/Pulumi.ci.yaml diff --git a/pulumi/stacks/Pulumi.ci.yaml b/pulumi/stacks/Pulumi.ci.yaml new file mode 100644 index 0000000..f5e729f --- /dev/null +++ b/pulumi/stacks/Pulumi.ci.yaml @@ -0,0 +1,56 @@ +# Pulumi.ci.yaml +config: + kargo:cert_manager: + enabled: true + namespace: cert-manager + version: latest + kargo:cilium: + enabled: false + kargo:compliance: + fisma: + ato: + authorized: "2025-03-27T00:00:00Z" + renew: "2026-03-27T00:00:00Z" + review: "2028-03-27T00:00:00Z" + enabled: true + level: moderate + nist: + auxiliary: + - ac-6.1 + - ac-2.13 + controls: + - ac-1 + - ac-2 + enabled: true + exceptions: + - ca-7.1 + - ma-2.2 + - si-12 + scip: + environment: prod + ownership: + operator: + contacts: + - seti2@nasa.gov + - alien51@nasa.gov + name: science-team-seti2-obs2819 + provider: + contacts: + - scip@nasa.gov + - bobert@nasa.gov + name: scip-team-xyz + provider: + name: kubevirt + regions: + - scip-west-1 + - scip-east-1 + - scip-lunar-2 + version: v2.87.11 + kargo:kubernetes: + context: admin@talos-kargo-docker + distribution: talos + kubeconfig: /workspaces/Kargo/.kube/config + kargo:multus: + enabled: false + kargo:vm: + enabled: false From f5c3a850ba64b4508eacde66fb9292f67f756373 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Mon, 16 Sep 2024 23:05:04 +0000 Subject: [PATCH 18/43] simplify wip --- pulumi/__main__.py | 58 ++++++++++++++++++++++++++++++++--- pulumi/src/kubevirt/deploy.py | 2 -- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/pulumi/__main__.py b/pulumi/__main__.py index 17f039e..a5c7858 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -25,6 +25,54 @@ global_depends_on = init["global_depends_on"] compliance_config = init["compliance_config"] +########################################## +# Deploy Moudles from ./src/ + +def deploy_module( + module_name: str, + config: pulumi.Config, + default_versions: Dict[str, Any], + global_depends_on: List[pulumi.Resource], + k8s_provider: Any, + deploy_function: Any, + export_name: str +) -> None: + """ + Helper function to deploy a module based on configuration. + + Args: + module_name (str): Name of the module. + config (pulumi.Config): Pulumi configuration object. + default_versions (Dict[str, Any]): Default versions for modules. + global_depends_on (List[pulumi.Resource]): Global dependencies. + k8s_provider (Any): Kubernetes provider. + deploy_function (Callable): Function to deploy the module. + export_name (str): Name of the export variable. + """ + module_config_dict, module_enabled = get_module_config(module_name, config, default_versions) + + if module_enabled: + # Dynamically import the module's types and deploy function + module_types = __import__(f"src.{module_name}.types", fromlist=['']) + ModuleConfigClass = getattr(module_types, f"{module_name.capitalize()}Config") + config_obj = ModuleConfigClass.merge(module_config_dict) + + module_deploy = __import__(f"src.{module_name}.deploy", fromlist=['']) + deploy_func = getattr(module_deploy, f"deploy_{module_name}_module") + + version, release, exported_value = deploy_func( + config_obj=config_obj, + global_depends_on=global_depends_on, + k8s_provider=k8s_provider, + ) + + versions[module_name] = version + configurations[module_name] = { + "enabled": module_enabled, + } + + pulumi.export(export_name, exported_value) + ########################################## # Deploy Cert Manager Module @@ -38,9 +86,8 @@ # Check if the cert_manager module is enabled & execute deployment logic if true if cert_manager_enabled: from src.cert_manager.types import CertManagerConfig - config_cert_manager = CertManagerConfig.merge(config_cert_manager_dict) - from src.cert_manager.deploy import deploy_cert_manager_module + config_cert_manager = CertManagerConfig.merge(config_cert_manager_dict) cert_manager_version, cert_manager_release, cert_manager_selfsigned_cert = deploy_cert_manager_module( config_cert_manager=config_cert_manager, @@ -54,6 +101,9 @@ "enabled": cert_manager_enabled, } + # append cert_manager_release to global_depends_on + global_depends_on.append(cert_manager_release) + pulumi.export("cert_manager_selfsigned_cert", cert_manager_selfsigned_cert) else: cert_manager_selfsigned_cert = None @@ -71,16 +121,14 @@ # Check if the kubevirt module is enabled & execute deployment logic if true if kubevirt_enabled: from src.kubevirt.types import KubeVirtConfig - config_kubevirt = KubeVirtConfig.merge(config_kubevirt_dict) - from src.kubevirt.deploy import deploy_kubevirt_module + config_kubevirt = KubeVirtConfig.merge(config_kubevirt_dict) # Pass cert_manager_release as a dependency if cert_manager is enabled kubevirt_version, kubevirt_operator = deploy_kubevirt_module( config_kubevirt=config_kubevirt, global_depends_on=global_depends_on, k8s_provider=k8s_provider, - cert_manager_release=cert_manager_release if cert_manager_enabled else None ) # Record the deployed version and configuration diff --git a/pulumi/src/kubevirt/deploy.py b/pulumi/src/kubevirt/deploy.py index b1f0cc3..0438bd2 100644 --- a/pulumi/src/kubevirt/deploy.py +++ b/pulumi/src/kubevirt/deploy.py @@ -20,7 +20,6 @@ def deploy_kubevirt_module( config_kubevirt: KubeVirtConfig, global_depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, - cert_manager_release: Optional[pulumi.Resource] = None ) -> Tuple[Optional[str], Optional[pulumi.Resource]]: """ Deploys the KubeVirt module with labels and annotations. @@ -29,7 +28,6 @@ def deploy_kubevirt_module( config_kubevirt (KubeVirtConfig): Configuration object for KubeVirt deployment. global_depends_on (List[pulumi.Resource]): A list of resources that the deployment depends on. k8s_provider (k8s.Provider): The Kubernetes provider for this deployment. - cert_manager_release (Optional[pulumi.Resource]): The cert-manager resource, if deployed. Returns: Tuple[Optional[str], Optional[pulumi.Resource]]: From 6c50c323b649559aeac17044e36de9fff62dca23 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Thu, 19 Sep 2024 21:58:09 -0700 Subject: [PATCH 19/43] rerefactor optimization - working state --- pulumi/__main__.py | 626 +------------------------------- pulumi/src/lib/config.py | 22 +- pulumi/src/lib/deploy_module.py | 88 ++--- pulumi/src/lib/introspection.py | 41 +++ pulumi/src/lib/metadata.py | 172 +++++++-- 5 files changed, 250 insertions(+), 699 deletions(-) create mode 100644 pulumi/src/lib/introspection.py diff --git a/pulumi/__main__.py b/pulumi/__main__.py index a5c7858..7fdd5e5 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -1,17 +1,8 @@ # __main__.py -# Main entry point for the Kargo Pulumi IaC program. -# This script is responsible for deploying the Kargo platform components. - -# Import Pulumi Libraries import pulumi -import pulumi_kubernetes as k8s - -# Import Python Standard Libraries -from typing import Any, Dict, List, Optional, Tuple - -# Local imports from src.lib.init import initialize_pulumi -from src.lib.config import get_module_config, export_results +from src.lib.config import export_results +from src.lib.deploy_module import deploy_module # Pulumi Initialization init = initialize_pulumi() @@ -25,611 +16,20 @@ global_depends_on = init["global_depends_on"] compliance_config = init["compliance_config"] -########################################## -# Deploy Moudles from ./src/ - -def deploy_module( - module_name: str, - config: pulumi.Config, - default_versions: Dict[str, Any], - global_depends_on: List[pulumi.Resource], - k8s_provider: Any, - deploy_function: Any, - export_name: str -) -> None: - """ - Helper function to deploy a module based on configuration. - - Args: - module_name (str): Name of the module. - config (pulumi.Config): Pulumi configuration object. - default_versions (Dict[str, Any]): Default versions for modules. - global_depends_on (List[pulumi.Resource]): Global dependencies. - k8s_provider (Any): Kubernetes provider. - deploy_function (Callable): Function to deploy the module. - export_name (str): Name of the export variable. - """ - module_config_dict, module_enabled = get_module_config(module_name, config, default_versions) - - if module_enabled: - # Dynamically import the module's types and deploy function - module_types = __import__(f"src.{module_name}.types", fromlist=['']) - ModuleConfigClass = getattr(module_types, f"{module_name.capitalize()}Config") - config_obj = ModuleConfigClass.merge(module_config_dict) - - module_deploy = __import__(f"src.{module_name}.deploy", fromlist=['']) - deploy_func = getattr(module_deploy, f"deploy_{module_name}_module") - - version, release, exported_value = deploy_func( - config_obj=config_obj, - global_depends_on=global_depends_on, - k8s_provider=k8s_provider, - ) - - versions[module_name] = version - configurations[module_name] = { - "enabled": module_enabled, - } +# List of modules to deploy +modules_to_deploy = ["cert_manager", "kubevirt"] - pulumi.export(export_name, exported_value) - -########################################## -# Deploy Cert Manager Module - -# Retrieve configuration and enable flag for the cert_manager module -config_cert_manager_dict, cert_manager_enabled = get_module_config( - 'cert_manager', - config, - default_versions, -) - -# Check if the cert_manager module is enabled & execute deployment logic if true -if cert_manager_enabled: - from src.cert_manager.types import CertManagerConfig - from src.cert_manager.deploy import deploy_cert_manager_module - config_cert_manager = CertManagerConfig.merge(config_cert_manager_dict) - - cert_manager_version, cert_manager_release, cert_manager_selfsigned_cert = deploy_cert_manager_module( - config_cert_manager=config_cert_manager, +# Deploy each module +for module_name in modules_to_deploy: + deploy_module( + module_name=module_name, + config=config, + default_versions=default_versions, global_depends_on=global_depends_on, k8s_provider=k8s_provider, + versions=versions, + configurations=configurations, ) - # Record the deployed version and configuration - versions["cert_manager"] = cert_manager_version - configurations["cert_manager"] = { - "enabled": cert_manager_enabled, - } - - # append cert_manager_release to global_depends_on - global_depends_on.append(cert_manager_release) - - pulumi.export("cert_manager_selfsigned_cert", cert_manager_selfsigned_cert) -else: - cert_manager_selfsigned_cert = None - -########################################## -# Deploy KubeVirt Module - -# Retrieve configuration and enable flag for the kubevirt module -config_kubevirt_dict, kubevirt_enabled = get_module_config( - 'kubevirt', - config, - default_versions, -) - -# Check if the kubevirt module is enabled & execute deployment logic if true -if kubevirt_enabled: - from src.kubevirt.types import KubeVirtConfig - from src.kubevirt.deploy import deploy_kubevirt_module - config_kubevirt = KubeVirtConfig.merge(config_kubevirt_dict) - - # Pass cert_manager_release as a dependency if cert_manager is enabled - kubevirt_version, kubevirt_operator = deploy_kubevirt_module( - config_kubevirt=config_kubevirt, - global_depends_on=global_depends_on, - k8s_provider=k8s_provider, - ) - - # Record the deployed version and configuration - versions["kubevirt"] = kubevirt_version - configurations["kubevirt"] = { - "enabled": kubevirt_enabled, - } - - pulumi.export("kubevirt_version", kubevirt_version) -else: - kubevirt_version = None - -########################################## -# Export Component Metadata Outputs: -# - Versions -# - Configurations +# Export Component Metadata Outputs: - Versions - Configurations export_results(versions, configurations, compliance_config) - - - -############################################ -## Deploy Kubevirt Module -# -## KubeVirt Module Configuration and Deployment -#config_kubevirt_dict, kubevirt_enabled = get_module_config('kubevirt', config, default_versions) -# -#if kubevirt_enabled: -# # Import the KubeVirtConfig data class and merge the user config with defaults -# from src.kubevirt.types import KubeVirtConfig -# config_kubevirt = KubeVirtConfig.merge(config_kubevirt_dict) -# -# # Import the deployment function for the KubeVirt module -# from src.kubevirt.deploy import deploy_kubevirt_module -# -# # Deploy the KubeVirt module -# kubevirt_version, kubevirt_operator = deploy_kubevirt_module( -# config_kubevirt=config_kubevirt, -# global_depends_on=global_depends_on, -# k8s_provider=k8s_provider, -# ) -# -# # Apply Git and compliance metadata to KubeVirt -# apply_git_metadata(kubevirt_operator, git_info) -# apply_compliance_metadata(kubevirt_operator, compliance_config.__dict__) -# -# # Record the deployed version & configuration -# versions["kubevirt"] = kubevirt_version -# configurations["kubevirt"] = { -# "enabled": kubevirt_enabled -# } -#else: -# kubevirt_operator = None - - -#from src.kubevirt.deploy import deploy_kubevirt -#from src.containerized_data_importer.deploy import deploy_cdi -#from src.cluster_network_addons.deploy import deploy_cnao -#from src.multus.deploy import deploy_multus -#from src.hostpath_provisioner.deploy import deploy as deploy_hostpath_provisioner -#from src.openunison.deploy import deploy_openunison -#from src.prometheus.deploy import deploy_prometheus -#from src.kubernetes_dashboard.deploy import deploy_kubernetes_dashboard -#from src.kv_manager.deploy import deploy_ui_for_kubevirt -#from src.ceph.deploy import deploy_rook_operator -#from src.vm.ubuntu import deploy_ubuntu_vm -#from src.vm.talos import deploy_talos_cluster -#from src.cilium.deploy import deploy_cilium -# -########################################### -## Kubernetes Configuration -#kubernetes_config = config.get_object("kubernetes") or {} -#kubeconfig = kubernetes_config.get("kubeconfig") -#kubernetes_context = kubernetes_config.get("context") -# -## Create a Kubernetes provider instance -#k8s_provider = Provider( -# "k8sProvider", -# kubeconfig=kubeconfig, -# context=kubernetes_context, -#) -# -## Initialize versions dictionary to track deployed component versions -#versions: Dict[str, Dict[str, Any]] = {} -# -########################################### -## Module Configuration and Enable Flags -# -## Retrieve configurations and enable flags for all modules -#config_cilium, cilium_enabled = get_module_config('cilium') -#config_cert_manager, cert_manager_enabled = get_module_config('cert_manager') -#config_kubevirt, kubevirt_enabled = get_module_config('kubevirt') -#config_cdi, cdi_enabled = get_module_config('cdi') -#config_multus, multus_enabled = get_module_config('multus') -#config_prometheus, prometheus_enabled = get_module_config('prometheus') -#config_openunison, openunison_enabled = get_module_config('openunison') -#config_hostpath_provisioner, hostpath_provisioner_enabled = get_module_config('hostpath_provisioner') -#config_cnao, cnao_enabled = get_module_config('cnao') -#config_kubernetes_dashboard, kubernetes_dashboard_enabled = get_module_config('kubernetes_dashboard') -#config_kubevirt_manager, kubevirt_manager_enabled = get_module_config('kubevirt_manager') -#config_vm, vm_enabled = get_module_config('vm') -#config_talos, talos_cluster_enabled = get_module_config('talos') -# -########################################### -## Dependency Management -# -## Initialize a list to keep track of dependencies between resources -#global_depends_on: List[pulumi.Resource] = [] -# -########################################### -## Export Component Versions -# -#pulumi.export("versions", versions) - -#def deploy_kubevirt_module() -> Tuple[Optional[str], Optional[pulumi.Resource]]: -# """ -# Deploys the KubeVirt module if enabled. -# -# Returns: -# A tuple containing the version and the KubeVirt operator resource. -# """ -# if not kubevirt_enabled: -# pulumi.log.info("KubeVirt module is disabled. Skipping deployment.") -# return None, None -# -# namespace = "kubevirt" -# kubevirt_version = config_kubevirt.get('version') -# kubevirt_emulation = config_kubevirt.get('emulation', False) -# -# depends_on = [] -# if cilium_enabled: -# depends_on.append(cilium_release) -# if cert_manager_enabled: -# depends_on.append(cert_manager_release) -# -# kubevirt_version, kubevirt_operator = deploy_kubevirt( -# depends_on=depends_on, -# namespace=namespace, -# version=kubevirt_version, -# emulation=kubevirt_emulation, -# k8s_provider=k8s_provider, -# ) -# -# versions["kubevirt"] = {"enabled": kubevirt_enabled, "version": kubevirt_version} -# -# if kubevirt_operator: -# global_depends_on.append(kubevirt_operator) -# -# return kubevirt_version, kubevirt_operator -# -#def deploy_multus_module() -> Tuple[Optional[str], Optional[pulumi.Resource]]: -# """ -# Deploys the Multus module if enabled. -# -# Returns: -# A tuple containing the version and the Multus Helm release resource. -# """ -# if not multus_enabled: -# pulumi.log.info("Multus module is disabled. Skipping deployment.") -# return None, None -# -# namespace = "multus" -# multus_version = config_multus.get('version', "master") -# bridge_name = config_multus.get('bridge_name', "br0") -# -# depends_on = [] -# if cilium_enabled: -# depends_on.append(cilium_release) -# if cert_manager_enabled: -# depends_on.append(cert_manager_release) -# -# multus_version, multus_release = deploy_multus( -# depends_on=depends_on, -# version=multus_version, -# bridge_name=bridge_name, -# k8s_provider=k8s_provider, -# ) -# -# versions["multus"] = {"enabled": multus_enabled, "version": multus_version} -# -# if multus_release: -# global_depends_on.append(multus_release) -# -# return multus_version, multus_release -# -#def deploy_hostpath_provisioner_module() -> Tuple[Optional[str], Optional[pulumi.Resource]]: -# """ -# Deploys the HostPath Provisioner module if enabled. -# -# Returns: -# A tuple containing the version and the Helm release resource. -# """ -# if not hostpath_provisioner_enabled: -# pulumi.log.info("HostPath Provisioner module is disabled. Skipping deployment.") -# return None, None -# -# if not cert_manager_enabled: -# error_msg = "HostPath Provisioner requires Cert Manager. Please enable Cert Manager and try again." -# pulumi.log.error(error_msg) -# raise Exception(error_msg) -# -# namespace = "hostpath-provisioner" -# hostpath_version = config_hostpath_provisioner.get('version') -# default_path = config_hostpath_provisioner.get('default_path', "/var/mnt/hostpath-provisioner") -# default_storage_class = config_hostpath_provisioner.get('default_storage_class', False) -# -# depends_on = [] -# if cilium_enabled: -# depends_on.append(cilium_release) -# if cert_manager_enabled: -# depends_on.append(cert_manager_release) -# if kubevirt_enabled: -# depends_on.append(kubevirt_operator) -# -# hostpath_version, hostpath_release = deploy_hostpath_provisioner( -# depends_on=depends_on, -# version=hostpath_version, -# namespace=namespace, -# default_path=default_path, -# default_storage_class=default_storage_class, -# k8s_provider=k8s_provider, -# ) -# -# versions["hostpath_provisioner"] = {"enabled": hostpath_provisioner_enabled, "version": hostpath_version} -# -# if hostpath_release: -# global_depends_on.append(hostpath_release) -# -# return hostpath_version, hostpath_release -# -#def deploy_cdi_module() -> Tuple[Optional[str], Optional[pulumi.Resource]]: -# """ -# Deploys the Containerized Data Importer (CDI) module if enabled. -# -# Returns: -# A tuple containing the version and the CDI Helm release resource. -# """ -# if not cdi_enabled: -# pulumi.log.info("CDI module is disabled. Skipping deployment.") -# return None, None -# -# namespace = "cdi" -# cdi_version = config_cdi.get('version') -# -# cdi_version, cdi_release = deploy_cdi( -# depends_on=global_depends_on, -# version=cdi_version, -# k8s_provider=k8s_provider, -# ) -# -# versions["cdi"] = {"enabled": cdi_enabled, "version": cdi_version} -# -# if cdi_release: -# global_depends_on.append(cdi_release) -# -# return cdi_version, cdi_release -# -#def deploy_prometheus_module() -> Tuple[Optional[str], Optional[pulumi.Resource]]: -# """ -# Deploys the Prometheus module if enabled. -# -# Returns: -# A tuple containing the version and the Prometheus Helm release resource. -# """ -# if not prometheus_enabled: -# pulumi.log.info("Prometheus module is disabled. Skipping deployment.") -# return None, None -# -# namespace = "monitoring" -# prometheus_version = config_prometheus.get('version') -# -# prometheus_version, prometheus_release = deploy_prometheus( -# depends_on=global_depends_on, -# namespace=namespace, -# version=prometheus_version, -# k8s_provider=k8s_provider, -# openunison_enabled=openunison_enabled, -# ) -# -# versions["prometheus"] = {"enabled": prometheus_enabled, "version": prometheus_version} -# -# if prometheus_release: -# global_depends_on.append(prometheus_release) -# -# return prometheus_version, prometheus_release -# -#def deploy_kubernetes_dashboard_module() -> Tuple[Optional[str], Optional[pulumi.Resource]]: -# """ -# Deploys the Kubernetes Dashboard module if enabled. -# -# Returns: -# A tuple containing the version and the Dashboard Helm release resource. -# """ -# if not kubernetes_dashboard_enabled: -# pulumi.log.info("Kubernetes Dashboard module is disabled. Skipping deployment.") -# return None, None -# -# namespace = "kubernetes-dashboard" -# dashboard_version = config_kubernetes_dashboard.get('version') -# -# depends_on = global_depends_on.copy() -# if cilium_enabled: -# depends_on.append(cilium_release) -# -# dashboard_version, dashboard_release = deploy_kubernetes_dashboard( -# depends_on=depends_on, -# namespace=namespace, -# version=dashboard_version, -# k8s_provider=k8s_provider, -# ) -# -# versions["kubernetes_dashboard"] = {"enabled": kubernetes_dashboard_enabled, "version": dashboard_version} -# -# if dashboard_release: -# global_depends_on.append(dashboard_release) -# -# return dashboard_version, dashboard_release -# -#def deploy_openunison_module() -> Tuple[Optional[str], Optional[pulumi.Resource]]: -# """ -# Deploys the OpenUnison module if enabled. -# -# Returns: -# A tuple containing the version and the OpenUnison Helm release resource. -# """ -# if not openunison_enabled: -# pulumi.log.info("OpenUnison module is disabled. Skipping deployment.") -# return None, None -# -# namespace = "openunison" -# openunison_version = config_openunison.get('version') -# domain_suffix = config_openunison.get('dns_suffix', "kargo.arpa") -# cluster_issuer = config_openunison.get('cluster_issuer', "cluster-selfsigned-issuer-ca") -# -# github_config = config_openunison.get('github', {}) -# github_client_id = github_config.get('client_id') -# github_client_secret = github_config.get('client_secret') -# github_teams = github_config.get('teams') -# -# if not github_client_id or not github_client_secret: -# error_msg = "OpenUnison requires GitHub OAuth credentials. Please provide 'client_id' and 'client_secret' in the configuration." -# pulumi.log.error(error_msg) -# raise Exception(error_msg) -# -# enabled_components = { -# "kubevirt": {"enabled": kubevirt_enabled}, -# "prometheus": {"enabled": prometheus_enabled}, -# } -# -# pulumi.export("enabled_components", enabled_components) -# -# openunison_version, openunison_release = deploy_openunison( -# depends_on=global_depends_on, -# namespace=namespace, -# version=openunison_version, -# k8s_provider=k8s_provider, -# domain_suffix=domain_suffix, -# cluster_issuer=cluster_issuer, -# ca_cert_b64=cert_manager_selfsigned_cert, -# kubernetes_dashboard_release=kubernetes_dashboard_release, -# github_client_id=github_client_id, -# github_client_secret=github_client_secret, -# github_teams=github_teams, -# enabled_components=enabled_components, -# ) -# -# versions["openunison"] = {"enabled": openunison_enabled, "version": openunison_version} -# -# if openunison_release: -# global_depends_on.append(openunison_release) -# -# return openunison_version, openunison_release -# -#def deploy_ubuntu_vm_module() -> Tuple[Optional[Any], Optional[Any]]: -# """ -# Deploys an Ubuntu VM using KubeVirt if enabled. -# -# Returns: -# A tuple containing the VM resource and the SSH service resource. -# """ -# if not vm_enabled: -# pulumi.log.info("Ubuntu VM module is disabled. Skipping deployment.") -# return None, None -# -# # Get the SSH public key from Pulumi Config or local filesystem -# ssh_pub_key = config.get("ssh_pub_key") -# if not ssh_pub_key: -# ssh_key_path = os.path.expanduser("~/.ssh/id_rsa.pub") -# try: -# with open(ssh_key_path, "r") as f: -# ssh_pub_key = f.read().strip() -# except FileNotFoundError: -# error_msg = f"SSH public key not found at {ssh_key_path}. Please provide 'ssh_pub_key' in the configuration." -# pulumi.log.error(error_msg) -# raise Exception(error_msg) -# -# # Merge default VM configuration with user-provided configuration -# default_vm_config = { -# "namespace": "default", -# "instance_name": "ubuntu", -# "image_name": "docker.io/containercraft/ubuntu:22.04", -# "node_port": 30590, -# "ssh_user": "kc2", -# "ssh_password": "kc2", -# "ssh_pub_key": ssh_pub_key, -# } -# -# vm_config = {**default_vm_config, **config_vm} -# -# # Deploy the Ubuntu VM -# ubuntu_vm, ssh_service = deploy_ubuntu_vm( -# vm_config=vm_config, -# k8s_provider=k8s_provider, -# depends_on=global_depends_on, -# ) -# -# versions["ubuntu_vm"] = {"enabled": vm_enabled, "name": ubuntu_vm.metadata["name"]} -# -# if ssh_service: -# global_depends_on.append(ssh_service) -# -# return ubuntu_vm, ssh_service -# -#def deploy_talos_cluster_module() -> Tuple[Optional[Any], Optional[Any]]: -# """ -# Deploys a Talos cluster using KubeVirt if enabled. -# -# Returns: -# A tuple containing the control plane VM pool and the worker VM pool resources. -# """ -# if not talos_cluster_enabled: -# pulumi.log.info("Talos cluster module is disabled. Skipping deployment.") -# return None, None -# -# depends_on = [] -# if cert_manager_enabled: -# depends_on.append(cert_manager_release) -# if multus_enabled: -# depends_on.append(multus_release) -# if cdi_enabled: -# depends_on.append(cdi_release) -# -# controlplane_vm_pool, worker_vm_pool = deploy_talos_cluster( -# config_talos=config_talos, -# k8s_provider=k8s_provider, -# parent=kubevirt_operator, -# depends_on=depends_on, -# ) -# -# versions["talos_cluster"] = { -# "enabled": talos_cluster_enabled, -# "running": config_talos.get("running", True), -# "controlplane": config_talos.get("controlplane", {}), -# "workers": config_talos.get("workers", {}), -# } -# -# return controlplane_vm_pool, worker_vm_pool - -# Deactivating Cilium module deployment until Talos 1.8 releases with cni optimizations -#def deploy_cilium_module() -> Tuple[Optional[str], Optional[pulumi.Resource]]: -# """ -# Deploys the Cilium CNI module if enabled. -# -# Returns: -# A tuple containing the Cilium version and the Helm release resource. -# """ -# if not cilium_enabled: -# pulumi.log.info("Cilium module is disabled. Skipping deployment.") -# return None, None -# -# namespace = "kube-system" -# l2_announcements = config_cilium.get('l2announcements', "192.168.1.70/28") -# l2_bridge_name = config_cilium.get('l2_bridge_name', "br0") -# cilium_version = config_cilium.get('version') -# -# # Deploy Cilium using the provided parameters -# cilium_version, cilium_release = deploy_cilium( -# name="cilium-cni", -# provider=k8s_provider, -# project_name=project_name, -# kubernetes_endpoint_service_address=None, # Replace with actual endpoint if needed -# namespace=namespace, -# version=cilium_version, -# l2_bridge_name=l2_bridge_name, -# l2announcements=l2_announcements, -# ) -# -# versions["cilium"] = {"enabled": cilium_enabled, "version": cilium_version} -# -# # Add the release to the global dependencies -# if cilium_release: -# global_depends_on.append(cilium_release) -# -# return cilium_version, cilium_release - -#kubevirt_version, kubevirt_operator = deploy_kubevirt_module() -#multus_version, multus_release = deploy_multus_module() -#hostpath_version, hostpath_release = deploy_hostpath_provisioner_module() -#cdi_version, cdi_release = deploy_cdi_module() -#prometheus_version, prometheus_release = deploy_prometheus_module() -#dashboard_version, kubernetes_dashboard_release = deploy_kubernetes_dashboard_module() -#openunison_version, openunison_release = deploy_openunison_module() -#ubuntu_vm, ubuntu_ssh_service = deploy_ubuntu_vm_module() -#talos_controlplane_vm_pool, talos_worker_vm_pool = deploy_talos_cluster_module() -#cilium_version, cilium_release = deploy_cilium_module() diff --git a/pulumi/src/lib/config.py b/pulumi/src/lib/config.py index 23d4e7e..a907792 100644 --- a/pulumi/src/lib/config.py +++ b/pulumi/src/lib/config.py @@ -32,21 +32,25 @@ def get_module_config( """ # Get the module's configuration from Pulumi config, default to an empty dict module_config = config.get_object(module_name) or {} - module_enabled = str(module_config.get('enabled', 'false')).lower() == "true" - # Remove 'enabled' key from the module configuration - module_config.pop('enabled', None) + # Check if the module is enabled + module_enabled = str(module_config.pop('enabled', 'false')).lower() == "true" - # Handle version injection into the module configuration - module_version = module_config.get('version') - if not module_version: - # No version specified in module config; use default version - module_version = default_versions.get(module_name) - module_config['version'] = module_version + # Use specified version or fall back to the default version if not specified + module_config['version'] = module_config.get('version', default_versions.get(module_name)) return module_config, module_enabled def export_results(versions: Dict[str, str], configurations: Dict[str, Dict[str, Any]], compliance: Dict[str, Any]): + """ + Exports the results of the deployment processes including versions, + configurations, and compliance information. + + Args: + versions (Dict[str, str]): A dictionary containing the versions of the deployed modules. + configurations (Dict[str, Dict[str, Any]]): A dictionary containing the configurations of the deployed modules. + compliance (Dict[str, Any]): A dictionary containing the compliance information. + """ pulumi.export("versions", versions) pulumi.export("configuration", configurations) pulumi.export("compliance", compliance) diff --git a/pulumi/src/lib/deploy_module.py b/pulumi/src/lib/deploy_module.py index e4bae17..b00caa8 100644 --- a/pulumi/src/lib/deploy_module.py +++ b/pulumi/src/lib/deploy_module.py @@ -1,10 +1,10 @@ -# pulumi/src/lib/deploy_module.py +# src/lib/deploy_module.py -# TODO: Implement the `deploy_module` function in __main__.py -# Proceed with objective after all modules have been implemented. - -from typing import Any, Dict, List, Tuple +import inspect import pulumi +from typing import Any, Dict, List +from src.lib.introspection import discover_config_class, discover_deploy_function +from src.lib.config import get_module_config def deploy_module( module_name: str, @@ -12,66 +12,58 @@ def deploy_module( default_versions: Dict[str, Any], global_depends_on: List[pulumi.Resource], k8s_provider: Any, - deploy_function: Any, - export_name: str + versions: Dict[str, str], + configurations: Dict[str, Dict[str, Any]] ) -> None: """ Helper function to deploy a module based on configuration. Args: module_name (str): Name of the module. - config (pulumi.Config): Pulumi configuration object. - default_versions (Dict[str, Any]): Default versions for modules. - global_depends_on (List[pulumi.Resource]): Global dependencies. - k8s_provider (Any): Kubernetes provider. - deploy_function (Callable): Function to deploy the module. - export_name (str): Name of the export variable. + config: Pulumi configuration object. + default_versions: Default versions for modules. + global_depends_on: Global dependencies. + k8s_provider: Kubernetes provider. + versions: Dictionary to store versions of deployed modules. + configurations: Dictionary to store configurations of deployed modules. """ + # Get the module's configuration from Pulumi config module_config_dict, module_enabled = get_module_config(module_name, config, default_versions) if module_enabled: - # Dynamically import the module's types and deploy function - module_types = __import__(f"src.{module_name}.types", fromlist=['']) - ModuleConfigClass = getattr(module_types, f"{module_name.capitalize()}Config") + # Discover the configuration class and deploy function dynamically + ModuleConfigClass = discover_config_class(module_name) + deploy_func = discover_deploy_function(module_name) + config_obj = ModuleConfigClass.merge(module_config_dict) - module_deploy = __import__(f"src.{module_name}.deploy", fromlist=['']) - deploy_func = getattr(module_deploy, f"deploy_{module_name}_module") + # Infer the required argument name for the deploy function + deploy_func_args = inspect.signature(deploy_func).parameters.keys() + config_arg_name = list(deploy_func_args)[0] # Assuming the first argument is the config object - version, release, exported_value = deploy_func( - config_obj=config_obj, + # Deploy the module using its deploy function with the correct arguments + result = deploy_func( + **{config_arg_name: config_obj}, global_depends_on=global_depends_on, k8s_provider=k8s_provider, ) - versions[module_name] = version - configurations[module_name] = { - "enabled": module_enabled, - } + # Handle the result based on its structure + if isinstance(result, tuple) and len(result) == 3: + version, release, exported_value = result + elif isinstance(result, tuple) and len(result) == 2: + version, release = result + exported_value = None + else: + raise ValueError(f"Unexpected return value structure from {module_name} deploy function") - pulumi.export(export_name, exported_value) + # Record the deployed version and configuration + versions[module_name] = version + configurations[module_name] = {"enabled": module_enabled} -# # Example Implementation -# from src.lib.deploy_helper import deploy_module + # Export any additional values if needed + if exported_value: + pulumi.export(f"{module_name}_exported_value", exported_value) -# # Deploy Cert Manager -# deploy_module( -# module_name='cert_manager', -# config=config, -# default_versions=default_versions, -# global_depends_on=global_depends_on, -# k8s_provider=k8s_provider, -# deploy_function=deploy_cert_manager_module, -# export_name='cert_manager_selfsigned_cert' -# ) -# -# # Deploy Ceph -# deploy_module( -# module_name='ceph', -# config=config, -# default_versions=default_versions, -# global_depends_on=global_depends_on, -# k8s_provider=k8s_provider, -# deploy_function=deploy_ceph_module, -# export_name='ceph_release' -# ) + # Add the release to global dependencies to maintain resource ordering + global_depends_on.append(release) diff --git a/pulumi/src/lib/introspection.py b/pulumi/src/lib/introspection.py new file mode 100644 index 0000000..5d73255 --- /dev/null +++ b/pulumi/src/lib/introspection.py @@ -0,0 +1,41 @@ +# src/lib/introspection.py +import inspect +import importlib +from typing import Type + +def discover_config_class(module_name: str) -> Type: + """ + Discovers and returns the configuration class from the module's types.py. + Args: + module_name (str): The name of the module. + Returns: + Type: The configuration class. + """ + types_module = importlib.import_module(f"src.{module_name}.types") + + # Inspect the module to find dataclasses + for name, obj in inspect.getmembers(types_module): + if inspect.isclass(obj) and hasattr(obj, "__dataclass_fields__"): + # Developers note: module config dataclass is the first dataclass in src//types.py + # src//types.py is a mandatory file and the first dataclass must exist and be the config class. + return obj + raise ValueError(f"No dataclass found in src.{module_name}.types") + +def discover_deploy_function(module_name: str) -> callable: + """ + Discovers and returns the deploy function from the module's deploy.py. + Args: + module_name (str): The name of the module. + Returns: + callable: The deploy function. + """ + deploy_module = importlib.import_module(f"src.{module_name}.deploy") + + # Look for a deploy function that matches the pattern deploy__module + function_name = f"deploy_{module_name}_module" + deploy_function = getattr(deploy_module, function_name, None) + + if not deploy_function: + raise ValueError(f"No deploy function named '{function_name}' found in src.{module_name}.deploy") + + return deploy_function diff --git a/pulumi/src/lib/metadata.py b/pulumi/src/lib/metadata.py index 83e8f02..1b0413e 100644 --- a/pulumi/src/lib/metadata.py +++ b/pulumi/src/lib/metadata.py @@ -7,70 +7,184 @@ def collect_git_info() -> Dict[str, str]: """ Retrieves the current Git repository's remote URL, branch, and commit hash. + + This function uses subprocess to run git commands that fetch the remote URL, the current branch, + and the latest commit hash. This information is useful for tracking which version of the code + is being deployed, which branch it’s from, and which repository it originates from. + + Returns: + Dict[str, str]: A dictionary containing the remote URL, branch name, and commit hash. """ try: + # Fetch the remote URL from git configuration remote = subprocess.check_output( ['git', 'config', '--get', 'remote.origin.url'], stderr=subprocess.STDOUT ).strip().decode('utf-8') + # Fetch the current branch name branch = subprocess.check_output( ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], stderr=subprocess.STDOUT ).strip().decode('utf-8') + # Fetch the latest commit hash commit = subprocess.check_output( ['git', 'rev-parse', 'HEAD'], stderr=subprocess.STDOUT ).strip().decode('utf-8') - return { - 'remote': remote, - 'branch': branch, - 'commit': commit - } + return {'remote': remote, 'branch': branch, 'commit': commit} except subprocess.CalledProcessError as e: + # In case of an error, log it and return 'N/A' for all values pulumi.log.error(f"Error fetching git information: {e.output.decode('utf-8')}") - return { - 'remote': 'N/A', - 'branch': 'N/A', - 'commit': 'N/A' - } + return {'remote': 'N/A', 'branch': 'N/A', 'commit': 'N/A'} + def generate_git_labels(git_info: Dict[str, str]) -> Dict[str, str]: - # Only essential metadata should be in labels (e.g., branch) - labels = { + """ + Generates labels for Kubernetes resources from git information. + + Labels are key/value pairs that are attached to objects, such as pods. Labels are intended to be + used to specify identifying attributes of objects that are meaningful and relevant to users. + + Args: + git_info (Dict[str, str]): A dictionary containing git information. + + Returns: + Dict[str, str]: A dictionary containing sanitized git branch name and shortened commit hash. + """ + return { "git.branch": sanitize_label_value(git_info.get("branch", "")), "git.commit": git_info.get("commit", "")[:7], # Shorten commit hash } - return {k: v for k, v in labels.items() if v} + def generate_git_annotations(git_info: Dict[str, str]) -> Dict[str, str]: - # Store more detailed information in annotations - annotations = { + """ + Generates annotations for Kubernetes resources from git information. + + Annotations are key/value pairs that can hold arbitrary non-identifying metadata. They are + intended to store information that can be useful for debugging, auditing, or other purposes. + + Args: + git_info (Dict[str, str]): A dictionary containing git information. + + Returns: + Dict[str, str]: A dictionary containing git remote URL, full commit hash, and branch name. + """ + return { "git.remote": git_info.get("remote", ""), "git.commit.full": git_info.get("commit", ""), "git.branch": git_info.get("branch", "") } - return {k: v for k, v in annotations.items() if v} + ########################################## # Global labels and annotations -# Singleton to store global labels and annotations -_global_labels = {} -_global_annotations = {} +########################################## + +class MetadataSingleton: + """ + Singleton class to manage global labels and annotations for Kubernetes resources. + + This class uses the singleton pattern to ensure only one instance is created. + This is important for global state management. + """ + + __instance = None + + def __new__(cls): + """ + Ensure only one instance of this class exists (Singleton pattern). + + This method checks if an instance already exists. If not, it creates one. + """ + if cls.__instance is None: + cls.__instance = super(MetadataSingleton, cls).__new__(cls) + cls.__instance._global_labels = {} + cls.__instance._global_annotations = {} + return cls.__instance + + @property + def global_labels(self): + """ + Property method to get global labels. + + Labels are useful for identifying and grouping Kubernetes resources. + """ + return self._global_labels -def set_global_labels(labels): - global _global_labels - _global_labels = labels + @global_labels.setter + def global_labels(self, labels): + """ + Property method to set global labels. -def set_global_annotations(annotations): - global _global_annotations - _global_annotations = annotations + Args: + labels (dict): A dictionary of labels to set globally. + """ + self._global_labels = labels -def get_global_labels(): - return _global_labels + @property + def global_annotations(self): + """ + Property method to get global annotations. -def get_global_annotations(): - return _global_annotations + Annotations provide additional context and useful metadata for Kubernetes resources. + """ + return self._global_annotations + + @global_annotations.setter + def global_annotations(self, annotations): + """ + Property method to set global annotations. + + Args: + annotations (dict): A dictionary of annotations to set globally. + """ + self._global_annotations = annotations + + +# Singleton instance for managing global labels and annotations +_metadata_singleton = MetadataSingleton() + + +def set_global_labels(labels: Dict[str, str]): + """ + Function to set global labels using the singleton instance. + + Args: + labels (Dict[str, str]): A dictionary of labels to set globally. + """ + _metadata_singleton.global_labels = labels + + +def set_global_annotations(annotations: Dict[str, str]): + """ + Function to set global annotations using the singleton instance. + + Args: + annotations (Dict[str, str]): A dictionary of annotations to set globally. + """ + _metadata_singleton.global_annotations = annotations + + +def get_global_labels() -> Dict[str, str]: + """ + Function to get global labels from the singleton instance. + + Returns: + Dict[str, str]: A dictionary of global labels. + """ + return _metadata_singleton.global_labels + + +def get_global_annotations() -> Dict[str, str]: + """ + Function to get global annotations from the singleton instance. + + Returns: + Dict[str, str]: A dictionary of global annotations. + """ + return _metadata_singleton.global_annotations From 28bf6ed120ca4962100857c157d2fc95d2e75544 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Thu, 19 Sep 2024 23:28:58 -0700 Subject: [PATCH 20/43] refactor file tree layout --- pulumi/__main__.py | 112 ++++++++++++------ pulumi/{src/aws => core}/__init__.py | 0 pulumi/{src/lib => core}/compliance.py | 9 +- pulumi/{src/lib => core}/config.py | 2 +- pulumi/{src/lib => core}/deploy_module.py | 9 +- .../{src/lib => core}/helm_chart_versions.py | 2 + pulumi/{src/lib => core}/init.py | 12 +- pulumi/{src/lib => core}/introspection.py | 11 +- pulumi/{src/lib => core}/metadata.py | 2 +- pulumi/{src/lib => core}/namespace.py | 2 +- pulumi/{src/lib => core}/types.py | 2 +- pulumi/{src/lib => core}/utils.py | 2 +- pulumi/{src/lib => core}/versions.py | 6 +- pulumi/{src => modules}/README.md | 0 pulumi/{src/azure => modules/aws}/__init__.py | 0 pulumi/{src => modules}/aws/deploy.py | 0 .../{src/ceph => modules/azure}/__init__.py | 0 .../cert_manager => modules/ceph}/__init__.py | 0 pulumi/{src => modules}/ceph/deploy.py | 0 .../{src => modules}/cert_manager/README.md | 0 .../cert_manager}/__init__.py | 0 .../{src => modules}/cert_manager/deploy.py | 12 +- pulumi/{src => modules}/cert_manager/types.py | 2 +- .../cilium}/__init__.py | 0 pulumi/{src => modules}/cilium/deploy.py | 0 .../cluster_network_addons}/__init__.py | 0 .../cluster_network_addons/deploy.py | 0 .../containerized_data_importer}/__init__.py | 0 .../containerized_data_importer/deploy.py | 0 .../hostpath_provisioner}/__init__.py | 0 .../hostpath_provisioner/deploy.py | 0 .../kubernetes}/__init__.py | 0 .../kubernetes_dashboard}/__init__.py | 0 .../kubernetes_dashboard/deploy.py | 0 .../kubevirt}/__init__.py | 0 pulumi/{src => modules}/kubevirt/deploy.py | 18 +-- pulumi/{src => modules}/kubevirt/types.py | 4 +- .../lib => modules/kv_manager}/__init__.py | 0 pulumi/{src => modules}/kv_manager/deploy.py | 0 .../local_path_storage/__init__.py | 0 .../local_path_storage/deploy.py | 0 pulumi/{src => modules}/multus/__init__.py | 0 pulumi/{src => modules}/multus/deploy.py | 0 .../{src => modules}/openunison/__init__.py | 0 .../openunison/assets/alertmanager.png | Bin .../openunison/assets/grafana.png | Bin .../openunison/assets/kubevirt.png | Bin .../openunison/assets/prometheus.png | Bin pulumi/{src => modules}/openunison/deploy.py | 0 .../openunison/encoded_assets.py | 0 .../{src => modules}/prometheus/__init__.py | 0 pulumi/{src => modules}/prometheus/deploy.py | 0 pulumi/{src => modules}/vm/__init__.py | 0 pulumi/{src => modules}/vm/talos.py | 0 pulumi/{src => modules}/vm/ubuntu.py | 0 55 files changed, 125 insertions(+), 82 deletions(-) rename pulumi/{src/aws => core}/__init__.py (100%) rename pulumi/{src/lib => core}/compliance.py (90%) rename pulumi/{src/lib => core}/config.py (99%) rename pulumi/{src/lib => core}/deploy_module.py (92%) rename pulumi/{src/lib => core}/helm_chart_versions.py (98%) rename pulumi/{src/lib => core}/init.py (92%) rename pulumi/{src/lib => core}/introspection.py (81%) rename pulumi/{src/lib => core}/metadata.py (99%) rename pulumi/{src/lib => core}/namespace.py (98%) rename pulumi/{src/lib => core}/types.py (99%) rename pulumi/{src/lib => core}/utils.py (99%) rename pulumi/{src/lib => core}/versions.py (97%) rename pulumi/{src => modules}/README.md (100%) rename pulumi/{src/azure => modules/aws}/__init__.py (100%) rename pulumi/{src => modules}/aws/deploy.py (100%) rename pulumi/{src/ceph => modules/azure}/__init__.py (100%) rename pulumi/{src/cert_manager => modules/ceph}/__init__.py (100%) rename pulumi/{src => modules}/ceph/deploy.py (100%) rename pulumi/{src => modules}/cert_manager/README.md (100%) rename pulumi/{src/cilium => modules/cert_manager}/__init__.py (100%) rename pulumi/{src => modules}/cert_manager/deploy.py (96%) rename pulumi/{src => modules}/cert_manager/types.py (95%) rename pulumi/{src/cluster_network_addons => modules/cilium}/__init__.py (100%) rename pulumi/{src => modules}/cilium/deploy.py (100%) rename pulumi/{src/containerized_data_importer => modules/cluster_network_addons}/__init__.py (100%) rename pulumi/{src => modules}/cluster_network_addons/deploy.py (100%) rename pulumi/{src/hostpath_provisioner => modules/containerized_data_importer}/__init__.py (100%) rename pulumi/{src => modules}/containerized_data_importer/deploy.py (100%) rename pulumi/{src/kubernetes => modules/hostpath_provisioner}/__init__.py (100%) rename pulumi/{src => modules}/hostpath_provisioner/deploy.py (100%) rename pulumi/{src/kubernetes_dashboard => modules/kubernetes}/__init__.py (100%) rename pulumi/{src/kubevirt => modules/kubernetes_dashboard}/__init__.py (100%) rename pulumi/{src => modules}/kubernetes_dashboard/deploy.py (100%) rename pulumi/{src/kv_manager => modules/kubevirt}/__init__.py (100%) rename pulumi/{src => modules}/kubevirt/deploy.py (94%) rename pulumi/{src => modules}/kubevirt/types.py (95%) rename pulumi/{src/lib => modules/kv_manager}/__init__.py (100%) rename pulumi/{src => modules}/kv_manager/deploy.py (100%) rename pulumi/{src => modules}/local_path_storage/__init__.py (100%) rename pulumi/{src => modules}/local_path_storage/deploy.py (100%) rename pulumi/{src => modules}/multus/__init__.py (100%) rename pulumi/{src => modules}/multus/deploy.py (100%) rename pulumi/{src => modules}/openunison/__init__.py (100%) rename pulumi/{src => modules}/openunison/assets/alertmanager.png (100%) rename pulumi/{src => modules}/openunison/assets/grafana.png (100%) rename pulumi/{src => modules}/openunison/assets/kubevirt.png (100%) rename pulumi/{src => modules}/openunison/assets/prometheus.png (100%) rename pulumi/{src => modules}/openunison/deploy.py (100%) rename pulumi/{src => modules}/openunison/encoded_assets.py (100%) rename pulumi/{src => modules}/prometheus/__init__.py (100%) rename pulumi/{src => modules}/prometheus/deploy.py (100%) rename pulumi/{src => modules}/vm/__init__.py (100%) rename pulumi/{src => modules}/vm/talos.py (100%) rename pulumi/{src => modules}/vm/ubuntu.py (100%) diff --git a/pulumi/__main__.py b/pulumi/__main__.py index 7fdd5e5..7a0c341 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -1,35 +1,81 @@ # __main__.py + +# Import General Purpose Libraries +from typing import List, Dict, Any + +# Import Pulumi Libraries import pulumi -from src.lib.init import initialize_pulumi -from src.lib.config import export_results -from src.lib.deploy_module import deploy_module - -# Pulumi Initialization -init = initialize_pulumi() - -# Retrieve initialized resources -config = init["config"] -k8s_provider = init["k8s_provider"] -versions = init["versions"] -configurations = init["configurations"] -default_versions = init["default_versions"] -global_depends_on = init["global_depends_on"] -compliance_config = init["compliance_config"] - -# List of modules to deploy -modules_to_deploy = ["cert_manager", "kubevirt"] - -# Deploy each module -for module_name in modules_to_deploy: - deploy_module( - module_name=module_name, - config=config, - default_versions=default_versions, - global_depends_on=global_depends_on, - k8s_provider=k8s_provider, - versions=versions, - configurations=configurations, - ) - -# Export Component Metadata Outputs: - Versions - Configurations -export_results(versions, configurations, compliance_config) +from pulumi_kubernetes import Provider + +# Import Local Libraries +from core.init import initialize_pulumi +from core.config import export_results +from core.deploy_module import deploy_module + + +def main(): + """ + Main entry point for the Kargo Kubevirt Pulumi IaC. + Initializes the Pulumi program, configures resources, and deploys specified modules. + """ + try: + # Initialize Pulumi resources + init = initialize_pulumi() + + # Retrieve initialized resources + config = init["config"] + k8s_provider = init["k8s_provider"] + versions = init["versions"] + configurations = init["configurations"] + default_versions = init["default_versions"] + global_depends_on = init["global_depends_on"] + compliance_config = init["compliance_config"] + + # Define the list of modules to deploy + modules_to_deploy = ["cert_manager", "kubevirt"] + + # Deploy each module + deploy_modules(modules_to_deploy, config, default_versions, global_depends_on, k8s_provider, versions, configurations) + + # Export results + export_results(versions, configurations, compliance_config) + + except Exception as e: + pulumi.log.error(f"Deployment failed: {str(e)}") + raise + +def deploy_modules( + modules: List[str], + config: pulumi.Config, + default_versions: Dict[str, Any], + global_depends_on: List[pulumi.Resource], + k8s_provider: Provider, + versions: Dict[str, str], + configurations: Dict[str, Dict[str, Any]] + ) -> None: + """ + Deploy the specified modules. + + Args: + modules (list): List of module names to deploy. + config (pulumi.Config): The Pulumi configuration object. + default_versions (dict): Default versions for modules. + global_depends_on (list): Global dependencies. + k8s_provider (Provider): Kubernetes provider object. + versions (dict): Dictionary to store versions of deployed modules. + configurations (dict): Dictionary to store configurations of deployed modules. + """ + for module_name in modules: + pulumi.log.info(f"Deploying module: {module_name}") + deploy_module( + module_name=module_name, + config=config, + default_versions=default_versions, + global_depends_on=global_depends_on, + k8s_provider=k8s_provider, + versions=versions, + configurations=configurations, + ) + +if __name__ == "__main__": + main() diff --git a/pulumi/src/aws/__init__.py b/pulumi/core/__init__.py similarity index 100% rename from pulumi/src/aws/__init__.py rename to pulumi/core/__init__.py diff --git a/pulumi/src/lib/compliance.py b/pulumi/core/compliance.py similarity index 90% rename from pulumi/src/lib/compliance.py rename to pulumi/core/compliance.py index f0e00c5..d5cc237 100644 --- a/pulumi/src/lib/compliance.py +++ b/pulumi/core/compliance.py @@ -1,11 +1,4 @@ -# src/lib/compliance.py - -import json -from typing import Dict -from .types import ComplianceConfig -from .utils import sanitize_label_value - -# src/lib/compliance.py +# core/compliance.py import json from typing import Dict diff --git a/pulumi/src/lib/config.py b/pulumi/core/config.py similarity index 99% rename from pulumi/src/lib/config.py rename to pulumi/core/config.py index a907792..ac2f285 100644 --- a/pulumi/src/lib/config.py +++ b/pulumi/core/config.py @@ -1,4 +1,4 @@ -# src/lib/config.py +# core/config.py # Description: Module Configuration Parsing & Loading """ diff --git a/pulumi/src/lib/deploy_module.py b/pulumi/core/deploy_module.py similarity index 92% rename from pulumi/src/lib/deploy_module.py rename to pulumi/core/deploy_module.py index b00caa8..4ccf2bc 100644 --- a/pulumi/src/lib/deploy_module.py +++ b/pulumi/core/deploy_module.py @@ -1,17 +1,18 @@ -# src/lib/deploy_module.py +# core/deploy_module.py import inspect import pulumi +import pulumi_kubernetes as k8s from typing import Any, Dict, List -from src.lib.introspection import discover_config_class, discover_deploy_function -from src.lib.config import get_module_config +from .introspection import discover_config_class, discover_deploy_function +from .config import get_module_config def deploy_module( module_name: str, config: pulumi.Config, default_versions: Dict[str, Any], global_depends_on: List[pulumi.Resource], - k8s_provider: Any, + k8s_provider: k8s.Provider, versions: Dict[str, str], configurations: Dict[str, Dict[str, Any]] ) -> None: diff --git a/pulumi/src/lib/helm_chart_versions.py b/pulumi/core/helm_chart_versions.py similarity index 98% rename from pulumi/src/lib/helm_chart_versions.py rename to pulumi/core/helm_chart_versions.py index ab3dc82..b873880 100644 --- a/pulumi/src/lib/helm_chart_versions.py +++ b/pulumi/core/helm_chart_versions.py @@ -1,3 +1,5 @@ +# core/helm_chart_versions.py + import requests import logging import yaml diff --git a/pulumi/src/lib/init.py b/pulumi/core/init.py similarity index 92% rename from pulumi/src/lib/init.py rename to pulumi/core/init.py index 42eba8e..418e3bd 100644 --- a/pulumi/src/lib/init.py +++ b/pulumi/core/init.py @@ -1,4 +1,4 @@ -# src/lib/init.py +# core/init.py # Description: Initializes Pulumi configuration, Kubernetes provider, and global resources. import os @@ -7,17 +7,17 @@ from pulumi_kubernetes import Provider from typing import Dict, List, Any -from src.lib.versions import load_default_versions -from src.lib.metadata import ( +from .versions import load_default_versions +from .metadata import ( collect_git_info, generate_git_labels, generate_git_annotations, set_global_labels, set_global_annotations ) -from src.lib.compliance import generate_compliance_labels, generate_compliance_annotations -from src.lib.types import ComplianceConfig -from src.lib.utils import generate_global_transformations +from .compliance import generate_compliance_labels, generate_compliance_annotations +from .types import ComplianceConfig +from .utils import generate_global_transformations def initialize_pulumi() -> Dict[str, Any]: """ diff --git a/pulumi/src/lib/introspection.py b/pulumi/core/introspection.py similarity index 81% rename from pulumi/src/lib/introspection.py rename to pulumi/core/introspection.py index 5d73255..6636d70 100644 --- a/pulumi/src/lib/introspection.py +++ b/pulumi/core/introspection.py @@ -1,4 +1,5 @@ -# src/lib/introspection.py +# core/introspection.py + import inspect import importlib from typing import Type @@ -11,7 +12,7 @@ def discover_config_class(module_name: str) -> Type: Returns: Type: The configuration class. """ - types_module = importlib.import_module(f"src.{module_name}.types") + types_module = importlib.import_module(f"modules.{module_name}.types") # Inspect the module to find dataclasses for name, obj in inspect.getmembers(types_module): @@ -19,7 +20,7 @@ def discover_config_class(module_name: str) -> Type: # Developers note: module config dataclass is the first dataclass in src//types.py # src//types.py is a mandatory file and the first dataclass must exist and be the config class. return obj - raise ValueError(f"No dataclass found in src.{module_name}.types") + raise ValueError(f"No dataclass found in modules.{module_name}.types") def discover_deploy_function(module_name: str) -> callable: """ @@ -29,13 +30,13 @@ def discover_deploy_function(module_name: str) -> callable: Returns: callable: The deploy function. """ - deploy_module = importlib.import_module(f"src.{module_name}.deploy") + deploy_module = importlib.import_module(f"modules.{module_name}.deploy") # Look for a deploy function that matches the pattern deploy__module function_name = f"deploy_{module_name}_module" deploy_function = getattr(deploy_module, function_name, None) if not deploy_function: - raise ValueError(f"No deploy function named '{function_name}' found in src.{module_name}.deploy") + raise ValueError(f"No deploy function named '{function_name}' found in modules.{module_name}.deploy") return deploy_function diff --git a/pulumi/src/lib/metadata.py b/pulumi/core/metadata.py similarity index 99% rename from pulumi/src/lib/metadata.py rename to pulumi/core/metadata.py index 1b0413e..c822f23 100644 --- a/pulumi/src/lib/metadata.py +++ b/pulumi/core/metadata.py @@ -1,4 +1,4 @@ -# src/lib/metadata.py +# core/metadata.py import subprocess from typing import Dict diff --git a/pulumi/src/lib/namespace.py b/pulumi/core/namespace.py similarity index 98% rename from pulumi/src/lib/namespace.py rename to pulumi/core/namespace.py index 6ba3fcc..8a269cb 100644 --- a/pulumi/src/lib/namespace.py +++ b/pulumi/core/namespace.py @@ -1,4 +1,4 @@ -# src/lib/namespace.py +# core/namespace.py """ Utility functions for managing Kubernetes namespaces within the Kargo PaaS platform. diff --git a/pulumi/src/lib/types.py b/pulumi/core/types.py similarity index 99% rename from pulumi/src/lib/types.py rename to pulumi/core/types.py index 3114f73..e7739c5 100644 --- a/pulumi/src/lib/types.py +++ b/pulumi/core/types.py @@ -1,4 +1,4 @@ -# src/lib/types.py +# core/types.py """ Types and data structures used across Kargo modules. diff --git a/pulumi/src/lib/utils.py b/pulumi/core/utils.py similarity index 99% rename from pulumi/src/lib/utils.py rename to pulumi/core/utils.py index 641b627..cb04a58 100644 --- a/pulumi/src/lib/utils.py +++ b/pulumi/core/utils.py @@ -1,4 +1,4 @@ -# src/lib/utils.py +# core/utils.py # Description: Utility functions for sanitizing compliance metadata labels. import re diff --git a/pulumi/src/lib/versions.py b/pulumi/core/versions.py similarity index 97% rename from pulumi/src/lib/versions.py rename to pulumi/core/versions.py index 20fec0e..52180e5 100644 --- a/pulumi/src/lib/versions.py +++ b/pulumi/core/versions.py @@ -1,4 +1,4 @@ -# src/lib/versions.py +# core/versions.py import json import os @@ -89,14 +89,14 @@ def load_versions_from_url(url): if versions_stack_name: # Attempt to load versions from a stack-specific file (e.g., ./versions/dev.json) current_dir = os.path.dirname(os.path.abspath(__file__)) - versions_dir = os.path.join(current_dir, '..', '..', 'versions') + versions_dir = os.path.join(current_dir, '..', 'versions') stack_versions_path = os.path.join(versions_dir, f'{stack_name}.json') default_versions = load_versions_from_file(stack_versions_path) # If versions are still not loaded, attempt to load from local default_versions.json if not default_versions: current_dir = os.path.dirname(os.path.abspath(__file__)) - default_versions_path = os.path.join(current_dir, '..', '..', 'default_versions.json') + default_versions_path = os.path.join(current_dir, '..', 'default_versions.json') default_versions = load_versions_from_file(default_versions_path) # If still not loaded, attempt to fetch from a remote URL based on the specified channel diff --git a/pulumi/src/README.md b/pulumi/modules/README.md similarity index 100% rename from pulumi/src/README.md rename to pulumi/modules/README.md diff --git a/pulumi/src/azure/__init__.py b/pulumi/modules/aws/__init__.py similarity index 100% rename from pulumi/src/azure/__init__.py rename to pulumi/modules/aws/__init__.py diff --git a/pulumi/src/aws/deploy.py b/pulumi/modules/aws/deploy.py similarity index 100% rename from pulumi/src/aws/deploy.py rename to pulumi/modules/aws/deploy.py diff --git a/pulumi/src/ceph/__init__.py b/pulumi/modules/azure/__init__.py similarity index 100% rename from pulumi/src/ceph/__init__.py rename to pulumi/modules/azure/__init__.py diff --git a/pulumi/src/cert_manager/__init__.py b/pulumi/modules/ceph/__init__.py similarity index 100% rename from pulumi/src/cert_manager/__init__.py rename to pulumi/modules/ceph/__init__.py diff --git a/pulumi/src/ceph/deploy.py b/pulumi/modules/ceph/deploy.py similarity index 100% rename from pulumi/src/ceph/deploy.py rename to pulumi/modules/ceph/deploy.py diff --git a/pulumi/src/cert_manager/README.md b/pulumi/modules/cert_manager/README.md similarity index 100% rename from pulumi/src/cert_manager/README.md rename to pulumi/modules/cert_manager/README.md diff --git a/pulumi/src/cilium/__init__.py b/pulumi/modules/cert_manager/__init__.py similarity index 100% rename from pulumi/src/cilium/__init__.py rename to pulumi/modules/cert_manager/__init__.py diff --git a/pulumi/src/cert_manager/deploy.py b/pulumi/modules/cert_manager/deploy.py similarity index 96% rename from pulumi/src/cert_manager/deploy.py rename to pulumi/modules/cert_manager/deploy.py index 32eeb86..e3aabed 100644 --- a/pulumi/src/cert_manager/deploy.py +++ b/pulumi/modules/cert_manager/deploy.py @@ -1,15 +1,15 @@ -# src/cert_manager/deploy.py +# modules/cert_manager/deploy.py import pulumi import pulumi_kubernetes as k8s from pulumi_kubernetes.apiextensions.CustomResource import CustomResource from typing import Optional, List, Dict, Any, Tuple -from src.lib.namespace import create_namespace -from src.lib.helm_chart_versions import get_latest_helm_chart_version -from src.cert_manager.types import CertManagerConfig -from src.lib.types import NamespaceConfig -from src.lib.metadata import get_global_annotations, get_global_labels +from core.namespace import create_namespace +from core.helm_chart_versions import get_latest_helm_chart_version +from core.types import NamespaceConfig +from core.metadata import get_global_annotations, get_global_labels +from .types import CertManagerConfig def deploy_cert_manager_module( config_cert_manager: CertManagerConfig, diff --git a/pulumi/src/cert_manager/types.py b/pulumi/modules/cert_manager/types.py similarity index 95% rename from pulumi/src/cert_manager/types.py rename to pulumi/modules/cert_manager/types.py index b0c0bcb..c3ad845 100644 --- a/pulumi/src/cert_manager/types.py +++ b/pulumi/modules/cert_manager/types.py @@ -1,4 +1,4 @@ -# src/cert_manager/types.py +# modules/cert_manager/types.py from dataclasses import dataclass from typing import Optional, Dict, Any diff --git a/pulumi/src/cluster_network_addons/__init__.py b/pulumi/modules/cilium/__init__.py similarity index 100% rename from pulumi/src/cluster_network_addons/__init__.py rename to pulumi/modules/cilium/__init__.py diff --git a/pulumi/src/cilium/deploy.py b/pulumi/modules/cilium/deploy.py similarity index 100% rename from pulumi/src/cilium/deploy.py rename to pulumi/modules/cilium/deploy.py diff --git a/pulumi/src/containerized_data_importer/__init__.py b/pulumi/modules/cluster_network_addons/__init__.py similarity index 100% rename from pulumi/src/containerized_data_importer/__init__.py rename to pulumi/modules/cluster_network_addons/__init__.py diff --git a/pulumi/src/cluster_network_addons/deploy.py b/pulumi/modules/cluster_network_addons/deploy.py similarity index 100% rename from pulumi/src/cluster_network_addons/deploy.py rename to pulumi/modules/cluster_network_addons/deploy.py diff --git a/pulumi/src/hostpath_provisioner/__init__.py b/pulumi/modules/containerized_data_importer/__init__.py similarity index 100% rename from pulumi/src/hostpath_provisioner/__init__.py rename to pulumi/modules/containerized_data_importer/__init__.py diff --git a/pulumi/src/containerized_data_importer/deploy.py b/pulumi/modules/containerized_data_importer/deploy.py similarity index 100% rename from pulumi/src/containerized_data_importer/deploy.py rename to pulumi/modules/containerized_data_importer/deploy.py diff --git a/pulumi/src/kubernetes/__init__.py b/pulumi/modules/hostpath_provisioner/__init__.py similarity index 100% rename from pulumi/src/kubernetes/__init__.py rename to pulumi/modules/hostpath_provisioner/__init__.py diff --git a/pulumi/src/hostpath_provisioner/deploy.py b/pulumi/modules/hostpath_provisioner/deploy.py similarity index 100% rename from pulumi/src/hostpath_provisioner/deploy.py rename to pulumi/modules/hostpath_provisioner/deploy.py diff --git a/pulumi/src/kubernetes_dashboard/__init__.py b/pulumi/modules/kubernetes/__init__.py similarity index 100% rename from pulumi/src/kubernetes_dashboard/__init__.py rename to pulumi/modules/kubernetes/__init__.py diff --git a/pulumi/src/kubevirt/__init__.py b/pulumi/modules/kubernetes_dashboard/__init__.py similarity index 100% rename from pulumi/src/kubevirt/__init__.py rename to pulumi/modules/kubernetes_dashboard/__init__.py diff --git a/pulumi/src/kubernetes_dashboard/deploy.py b/pulumi/modules/kubernetes_dashboard/deploy.py similarity index 100% rename from pulumi/src/kubernetes_dashboard/deploy.py rename to pulumi/modules/kubernetes_dashboard/deploy.py diff --git a/pulumi/src/kv_manager/__init__.py b/pulumi/modules/kubevirt/__init__.py similarity index 100% rename from pulumi/src/kv_manager/__init__.py rename to pulumi/modules/kubevirt/__init__.py diff --git a/pulumi/src/kubevirt/deploy.py b/pulumi/modules/kubevirt/deploy.py similarity index 94% rename from pulumi/src/kubevirt/deploy.py rename to pulumi/modules/kubevirt/deploy.py index 0438bd2..534218e 100644 --- a/pulumi/src/kubevirt/deploy.py +++ b/pulumi/modules/kubevirt/deploy.py @@ -4,17 +4,18 @@ import yaml import tempfile import os -from typing import Optional, List, Tuple, Dict +from typing import Optional, List, Tuple, Dict, Any import pulumi import pulumi_kubernetes as k8s from pulumi_kubernetes.apiextensions.CustomResource import CustomResource from pulumi_kubernetes.meta.v1 import ObjectMetaArgs -from src.lib.namespace import create_namespace -from src.lib.types import NamespaceConfig +from core.types import NamespaceConfig +from core.namespace import create_namespace +from core.metadata import get_global_labels, get_global_annotations + from .types import KubeVirtConfig -from src.lib.metadata import get_global_labels, get_global_annotations def deploy_kubevirt_module( config_kubevirt: KubeVirtConfig, @@ -45,7 +46,7 @@ def deploy_kubevirt_module( # Now deploy KubeVirt, ensuring it depends on the namespace creation kubevirt_version, kubevirt_operator = deploy_kubevirt( config_kubevirt=config_kubevirt, - depends_on=namespace_resource, + depends_on=[namespace_resource], k8s_provider=k8s_provider, namespace_resource=namespace_resource ) @@ -93,7 +94,7 @@ def deploy_kubevirt( # Transform the YAML and set the correct namespace transformed_yaml = _transform_yaml(kubevirt_yaml, namespace) - def kubevirt_transform(obj: dict, opts: pulumi.ResourceOptions): + def kubevirt_transform(obj: dict, opts: pulumi.ResourceOptions) -> pulumi.ResourceTransformationResult: """ Transformation function to add labels and annotations to Kubernetes objects in the KubeVirt operator YAML manifests. @@ -117,8 +118,7 @@ def kubevirt_transform(obj: dict, opts: pulumi.ResourceOptions): obj["spec"]["template"]["metadata"].setdefault("annotations", {}) obj["spec"]["template"]["metadata"]["annotations"].update(annotations) - # Debugging: print the transformed object - #print(f"Transformed object: {obj['metadata']}") + return pulumi.ResourceTransformationResult(obj, opts) # Write the transformed YAML to a temporary file with tempfile.NamedTemporaryFile(delete=False, mode='w') as temp_file: @@ -192,7 +192,7 @@ def kubevirt_transform(obj: dict, opts: pulumi.ResourceOptions): return version, operator -def _transform_yaml(yaml_data, namespace: str) -> List[Dict]: +def _transform_yaml(yaml_data: Any, namespace: str) -> List[Dict]: """ Helper function to transform YAML to set namespace and modify resources. diff --git a/pulumi/src/kubevirt/types.py b/pulumi/modules/kubevirt/types.py similarity index 95% rename from pulumi/src/kubevirt/types.py rename to pulumi/modules/kubevirt/types.py index 90cc7d0..ab247a2 100644 --- a/pulumi/src/kubevirt/types.py +++ b/pulumi/modules/kubevirt/types.py @@ -1,9 +1,9 @@ -# src/kubevirt/types.py +# modules/kubevirt/types.py from dataclasses import dataclass, field from typing import Optional, Dict, Any import pulumi -from src.lib.metadata import get_global_labels, get_global_annotations +from core.metadata import get_global_labels, get_global_annotations @dataclass class KubeVirtConfig: diff --git a/pulumi/src/lib/__init__.py b/pulumi/modules/kv_manager/__init__.py similarity index 100% rename from pulumi/src/lib/__init__.py rename to pulumi/modules/kv_manager/__init__.py diff --git a/pulumi/src/kv_manager/deploy.py b/pulumi/modules/kv_manager/deploy.py similarity index 100% rename from pulumi/src/kv_manager/deploy.py rename to pulumi/modules/kv_manager/deploy.py diff --git a/pulumi/src/local_path_storage/__init__.py b/pulumi/modules/local_path_storage/__init__.py similarity index 100% rename from pulumi/src/local_path_storage/__init__.py rename to pulumi/modules/local_path_storage/__init__.py diff --git a/pulumi/src/local_path_storage/deploy.py b/pulumi/modules/local_path_storage/deploy.py similarity index 100% rename from pulumi/src/local_path_storage/deploy.py rename to pulumi/modules/local_path_storage/deploy.py diff --git a/pulumi/src/multus/__init__.py b/pulumi/modules/multus/__init__.py similarity index 100% rename from pulumi/src/multus/__init__.py rename to pulumi/modules/multus/__init__.py diff --git a/pulumi/src/multus/deploy.py b/pulumi/modules/multus/deploy.py similarity index 100% rename from pulumi/src/multus/deploy.py rename to pulumi/modules/multus/deploy.py diff --git a/pulumi/src/openunison/__init__.py b/pulumi/modules/openunison/__init__.py similarity index 100% rename from pulumi/src/openunison/__init__.py rename to pulumi/modules/openunison/__init__.py diff --git a/pulumi/src/openunison/assets/alertmanager.png b/pulumi/modules/openunison/assets/alertmanager.png similarity index 100% rename from pulumi/src/openunison/assets/alertmanager.png rename to pulumi/modules/openunison/assets/alertmanager.png diff --git a/pulumi/src/openunison/assets/grafana.png b/pulumi/modules/openunison/assets/grafana.png similarity index 100% rename from pulumi/src/openunison/assets/grafana.png rename to pulumi/modules/openunison/assets/grafana.png diff --git a/pulumi/src/openunison/assets/kubevirt.png b/pulumi/modules/openunison/assets/kubevirt.png similarity index 100% rename from pulumi/src/openunison/assets/kubevirt.png rename to pulumi/modules/openunison/assets/kubevirt.png diff --git a/pulumi/src/openunison/assets/prometheus.png b/pulumi/modules/openunison/assets/prometheus.png similarity index 100% rename from pulumi/src/openunison/assets/prometheus.png rename to pulumi/modules/openunison/assets/prometheus.png diff --git a/pulumi/src/openunison/deploy.py b/pulumi/modules/openunison/deploy.py similarity index 100% rename from pulumi/src/openunison/deploy.py rename to pulumi/modules/openunison/deploy.py diff --git a/pulumi/src/openunison/encoded_assets.py b/pulumi/modules/openunison/encoded_assets.py similarity index 100% rename from pulumi/src/openunison/encoded_assets.py rename to pulumi/modules/openunison/encoded_assets.py diff --git a/pulumi/src/prometheus/__init__.py b/pulumi/modules/prometheus/__init__.py similarity index 100% rename from pulumi/src/prometheus/__init__.py rename to pulumi/modules/prometheus/__init__.py diff --git a/pulumi/src/prometheus/deploy.py b/pulumi/modules/prometheus/deploy.py similarity index 100% rename from pulumi/src/prometheus/deploy.py rename to pulumi/modules/prometheus/deploy.py diff --git a/pulumi/src/vm/__init__.py b/pulumi/modules/vm/__init__.py similarity index 100% rename from pulumi/src/vm/__init__.py rename to pulumi/modules/vm/__init__.py diff --git a/pulumi/src/vm/talos.py b/pulumi/modules/vm/talos.py similarity index 100% rename from pulumi/src/vm/talos.py rename to pulumi/modules/vm/talos.py diff --git a/pulumi/src/vm/ubuntu.py b/pulumi/modules/vm/ubuntu.py similarity index 100% rename from pulumi/src/vm/ubuntu.py rename to pulumi/modules/vm/ubuntu.py From d42272b2a3131d9eb36480648231f8a7c25ec8ef Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Fri, 20 Sep 2024 11:39:11 -0700 Subject: [PATCH 21/43] refactor file tree layout --- pulumi/core/metadata.py | 3 +++ pulumi/stacks/Pulumi.ci.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pulumi/core/metadata.py b/pulumi/core/metadata.py index c822f23..cc69ab8 100644 --- a/pulumi/core/metadata.py +++ b/pulumi/core/metadata.py @@ -1,5 +1,8 @@ # core/metadata.py +# TODO: enhance with support for propagation of labels/annotations on AWS resources +# TODO: enhance with + import subprocess from typing import Dict from .utils import sanitize_label_value, extract_repo_name diff --git a/pulumi/stacks/Pulumi.ci.yaml b/pulumi/stacks/Pulumi.ci.yaml index f5e729f..ef43a3b 100644 --- a/pulumi/stacks/Pulumi.ci.yaml +++ b/pulumi/stacks/Pulumi.ci.yaml @@ -49,7 +49,7 @@ config: kargo:kubernetes: context: admin@talos-kargo-docker distribution: talos - kubeconfig: /workspaces/Kargo/.kube/config + kubeconfig: /Users/usrbinkat/drive/Git/ccio/kargo/.kube/config kargo:multus: enabled: false kargo:vm: From 565040e2c58892173dad2f847a495281698c7ced Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Fri, 20 Sep 2024 18:54:07 -0700 Subject: [PATCH 22/43] add code of conduct and contributor docs --- CODE_OF_CONDUCT.md | 103 +++++++++++++++++++++++++ CONTRIBUTOR.md | 183 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 286 insertions(+) create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTOR.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..c7e6885 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,103 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [conduct@containercraft.org](mailto:conduct@containercraft.org). All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression towards or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). + +For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. + +--- + +## Reporting Guide + +To make sure everyone understands how to report a Code of Conduct violation, here are some detailed instructions: + +1. **Write down what happened.** + - Include the times and dates of incidents if possible. + - Remember, if you feel any aspect of this experience might help make things better for the community, it is worth reporting. + +2. **Submit your report.** + - You can report via email: [conduct@containercraft.org](mailto:conduct@containercraft.org). + +3. **Wait for response.** + - You should get a response from us within a few days. We take every report seriously and will inform you of any steps we will be taking as a result of your complaint. + +## Values and Principles + +At ContainerCraft, our core principles revolve around creating an inclusive, safe, and growth-oriented community. We believe that each individual has the potential to contribute meaningfully, and we strive to provide an environment that fosters learning, respect, and collaboration. We commit to: + +- Continually improve community health and inclusivity. +- Promote a culture where new ideas are welcome, and learning from mistakes is encouraged. +- Prioritize the safety and well-being of our members above all else. + +Thank you for helping to make this a welcoming and respectful community for all. diff --git a/CONTRIBUTOR.md b/CONTRIBUTOR.md new file mode 100644 index 0000000..a6e5cec --- /dev/null +++ b/CONTRIBUTOR.md @@ -0,0 +1,183 @@ +# Contributing to Kargo + +First off, thank you for your interest in Kargo! The Kargo project is designed to provide a delightful Developer Experience (DX) and User Experience (UX), and we welcome contributions from the community to help continue this mission. Whether you're fixing a bug, adding a new feature, or improving documentation, your contributions are greatly appreciated. + +--- + +## Table of Contents + +1. [Code of Conduct](#code-of-conduct) +2. [Getting Started](#getting-started) + - [Prerequisites](#prerequisites) + - [Setting up Development Environment](#setting-up-development-environment) +3. [How to Contribute](#how-to-contribute) + - [Reporting Bugs](#reporting-bugs) + - [Suggesting Enhancements](#suggesting-enhancements) + - [Submitting Changes](#submitting-changes) +4. [Style Guides](#style-guides) + - [Python Style Guide](#python-style-guide) + - [Commit Messages](#commit-messages) + - [Documentation](#documentation) +5. [Testing](#testing) +6. [Continuous Integration](#continuous-integration) +7. [Communication](#communication) +8. [Acknowledgements](#acknowledgements) +9. [Additional Resources](#additional-resources) + +--- + +## Code of Conduct + +This project adheres to the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [emcee@braincraft.io](mailto:emcee@braincraft.io). + +--- + +## Getting Started + +### Prerequisites + +- **Github Account**: +- **Docker / Docker Desktop**: +- **VSCode**: +- **VSCode | Devcontainer Extension**: + +### Setting up Development Environment + +1. **Fork the Repository**: + - Navigate to the [Kargo GitHub repository](https://github.com/your-org/kargo) and click "Fork". + +2. **Clone the Forked Repository**: + ```sh + git clone https://github.com/your-username/kargo.git + cd kargo + ``` + +3. **Launch VSCode**: + + > NOTE: When prompted, relaunch in devcontainer wich will supply all dependencies. + + ```sh + code . + ``` + +4. **Login & Install Dependencies**: + ```sh + pulumi login + pulumi install + ``` + +5. **Configure Pre-commit Hooks**: + ```sh + pip install pre-commit + pre-commit install + ``` + +--- + +## How to Contribute + +### Reporting Bugs + +If you've found a bug, please open an issue on GitHub. Fill out the provided template with as much detail as possible, including: +- The version of Kargo you're using +- Steps to reproduce the bug +- Any relevant logs or screenshots + +### Suggesting Enhancements + +To suggest an enhancement, please open an issue with the "enhancement" label. Provide a clear description of the improvement and why it would be beneficial for the project. + +### Submitting Changes + +1. **Create a Branch**: + ```sh + git checkout -b feature/user/my-new-feature + ``` + +2. **Make Your Changes**: + - Ensure your code adheres to the [Python Style Guide](#python-style-guide). + - Write tests to cover your changes. + +3. **Commit Your Changes**: + ```sh + git add . + git commit -m "feat: add new feature" + ``` + +4. **Push to Your Fork**: + ```sh + git push origin user/feature/my-new-feature + ``` + +5. **Open a Pull Request**: + - Navigate to the repository on GitHub and click "Compare & pull request". Fill in the template and submit. + +--- + +## Style Guides + +### Python Style Guide + +Adhere to [PEP 8](https://www.python.org/dev/peps/pep-0008/) and aim for clean, readable code. We recommend using `flake8` and `black` to maintain code quality: +```sh +pip install flake8 black +flake8 . +black . +``` + +### Commit Messages + +- Follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. +- Example Commit Message: + ``` + feat: add support for Kubernetes 1.24 + + This commit adds compatibility with Kubernetes 1.24, ensuring all API changes are accommodated. + ``` + +### Documentation + +- Use clear and concise docstrings for functions and classes. +- Update `README.md` and other relevant documentation files as applicable. + +--- + +## Testing + +1. **Unit Tests**: Your code should be accompanied by unit tests to ensure robust coverage. +2. **Running Tests**: + ```sh + pytest + ``` + +--- + +## Continuous Integration + +We use GitHub Actions for CI/CD. All pull requests will be automatically tested. Please ensure your PR passes all checks before requesting a review. + +--- + +## Communication + +- **GitHub Issues**: For proposing features, reporting bugs, and suggesting improvements. +- **Email**: For sensitive or private communication, please contact [emcee@braincraft.io](mailto:emcee@braincraft.io). + +--- + +## Acknowledgements + +Thank you to all the contributors who make this project possible. Your time and effort are sincerely appreciated! + +--- + +## Additional Resources + +- [Pulumi Documentation](https://www.pulumi.com/docs/) +- [Kubernetes Documentation](https://kubernetes.io/docs/) +- [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/) +- [GitHub Actions Documentation](https://docs.github.com/en/actions) + +--- + +Thank you for contributing to Kargo! From 8f2f4aa6035bd1a411bd1f7210a75ad816e9c476 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Fri, 20 Sep 2024 18:54:31 -0700 Subject: [PATCH 23/43] refine core and module codebase --- pulumi/README.md | 154 +++++++++++-- pulumi/__main__.py | 30 +-- pulumi/core/compliance.py | 24 +- pulumi/core/config.py | 15 +- pulumi/core/deploy_module.py | 87 +++++--- pulumi/core/helm_chart_versions.py | 31 ++- pulumi/core/init.py | 31 +-- pulumi/core/introspection.py | 21 +- pulumi/core/metadata.py | 34 +-- pulumi/core/namespace.py | 24 +- pulumi/core/types.py | 16 +- pulumi/core/utils.py | 55 +++-- pulumi/core/versions.py | 73 ++---- pulumi/modules/cert_manager/deploy.py | 306 ++++++++++++-------------- pulumi/modules/cert_manager/types.py | 11 +- pulumi/modules/kubevirt/deploy.py | 25 ++- pulumi/modules/kubevirt/types.py | 36 +-- 17 files changed, 542 insertions(+), 431 deletions(-) diff --git a/pulumi/README.md b/pulumi/README.md index b66f3d8..96d675b 100644 --- a/pulumi/README.md +++ b/pulumi/README.md @@ -3,14 +3,15 @@ Kargo Kubevirt Kubernetes PaaS - Pulumi Python Infrastructure as Code (IaC) > Prime Directive: "Features are nice. Quality is paramount." -> + > Quality is not just about the product or code. Enjoyable developer experience is imperative to the survival of FOSS. -ContainerCraft is a Developer Experience (DX) and User Experience (UX) obsessed project. As part of the CCIO Open Source education and skill development ecosystem, Kargo project's survival is dependent on the happiness of community developers and users. +ContainerCraft is a Developer Experience (DX) and User Experience (UX) obsessed project. As part of the CCIO Open Source education and skill development ecosystem, the Kargo project's survival is dependent on the happiness of community developers and users. + +### **Developer Directives** -#### **Developer Directives** - **Improve Code Maintainability:** Enhance structure and organization. Prioritize readable, reusable, and extensible code. -- **Optimize Performance:** Code execution performance. honor configuration. Do not execute inactive code. +- **Optimize Performance:** Improve code execution performance. Honor configuration. Do not execute inactive code. - **Establish Standard Practices:** Develop a consistent approach to configuration handling, module deployment, and code organization to guide future development. --- @@ -19,26 +20,139 @@ ContainerCraft is a Developer Experience (DX) and User Experience (UX) obsessed The following guidelines promote happiness as a means of promoting growth and sustainability. -| **Imperative** | **Explanation** | -|--------------------------|-------------------------------------------------------------------------------------------------------| -| **User Experience (UX)** | Provide clear error messages, and logging to improve intuitive learning, development, and debugging. | -| **Developer Experience (DX)** | Optimize DX with clear documentation, examples, and architecture. | -| **Configurable Modules** | Support user module customization via the pulumi stack configuration pattern. | -| **Module Data Classes** | Utilize typed dataclasses to safely encapsulate module configuration. | -| **Sane Defaults in Data Classes** | Include sensible default values for module configurations. | +| **Imperative** | **Explanation** | +|----------------------------|--------------------------------------------------------------------------------------------------| +| **User Experience (UX)** | Provide clear error messages and logging to improve intuitive learning, development, and debugging. | +| **Developer Experience (DX)** | Optimize DX with clear documentation, examples, and architecture. | +| **Configurable Modules** | Support user module customization via the Pulumi stack configuration pattern. | +| **Module Data Classes** | Utilize typed dataclasses to safely encapsulate module configuration. | +| **Sane Defaults in Data Classes** | Include sensible default values for module configurations. | | **User Configuration Handling** | Merge user-provided configurations with defaults. Allow minimal input for module configuration. | -| **Simple Function Signatures** | Reduce parameter count. Encapsulate configuration objects in function signatures. | -| **Type Annotations** | Enhance readability and maintainability with type annotations. | -| **Safe Function Signatures** | Enforce type safety. Raise unknown configuration keys gracefully. | +| **Simple Function Signatures** | Reduce parameter count. Encapsulate configuration objects in function signatures. | +| **Type Annotations** | Enhance readability and maintainability with type annotations. | +| **Safe Function Signatures** | Enforce type safety. Raise unknown configuration keys gracefully. | | **Maintain a streamlined entrypoint** | Minimize top-level code. Encapsulate module logic within the module directory and code. | -| **Reuse + Dedupe code** | Refactor common patterns and logic into shared utilities in `src/lib/`. Adopt shared utilities when possible. | -| **Version Control Dependencies** | Utilize `versions.py` to manage component versions within `__main__.py`. | -| **Transparency** | Return informative version and configuration values to `version` and `configuration` dictionaries. | -| **Conditional Execution** | Use conditional imports and execution. Prevent unnecessary code execution when modules are disabled. | -| **Remove Deprecated Code** | Eliminate deprecated feature code. | +| **Reuse + Dedupe code** | Refactor common patterns and logic into shared utilities in `core/utils.py`. Adopt shared utilities when possible. | +| **Version Control Dependencies** | Utilize `versions.py` to manage component versions within `__main__.py`. | +| **Transparency** | Return informative version and configuration values to `version` and `configuration` dictionaries. | +| **Conditional Execution** | Use conditional imports and execution. Prevent unnecessary code execution when modules are disabled. | +| **Remove Deprecated Code** | Eliminate deprecated feature code. | --- ### **Detailed Breakdown** -**TODO**: Add detailed breakdown of each imperative and explanation with code snippet examples and links to exemplary code. +1. **User Experience (UX):** + - **Clear Error Messages:** Provide meaningful error messages that guide users to resolve issues. + - **Uniform Logging:** Use consistent logging practices to make debugging easier. For instance: + ```python + pulumi.log.info(f"Deploying module: {module_name}") + ``` + +2. **Developer Experience (DX):** + - **Documentation:** Add comprehensive docstrings and comments. For instance: + ```python + def deploy_module(...): + """ + Helper function to deploy a module based on configuration. + ... + """ + ``` + - **Examples:** Include example configurations and usage in the documentation. + +3. **Configurable Modules:** + - **Pulumi Stack Configuration:** Use the Pulumi config object to roll out custom module configurations. + - **Example:** + ```python + module_config = config.get_object("module_name") or {} + ``` + +4. **Module Data Classes:** + - **Typed Data Classes:** Encapsulate configurations clearly using `dataclass`. Example: + ```python + from dataclasses import dataclass + @dataclass + class KubeVirtConfig: + namespace: str = "default" + ``` + +5. **Sane Defaults in Data Classes:** + - **Sensible Defaults:** Set reasonable default values to ensure minimal user configuration. + - **Example:** + ```python + @dataclass + class CertManagerConfig: + namespace: str = "cert-manager" + install_crds: bool = True + ``` + +6. **User Configuration Handling:** + - **Merge Configurations:** Combine user-provided configurations with defaults. Example: + ```python + @staticmethod + def merge(user_config: Dict[str, Any]) -> 'CertManagerConfig': + default_config = CertManagerConfig() + for key, value in user_config.items(): + if hasattr(default_config, key): + setattr(default_config, key, value) + return default_config + ``` + +7. **Simple Function Signatures:** + - **Reduce Parameters:** Keep function signatures minimal by encapsulating configurations. + - **Example:** + ```python + def deploy_module(module_config: ModuleConfig) + ``` + +8. **Type Annotations:** + - **Enhance Readability:** + ```python + def deploy_module(module_name: str, config: pulumi.Config) -> None: + ``` + +9. **Safe Function Signatures:** + - **Type Safety:** Use consistent type checks and raise meaningful errors. Example: + ```python + if not hasattr(default_config, key): + pulumi.log.warn(f"Unknown configuration key '{key}' in config.") + ``` + +10. **Streamlined Entrypoint:** + - **Minimize Top-Level Code:** Encapsulate logic within the module and related files. Example: + ```python + if __name__ == "__main__": + main() + ``` + +11. **Reuse + Dedupe Code:** + - **Central Utilities:** Use common patterns by placing reusable code in `core/utils.py`. + - **Example:** + ```python + from core.utils import sanitize_label_value, extract_repo_name + ``` + +12. **Version Control Dependencies:** + - **Manage Versions:** Control component versions within `versions.py` to maintain consistency. + - **Example:** + ```python + default_versions = load_default_versions(config) + ``` + +13. **Transparency:** + - **Informative Outputs:** Export configuration and version information. + - **Example:** + ```python + pulumi.export("versions", versions) + ``` + +14. **Conditional Execution:** + - **Avoid Unnecessary Execution:** Load and execute only the necessary modules. + - **Example:** + ```python + if module_enabled: + deploy_func(...) + ``` + +15. **Remove Deprecated Code:** + - **Eliminate Obsolete Features:** Keep the codebase clean and update features as required. diff --git a/pulumi/__main__.py b/pulumi/__main__.py index 7a0c341..d749681 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -1,49 +1,39 @@ -# __main__.py +# ./pulumi/__main__.py +# Description: -# Import General Purpose Libraries from typing import List, Dict, Any -# Import Pulumi Libraries import pulumi from pulumi_kubernetes import Provider -# Import Local Libraries from core.init import initialize_pulumi from core.config import export_results from core.deploy_module import deploy_module def main(): - """ - Main entry point for the Kargo Kubevirt Pulumi IaC. - Initializes the Pulumi program, configures resources, and deploys specified modules. - """ try: - # Initialize Pulumi resources init = initialize_pulumi() - # Retrieve initialized resources config = init["config"] k8s_provider = init["k8s_provider"] versions = init["versions"] configurations = init["configurations"] default_versions = init["default_versions"] global_depends_on = init["global_depends_on"] - compliance_config = init["compliance_config"] - # Define the list of modules to deploy modules_to_deploy = ["cert_manager", "kubevirt"] - # Deploy each module deploy_modules(modules_to_deploy, config, default_versions, global_depends_on, k8s_provider, versions, configurations) - # Export results + compliance_config = init.get("compliance_config", {}) export_results(versions, configurations, compliance_config) except Exception as e: pulumi.log.error(f"Deployment failed: {str(e)}") raise + def deploy_modules( modules: List[str], config: pulumi.Config, @@ -53,18 +43,7 @@ def deploy_modules( versions: Dict[str, str], configurations: Dict[str, Dict[str, Any]] ) -> None: - """ - Deploy the specified modules. - Args: - modules (list): List of module names to deploy. - config (pulumi.Config): The Pulumi configuration object. - default_versions (dict): Default versions for modules. - global_depends_on (list): Global dependencies. - k8s_provider (Provider): Kubernetes provider object. - versions (dict): Dictionary to store versions of deployed modules. - configurations (dict): Dictionary to store configurations of deployed modules. - """ for module_name in modules: pulumi.log.info(f"Deploying module: {module_name}") deploy_module( @@ -77,5 +56,6 @@ def deploy_modules( configurations=configurations, ) + if __name__ == "__main__": main() diff --git a/pulumi/core/compliance.py b/pulumi/core/compliance.py index d5cc237..329a036 100644 --- a/pulumi/core/compliance.py +++ b/pulumi/core/compliance.py @@ -1,4 +1,4 @@ -# core/compliance.py +# ./pulumi/core/compliance.py import json from typing import Dict @@ -6,8 +6,16 @@ from .utils import sanitize_label_value def generate_compliance_labels(compliance_config: ComplianceConfig) -> Dict[str, str]: + """ + Generates compliance labels based on the given compliance configuration. + + Args: + compliance_config (ComplianceConfig): The compliance configuration object. + + Returns: + Dict[str, str]: A dictionary of compliance labels. + """ labels = {} - # Only essential selection flags should be kept in labels if compliance_config.fisma.enabled: labels['compliance.fisma.enabled'] = 'true' if compliance_config.nist.enabled: @@ -17,18 +25,24 @@ def generate_compliance_labels(compliance_config: ComplianceConfig) -> Dict[str, return labels def generate_compliance_annotations(compliance_config: ComplianceConfig) -> Dict[str, str]: + """ + Generates compliance annotations based on the given compliance configuration. + + Args: + compliance_config (ComplianceConfig): The compliance configuration object. + + Returns: + Dict[str, str]: A dictionary of compliance annotations. + """ annotations = {} - # Serialize the more complex metadata into JSON objects or strings if compliance_config.fisma.level: annotations['compliance.fisma.level'] = compliance_config.fisma.level if compliance_config.fisma.ato: annotations['compliance.fisma.ato'] = json.dumps(compliance_config.fisma.ato) # Store as JSON - if compliance_config.nist.controls: annotations['compliance.nist.controls'] = json.dumps(compliance_config.nist.controls) # Store as JSON array if compliance_config.nist.auxiliary: annotations['compliance.nist.auxiliary'] = json.dumps(compliance_config.nist.auxiliary) if compliance_config.nist.exceptions: annotations['compliance.nist.exceptions'] = json.dumps(compliance_config.nist.exceptions) - return annotations diff --git a/pulumi/core/config.py b/pulumi/core/config.py index ac2f285..aeb9692 100644 --- a/pulumi/core/config.py +++ b/pulumi/core/config.py @@ -1,4 +1,4 @@ -# core/config.py +# ./pulumi/core/config.py # Description: Module Configuration Parsing & Loading """ @@ -26,25 +26,16 @@ def get_module_config( default_versions (Dict[str, Any]): A dictionary of default versions for modules. Returns: - Tuple[Dict[str, Any], bool]: - A tuple containing the module's configuration dictionary - and a boolean indicating if the module is enabled. + Tuple[Dict[str, Any], bool]: A tuple containing the module's configuration dictionary and a boolean indicating if the module is enabled. """ - # Get the module's configuration from Pulumi config, default to an empty dict module_config = config.get_object(module_name) or {} - - # Check if the module is enabled module_enabled = str(module_config.pop('enabled', 'false')).lower() == "true" - - # Use specified version or fall back to the default version if not specified module_config['version'] = module_config.get('version', default_versions.get(module_name)) - return module_config, module_enabled def export_results(versions: Dict[str, str], configurations: Dict[str, Dict[str, Any]], compliance: Dict[str, Any]): """ - Exports the results of the deployment processes including versions, - configurations, and compliance information. + Exports the results of the deployment processes including versions, configurations, and compliance information. Args: versions (Dict[str, str]): A dictionary containing the versions of the deployed modules. diff --git a/pulumi/core/deploy_module.py b/pulumi/core/deploy_module.py index 4ccf2bc..b220c92 100644 --- a/pulumi/core/deploy_module.py +++ b/pulumi/core/deploy_module.py @@ -1,9 +1,10 @@ -# core/deploy_module.py +# ./pulumi/core/deploy_module.py +# Description: import inspect import pulumi import pulumi_kubernetes as k8s -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from .introspection import discover_config_class, discover_deploy_function from .config import get_module_config @@ -21,13 +22,33 @@ def deploy_module( Args: module_name (str): Name of the module. - config: Pulumi configuration object. - default_versions: Default versions for modules. - global_depends_on: Global dependencies. - k8s_provider: Kubernetes provider. - versions: Dictionary to store versions of deployed modules. - configurations: Dictionary to store configurations of deployed modules. + config (pulumi.Config): Pulumi configuration object. + default_versions (Dict[str, Any]): Default versions for modules. + global_depends_on (List[pulumi.Resource]): Global dependencies. + k8s_provider (k8s.Provider): Kubernetes provider. + versions (Dict[str, str]): Dictionary to store versions of deployed modules. + configurations (Dict[str, Dict[str, Any]]): Dictionary to store configurations of deployed modules. + + Raises: + TypeError: If any arguments have incorrect types. + ValueError: If any module-specific errors occur. """ + # Validate parameter types + if not isinstance(module_name, str): + raise TypeError("module_name must be a string") + if not isinstance(config, pulumi.Config): + raise TypeError("config must be an instance of pulumi.Config") + if not isinstance(default_versions, dict): + raise TypeError("default_versions must be a dictionary") + if not isinstance(global_depends_on, list): + raise TypeError("global_depends_on must be a list") + if not isinstance(k8s_provider, k8s.Provider): + raise TypeError("k8s_provider must be an instance of pulumi_kubernetes.Provider") + if not isinstance(versions, dict): + raise TypeError("versions must be a dictionary") + if not isinstance(configurations, dict): + raise TypeError("configurations must be a dictionary") + # Get the module's configuration from Pulumi config module_config_dict, module_enabled = get_module_config(module_name, config, default_versions) @@ -43,28 +64,36 @@ def deploy_module( config_arg_name = list(deploy_func_args)[0] # Assuming the first argument is the config object # Deploy the module using its deploy function with the correct arguments - result = deploy_func( - **{config_arg_name: config_obj}, - global_depends_on=global_depends_on, - k8s_provider=k8s_provider, - ) + try: + result = deploy_func( + **{config_arg_name: config_obj}, + global_depends_on=global_depends_on, + k8s_provider=k8s_provider, + ) + + # Handle the result based on its structure + if isinstance(result, tuple) and len(result) == 3: + version, release, exported_value = result + elif isinstance(result, tuple) and len(result) == 2: + version, release = result + exported_value = None + else: + raise ValueError(f"Unexpected return value structure from {module_name} deploy function") + + # Record the deployed version and configuration + versions[module_name] = version + configurations[module_name] = {"enabled": module_enabled} - # Handle the result based on its structure - if isinstance(result, tuple) and len(result) == 3: - version, release, exported_value = result - elif isinstance(result, tuple) and len(result) == 2: - version, release = result - exported_value = None - else: - raise ValueError(f"Unexpected return value structure from {module_name} deploy function") + # Export any additional values if needed + if exported_value: + pulumi.export(f"{module_name}_exported_value", exported_value) - # Record the deployed version and configuration - versions[module_name] = version - configurations[module_name] = {"enabled": module_enabled} + # Add the release to global dependencies to maintain resource ordering + global_depends_on.append(release) - # Export any additional values if needed - if exported_value: - pulumi.export(f"{module_name}_exported_value", exported_value) + except Exception as e: + pulumi.log.error(f"Deployment failed for module {module_name}: {str(e)}") + raise - # Add the release to global dependencies to maintain resource ordering - global_depends_on.append(release) + else: + pulumi.log.info(f"Module {module_name} is not enabled.") diff --git a/pulumi/core/helm_chart_versions.py b/pulumi/core/helm_chart_versions.py index b873880..8d052df 100644 --- a/pulumi/core/helm_chart_versions.py +++ b/pulumi/core/helm_chart_versions.py @@ -1,23 +1,35 @@ -# core/helm_chart_versions.py +# ./pulumi/core/helm_chart_versions.py +# Description: import requests import logging import yaml from packaging.version import parse as parse_version, InvalidVersion, Version +from typing import Dict, Any # Set up basic logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') -def is_stable_version(version_str): - """Check if the version string is a valid and stable semantic version.""" +HELM_CHART_URL_TEMPLATE = "https://github.com/containercraft/kargo/releases/latest/download/" +CHART_NOT_FOUND = "Chart not found" + +def is_stable_version(version_str: str) -> bool: + """ + Check if the version string is a valid and stable semantic version. + + Args: + version_str (str): The version string to check. + + Returns: + bool: True if the version is stable, False otherwise. + """ try: parsed_version = parse_version(version_str) - # Check if it's a stable version (no pre-release or dev metadata) return isinstance(parsed_version, Version) and not parsed_version.is_prerelease and not parsed_version.is_devrelease except InvalidVersion: return False -def get_latest_helm_chart_version(url, chart_name): +def get_latest_helm_chart_version(url: str, chart_name: str) -> str: """ Fetches the latest stable version of a Helm chart from a given URL. @@ -27,10 +39,6 @@ def get_latest_helm_chart_version(url, chart_name): Returns: str: The latest stable version of the Helm chart, or an error message if the chart is not found or an error occurs during fetching. - - Raises: - requests.RequestException: If an error occurs during the HTTP request. - """ try: logging.info(f"Fetching URL: {url}") @@ -41,16 +49,15 @@ def get_latest_helm_chart_version(url, chart_name): index = yaml.safe_load(response.content) if chart_name in index['entries']: chart_versions = index['entries'][chart_name] - # Filter out non-stable versions and sort stable_versions = [v for v in chart_versions if is_stable_version(v['version'])] if not stable_versions: logging.info(f"No stable versions found for chart '{chart_name}'.") - return "No stable version found" + return CHART_NOT_FOUND latest_chart = max(stable_versions, key=lambda x: parse_version(x['version'])) return latest_chart['version'] else: logging.info(f"No chart named '{chart_name}' found in repository.") - return "Chart not found" + return CHART_NOT_FOUND except requests.RequestException as e: logging.error(f"Error fetching data: {e}") diff --git a/pulumi/core/init.py b/pulumi/core/init.py index 418e3bd..f7f0059 100644 --- a/pulumi/core/init.py +++ b/pulumi/core/init.py @@ -1,4 +1,4 @@ -# core/init.py +# ./pulumi/core/init.py # Description: Initializes Pulumi configuration, Kubernetes provider, and global resources. import os @@ -22,72 +22,53 @@ def initialize_pulumi() -> Dict[str, Any]: """ Initializes the Pulumi configuration, Kubernetes provider, and global resources. + + Returns: + Dict[str, Any]: A dictionary containing initialized resources. """ - # Load Pulumi config config = pulumi.Config() stack_name = pulumi.get_stack() project_name = pulumi.get_project() - # Load default versions for modules default_versions = load_default_versions(config) - - # Initialize dictionaries for versions and configurations versions: Dict[str, str] = {} configurations: Dict[str, Dict[str, Any]] = {} - - # Initialize a list to manage dependencies globally global_depends_on: List[pulumi.Resource] = [] - # Retrieve Kubernetes settings from Pulumi config or environment variables kubernetes_config = config.get_object("kubernetes") or {} kubeconfig = kubernetes_config.get("kubeconfig") or os.getenv('KUBECONFIG') kubernetes_context = kubernetes_config.get("context") - # Log the Kubernetes configuration details - pulumi.log.info(f"kubeconfig: {kubeconfig}") - pulumi.log.info(f"kubernetes_context: {kubernetes_context}") + pulumi.log.info(f"Kubeconfig: {kubeconfig}") + pulumi.log.info(f"Kubernetes context: {kubernetes_context}") - # Create a Kubernetes provider instance k8s_provider = Provider( "k8sProvider", kubeconfig=kubeconfig, context=kubernetes_context, ) - # Retrieve the Git repository source, branch, and commit metadata git_info = collect_git_info() - - # Append Git metadata to the configurations dictionary under 'source_repository' configurations["source_repository"] = { "remote": git_info["remote"], "branch": git_info["branch"], "commit": git_info["commit"] } - # Collect compliance configuration compliance_config_dict = config.get_object('compliance') or {} compliance_config = ComplianceConfig.merge(compliance_config_dict) - - # Generate compliance labels and annotations compliance_labels = generate_compliance_labels(compliance_config) compliance_annotations = generate_compliance_annotations(compliance_config) - # Generate Git labels and annotations git_labels = generate_git_labels(git_info) git_annotations = generate_git_annotations(git_info) - - # Combine labels and annotations global_labels = {**compliance_labels, **git_labels} global_annotations = {**compliance_annotations, **git_annotations} - # Store global labels and annotations for access in other modules set_global_labels(global_labels) set_global_annotations(global_annotations) - - # Apply global transformations to all resources generate_global_transformations(global_labels, global_annotations) - # Return all initialized resources return { "config": config, "stack_name": stack_name, diff --git a/pulumi/core/introspection.py b/pulumi/core/introspection.py index 6636d70..670eab0 100644 --- a/pulumi/core/introspection.py +++ b/pulumi/core/introspection.py @@ -1,42 +1,39 @@ -# core/introspection.py +# ./pulumi/core/introspection.py +# Description: import inspect import importlib -from typing import Type +from typing import Type, Callable, Optional def discover_config_class(module_name: str) -> Type: """ Discovers and returns the configuration class from the module's types.py. + Args: module_name (str): The name of the module. + Returns: Type: The configuration class. """ types_module = importlib.import_module(f"modules.{module_name}.types") - - # Inspect the module to find dataclasses for name, obj in inspect.getmembers(types_module): if inspect.isclass(obj) and hasattr(obj, "__dataclass_fields__"): - # Developers note: module config dataclass is the first dataclass in src//types.py - # src//types.py is a mandatory file and the first dataclass must exist and be the config class. return obj raise ValueError(f"No dataclass found in modules.{module_name}.types") -def discover_deploy_function(module_name: str) -> callable: +def discover_deploy_function(module_name: str) -> Callable: """ Discovers and returns the deploy function from the module's deploy.py. + Args: module_name (str): The name of the module. + Returns: - callable: The deploy function. + Callable: The deploy function. """ deploy_module = importlib.import_module(f"modules.{module_name}.deploy") - - # Look for a deploy function that matches the pattern deploy__module function_name = f"deploy_{module_name}_module" deploy_function = getattr(deploy_module, function_name, None) - if not deploy_function: raise ValueError(f"No deploy function named '{function_name}' found in modules.{module_name}.deploy") - return deploy_function diff --git a/pulumi/core/metadata.py b/pulumi/core/metadata.py index cc69ab8..fd21729 100644 --- a/pulumi/core/metadata.py +++ b/pulumi/core/metadata.py @@ -1,9 +1,12 @@ -# core/metadata.py - -# TODO: enhance with support for propagation of labels/annotations on AWS resources -# TODO: enhance with +# ./pulumi/core/metadata.py +# Description: +# TODO: enhance with support for propagation of labels annotations on AWS resources +# TODO: enhance by adding additional data to global tags / labels / annotation metadata +# - git release tag import subprocess +import threading +import pulumi from typing import Dict from .utils import sanitize_label_value, extract_repo_name @@ -83,7 +86,6 @@ def generate_git_annotations(git_info: Dict[str, str]) -> Dict[str, str]: "git.branch": git_info.get("branch", "") } - ########################################## # Global labels and annotations ########################################## @@ -96,6 +98,7 @@ class MetadataSingleton: This is important for global state management. """ + __lock = threading.Lock() __instance = None def __new__(cls): @@ -104,14 +107,15 @@ def __new__(cls): This method checks if an instance already exists. If not, it creates one. """ - if cls.__instance is None: - cls.__instance = super(MetadataSingleton, cls).__new__(cls) - cls.__instance._global_labels = {} - cls.__instance._global_annotations = {} + with cls.__lock: + if cls.__instance is None: + cls.__instance = super(MetadataSingleton, cls).__new__(cls) + cls.__instance._global_labels = {} + cls.__instance._global_annotations = {} return cls.__instance @property - def global_labels(self): + def global_labels(self) -> Dict[str, str]: """ Property method to get global labels. @@ -120,17 +124,17 @@ def global_labels(self): return self._global_labels @global_labels.setter - def global_labels(self, labels): + def global_labels(self, labels: Dict[str, str]): """ Property method to set global labels. Args: - labels (dict): A dictionary of labels to set globally. + labels (Dict[str, str]): A dictionary of labels to set globally. """ self._global_labels = labels @property - def global_annotations(self): + def global_annotations(self) -> Dict[str, str]: """ Property method to get global annotations. @@ -139,12 +143,12 @@ def global_annotations(self): return self._global_annotations @global_annotations.setter - def global_annotations(self, annotations): + def global_annotations(self, annotations: Dict[str, str]): """ Property method to set global annotations. Args: - annotations (dict): A dictionary of annotations to set globally. + annotations (Dict[str, str]): A dictionary of annotations to set globally. """ self._global_annotations = annotations diff --git a/pulumi/core/namespace.py b/pulumi/core/namespace.py index 8a269cb..7fb7a21 100644 --- a/pulumi/core/namespace.py +++ b/pulumi/core/namespace.py @@ -1,12 +1,5 @@ -# core/namespace.py - -""" -Utility functions for managing Kubernetes namespaces within the Kargo PaaS platform. - -This module provides functions to create and manage Kubernetes namespaces -using Pulumi and the Kubernetes provider. It leverages the NamespaceConfig -data class to standardize configurations and ensure consistency across deployments. -""" +# ./pulumi/core/namespace.py +# Description: import pulumi import pulumi_kubernetes as k8s @@ -18,6 +11,17 @@ def create_namespace( k8s_provider: k8s.Provider, depends_on: Optional[List[pulumi.Resource]] = None, ) -> k8s.core.v1.Namespace: + """ + Creates a Kubernetes namespace with the provided configuration. + + Args: + config (NamespaceConfig): The configuration object for the namespace. + k8s_provider (k8s.Provider): The Kubernetes provider. + depends_on (Optional[List[pulumi.Resource]]): List of resources this namespace depends on. + + Returns: + k8s.core.v1.Namespace: The created namespace resource. + """ if depends_on is None: depends_on = [] @@ -42,7 +46,7 @@ def create_namespace( update=config.custom_timeouts.get("update", "10m"), delete=config.custom_timeouts.get("delete", "10m"), ), - ) + ), ) return namespace_resource diff --git a/pulumi/core/types.py b/pulumi/core/types.py index e7739c5..178c34d 100644 --- a/pulumi/core/types.py +++ b/pulumi/core/types.py @@ -1,4 +1,4 @@ -# core/types.py +# ./pulumi/core/types.py """ Types and data structures used across Kargo modules. @@ -9,13 +9,9 @@ from dataclasses import dataclass, field from typing import Optional, List, Dict, Any -import pulumi @dataclass class NamespaceConfig: - """ - Configuration for creating or managing a Kubernetes namespace. - """ name: str labels: Dict[str, str] = field(default_factory=lambda: {"ccio.v1/app": "kargo"}) annotations: Dict[str, str] = field(default_factory=dict) @@ -56,10 +52,18 @@ class ComplianceConfig: @staticmethod def merge(user_config: Dict[str, Any]) -> 'ComplianceConfig': + """ + Merges user-provided compliance configuration with default configuration. + + Args: + user_config (Dict[str, Any]): The user-provided compliance configuration. + + Returns: + ComplianceConfig: The merged compliance configuration object. + """ default_config = ComplianceConfig() for key, value in user_config.items(): if hasattr(default_config, key): - # Recursively merge nested configurations nested_config = getattr(default_config, key) for nested_key, nested_value in value.items(): if hasattr(nested_config, nested_key): diff --git a/pulumi/core/utils.py b/pulumi/core/utils.py index cb04a58..f995d25 100644 --- a/pulumi/core/utils.py +++ b/pulumi/core/utils.py @@ -1,31 +1,41 @@ -# core/utils.py +# ./pulumi/core/utils.py # Description: Utility functions for sanitizing compliance metadata labels. import re import pulumi import pulumi_kubernetes as k8s -from typing import Optional +from typing import Optional, Dict -def generate_global_transformations(global_labels: dict, global_annotations: dict): + +def generate_global_transformations(global_labels: Dict[str, str], global_annotations: Dict[str, str]): """ Registers a global transformation function that applies the provided labels and annotations to all resources. + + Args: + global_labels (Dict[str, str]): Global label set to apply to all resources. + global_annotations (Dict[str, str]): Global annotations to apply to all resources. """ def global_transform(args: pulumi.ResourceTransformationArgs) -> Optional[pulumi.ResourceTransformationResult]: props = args.props if 'metadata' in props: metadata = props['metadata'] if isinstance(metadata, dict): - metadata.setdefault('labels', {}) + if metadata.get('labels') is None: + metadata['labels'] = {} metadata['labels'].update(global_labels) - metadata.setdefault('annotations', {}) + + if metadata.get('annotations') is None: + metadata['annotations'] = {} metadata['annotations'].update(global_annotations) + elif isinstance(metadata, k8s.meta.v1.ObjectMetaArgs): if metadata.labels is None: metadata.labels = {} - metadata.labels = {**metadata.labels, **global_labels} + metadata.labels.update(global_labels) + if metadata.annotations is None: metadata.annotations = {} - metadata.annotations = {**metadata.annotations, **global_annotations} + metadata.annotations.update(global_annotations) props['metadata'] = metadata else: props['metadata'] = { @@ -37,18 +47,35 @@ def global_transform(args: pulumi.ResourceTransformationArgs) -> Optional[pulumi pulumi.runtime.register_stack_transformation(global_transform) def sanitize_label_value(value: str) -> str: + """ + Sanitize label values to meet Kubernetes metadata requirements. + + Replaces invalid characters with '-' + + Args: + value (str): The string value to sanitize. + + Returns: + str: The sanitized string value, truncated to 63 characters. + """ value = value.lower() - # Replace invalid characters with '-' sanitized = re.sub(r'[^a-z0-9_.-]', '-', value) - # Remove leading and trailing non-alphanumeric characters - sanitized = re.sub(r'^[^a-z0-9]+', '', sanitized) - sanitized = re.sub(r'[^a-z0-9]+$', '', sanitized) - # Truncate to 63 characters + sanitized = re.sub(r'^[^a-z0-9]+', '', sanitized) # Remove leading non-alphanumeric chars + sanitized = re.sub(r'[^a-z0-9]+$', '', sanitized) # Remove trailing non-alphanumeric chars return sanitized[:63] def extract_repo_name(remote_url: str) -> str: - # Extract the repository name from the remote URL - # Example: 'https://github.com/ContainerCraft/Kargo.git' -> 'ContainerCraft/Kargo' + """ + Extract the repository name from the remote URL. + + Example: 'https://github.com/ContainerCraft/Kargo.git' -> 'ContainerCraft/Kargo' + + Args: + remote_url (str): The remote URL from which to extract the repo name. + + Returns: + str: The extracted repository name. + """ match = re.search(r'[:/]([^/:]+/[^/\.]+)(\.git)?$', remote_url) if match: return match.group(1) diff --git a/pulumi/core/versions.py b/pulumi/core/versions.py index 52180e5..e02c055 100644 --- a/pulumi/core/versions.py +++ b/pulumi/core/versions.py @@ -1,10 +1,13 @@ -# core/versions.py - +# ./pulumi/core/versions.py import json import os import pulumi import requests +#DEFAULT_VERSIONS_URL_TEMPLATE = 'https://github.com/containercraft/kargo/releases/latest/download/' +# TODO: replace with official github releases artifact URLs when released +DEFAULT_VERSIONS_URL_TEMPLATE = 'https://raw.githubusercontent.com/ContainerCraft/Kargo/rerefactor/pulumi/' + def load_default_versions(config: pulumi.Config) -> dict: """ Loads the default versions for modules based on the specified configuration settings. @@ -24,92 +27,60 @@ def load_default_versions(config: pulumi.Config) -> dict: Raises: Exception: If default versions cannot be loaded from any source. """ - # Get the current stack name (e.g., 'dev', 'prod') stack_name = pulumi.get_stack() + default_versions_source = config.get('default_versions.source') + versions_channel = config.get('versions.channel') or 'stable' + versions_stack_name = config.get_bool('versions.stack_name') or False + default_versions = {} - # Retrieve configuration settings from Pulumi config - default_versions_source = config.get('default_versions.source') # User-specified source for versions - versions_channel = config.get('versions.channel') or 'stable' # Channel to use ('stable' by default) - versions_stack_name = config.get_bool('versions.stack_name') or False # Use stack-specific versions if true - - default_versions = {} # Initialize the default versions dictionary - - def load_versions_from_file(file_path): - """ - Loads versions from a local JSON file. - - Args: - file_path: The path to the JSON file containing versions. - - Returns: - A dictionary of versions if successful, otherwise None. - """ + def load_versions_from_file(file_path: str) -> dict: + """Loads versions from a local JSON file.""" try: with open(file_path, 'r') as f: versions = json.load(f) pulumi.log.info(f"Loaded default versions from file: {file_path}") return versions - except FileNotFoundError: - pulumi.log.warn(f"Default versions file not found at {file_path}.") - except json.JSONDecodeError as e: - pulumi.log.error(f"Error decoding JSON from {file_path}: {e}") - return None + except (FileNotFoundError, json.JSONDecodeError) as e: + pulumi.log.warn(f"Error loading versions from file {file_path}: {e}") + return {} - def load_versions_from_url(url): + def load_versions_from_url(url: str) -> dict: + """Loads versions from a remote URL.""" try: response = requests.get(url) response.raise_for_status() versions = response.json() pulumi.log.info(f"Loaded default versions from URL: {url}") return versions - except requests.RequestException as e: - pulumi.log.warn(f"Failed to fetch default versions from {url}: {e}") - except json.JSONDecodeError as e: - pulumi.log.error(f"Error decoding JSON from {url}: {e}") - return None + except (requests.RequestException, json.JSONDecodeError) as e: + pulumi.log.warn(f"Error loading versions from URL {url}: {e}") + return {} - # Determine the precedence of version sources if default_versions_source: - # User has specified a source for default versions via Pulumi config if default_versions_source.startswith(('http://', 'https://')): - # The source is a URL default_versions = load_versions_from_url(default_versions_source) else: - # The source is assumed to be a file path default_versions = load_versions_from_file(default_versions_source) if not default_versions: - # Failed to load versions from the specified source - pulumi.log.error(f"Failed to load default versions from specified source: {default_versions_source}") - raise Exception("Cannot proceed without default versions.") + raise Exception(f"Failed to load default versions from specified source: {default_versions_source}") else: - # No user-specified source; proceed with other options in order of precedence - if versions_stack_name: - # Attempt to load versions from a stack-specific file (e.g., ./versions/dev.json) current_dir = os.path.dirname(os.path.abspath(__file__)) - versions_dir = os.path.join(current_dir, '..', 'versions') - stack_versions_path = os.path.join(versions_dir, f'{stack_name}.json') + stack_versions_path = os.path.join(current_dir, '..', 'versions', f'{stack_name}.json') default_versions = load_versions_from_file(stack_versions_path) - # If versions are still not loaded, attempt to load from local default_versions.json if not default_versions: current_dir = os.path.dirname(os.path.abspath(__file__)) default_versions_path = os.path.join(current_dir, '..', 'default_versions.json') default_versions = load_versions_from_file(default_versions_path) - # If still not loaded, attempt to fetch from a remote URL based on the specified channel if not default_versions: - # Construct the URL based on the channel (e.g., 'stable' or 'latest') - base_url = 'https://github.com/containercraft/kargo/releases/latest/download/' - versions_filename = f'{versions_channel}_versions.json' - versions_url = f'{base_url}{versions_filename}' + versions_url = f'{DEFAULT_VERSIONS_URL_TEMPLATE}{versions_channel}_versions.json' default_versions = load_versions_from_url(versions_url) - # If versions are still not loaded after all attempts, log an error and raise an exception if not default_versions: - pulumi.log.error("Could not load default versions from any source. Cannot proceed.") raise Exception("Cannot proceed without default versions.") return default_versions diff --git a/pulumi/modules/cert_manager/deploy.py b/pulumi/modules/cert_manager/deploy.py index e3aabed..5065f31 100644 --- a/pulumi/modules/cert_manager/deploy.py +++ b/pulumi/modules/cert_manager/deploy.py @@ -1,9 +1,11 @@ -# modules/cert_manager/deploy.py +# ./pulumi/modules/cert_manager/deploy.py +# Description: Deploys the CertManager module using Helm with labels and annotations. +from typing import List, Dict, Any, Tuple, Optional # Add Optional import +import requests import pulumi import pulumi_kubernetes as k8s from pulumi_kubernetes.apiextensions.CustomResource import CustomResource -from typing import Optional, List, Dict, Any, Tuple from core.namespace import create_namespace from core.helm_chart_versions import get_latest_helm_chart_version @@ -16,91 +18,66 @@ def deploy_cert_manager_module( global_depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, ) -> Tuple[str, pulumi.Resource, str]: + """ + Deploys the CertManager module with labels and annotations. + + Args: + config_cert_manager (CertManagerConfig): Configuration object for CertManager deployment. + global_depends_on (List[pulumi.Resource]): A list of resources that the deployment depends on. + k8s_provider (k8s.Provider): The Kubernetes provider for this deployment. - # Deploy cert-manager and retrieve the version, Helm release, and CA certificate + Returns: + Tuple[str, pulumi.Resource, str]: The deployed CertManager version, release resource, and Base64-encoded CA certificate. + """ cert_manager_version, release, ca_cert_b64 = deploy_cert_manager( config_cert_manager=config_cert_manager, depends_on=global_depends_on, k8s_provider=k8s_provider, ) - # Append the release to global dependencies to maintain resource ordering global_depends_on.append(release) - # Export the CA certificate for use by other modules or consumers - pulumi.export("cert_manager_selfsigned_cert", ca_cert_b64) - return cert_manager_version, release, ca_cert_b64 def deploy_cert_manager( config_cert_manager: CertManagerConfig, - depends_on: Optional[List[pulumi.Resource]], + depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider ) -> Tuple[str, k8s.helm.v3.Release, str]: + """ + Deploys CertManager using Helm with labels and annotations. - # Extract configuration details + Args: + config_cert_manager (CertManagerConfig): Configuration object for CertManager deployment. + depends_on (List[pulumi.Resource]): List of resources that this deployment depends on. + k8s_provider (k8s.Provider): The Kubernetes provider for this deployment. + + Returns: + Tuple[str, k8s.helm.v3.Release, str]: The CertManager version, release resource, and Base64-encoded CA certificate. + """ namespace = config_cert_manager.namespace version = config_cert_manager.version cluster_issuer_name = config_cert_manager.cluster_issuer install_crds = config_cert_manager.install_crds - # Create the namespace for cert-manager - namespace_config = NamespaceConfig( - name=namespace, - ) - namespace_resource = create_namespace( - config=namespace_config, - k8s_provider=k8s_provider, - depends_on=depends_on, - ) + namespace_config = NamespaceConfig(name=namespace) + namespace_resource = create_namespace(namespace_config, k8s_provider, depends_on) - # Define Helm chart details chart_name = "cert-manager" chart_url = "https://charts.jetstack.io" + version = get_helm_chart_version(chart_url, chart_name, version) - # Determine version - if version == 'latest' or version is None: - version = get_latest_helm_chart_version(f"{chart_url}/index.yaml", chart_name) - version = version.lstrip("v") - pulumi.log.info(f"Setting Helm release version to latest: {chart_name}/{version}") - else: - pulumi.log.info(f"Using Helm release version: {chart_name}/{version}") - - # Generate Helm values - helm_values = gen_helm_values(config_cert_manager) + helm_values = generate_helm_values(config_cert_manager) - # Access global labels and annotations global_labels = get_global_labels() global_annotations = get_global_annotations() - # Define transformation for Helm resources def helm_transform(args: pulumi.ResourceTransformationArgs): - if args.resource and args.resource.__class__.__name__.startswith("k8s"): - props = args.props - if 'metadata' in props: - metadata = props['metadata'] - if isinstance(metadata, dict): - metadata.setdefault('labels', {}) - metadata['labels'].update(global_labels) - metadata.setdefault('annotations', {}) - metadata['annotations'].update(global_annotations) - elif isinstance(metadata, k8s.meta.v1.ObjectMetaArgs): - if metadata.labels is None: - metadata.labels = {} - metadata.labels = {**metadata.labels, **global_labels} - if metadata.annotations is None: - metadata.annotations = {} - metadata.annotations = {**metadata.annotations, **global_annotations} - props['metadata'] = metadata - else: - props['metadata'] = { - 'labels': global_labels, - 'annotations': global_annotations - } - return pulumi.ResourceTransformationResult(props, args.opts) - return None - - # Deploy cert-manager using Helm + metadata = args.props.get('metadata', {}) + set_resource_metadata(metadata, get_global_labels(), get_global_annotations()) + args.props['metadata'] = metadata + return pulumi.ResourceTransformationResult(args.props, args.opts) + release = k8s.helm.v3.Release( chart_name, k8s.helm.v3.ReleaseArgs( @@ -108,9 +85,7 @@ def helm_transform(args: pulumi.ResourceTransformationArgs): version=version, namespace=namespace, skip_await=False, - repository_opts=k8s.helm.v3.RepositoryOptsArgs( - repo=chart_url - ), + repository_opts=k8s.helm.v3.RepositoryOptsArgs(repo=chart_url), values=helm_values, ), opts=pulumi.ResourceOptions( @@ -118,34 +93,111 @@ def helm_transform(args: pulumi.ResourceTransformationArgs): parent=namespace_resource, depends_on=[namespace_resource] + (depends_on or []), transformations=[helm_transform], - custom_timeouts=pulumi.CustomTimeouts( - create="8m", - update="4m", - delete="4m" - ) + custom_timeouts=pulumi.CustomTimeouts(create="8m", update="4m", delete="4m") ) ) - # Create ClusterIssuer resources + cluster_issuer_root, cluster_issuer_ca_certificate, cluster_issuer = create_cluster_issuers( + cluster_issuer_name, namespace, k8s_provider, release + ) + + ca_secret = k8s.core.v1.Secret( + "cluster-selfsigned-issuer-ca-secret", + metadata={"name": "cluster-selfsigned-issuer-ca", "namespace": namespace}, + opts=pulumi.ResourceOptions(provider=k8s_provider, parent=cluster_issuer, depends_on=[cluster_issuer]) + ) + + ca_data_tls_crt_b64 = ca_secret.data.apply(lambda data: data["tls.crt"]) + + return version, release, ca_data_tls_crt_b64 + +def generate_helm_values(config_cert_manager: CertManagerConfig) -> Dict[str, Any]: + """ + Generates Helm values for CertManager from the configuration object. + + Args: + config_cert_manager (CertManagerConfig): The configuration object for CertManager. + + Returns: + Dict[str, Any]: The Helm values dictionary for CertManager. + """ + return { + 'replicaCount': 1, + 'installCRDs': config_cert_manager.install_crds, + 'resources': { + 'limits': {'cpu': '500m', 'memory': '1024Mi'}, + 'requests': {'cpu': '250m', 'memory': '512Mi'} + } + } + +def get_helm_chart_version(chart_url: str, chart_name: str, version: Optional[str]) -> str: + """ + Fetches or verifies the Helm chart version. + + Args: + chart_url (str): The Helm chart repository URL. + chart_name (str): The name of the Helm chart. + version (Optional[str]): The specified version or 'latest'. + + Returns: + str: The resolved version of the Helm chart. + """ + if version == 'latest' or version is None: + version = get_latest_helm_chart_version(f"{chart_url}/index.yaml", chart_name).lstrip("v") + pulumi.log.info(f"Setting Helm release version to latest: {chart_name}/{version}") + else: + pulumi.log.info(f"Using Helm release version: {chart_name}/{version}") + return version + +def set_resource_metadata(metadata: Any, global_labels: Dict[str, str], global_annotations: Dict[str, str]): + """ + Sets metadata for a resource with global labels and annotations. + + Args: + metadata: The metadata object or dictionary. + global_labels (Dict[str, str]): The global labels. + global_annotations (Dict[str, str]): The global annotations. + """ + if isinstance(metadata, dict): + if metadata.get('labels') is None: + metadata['labels'] = {} + metadata.setdefault('labels', {}).update(global_labels) + + if metadata.get('annotations') is None: + metadata['annotations'] = {} + metadata.setdefault('annotations', {}).update(global_annotations) + + elif isinstance(metadata, k8s.meta.v1.ObjectMetaArgs): + if metadata.labels is None: + metadata.labels = {} + metadata.labels.update(global_labels) + + if metadata.annotations is None: + metadata.annotations = {} + metadata.annotations.update(global_annotations) + +def create_cluster_issuers(cluster_issuer_name: str, namespace: str, k8s_provider: k8s.Provider, release: pulumi.Resource) -> Tuple[CustomResource, CustomResource, CustomResource]: + """ + Creates cluster issuers for cert-manager. + + Args: + cluster_issuer_name (str): The name of the cluster issuer. + namespace (str): The namespace for the cluster issuer. + k8s_provider (k8s.Provider): The Kubernetes provider. + release (pulumi.Resource): The release resource. + + Returns: + Tuple[CustomResource, CustomResource, CustomResource]: The created cluster issuers. + """ cluster_issuer_root = CustomResource( "cluster-selfsigned-issuer-root", api_version="cert-manager.io/v1", kind="ClusterIssuer", - metadata={ - "name": "cluster-selfsigned-issuer-root", - }, - spec={ - "selfSigned": {} - }, + metadata={"name": "cluster-selfsigned-issuer-root"}, + spec={"selfSigned": {}}, opts=pulumi.ResourceOptions( - provider=k8s_provider, - parent=release, - depends_on=[release], - custom_timeouts=pulumi.CustomTimeouts( - create="5m", - update="10m", - delete="10m" - ) + provider=k8s_provider, parent=release, depends_on=[release], + custom_timeouts=pulumi.CustomTimeouts(create="5m", update="10m", delete="10m") ) ) @@ -153,35 +205,19 @@ def helm_transform(args: pulumi.ResourceTransformationArgs): "cluster-selfsigned-issuer-ca", api_version="cert-manager.io/v1", kind="Certificate", - metadata={ - "name": "cluster-selfsigned-issuer-ca", - "namespace": namespace, - }, + metadata={"name": "cluster-selfsigned-issuer-ca", "namespace": namespace}, spec={ "commonName": "cluster-selfsigned-issuer-ca", "duration": "2160h0m0s", "isCA": True, - "issuerRef": { - "group": "cert-manager.io", - "kind": "ClusterIssuer", - "name": "cluster-selfsigned-issuer-root", - }, - "privateKey": { - "algorithm": "ECDSA", - "size": 256 - }, + "issuerRef": {"group": "cert-manager.io", "kind": "ClusterIssuer", "name": "cluster-selfsigned-issuer-root"}, + "privateKey": {"algorithm": "ECDSA", "size": 256}, "renewBefore": "360h0m0s", "secretName": "cluster-selfsigned-issuer-ca" }, opts=pulumi.ResourceOptions( - provider=k8s_provider, - parent=cluster_issuer_root, - depends_on=[cluster_issuer_root], - custom_timeouts=pulumi.CustomTimeouts( - create="5m", - update="10m", - delete="10m" - ) + provider=k8s_provider, parent=cluster_issuer_root, depends_on=[cluster_issuer_root], + custom_timeouts=pulumi.CustomTimeouts(create="5m", update="10m", delete="10m") ) ) @@ -189,70 +225,12 @@ def helm_transform(args: pulumi.ResourceTransformationArgs): cluster_issuer_name, api_version="cert-manager.io/v1", kind="ClusterIssuer", - metadata={ - "name": cluster_issuer_name, - }, - spec={ - "ca": { - "secretName": "cluster-selfsigned-issuer-ca", - } - }, + metadata={"name": cluster_issuer_name}, + spec={"ca": {"secretName": "cluster-selfsigned-issuer-ca"}}, opts=pulumi.ResourceOptions( - provider=k8s_provider, - parent=cluster_issuer_ca_certificate, - depends_on=[cluster_issuer_ca_certificate], - custom_timeouts=pulumi.CustomTimeouts( - create="4m", - update="4m", - delete="4m" - ) + provider=k8s_provider, parent=cluster_issuer_ca_certificate, depends_on=[cluster_issuer_ca_certificate], + custom_timeouts=pulumi.CustomTimeouts(create="4m", update="4m", delete="4m") ) ) - # Retrieve the CA certificate secret - ca_secret = k8s.core.v1.Secret( - "cluster-selfsigned-issuer-ca-secret", - metadata={ - "name": "cluster-selfsigned-issuer-ca", - "namespace": namespace, - }, - opts=pulumi.ResourceOptions( - provider=k8s_provider, - parent=cluster_issuer, - depends_on=[cluster_issuer], - ) - ) - - # Extract the base64-encoded CA certificate - ca_data_tls_crt_b64 = ca_secret.data.apply(lambda data: data["tls.crt"]) - - return version, release, ca_data_tls_crt_b64 - -def gen_helm_values( - config_cert_manager: CertManagerConfig - ) -> Dict[str, Any]: - """ - Generates custom Helm values for the cert-manager Helm chart. - - Args: - config_cert_manager (CertManagerConfig): The configuration for cert-manager. - - Returns: - Dict[str, Any]: A dictionary of Helm chart values. - """ - # Define custom values for the cert-manager Helm chart - helm_values = { - 'replicaCount': 1, - 'installCRDs': config_cert_manager.install_crds, - 'resources': { - 'limits': { - 'cpu': '500m', - 'memory': '1024Mi' - }, - 'requests': { - 'cpu': '250m', - 'memory': '512Mi' - } - } - } - return helm_values + return cluster_issuer_root, cluster_issuer_ca_certificate, cluster_issuer diff --git a/pulumi/modules/cert_manager/types.py b/pulumi/modules/cert_manager/types.py index c3ad845..a621295 100644 --- a/pulumi/modules/cert_manager/types.py +++ b/pulumi/modules/cert_manager/types.py @@ -1,4 +1,4 @@ -# modules/cert_manager/types.py +# ./pulumi/modules/cert_manager/types.py from dataclasses import dataclass from typing import Optional, Dict, Any @@ -13,6 +13,15 @@ class CertManagerConfig: @staticmethod def merge(user_config: Dict[str, Any]) -> 'CertManagerConfig': + """ + Merges user-provided configuration with default configuration. + + Args: + user_config (Dict[str, Any]): The user-provided configuration. + + Returns: + CertManagerConfig: The merged configuration object. + """ default_config = CertManagerConfig() for key, value in user_config.items(): if hasattr(default_config, key): diff --git a/pulumi/modules/kubevirt/deploy.py b/pulumi/modules/kubevirt/deploy.py index 534218e..699c424 100644 --- a/pulumi/modules/kubevirt/deploy.py +++ b/pulumi/modules/kubevirt/deploy.py @@ -1,4 +1,5 @@ -# src/kubevirt/deploy.py +# ./pulumi/modules/kubevirt/deploy.py +# Description: import requests import yaml @@ -65,6 +66,15 @@ def deploy_kubevirt( ) -> Tuple[str, k8s.yaml.ConfigFile]: """ Deploys KubeVirt into the Kubernetes cluster using its YAML manifests and applies labels and annotations. + + Args: + config_kubevirt (KubeVirtConfig): Configuration object for KubeVirt deployment. + depends_on (List[pulumi.Resource]): List of resources that this deployment depends on. + k8s_provider (k8s.Provider): The Kubernetes provider for this deployment. + namespace_resource (pulumi.Resource): The namespace resource. + + Returns: + Tuple[str, k8s.yaml.ConfigFile]: The KubeVirt version and the operator resource. """ # Extract configuration details from the KubeVirtConfig object namespace = config_kubevirt.namespace @@ -94,10 +104,17 @@ def deploy_kubevirt( # Transform the YAML and set the correct namespace transformed_yaml = _transform_yaml(kubevirt_yaml, namespace) - def kubevirt_transform(obj: dict, opts: pulumi.ResourceOptions) -> pulumi.ResourceTransformationResult: + def kubevirt_transform(obj: Dict[str, Any], opts: pulumi.ResourceOptions) -> pulumi.ResourceTransformationResult: """ Transformation function to add labels and annotations to Kubernetes objects in the KubeVirt operator YAML manifests. + + Args: + obj (Dict[str, Any]): The resource object to transform. + opts (pulumi.ResourceOptions): The resource options for the transformation. + + Returns: + pulumi.ResourceTransformationResult: The transformed resource and options. """ # Ensure the 'metadata' field exists in the object obj.setdefault("metadata", {}) @@ -192,7 +209,7 @@ def kubevirt_transform(obj: dict, opts: pulumi.ResourceOptions) -> pulumi.Resour return version, operator -def _transform_yaml(yaml_data: Any, namespace: str) -> List[Dict]: +def _transform_yaml(yaml_data: Any, namespace: str) -> List[Dict[str, Any]]: """ Helper function to transform YAML to set namespace and modify resources. @@ -201,7 +218,7 @@ def _transform_yaml(yaml_data: Any, namespace: str) -> List[Dict]: namespace: The namespace to set for the resources. Returns: - List[Dict]: The transformed YAML with the appropriate namespace. + List[Dict[str, Any]]: The transformed YAML with the appropriate namespace. """ transformed = [] for resource in yaml_data: diff --git a/pulumi/modules/kubevirt/types.py b/pulumi/modules/kubevirt/types.py index ab247a2..3054004 100644 --- a/pulumi/modules/kubevirt/types.py +++ b/pulumi/modules/kubevirt/types.py @@ -1,60 +1,44 @@ -# modules/kubevirt/types.py +# ./pulumi/modules/kubevirt/types.py +# Description: from dataclasses import dataclass, field from typing import Optional, Dict, Any + import pulumi from core.metadata import get_global_labels, get_global_annotations @dataclass class KubeVirtConfig: - """ - Configuration for deploying the KubeVirt module. - - Attributes: - namespace (str): The Kubernetes namespace where KubeVirt will be deployed. - Defaults to "kubevirt". - version (Optional[str]): The version of KubeVirt to deploy. - Defaults to None, which fetches the latest version. - use_emulation (bool): Whether to use emulation for KVM. - Defaults to False. - labels (Dict[str, str]): Labels to apply to KubeVirt resources. - Defaults to an empty dict. - annotations (Dict[str, Any]): Annotations to apply to KubeVirt resources. - Defaults to an empty dict. - """ namespace: str = "kubevirt" version: Optional[str] = None use_emulation: bool = False labels: Dict[str, str] = field(default_factory=dict) annotations: Dict[str, Any] = field(default_factory=dict) - @staticmethod - def merge(user_config: Dict[str, Any]) -> 'KubeVirtConfig': + @classmethod + def merge(cls, user_config: Dict[str, Any]) -> 'KubeVirtConfig': """ - Merges user-provided configuration with default values and global labels and annotations. + Merge user-provided configuration with default configuration. Args: - user_config (Dict[str, Any]): A dictionary containing user-provided configuration options. + user_config (Dict[str, Any]): The user-provided configuration. Returns: - KubeVirtConfig: An instance of KubeVirtConfig with merged settings. + KubeVirtConfig: The merged configuration object. """ - default_config = KubeVirtConfig() + default_config = cls() merged_config = default_config.__dict__.copy() - # Merge user-provided config for key, value in user_config.items(): if hasattr(default_config, key): merged_config[key] = value else: pulumi.log.warn(f"Unknown configuration key '{key}' in kubevirt config.") - # Retrieve global labels and annotations global_labels = get_global_labels() global_annotations = get_global_annotations() - # Merge global labels and annotations into the config merged_config['labels'].update(global_labels) merged_config['annotations'].update(global_annotations) - return KubeVirtConfig(**merged_config) + return cls(**merged_config) From 0252bfb477b3157c3aeaf534357b6906672f829d Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sat, 21 Sep 2024 12:04:04 -0700 Subject: [PATCH 24/43] refine core and module codebase --- pulumi/__main__.py | 3 - pulumi/core/config.py | 14 +- pulumi/core/deploy_module.py | 14 +- pulumi/core/helm_chart_versions.py | 20 --- pulumi/core/init.py | 100 +++++++------- pulumi/core/introspection.py | 2 +- pulumi/core/metadata.py | 187 ++++----------------------- pulumi/core/versions.py | 15 ++- pulumi/modules/cert_manager/types.py | 18 +-- pulumi/modules/kubevirt/types.py | 9 -- 10 files changed, 108 insertions(+), 274 deletions(-) diff --git a/pulumi/__main__.py b/pulumi/__main__.py index d749681..235a764 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -10,7 +10,6 @@ from core.config import export_results from core.deploy_module import deploy_module - def main(): try: init = initialize_pulumi() @@ -33,7 +32,6 @@ def main(): pulumi.log.error(f"Deployment failed: {str(e)}") raise - def deploy_modules( modules: List[str], config: pulumi.Config, @@ -56,6 +54,5 @@ def deploy_modules( configurations=configurations, ) - if __name__ == "__main__": main() diff --git a/pulumi/core/config.py b/pulumi/core/config.py index aeb9692..681c9c2 100644 --- a/pulumi/core/config.py +++ b/pulumi/core/config.py @@ -13,10 +13,10 @@ import pulumi def get_module_config( - module_name: str, - config: pulumi.Config, - default_versions: Dict[str, Any], -) -> Tuple[Dict[str, Any], bool]: + module_name: str, + config: pulumi.Config, + default_versions: Dict[str, Any], + ) -> Tuple[Dict[str, Any], bool]: """ Retrieves and prepares the configuration for a module. @@ -33,7 +33,11 @@ def get_module_config( module_config['version'] = module_config.get('version', default_versions.get(module_name)) return module_config, module_enabled -def export_results(versions: Dict[str, str], configurations: Dict[str, Dict[str, Any]], compliance: Dict[str, Any]): +def export_results( + versions: Dict[str, str], + configurations: Dict[str, Dict[str, Any]], + compliance: Dict[str, Any] + ): """ Exports the results of the deployment processes including versions, configurations, and compliance information. diff --git a/pulumi/core/deploy_module.py b/pulumi/core/deploy_module.py index b220c92..f95a556 100644 --- a/pulumi/core/deploy_module.py +++ b/pulumi/core/deploy_module.py @@ -4,7 +4,7 @@ import inspect import pulumi import pulumi_kubernetes as k8s -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Type from .introspection import discover_config_class, discover_deploy_function from .config import get_module_config @@ -33,7 +33,6 @@ def deploy_module( TypeError: If any arguments have incorrect types. ValueError: If any module-specific errors occur. """ - # Validate parameter types if not isinstance(module_name, str): raise TypeError("module_name must be a string") if not isinstance(config, pulumi.Config): @@ -49,21 +48,17 @@ def deploy_module( if not isinstance(configurations, dict): raise TypeError("configurations must be a dictionary") - # Get the module's configuration from Pulumi config module_config_dict, module_enabled = get_module_config(module_name, config, default_versions) if module_enabled: - # Discover the configuration class and deploy function dynamically ModuleConfigClass = discover_config_class(module_name) deploy_func = discover_deploy_function(module_name) config_obj = ModuleConfigClass.merge(module_config_dict) - # Infer the required argument name for the deploy function deploy_func_args = inspect.signature(deploy_func).parameters.keys() - config_arg_name = list(deploy_func_args)[0] # Assuming the first argument is the config object + config_arg_name = list(deploy_func_args)[0] - # Deploy the module using its deploy function with the correct arguments try: result = deploy_func( **{config_arg_name: config_obj}, @@ -71,7 +66,6 @@ def deploy_module( k8s_provider=k8s_provider, ) - # Handle the result based on its structure if isinstance(result, tuple) and len(result) == 3: version, release, exported_value = result elif isinstance(result, tuple) and len(result) == 2: @@ -80,20 +74,16 @@ def deploy_module( else: raise ValueError(f"Unexpected return value structure from {module_name} deploy function") - # Record the deployed version and configuration versions[module_name] = version configurations[module_name] = {"enabled": module_enabled} - # Export any additional values if needed if exported_value: pulumi.export(f"{module_name}_exported_value", exported_value) - # Add the release to global dependencies to maintain resource ordering global_depends_on.append(release) except Exception as e: pulumi.log.error(f"Deployment failed for module {module_name}: {str(e)}") raise - else: pulumi.log.info(f"Module {module_name} is not enabled.") diff --git a/pulumi/core/helm_chart_versions.py b/pulumi/core/helm_chart_versions.py index 8d052df..019c342 100644 --- a/pulumi/core/helm_chart_versions.py +++ b/pulumi/core/helm_chart_versions.py @@ -14,15 +14,6 @@ CHART_NOT_FOUND = "Chart not found" def is_stable_version(version_str: str) -> bool: - """ - Check if the version string is a valid and stable semantic version. - - Args: - version_str (str): The version string to check. - - Returns: - bool: True if the version is stable, False otherwise. - """ try: parsed_version = parse_version(version_str) return isinstance(parsed_version, Version) and not parsed_version.is_prerelease and not parsed_version.is_devrelease @@ -30,22 +21,11 @@ def is_stable_version(version_str: str) -> bool: return False def get_latest_helm_chart_version(url: str, chart_name: str) -> str: - """ - Fetches the latest stable version of a Helm chart from a given URL. - - Args: - url (str): The URL of the Helm chart repository. - chart_name (str): The name of the Helm chart. - - Returns: - str: The latest stable version of the Helm chart, or an error message if the chart is not found or an error occurs during fetching. - """ try: logging.info(f"Fetching URL: {url}") response = requests.get(url) response.raise_for_status() - # Parse the YAML content index = yaml.safe_load(response.content) if chart_name in index['entries']: chart_versions = index['entries'][chart_name] diff --git a/pulumi/core/init.py b/pulumi/core/init.py index f7f0059..404d180 100644 --- a/pulumi/core/init.py +++ b/pulumi/core/init.py @@ -20,66 +20,64 @@ from .utils import generate_global_transformations def initialize_pulumi() -> Dict[str, Any]: - """ - Initializes the Pulumi configuration, Kubernetes provider, and global resources. - - Returns: - Dict[str, Any]: A dictionary containing initialized resources. - """ config = pulumi.Config() stack_name = pulumi.get_stack() project_name = pulumi.get_project() - default_versions = load_default_versions(config) - versions: Dict[str, str] = {} - configurations: Dict[str, Dict[str, Any]] = {} - global_depends_on: List[pulumi.Resource] = [] + try: + default_versions = load_default_versions(config) + versions: Dict[str, str] = {} + configurations: Dict[str, Dict[str, Any]] = {} + global_depends_on: List[pulumi.Resource] = [] - kubernetes_config = config.get_object("kubernetes") or {} - kubeconfig = kubernetes_config.get("kubeconfig") or os.getenv('KUBECONFIG') - kubernetes_context = kubernetes_config.get("context") + kubernetes_config = config.get_object("kubernetes") or {} + kubeconfig = kubernetes_config.get("kubeconfig") or os.getenv('KUBECONFIG') + kubernetes_context = kubernetes_config.get("context") - pulumi.log.info(f"Kubeconfig: {kubeconfig}") - pulumi.log.info(f"Kubernetes context: {kubernetes_context}") + pulumi.log.info(f"Kubeconfig: {kubeconfig}") + pulumi.log.info(f"Kubernetes context: {kubernetes_context}") - k8s_provider = Provider( - "k8sProvider", - kubeconfig=kubeconfig, - context=kubernetes_context, - ) + k8s_provider = Provider( + "k8sProvider", + kubeconfig=kubeconfig, + context=kubernetes_context, + ) - git_info = collect_git_info() - configurations["source_repository"] = { - "remote": git_info["remote"], - "branch": git_info["branch"], - "commit": git_info["commit"] - } + git_info = collect_git_info() + configurations["source_repository"] = { + "remote": git_info["remote"], + "branch": git_info["branch"], + "commit": git_info["commit"] + } - compliance_config_dict = config.get_object('compliance') or {} - compliance_config = ComplianceConfig.merge(compliance_config_dict) - compliance_labels = generate_compliance_labels(compliance_config) - compliance_annotations = generate_compliance_annotations(compliance_config) + compliance_config_dict = config.get_object('compliance') or {} + compliance_config = ComplianceConfig.merge(compliance_config_dict) + compliance_labels = generate_compliance_labels(compliance_config) + compliance_annotations = generate_compliance_annotations(compliance_config) - git_labels = generate_git_labels(git_info) - git_annotations = generate_git_annotations(git_info) - global_labels = {**compliance_labels, **git_labels} - global_annotations = {**compliance_annotations, **git_annotations} + git_labels = generate_git_labels(git_info) + git_annotations = generate_git_annotations(git_info) + global_labels = {**compliance_labels, **git_labels} + global_annotations = {**compliance_annotations, **git_annotations} - set_global_labels(global_labels) - set_global_annotations(global_annotations) - generate_global_transformations(global_labels, global_annotations) + set_global_labels(global_labels) + set_global_annotations(global_annotations) + generate_global_transformations(global_labels, global_annotations) - return { - "config": config, - "stack_name": stack_name, - "project_name": project_name, - "default_versions": default_versions, - "versions": versions, - "configurations": configurations, - "global_depends_on": global_depends_on, - "k8s_provider": k8s_provider, - "git_info": git_info, - "compliance_config": compliance_config, - "global_labels": global_labels, - "global_annotations": global_annotations, - } + return { + "config": config, + "stack_name": stack_name, + "project_name": project_name, + "default_versions": default_versions, + "versions": versions, + "configurations": configurations, + "global_depends_on": global_depends_on, + "k8s_provider": k8s_provider, + "git_info": git_info, + "compliance_config": compliance_config, + "global_labels": global_labels, + "global_annotations": global_annotations, + } + except Exception as e: + pulumi.log.error(f"Initialization error: {str(e)}") + raise diff --git a/pulumi/core/introspection.py b/pulumi/core/introspection.py index 670eab0..14a7dc3 100644 --- a/pulumi/core/introspection.py +++ b/pulumi/core/introspection.py @@ -3,7 +3,7 @@ import inspect import importlib -from typing import Type, Callable, Optional +from typing import Type, Callable def discover_config_class(module_name: str) -> Type: """ diff --git a/pulumi/core/metadata.py b/pulumi/core/metadata.py index fd21729..3f2e288 100644 --- a/pulumi/core/metadata.py +++ b/pulumi/core/metadata.py @@ -5,10 +5,28 @@ # - git release tag import subprocess -import threading import pulumi from typing import Dict -from .utils import sanitize_label_value, extract_repo_name + +class MetadataSingleton: + _instance: Dict[str, Dict[str, str]] = {} + + def __new__(cls, *args, **kwargs): + if not cls._instance: + cls._instance = {"_global_labels": {}, "_global_annotations": {}} + return cls._instance + +def set_global_labels(labels: Dict[str, str]): + MetadataSingleton()["_global_labels"] = labels + +def set_global_annotations(annotations: Dict[str, str]): + MetadataSingleton()["_global_annotations"] = annotations + +def get_global_labels() -> Dict[str, str]: + return MetadataSingleton()["_global_labels"] + +def get_global_annotations() -> Dict[str, str]: + return MetadataSingleton()["_global_annotations"] def collect_git_info() -> Dict[str, str]: """ @@ -22,176 +40,23 @@ def collect_git_info() -> Dict[str, str]: Dict[str, str]: A dictionary containing the remote URL, branch name, and commit hash. """ try: - # Fetch the remote URL from git configuration - remote = subprocess.check_output( - ['git', 'config', '--get', 'remote.origin.url'], - stderr=subprocess.STDOUT - ).strip().decode('utf-8') - - # Fetch the current branch name - branch = subprocess.check_output( - ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], - stderr=subprocess.STDOUT - ).strip().decode('utf-8') - - # Fetch the latest commit hash - commit = subprocess.check_output( - ['git', 'rev-parse', 'HEAD'], - stderr=subprocess.STDOUT - ).strip().decode('utf-8') - + remote = subprocess.check_output(['git', 'config', '--get', 'remote.origin.url'], stderr=subprocess.STDOUT).strip().decode('utf-8') + branch = subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], stderr=subprocess.STDOUT).strip().decode('utf-8') + commit = subprocess.check_output(['git', 'rev-parse', 'HEAD'], stderr=subprocess.STDOUT).strip().decode('utf-8') return {'remote': remote, 'branch': branch, 'commit': commit} - except subprocess.CalledProcessError as e: - # In case of an error, log it and return 'N/A' for all values - pulumi.log.error(f"Error fetching git information: {e.output.decode('utf-8')}") + pulumi.log.error(f"Error fetching git information: {e}") return {'remote': 'N/A', 'branch': 'N/A', 'commit': 'N/A'} - def generate_git_labels(git_info: Dict[str, str]) -> Dict[str, str]: - """ - Generates labels for Kubernetes resources from git information. - - Labels are key/value pairs that are attached to objects, such as pods. Labels are intended to be - used to specify identifying attributes of objects that are meaningful and relevant to users. - - Args: - git_info (Dict[str, str]): A dictionary containing git information. - - Returns: - Dict[str, str]: A dictionary containing sanitized git branch name and shortened commit hash. - """ return { - "git.branch": sanitize_label_value(git_info.get("branch", "")), - "git.commit": git_info.get("commit", "")[:7], # Shorten commit hash + "git.branch": git_info.get("branch", ""), + "git.commit": git_info.get("commit", "")[:7], } - def generate_git_annotations(git_info: Dict[str, str]) -> Dict[str, str]: - """ - Generates annotations for Kubernetes resources from git information. - - Annotations are key/value pairs that can hold arbitrary non-identifying metadata. They are - intended to store information that can be useful for debugging, auditing, or other purposes. - - Args: - git_info (Dict[str, str]): A dictionary containing git information. - - Returns: - Dict[str, str]: A dictionary containing git remote URL, full commit hash, and branch name. - """ return { "git.remote": git_info.get("remote", ""), "git.commit.full": git_info.get("commit", ""), "git.branch": git_info.get("branch", "") } - -########################################## -# Global labels and annotations -########################################## - -class MetadataSingleton: - """ - Singleton class to manage global labels and annotations for Kubernetes resources. - - This class uses the singleton pattern to ensure only one instance is created. - This is important for global state management. - """ - - __lock = threading.Lock() - __instance = None - - def __new__(cls): - """ - Ensure only one instance of this class exists (Singleton pattern). - - This method checks if an instance already exists. If not, it creates one. - """ - with cls.__lock: - if cls.__instance is None: - cls.__instance = super(MetadataSingleton, cls).__new__(cls) - cls.__instance._global_labels = {} - cls.__instance._global_annotations = {} - return cls.__instance - - @property - def global_labels(self) -> Dict[str, str]: - """ - Property method to get global labels. - - Labels are useful for identifying and grouping Kubernetes resources. - """ - return self._global_labels - - @global_labels.setter - def global_labels(self, labels: Dict[str, str]): - """ - Property method to set global labels. - - Args: - labels (Dict[str, str]): A dictionary of labels to set globally. - """ - self._global_labels = labels - - @property - def global_annotations(self) -> Dict[str, str]: - """ - Property method to get global annotations. - - Annotations provide additional context and useful metadata for Kubernetes resources. - """ - return self._global_annotations - - @global_annotations.setter - def global_annotations(self, annotations: Dict[str, str]): - """ - Property method to set global annotations. - - Args: - annotations (Dict[str, str]): A dictionary of annotations to set globally. - """ - self._global_annotations = annotations - - -# Singleton instance for managing global labels and annotations -_metadata_singleton = MetadataSingleton() - - -def set_global_labels(labels: Dict[str, str]): - """ - Function to set global labels using the singleton instance. - - Args: - labels (Dict[str, str]): A dictionary of labels to set globally. - """ - _metadata_singleton.global_labels = labels - - -def set_global_annotations(annotations: Dict[str, str]): - """ - Function to set global annotations using the singleton instance. - - Args: - annotations (Dict[str, str]): A dictionary of annotations to set globally. - """ - _metadata_singleton.global_annotations = annotations - - -def get_global_labels() -> Dict[str, str]: - """ - Function to get global labels from the singleton instance. - - Returns: - Dict[str, str]: A dictionary of global labels. - """ - return _metadata_singleton.global_labels - - -def get_global_annotations() -> Dict[str, str]: - """ - Function to get global annotations from the singleton instance. - - Returns: - Dict[str, str]: A dictionary of global annotations. - """ - return _metadata_singleton.global_annotations diff --git a/pulumi/core/versions.py b/pulumi/core/versions.py index e02c055..6c25b57 100644 --- a/pulumi/core/versions.py +++ b/pulumi/core/versions.py @@ -8,7 +8,7 @@ # TODO: replace with official github releases artifact URLs when released DEFAULT_VERSIONS_URL_TEMPLATE = 'https://raw.githubusercontent.com/ContainerCraft/Kargo/rerefactor/pulumi/' -def load_default_versions(config: pulumi.Config) -> dict: +def load_default_versions(config: pulumi.Config, force_refresh=False) -> dict: """ Loads the default versions for modules based on the specified configuration settings. @@ -27,6 +27,14 @@ def load_default_versions(config: pulumi.Config) -> dict: Raises: Exception: If default versions cannot be loaded from any source. """ + cache_file = '/tmp/default_versions.json' + if not force_refresh and os.path.exists(cache_file): + try: + with open(cache_file) as f: + return json.load(f) + except Exception as e: + pulumi.log.warn(f"Error reading cache file: {e}") + stack_name = pulumi.get_stack() default_versions_source = config.get('default_versions.source') versions_channel = config.get('versions.channel') or 'stable' @@ -34,7 +42,6 @@ def load_default_versions(config: pulumi.Config) -> dict: default_versions = {} def load_versions_from_file(file_path: str) -> dict: - """Loads versions from a local JSON file.""" try: with open(file_path, 'r') as f: versions = json.load(f) @@ -45,7 +52,6 @@ def load_versions_from_file(file_path: str) -> dict: return {} def load_versions_from_url(url: str) -> dict: - """Loads versions from a remote URL.""" try: response = requests.get(url) response.raise_for_status() @@ -83,4 +89,7 @@ def load_versions_from_url(url: str) -> dict: if not default_versions: raise Exception("Cannot proceed without default versions.") + with open(cache_file, 'w') as f: + json.dump(default_versions, f) + return default_versions diff --git a/pulumi/modules/cert_manager/types.py b/pulumi/modules/cert_manager/types.py index a621295..7928626 100644 --- a/pulumi/modules/cert_manager/types.py +++ b/pulumi/modules/cert_manager/types.py @@ -1,4 +1,13 @@ # ./pulumi/modules/cert_manager/types.py +""" +Merges user-provided configuration with default configuration. + +Args: + user_config (Dict[str, Any]): The user-provided configuration. + +Returns: + CertManagerConfig: The merged configuration object. +""" from dataclasses import dataclass from typing import Optional, Dict, Any @@ -13,15 +22,6 @@ class CertManagerConfig: @staticmethod def merge(user_config: Dict[str, Any]) -> 'CertManagerConfig': - """ - Merges user-provided configuration with default configuration. - - Args: - user_config (Dict[str, Any]): The user-provided configuration. - - Returns: - CertManagerConfig: The merged configuration object. - """ default_config = CertManagerConfig() for key, value in user_config.items(): if hasattr(default_config, key): diff --git a/pulumi/modules/kubevirt/types.py b/pulumi/modules/kubevirt/types.py index 3054004..872c9ee 100644 --- a/pulumi/modules/kubevirt/types.py +++ b/pulumi/modules/kubevirt/types.py @@ -17,15 +17,6 @@ class KubeVirtConfig: @classmethod def merge(cls, user_config: Dict[str, Any]) -> 'KubeVirtConfig': - """ - Merge user-provided configuration with default configuration. - - Args: - user_config (Dict[str, Any]): The user-provided configuration. - - Returns: - KubeVirtConfig: The merged configuration object. - """ default_config = cls() merged_config = default_config.__dict__.copy() From 3044f706570778a494b1db662ee07d07c9e70244 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sat, 21 Sep 2024 12:31:49 -0700 Subject: [PATCH 25/43] refine core and module codebase --- pulumi/README.md | 331 ++++++++++++++++++++---- pulumi/core/metadata.py | 27 +- pulumi/core/types.py | 1 + pulumi/core/utils.py | 76 ++---- pulumi/modules/cert_manager/deploy.py | 88 +------ pulumi/modules/kubevirt/deploy.py | 81 +----- pulumi/stacks/Pulumi.optiplexprime.yaml | 41 +++ 7 files changed, 361 insertions(+), 284 deletions(-) diff --git a/pulumi/README.md b/pulumi/README.md index 96d675b..c0854a3 100644 --- a/pulumi/README.md +++ b/pulumi/README.md @@ -1,56 +1,79 @@ -## **Developer & Architecture Ethos** - -Kargo Kubevirt Kubernetes PaaS - Pulumi Python Infrastructure as Code (IaC) +# Kargo Kubevirt Kubernetes PaaS - Pulumi Python Infrastructure as Code (IaC) -> Prime Directive: "Features are nice. Quality is paramount." +## **Developer & Architecture Ethos** +> **Prime Directive:** "Features are nice. Quality is paramount." +> > Quality is not just about the product or code. Enjoyable developer experience is imperative to the survival of FOSS. ContainerCraft is a Developer Experience (DX) and User Experience (UX) obsessed project. As part of the CCIO Open Source education and skill development ecosystem, the Kargo project's survival is dependent on the happiness of community developers and users. ### **Developer Directives** -- **Improve Code Maintainability:** Enhance structure and organization. Prioritize readable, reusable, and extensible code. -- **Optimize Performance:** Improve code execution performance. Honor configuration. Do not execute inactive code. -- **Establish Standard Practices:** Develop a consistent approach to configuration handling, module deployment, and code organization to guide future development. +- **Improve Code Maintainability**: Enhance structure and organization. Prioritize readable, reusable, and extensible code. +- **Optimize Performance**: Improve code execution performance. Honor configuration. Do not execute inactive code. +- **Establish Standard Practices**: Develop a consistent approach to configuration handling, module deployment, and code organization to guide future development. --- -### **Developer Imperatives** - -The following guidelines promote happiness as a means of promoting growth and sustainability. - -| **Imperative** | **Explanation** | -|----------------------------|--------------------------------------------------------------------------------------------------| -| **User Experience (UX)** | Provide clear error messages and logging to improve intuitive learning, development, and debugging. | -| **Developer Experience (DX)** | Optimize DX with clear documentation, examples, and architecture. | -| **Configurable Modules** | Support user module customization via the Pulumi stack configuration pattern. | -| **Module Data Classes** | Utilize typed dataclasses to safely encapsulate module configuration. | -| **Sane Defaults in Data Classes** | Include sensible default values for module configurations. | -| **User Configuration Handling** | Merge user-provided configurations with defaults. Allow minimal input for module configuration. | -| **Simple Function Signatures** | Reduce parameter count. Encapsulate configuration objects in function signatures. | -| **Type Annotations** | Enhance readability and maintainability with type annotations. | -| **Safe Function Signatures** | Enforce type safety. Raise unknown configuration keys gracefully. | -| **Maintain a streamlined entrypoint** | Minimize top-level code. Encapsulate module logic within the module directory and code. | -| **Reuse + Dedupe code** | Refactor common patterns and logic into shared utilities in `core/utils.py`. Adopt shared utilities when possible. | -| **Version Control Dependencies** | Utilize `versions.py` to manage component versions within `__main__.py`. | -| **Transparency** | Return informative version and configuration values to `version` and `configuration` dictionaries. | -| **Conditional Execution** | Use conditional imports and execution. Prevent unnecessary code execution when modules are disabled. | -| **Remove Deprecated Code** | Eliminate deprecated feature code. | +## **Getting Started** + +### **Prerequisites** + +- [Pulumi CLI](https://www.pulumi.com/docs/get-started/) +- Python 3.6 or higher +- Python dependencies (install via `pip install -r requirements.txt`) +- Kubernetes cluster (with `kubectl` configured) +- Helm CLI (for Helm-related operations) + +### **Setting Up Your Environment** + +1. **Clone the repository:** + + ```sh + git clone https://github.com/containercraft/kargo.git + cd kargo + ``` + +2. **Configure your Pulumi stack:** + + ```sh + pulumi stack init + ``` + +3. **Set up your Pulumi configuration:** + + ```sh + pulumi config set kubernetes:kubeconfig + ``` + +4. **Install dependencies:** + + ```sh + pip install -r requirements.txt + ``` + +5. **Deploy the stack:** + + ```sh + pulumi up + ``` --- -### **Detailed Breakdown** +## **Developer Imperatives** -1. **User Experience (UX):** +### Detailed Breakdown + +1. **User Experience (UX)** - **Clear Error Messages:** Provide meaningful error messages that guide users to resolve issues. - - **Uniform Logging:** Use consistent logging practices to make debugging easier. For instance: + - **Uniform Logging:** Use consistent logging practices to make debugging easier. ```python pulumi.log.info(f"Deploying module: {module_name}") ``` -2. **Developer Experience (DX):** - - **Documentation:** Add comprehensive docstrings and comments. For instance: +2. **Developer Experience (DX)** + - **Documentation:** Add comprehensive docstrings and comments. ```python def deploy_module(...): """ @@ -60,15 +83,14 @@ The following guidelines promote happiness as a means of promoting growth and su ``` - **Examples:** Include example configurations and usage in the documentation. -3. **Configurable Modules:** +3. **Configurable Modules** - **Pulumi Stack Configuration:** Use the Pulumi config object to roll out custom module configurations. - - **Example:** ```python module_config = config.get_object("module_name") or {} ``` -4. **Module Data Classes:** - - **Typed Data Classes:** Encapsulate configurations clearly using `dataclass`. Example: +4. **Module Data Classes** + - **Typed Data Classes:** Encapsulate configurations clearly using `dataclass`. ```python from dataclasses import dataclass @dataclass @@ -76,9 +98,8 @@ The following guidelines promote happiness as a means of promoting growth and su namespace: str = "default" ``` -5. **Sane Defaults in Data Classes:** +5. **Sane Defaults in Data Classes** - **Sensible Defaults:** Set reasonable default values to ensure minimal user configuration. - - **Example:** ```python @dataclass class CertManagerConfig: @@ -86,8 +107,8 @@ The following guidelines promote happiness as a means of promoting growth and su install_crds: bool = True ``` -6. **User Configuration Handling:** - - **Merge Configurations:** Combine user-provided configurations with defaults. Example: +6. **User Configuration Handling** + - **Merge Configurations:** Combine user-provided configurations with defaults. ```python @staticmethod def merge(user_config: Dict[str, Any]) -> 'CertManagerConfig': @@ -98,61 +119,257 @@ The following guidelines promote happiness as a means of promoting growth and su return default_config ``` -7. **Simple Function Signatures:** +7. **Simple Function Signatures** - **Reduce Parameters:** Keep function signatures minimal by encapsulating configurations. - - **Example:** ```python def deploy_module(module_config: ModuleConfig) ``` -8. **Type Annotations:** +8. **Type Annotations** - **Enhance Readability:** ```python def deploy_module(module_name: str, config: pulumi.Config) -> None: ``` -9. **Safe Function Signatures:** - - **Type Safety:** Use consistent type checks and raise meaningful errors. Example: +9. **Safe Function Signatures** + - **Type Safety:** Use consistent type checks and raise meaningful errors. ```python if not hasattr(default_config, key): pulumi.log.warn(f"Unknown configuration key '{key}' in config.") ``` -10. **Streamlined Entrypoint:** - - **Minimize Top-Level Code:** Encapsulate logic within the module and related files. Example: +10. **Streamlined Entrypoint** + - **Minimize Top-Level Code:** Encapsulate logic within the module and related files. ```python if __name__ == "__main__": main() ``` -11. **Reuse + Dedupe Code:** +11. **Reuse + Dedupe Code** - **Central Utilities:** Use common patterns by placing reusable code in `core/utils.py`. - - **Example:** ```python from core.utils import sanitize_label_value, extract_repo_name ``` -12. **Version Control Dependencies:** +12. **Version Control Dependencies** - **Manage Versions:** Control component versions within `versions.py` to maintain consistency. - - **Example:** ```python default_versions = load_default_versions(config) ``` -13. **Transparency:** +13. **Transparency** - **Informative Outputs:** Export configuration and version information. - - **Example:** ```python pulumi.export("versions", versions) ``` -14. **Conditional Execution:** +14. **Conditional Execution** - **Avoid Unnecessary Execution:** Load and execute only the necessary modules. - - **Example:** ```python if module_enabled: deploy_func(...) ``` -15. **Remove Deprecated Code:** +15. **Remove Deprecated Code** - **Eliminate Obsolete Features:** Keep the codebase clean and update features as required. + +--- + +## **Developing New Modules** + +### **Directory Structure** + +Maintain a consistent directory structure for new modules. Below is an example structure: + +``` +kargo/ +README.md + pulumi/ + __main__.py + requirements.txt + core/ + __init__.py + utils.py + ... + modules/ + / + __init__.py + deploy.py + types.py + ... +``` + +### **Creating a New Module** + +1. **Define Configuration:** + + Add a new `types.py` file under your module directory to define the configuration dataclass: + + ```python + from dataclasses import dataclass, field + from typing import Optional, Dict, Any + + @dataclass + class NewModuleConfig: + version: Optional[str] = None + param1: str = "default_value" + labels: Dict[str, str] = field(default_factory=dict) + annotations: Dict[str, Any] = field(default_factory=dict) + + @staticmethod + def merge(user_config: Dict[str, Any]) -> 'NewModuleConfig': + default_config = NewModuleConfig() + for key, value in user_config.items(): + if hasattr(default_config, key): + setattr(default_config, key, value) + else: + pulumi.log.warn(f"Unknown configuration key '{key}' in new_module config.") + return default_config + ``` + +2. **Deploy Function:** + + Define the deployment logic in `deploy.py`: + + ```python + from typing import List, Dict, Any, Tuple, Optional + import pulumi + import pulumi_kubernetes as k8s + + from core.types import NamespaceConfig + from core.namespace import create_namespace + from core.metadata import get_global_annotations, get_global_labels + from core.utils import set_resource_metadata + from .types import NewModuleConfig + + def deploy_new_module( + config_new_module: NewModuleConfig, + global_depends_on: List[pulumi.Resource], + k8s_provider: k8s.Provider, + ) -> Tuple[Optional[str], Optional[pulumi.Resource]]: + # Define deployment logic here + namespace_config = NamespaceConfig(name=config_new_module.namespace) + namespace_resource = create_namespace(namespace_config, k8s_provider, global_depends_on) + + # Implement specific resource creation and transformation logic + ... + + return version, deployed_resource + ``` + +3. **Update `__main__.py`:** + + Ensure your module is included in the main deployment script: + + ```python + from typing import List, Dict, Any + + import pulumi + from pulumi_kubernetes import Provider + + from core.init import initialize_pulumi + from core.config import export_results + from core.deploy_module import deploy_module + + def main(): + try: + init = initialize_pulumi() + + config = init["config"] + k8s_provider = init["k8s_provider"] + versions = init["versions"] + configurations = init["configurations"] + default_versions = init["default_versions"] + global_depends_on = init["global_depends_on"] + + modules_to_deploy = ["cert_manager", "kubevirt", "new_module"] # Add your module here + + deploy_modules(modules_to_deploy, config, default_versions, global_depends_on, k8s_provider, versions, configurations) + + compliance_config = init.get("compliance_config", {}) + export_results(versions, configurations, compliance_config) + + except Exception as e: + pulumi.log.error(f"Deployment failed: {str(e)}") + raise + + def deploy_modules( + modules: List[str], + config: pulumi.Config, + default_versions: Dict[str, Any], + global_depends_on: List[pulumi.Resource], + k8s_provider: Provider, + versions: Dict[str, str], + configurations: Dict[str, Dict[str, Any]] + ) -> None: + + for module_name in modules: + pulumi.log.info(f"Deploying module: {module_name}") + deploy_module( + module_name=module_name, + config=config, + default_versions=default_versions, + global_depends_on=global_depends_on, + k8s_provider=k8s_provider, + versions=versions, + configurations=configurations, + ) + + if __name__ == "__main__": + main() + ``` + +### **Common Utilities** + +Refer to `core/utils.py` for common helper functions. For example, `set_resource_metadata` to apply global labels and annotations: + +```python +from typing import Optional, Dict, Any +import re +import pulumi +import pulumi_kubernetes as k8s + +def set_resource_metadata(metadata: Any, global_labels: Dict[str, str], global_annotations: Dict[str, str]): + if isinstance(metadata, dict): + if metadata.get('labels') is None: + metadata['labels'] = {} + metadata.setdefault('labels', {}).update(global_labels) + + if metadata.get('annotations') is None: + metadata['annotations'] = {} + metadata.setdefault('annotations', {}).update(global_annotations) + + elif isinstance(metadata, k8s.meta.v1.ObjectMetaArgs): + if metadata.labels is None: + metadata.labels = {} + metadata.labels.update(global_labels) + + if metadata.annotations is None: + metadata.annotations = {} + metadata.annotations.update(global_annotations) + +def sanitize_label_value(value: str) -> str: + value = value.lower() + sanitized = re.sub(r'[^a-z0-9_.-]', '-', value) + sanitized = re.sub(r'^[^a-z0-9]+', '', sanitized) + sanitized = re.sub(r'[^a-z0-9]+$', '', sanitized) + return sanitized[:63] +``` + +### **Version Control** + +Manage module versions within `core/versions.py`: + +```python +import json +import os +import pulumi +import requests + +DEFAULT_VERSIONS_URL_TEMPLATE = 'https://raw.githubusercontent.com/ContainerCraft/Kargo/rerefactor/pulumi/' + +def load_default_versions(config: pulumi.Config, force_refresh=False) -> dict: + ... + # Function implementation + ... +``` diff --git a/pulumi/core/metadata.py b/pulumi/core/metadata.py index 3f2e288..6b260f8 100644 --- a/pulumi/core/metadata.py +++ b/pulumi/core/metadata.py @@ -6,39 +6,34 @@ import subprocess import pulumi +import threading from typing import Dict class MetadataSingleton: - _instance: Dict[str, Dict[str, str]] = {} + _instance = None + __lock = threading.Lock() def __new__(cls, *args, **kwargs): if not cls._instance: - cls._instance = {"_global_labels": {}, "_global_annotations": {}} + with cls.__lock: + if not cls._instance: + cls._instance = super(MetadataSingleton, cls).__new__(cls) + cls._instance._data = {"_global_labels": {}, "_global_annotations": {}} return cls._instance def set_global_labels(labels: Dict[str, str]): - MetadataSingleton()["_global_labels"] = labels + MetadataSingleton()._data["_global_labels"] = labels def set_global_annotations(annotations: Dict[str, str]): - MetadataSingleton()["_global_annotations"] = annotations + MetadataSingleton()._data["_global_annotations"] = annotations def get_global_labels() -> Dict[str, str]: - return MetadataSingleton()["_global_labels"] + return MetadataSingleton()._data["_global_labels"] def get_global_annotations() -> Dict[str, str]: - return MetadataSingleton()["_global_annotations"] + return MetadataSingleton()._data["_global_annotations"] def collect_git_info() -> Dict[str, str]: - """ - Retrieves the current Git repository's remote URL, branch, and commit hash. - - This function uses subprocess to run git commands that fetch the remote URL, the current branch, - and the latest commit hash. This information is useful for tracking which version of the code - is being deployed, which branch it’s from, and which repository it originates from. - - Returns: - Dict[str, str]: A dictionary containing the remote URL, branch name, and commit hash. - """ try: remote = subprocess.check_output(['git', 'config', '--get', 'remote.origin.url'], stderr=subprocess.STDOUT).strip().decode('utf-8') branch = subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], stderr=subprocess.STDOUT).strip().decode('utf-8') diff --git a/pulumi/core/types.py b/pulumi/core/types.py index 178c34d..e928428 100644 --- a/pulumi/core/types.py +++ b/pulumi/core/types.py @@ -7,6 +7,7 @@ within the Kargo PaaS platform. """ +import pulumi from dataclasses import dataclass, field from typing import Optional, List, Dict, Any diff --git a/pulumi/core/utils.py b/pulumi/core/utils.py index f995d25..99a9b85 100644 --- a/pulumi/core/utils.py +++ b/pulumi/core/utils.py @@ -4,39 +4,36 @@ import re import pulumi import pulumi_kubernetes as k8s -from typing import Optional, Dict +from typing import Optional, Dict, Any -def generate_global_transformations(global_labels: Dict[str, str], global_annotations: Dict[str, str]): - """ - Registers a global transformation function that applies the provided labels and annotations to all resources. +def set_resource_metadata(metadata: Any, global_labels: Dict[str, str], global_annotations: Dict[str, str]): + if isinstance(metadata, dict): + if metadata.get('labels') is None: + metadata['labels'] = {} + metadata.setdefault('labels', {}).update(global_labels) + + if metadata.get('annotations') is None: + metadata['annotations'] = {} + metadata.setdefault('annotations', {}).update(global_annotations) + + elif isinstance(metadata, k8s.meta.v1.ObjectMetaArgs): + if metadata.labels is None: + metadata.labels = {} + metadata.labels.update(global_labels) + + if metadata.annotations is None: + metadata.annotations = {} + metadata.annotations.update(global_annotations) - Args: - global_labels (Dict[str, str]): Global label set to apply to all resources. - global_annotations (Dict[str, str]): Global annotations to apply to all resources. - """ + +def generate_global_transformations(global_labels: Dict[str, str], global_annotations: Dict[str, str]): def global_transform(args: pulumi.ResourceTransformationArgs) -> Optional[pulumi.ResourceTransformationResult]: props = args.props if 'metadata' in props: metadata = props['metadata'] - if isinstance(metadata, dict): - if metadata.get('labels') is None: - metadata['labels'] = {} - metadata['labels'].update(global_labels) - - if metadata.get('annotations') is None: - metadata['annotations'] = {} - metadata['annotations'].update(global_annotations) - - elif isinstance(metadata, k8s.meta.v1.ObjectMetaArgs): - if metadata.labels is None: - metadata.labels = {} - metadata.labels.update(global_labels) - - if metadata.annotations is None: - metadata.annotations = {} - metadata.annotations.update(global_annotations) - props['metadata'] = metadata + set_resource_metadata(metadata, global_labels, global_annotations) + props['metadata'] = metadata else: props['metadata'] = { 'labels': global_labels, @@ -46,36 +43,15 @@ def global_transform(args: pulumi.ResourceTransformationArgs) -> Optional[pulumi pulumi.runtime.register_stack_transformation(global_transform) -def sanitize_label_value(value: str) -> str: - """ - Sanitize label values to meet Kubernetes metadata requirements. - - Replaces invalid characters with '-' - - Args: - value (str): The string value to sanitize. - Returns: - str: The sanitized string value, truncated to 63 characters. - """ +def sanitize_label_value(value: str) -> str: value = value.lower() sanitized = re.sub(r'[^a-z0-9_.-]', '-', value) - sanitized = re.sub(r'^[^a-z0-9]+', '', sanitized) # Remove leading non-alphanumeric chars - sanitized = re.sub(r'[^a-z0-9]+$', '', sanitized) # Remove trailing non-alphanumeric chars + sanitized = re.sub(r'^[^a-z0-9]+', '', sanitized) + sanitized = re.sub(r'[^a-z0-9]+$', '', sanitized) return sanitized[:63] def extract_repo_name(remote_url: str) -> str: - """ - Extract the repository name from the remote URL. - - Example: 'https://github.com/ContainerCraft/Kargo.git' -> 'ContainerCraft/Kargo' - - Args: - remote_url (str): The remote URL from which to extract the repo name. - - Returns: - str: The extracted repository name. - """ match = re.search(r'[:/]([^/:]+/[^/\.]+)(\.git)?$', remote_url) if match: return match.group(1) diff --git a/pulumi/modules/cert_manager/deploy.py b/pulumi/modules/cert_manager/deploy.py index 5065f31..7c6a800 100644 --- a/pulumi/modules/cert_manager/deploy.py +++ b/pulumi/modules/cert_manager/deploy.py @@ -1,8 +1,7 @@ # ./pulumi/modules/cert_manager/deploy.py # Description: Deploys the CertManager module using Helm with labels and annotations. -from typing import List, Dict, Any, Tuple, Optional # Add Optional import -import requests +from typing import List, Dict, Any, Tuple, Optional import pulumi import pulumi_kubernetes as k8s from pulumi_kubernetes.apiextensions.CustomResource import CustomResource @@ -11,6 +10,7 @@ from core.helm_chart_versions import get_latest_helm_chart_version from core.types import NamespaceConfig from core.metadata import get_global_annotations, get_global_labels +from core.utils import set_resource_metadata from .types import CertManagerConfig def deploy_cert_manager_module( @@ -18,17 +18,6 @@ def deploy_cert_manager_module( global_depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, ) -> Tuple[str, pulumi.Resource, str]: - """ - Deploys the CertManager module with labels and annotations. - - Args: - config_cert_manager (CertManagerConfig): Configuration object for CertManager deployment. - global_depends_on (List[pulumi.Resource]): A list of resources that the deployment depends on. - k8s_provider (k8s.Provider): The Kubernetes provider for this deployment. - - Returns: - Tuple[str, pulumi.Resource, str]: The deployed CertManager version, release resource, and Base64-encoded CA certificate. - """ cert_manager_version, release, ca_cert_b64 = deploy_cert_manager( config_cert_manager=config_cert_manager, depends_on=global_depends_on, @@ -36,6 +25,7 @@ def deploy_cert_manager_module( ) global_depends_on.append(release) + pulumi.export("cert_manager_selfsigned_cert", ca_cert_b64) return cert_manager_version, release, ca_cert_b64 @@ -44,17 +34,6 @@ def deploy_cert_manager( depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider ) -> Tuple[str, k8s.helm.v3.Release, str]: - """ - Deploys CertManager using Helm with labels and annotations. - - Args: - config_cert_manager (CertManagerConfig): Configuration object for CertManager deployment. - depends_on (List[pulumi.Resource]): List of resources that this deployment depends on. - k8s_provider (k8s.Provider): The Kubernetes provider for this deployment. - - Returns: - Tuple[str, k8s.helm.v3.Release, str]: The CertManager version, release resource, and Base64-encoded CA certificate. - """ namespace = config_cert_manager.namespace version = config_cert_manager.version cluster_issuer_name = config_cert_manager.cluster_issuer @@ -74,7 +53,7 @@ def deploy_cert_manager( def helm_transform(args: pulumi.ResourceTransformationArgs): metadata = args.props.get('metadata', {}) - set_resource_metadata(metadata, get_global_labels(), get_global_annotations()) + set_resource_metadata(metadata, global_labels, global_annotations) args.props['metadata'] = metadata return pulumi.ResourceTransformationResult(args.props, args.opts) @@ -112,15 +91,6 @@ def helm_transform(args: pulumi.ResourceTransformationArgs): return version, release, ca_data_tls_crt_b64 def generate_helm_values(config_cert_manager: CertManagerConfig) -> Dict[str, Any]: - """ - Generates Helm values for CertManager from the configuration object. - - Args: - config_cert_manager (CertManagerConfig): The configuration object for CertManager. - - Returns: - Dict[str, Any]: The Helm values dictionary for CertManager. - """ return { 'replicaCount': 1, 'installCRDs': config_cert_manager.install_crds, @@ -131,17 +101,6 @@ def generate_helm_values(config_cert_manager: CertManagerConfig) -> Dict[str, An } def get_helm_chart_version(chart_url: str, chart_name: str, version: Optional[str]) -> str: - """ - Fetches or verifies the Helm chart version. - - Args: - chart_url (str): The Helm chart repository URL. - chart_name (str): The name of the Helm chart. - version (Optional[str]): The specified version or 'latest'. - - Returns: - str: The resolved version of the Helm chart. - """ if version == 'latest' or version is None: version = get_latest_helm_chart_version(f"{chart_url}/index.yaml", chart_name).lstrip("v") pulumi.log.info(f"Setting Helm release version to latest: {chart_name}/{version}") @@ -149,46 +108,7 @@ def get_helm_chart_version(chart_url: str, chart_name: str, version: Optional[st pulumi.log.info(f"Using Helm release version: {chart_name}/{version}") return version -def set_resource_metadata(metadata: Any, global_labels: Dict[str, str], global_annotations: Dict[str, str]): - """ - Sets metadata for a resource with global labels and annotations. - - Args: - metadata: The metadata object or dictionary. - global_labels (Dict[str, str]): The global labels. - global_annotations (Dict[str, str]): The global annotations. - """ - if isinstance(metadata, dict): - if metadata.get('labels') is None: - metadata['labels'] = {} - metadata.setdefault('labels', {}).update(global_labels) - - if metadata.get('annotations') is None: - metadata['annotations'] = {} - metadata.setdefault('annotations', {}).update(global_annotations) - - elif isinstance(metadata, k8s.meta.v1.ObjectMetaArgs): - if metadata.labels is None: - metadata.labels = {} - metadata.labels.update(global_labels) - - if metadata.annotations is None: - metadata.annotations = {} - metadata.annotations.update(global_annotations) - def create_cluster_issuers(cluster_issuer_name: str, namespace: str, k8s_provider: k8s.Provider, release: pulumi.Resource) -> Tuple[CustomResource, CustomResource, CustomResource]: - """ - Creates cluster issuers for cert-manager. - - Args: - cluster_issuer_name (str): The name of the cluster issuer. - namespace (str): The namespace for the cluster issuer. - k8s_provider (k8s.Provider): The Kubernetes provider. - release (pulumi.Resource): The release resource. - - Returns: - Tuple[CustomResource, CustomResource, CustomResource]: The created cluster issuers. - """ cluster_issuer_root = CustomResource( "cluster-selfsigned-issuer-root", api_version="cert-manager.io/v1", diff --git a/pulumi/modules/kubevirt/deploy.py b/pulumi/modules/kubevirt/deploy.py index 699c424..ef7ee39 100644 --- a/pulumi/modules/kubevirt/deploy.py +++ b/pulumi/modules/kubevirt/deploy.py @@ -15,6 +15,7 @@ from core.types import NamespaceConfig from core.namespace import create_namespace from core.metadata import get_global_labels, get_global_annotations +from core.utils import set_resource_metadata from .types import KubeVirtConfig @@ -23,20 +24,6 @@ def deploy_kubevirt_module( global_depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, ) -> Tuple[Optional[str], Optional[pulumi.Resource]]: - """ - Deploys the KubeVirt module with labels and annotations. - - Args: - config_kubevirt (KubeVirtConfig): Configuration object for KubeVirt deployment. - global_depends_on (List[pulumi.Resource]): A list of resources that the deployment depends on. - k8s_provider (k8s.Provider): The Kubernetes provider for this deployment. - - Returns: - Tuple[Optional[str], Optional[pulumi.Resource]]: - - The deployed KubeVirt version. - - The KubeVirt operator resource. - """ - # Create the namespace for KubeVirt first, independently namespace_config = NamespaceConfig(name=config_kubevirt.namespace) namespace_resource = create_namespace( config=namespace_config, @@ -44,7 +31,6 @@ def deploy_kubevirt_module( depends_on=global_depends_on ) - # Now deploy KubeVirt, ensuring it depends on the namespace creation kubevirt_version, kubevirt_operator = deploy_kubevirt( config_kubevirt=config_kubevirt, depends_on=[namespace_resource], @@ -52,38 +38,22 @@ def deploy_kubevirt_module( namespace_resource=namespace_resource ) - # Optionally add KubeVirt to global dependencies global_depends_on.append(kubevirt_operator) return kubevirt_version, kubevirt_operator - def deploy_kubevirt( config_kubevirt: KubeVirtConfig, depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, namespace_resource: pulumi.Resource ) -> Tuple[str, k8s.yaml.ConfigFile]: - """ - Deploys KubeVirt into the Kubernetes cluster using its YAML manifests and applies labels and annotations. - - Args: - config_kubevirt (KubeVirtConfig): Configuration object for KubeVirt deployment. - depends_on (List[pulumi.Resource]): List of resources that this deployment depends on. - k8s_provider (k8s.Provider): The Kubernetes provider for this deployment. - namespace_resource (pulumi.Resource): The namespace resource. - - Returns: - Tuple[str, k8s.yaml.ConfigFile]: The KubeVirt version and the operator resource. - """ - # Extract configuration details from the KubeVirtConfig object namespace = config_kubevirt.namespace version = config_kubevirt.version use_emulation = config_kubevirt.use_emulation labels = config_kubevirt.labels annotations = config_kubevirt.annotations - # Fetch or use the specified KubeVirt version if version == 'latest' or version is None: kubevirt_stable_version_url = 'https://storage.googleapis.com/kubevirt-prow/release/kubevirt/kubevirt/stable.txt' response = requests.get(kubevirt_stable_version_url) @@ -94,56 +64,27 @@ def deploy_kubevirt( else: pulumi.log.info(f"Using KubeVirt version: {version}") - # Download the KubeVirt operator YAML kubevirt_operator_url = f'https://github.com/kubevirt/kubevirt/releases/download/v{version}/kubevirt-operator.yaml' response = requests.get(kubevirt_operator_url) if response.status_code != 200: raise Exception(f"Failed to download KubeVirt operator YAML from {kubevirt_operator_url}") kubevirt_yaml = yaml.safe_load_all(response.text) - # Transform the YAML and set the correct namespace transformed_yaml = _transform_yaml(kubevirt_yaml, namespace) def kubevirt_transform(obj: Dict[str, Any], opts: pulumi.ResourceOptions) -> pulumi.ResourceTransformationResult: - """ - Transformation function to add labels and annotations to Kubernetes objects - in the KubeVirt operator YAML manifests. - - Args: - obj (Dict[str, Any]): The resource object to transform. - opts (pulumi.ResourceOptions): The resource options for the transformation. - - Returns: - pulumi.ResourceTransformationResult: The transformed resource and options. - """ - # Ensure the 'metadata' field exists in the object obj.setdefault("metadata", {}) - - # Apply labels - obj["metadata"].setdefault("labels", {}) - obj["metadata"]["labels"].update(labels) - - # Apply annotations - obj["metadata"].setdefault("annotations", {}) - obj["metadata"]["annotations"].update(annotations) - - # If this is a Pod or a Deployment, ensure the template also has metadata + set_resource_metadata(obj["metadata"], labels, annotations) if "spec" in obj and "template" in obj["spec"]: - obj["spec"]["template"].setdefault("metadata", {}) - obj["spec"]["template"]["metadata"].setdefault("labels", {}) - obj["spec"]["template"]["metadata"]["labels"].update(labels) - obj["spec"]["template"]["metadata"].setdefault("annotations", {}) - obj["spec"]["template"]["metadata"]["annotations"].update(annotations) - + template_meta = obj["spec"]["template"].setdefault("metadata", {}) + set_resource_metadata(template_meta, labels, annotations) return pulumi.ResourceTransformationResult(obj, opts) - # Write the transformed YAML to a temporary file with tempfile.NamedTemporaryFile(delete=False, mode='w') as temp_file: yaml.dump_all(transformed_yaml, temp_file) temp_file_path = temp_file.name try: - # Deploy the KubeVirt operator with transformation operator = k8s.yaml.ConfigFile( 'kubevirt-operator', file=temp_file_path, @@ -160,14 +101,11 @@ def kubevirt_transform(obj: Dict[str, Any], opts: pulumi.ResourceOptions) -> pul ) ) finally: - # Clean up the temporary file after Pulumi has used it pulumi.Output.all().apply(lambda _: os.unlink(temp_file_path)) - # Deploy the KubeVirt custom resource if use_emulation: pulumi.log.info("KVM Emulation enabled for KubeVirt.") - # Create the KubeVirt custom resource object kubevirt_custom_resource_spec = { "configuration": { "developerConfiguration": { @@ -208,18 +146,7 @@ def kubevirt_transform(obj: Dict[str, Any], opts: pulumi.ResourceOptions) -> pul return version, operator - def _transform_yaml(yaml_data: Any, namespace: str) -> List[Dict[str, Any]]: - """ - Helper function to transform YAML to set namespace and modify resources. - - Args: - yaml_data: The YAML data to transform. - namespace: The namespace to set for the resources. - - Returns: - List[Dict[str, Any]]: The transformed YAML with the appropriate namespace. - """ transformed = [] for resource in yaml_data: if resource.get('kind') == 'Namespace': diff --git a/pulumi/stacks/Pulumi.optiplexprime.yaml b/pulumi/stacks/Pulumi.optiplexprime.yaml index 39b66a5..25207a3 100644 --- a/pulumi/stacks/Pulumi.optiplexprime.yaml +++ b/pulumi/stacks/Pulumi.optiplexprime.yaml @@ -1,4 +1,45 @@ config: + kargo:compliance: + fisma: + enforcing: warn # accepts: strict, warn, disabled + enabled: true + ato: + authorized: "2025-03-27T00:00:00Z" + renew: "2026-03-27T00:00:00Z" + review: "2028-03-27T00:00:00Z" + level: moderate + nist: + auxiliary: + - ac-6.1 + - ac-2.13 + controls: + - ac-1 + - ac-2 + enabled: true + exceptions: + - ca-7.1 + - ma-2.2 + - si-12 + scip: + environment: prod + ownership: + operator: + contacts: + - seti2@nasa.gov + - alien51@nasa.gov + name: science-team-seti2-obs2819 + provider: + contacts: + - scip@nasa.gov + - bobert@nasa.gov + name: scip-team-xyz + provider: + name: kubevirt + regions: + - scip-west-1 + - scip-east-1 + - scip-lunar-2 + version: v2.87.11 kargo:kubernetes: context: usrbinkat-optiplexprime # Kubernetes context for the stack kargo:talos: From 4100b8e00859513188af812886d99434047245b8 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sat, 21 Sep 2024 14:22:01 -0700 Subject: [PATCH 26/43] add documentation --- pulumi/core/README.md | 190 ++++++++++++++++++++++++++++++ pulumi/modules/kubevirt/README.md | 181 ++++++++++++++++++++++++++++ 2 files changed, 371 insertions(+) create mode 100644 pulumi/core/README.md create mode 100644 pulumi/modules/kubevirt/README.md diff --git a/pulumi/core/README.md b/pulumi/core/README.md new file mode 100644 index 0000000..80a8c12 --- /dev/null +++ b/pulumi/core/README.md @@ -0,0 +1,190 @@ +# Core Module Development Guide + +This document provides an in-depth guide for developers, contributors, triage maintainers, and project maintainers working with the `core` module in the Kargo KubeVirt Kubernetes PaaS project. It includes a thorough overview of available functions, types, classes, and other core infrastructure details essential for productive module development. + +## Table of Contents + +- [Introduction](#introduction) +- [Core Components](#core-components) + - [Compliance](#compliance) + - [Configuration](#configuration) + - [Deployment](#deployment) + - [Helm Chart Versions](#helm-chart-versions) + - [Initialization](#initialization) + - [Introspection](#introspection) + - [Metadata](#metadata) + - [Namespace Management](#namespace-management) + - [Types](#types) + - [Utilities](#utilities) + - [Version Management](#version-management) +- [Best Practices](#best-practices) +- [Troubleshooting](#troubleshooting) +- [Contributing Guidelines](#contributing-guidelines) +- [Additional Resources](#additional-resources) + +## Introduction + +The `core` module is the backbone of the Kargo KubeVirt Kubernetes PaaS project. It houses essential functions, types, classes, and globals that facilitate the development and deployment of modules. This guide aims to equip you with the necessary knowledge to extend or maintain the core functionality effectively. + +## Core Components + +The core module is a collection of utilities and infrastructure necessary for the consistent and efficient deployment of modules in the Kargo platform. Below, we detail the key components and their usage. + +### Compliance + +Located in `pulumi/core/compliance.py`, this section deals with generating compliance-related labels and annotations. + +#### Functions + +- **`generate_compliance_labels(compliance_config: ComplianceConfig) -> Dict[str, str]`** + Generates compliance labels. +- **`generate_compliance_annotations(compliance_config: ComplianceConfig) -> Dict[str, str]`** + Generates compliance annotations. + +#### Key Types + +- **`ComplianceConfig`** + Central configuration object for compliance settings. +- **`FismaConfig`, `NistConfig`, `ScipConfig`** + Nested configuration types used within `ComplianceConfig`. + +### Configuration + +Located in `pulumi/core/config.py`, this section handles module configuration parsing and loading. + +#### Functions + +- **`get_module_config(module_name: str, config: pulumi.Config, default_versions: Dict[str, Any]) -> Tuple[Dict[str, Any], bool]`** + Retrieves and prepares the configuration for a module. +- **`export_results(versions: Dict[str, str], configurations: Dict[str, Dict[str, Any]], compliance: Dict[str, Any])`** + Exports deployment results including versions, configurations, and compliance information. + +### Deployment + +Located in `pulumi/core/deploy_module.py`, this section assists in deploying individual modules based on configuration. + +#### Functions + +- **`deploy_module(module_name: str, config: pulumi.Config, default_versions: Dict[str, Any], global_depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, versions: Dict[str, str], configurations: Dict[str, Dict[str, Any]]) -> None`** + Helper function to deploy a module based on configuration. + +### Helm Chart Versions + +Located in `pulumi/core/helm_chart_versions.py`, this section provides functionality to manage Helm chart versions. + +#### Functions + +- **`is_stable_version(version_str: str) -> bool`** + Determines if a version string represents a stable version. +- **`get_latest_helm_chart_version(url: str, chart_name: str) -> str`** + Fetches the latest stable version of a Helm chart from a given URL. + +### Initialization + +Located in `pulumi/core/init.py`, this section initializes Pulumi configuration, Kubernetes provider, and global resources. + +#### Functions + +- **`initialize_pulumi() -> Dict[str, Any]`** + Initializes Pulumi configuration, Kubernetes provider, and global resources. + +### Introspection + +Located in `pulumi/core/introspection.py`, this section provides functionality for discovering configuration classes and deploy functions. + +#### Functions + +- **`discover_config_class(module_name: str) -> Type`** + Discovers and returns the configuration class from a module's `types.py`. +- **`discover_deploy_function(module_name: str) -> Callable`** + Discovers and returns the deploy function from a module's `deploy.py`. + +### Metadata + +Located in `pulumi/core/metadata.py`, this section manages global metadata such as labels and annotations. + +#### Functions + +- **`set_global_labels(labels: Dict[str, str])`** + Sets global labels. +- **`set_global_annotations(annotations: Dict[str, str])`** + Sets global annotations. +- **`get_global_labels() -> Dict[str, str]`** + Retrieves global labels. +- **`get_global_annotations() -> Dict[str, str]`** + Retrieves global annotations. +- **`collect_git_info() -> Dict[str, str]`** + Collects Git repository information. +- **`generate_git_labels(git_info: Dict[str, str]) -> Dict[str, str]`** + Generates git-related labels. +- **`generate_git_annotations(git_info: Dict[str, str]) -> Dict[str, str]`** + Generates git-related annotations. + +### Namespace Management + +Located in `pulumi/core/namespace.py`, this section handles the creation and management of Kubernetes namespaces. + +#### Functions + +- **`create_namespace(config: NamespaceConfig, k8s_provider: k8s.Provider, depends_on: Optional[List[pulumi.Resource]] = None) -> k8s.core.v1.Namespace`** + Creates a Kubernetes namespace with the provided configuration. + +### Types + +Located in `pulumi/core/types.py`, this section defines shared configuration types that are utilized by various modules. + +#### Key Types + +- **`NamespaceConfig`** + Configuration object for Kubernetes namespaces. +- **`ComplianceConfig`, `FismaConfig`, `NistConfig`, `ScipConfig`** + Configuration objects for compliance settings. + +### Utilities + +Located in `pulumi/core/utils.py`, this section provides various utility functions for sanitizing labels, handling metadata, and more. + +#### Functions + +- **`set_resource_metadata(metadata: Any, global_labels: Dict[str, str], global_annotations: Dict[str, str])`** + Sets metadata for a given resource. +- **`generate_global_transformations(global_labels: Dict[str, str], global_annotations: Dict[str, str])`** + Generates global transformations for resources. +- **`sanitize_label_value(value: str) -> str`** + Sanitizes a label value to ensure compliance with Kubernetes naming conventions. +- **`extract_repo_name(remote_url: str) -> str`** + Extracts the repository name from a Git remote URL. + +### Version Management + +Located in `pulumi/core/versions.py`, this section loads default versions for modules based on specified configuration settings. + +#### Functions + +- **`load_default_versions(config: pulumi.Config, force_refresh=False) -> dict`** + Loads the default versions for modules based on configuration settings. + +## Best Practices + +- **Consistent Naming**: Follow consistent naming conventions for functions and variables. +- **Documentation**: Use detailed docstrings and comments to document your code. +- **Type Annotations**: Use type annotations to ensure readability and type safety. +- **Reusable Code**: Centralize reusable code in the `core` module to ensure consistency across modules. +- **Version Control**: Manage component versions to maintain consistency and avoid conflicts. + +## Troubleshooting + +- **Error Logging**: Use Pulumi's logging functions (`pulumi.log.info`, `pulumi.log.warn`, `pulumi.log.error`) to provide meaningful error messages. +- **Configuration Issues**: Ensure all configuration options are correctly set and validate input using type checks. +- **Module Dependencies**: Verify dependencies between modules are correctly resolved using Pulumi's dependency management features. + +## Contributing Guidelines + +- **Submit Issues**: Report bugs or request features via GitHub issues. +- **Pull Requests**: Submit pull requests with detailed descriptions and follow the project's coding standards. +- **Code Reviews**: Participate in code reviews to maintain code quality. + +## Additional Resources + +- **Kargo KubeVirt Documentation**: [GitHub Repository](https://github.com/containercraft/kargo) +- **Pulumi Documentation**: [Pulumi Documentation](https://www.pulumi.com/docs/) diff --git a/pulumi/modules/kubevirt/README.md b/pulumi/modules/kubevirt/README.md new file mode 100644 index 0000000..a9ba7b3 --- /dev/null +++ b/pulumi/modules/kubevirt/README.md @@ -0,0 +1,181 @@ +# KubeVirt Module + +ContainerCraft Kargo Kubevirt PaaS KubeVirt module. + +The `kubevirt` module automates the deployment and configuration of KubeVirt in your Kubernetes environment using [KubeVirt](https://kubevirt.io/). KubeVirt is a Kubernetes-native solution to run and manage virtual machines alongside container workloads. This module simplifies the setup, configuration, and management of KubeVirt components for a seamless virtualization experience within Kubernetes. + +--- + +## Table of Contents + +- [Introduction](#introduction) +- [Prerequisites](#prerequisites) +- [Features](#features) +- [Enabling the Module](#enabling-the-module) +- [Configuration](#configuration) + - [Default Configuration](#default-configuration) + - [Custom Configuration](#custom-configuration) +- [Module Components](#module-components) + - [Namespace Creation](#namespace-creation) + - [KubeVirt Deployment](#kubevirt-deployment) + - [Custom Resource Creation](#custom-resource-creation) +- [Integration with Kargo PaaS](#integration-with-kargo-paas) +- [Best Practices](#best-practices) +- [Troubleshooting](#troubleshooting) +- [Additional Resources](#additional-resources) + +--- + +## Introduction + +This guide provides an overview of the KubeVirt module, instructions on how to enable and configure it, and explanations of its functionality within the Kargo PaaS platform. Whether you're new to Kubernetes, KubeVirt, or ContainerCraft Kargo PaaS, this guide will help you get started. + +## Prerequisites + +Before deploying the module, ensure the following prerequisites are met: + +- [Pulumi CLI](https://www.pulumi.com/docs/get-started/) +- Python 3.6 or higher +- Python dependencies (install via `pip install -r requirements.txt`) +- Kubernetes cluster (with `kubectl` configured) +- Properly configured `kubeconfig` file + +--- + +## Features + +- **Automated Deployment**: Deploys KubeVirt using the official operator YAMLs. +- **Version Management**: Supports explicit version pinning or accepts `latest`. +- **Namespace Isolation**: Deploys KubeVirt in a dedicated namespace. +- **Customizable Configuration**: Allows setting overrides for customization, including emulation and feature gates. +- **Metadata Propagation**: Applies global labels and annotations consistently across resources. + +--- + +## Enabling the Module + +The KubeVirt module is enabled by default. Customization can be configured in the Pulumi Stack Configuration. + +### Example Pulumi Configuration + +```yaml +# Pulumi..yaml + +config: + kubevirt: + enabled: true # (default: true) +``` + +Alternatively, you can set configuration values via the Pulumi CLI: + +```bash +pulumi config set --path kubevirt.key value +``` + +--- + +## Configuration + +### Default Configuration + +The module comes with sensible defaults to simplify deployment: + +- **Namespace**: `kubevirt` +- **Version**: Uses the default version specified in `default_versions.json` +- **Use Emulation**: `false` +- **Labels and Annotations**: Derived from global configurations + +### Custom Configuration + +You can customize the module's behavior by providing additional configuration options. + +#### Available Configuration Options + +- **namespace** *(string)*: The namespace where KubeVirt will be deployed. +- **version** *(string)*: The version of the KubeVirt operator YAML to deploy. Use `'latest'` to fetch the latest version. +- **use_emulation** *(bool)*: Whether to enable KVM emulation. +- **labels** *(dict)*: Custom labels to apply to resources. +- **annotations** *(dict)*: Custom annotations to apply to resources. + +#### Example Custom Configuration + +```yaml +# Pulumi..yaml +# Default values are shown for reference + +config: + kubevirt: + enabled: true + version: "0.43.0" + namespace: "custom-kubevirt" + use_emulation: true + labels: + app.kubernetes.io/name: "kubevirt" + annotations: + organization: "ContainerCraft" +``` + +--- + +## Module Components + +### Namespace Creation + +The module creates a dedicated namespace for KubeVirt to ensure isolation and better management. + +- **Namespace**: Configurable via the `namespace` parameter. +- **Labels and Annotations**: Applied as per best practices for identification. + +### KubeVirt Deployment + +The module deploys KubeVirt using the official operator YAML from the KubeVirt GitHub repository. + +- **Operator YAML**: Downloaded from the specified version or the latest release. +- **Custom Values**: Includes configurations for namespaces, labels, and annotations. +- **Version**: Configurable; defaults to the version specified in `default_versions.json`. + +### Custom Resource Creation + +To manage KubeVirt operational settings, the module creates custom resources with specified configurations. + +- **KubeVirt CR**: Customizes KubeVirt to enable emulation and additional feature gates. +- **SMBIOS Configuration**: Adds specific values to the SMBIOS configuration for virtual machines. +- **Namespace and Resources**: The custom resource is applied in the specified namespace with the appropriate labels and annotations. + +## Integration with Kargo PaaS + +KubeVirt integrates seamlessly with the Kargo Kubevirt PaaS, making it simple to manage and run virtual machines alongside container workloads. The deployment process is optimized for consistency and simplicity, ensuring a smooth user experience within the Kubernetes ecosystem. + +--- + +## Best Practices + +- **Namespace Isolation**: Always deploy KubeVirt in a dedicated namespace to avoid conflicts. +- **Version Pinning**: Pin specific versions in production to avoid unintended issues with new releases. +- **Emulation Enablement**: Enable `use_emulation` only if you intend to run KubeVirt on non-bare-metal setups. + +--- + +## Troubleshooting + +### Common Issues + +- **Connection Errors**: Ensure your `kubeconfig` and Kubernetes context are correctly configured. +- **Version Conflicts**: If deployment fails due to version issues, verify the specified version is available in the KubeVirt repository. Alternatively use `'latest'` and Kargo will fetch the latest version. +- **Namespace Issues**: Ensure the specified namespace is unique or does not conflict with existing namespaces. + +### Debugging Steps + +1. **Check Pulumi Logs**: Look for error messages during deployment. +2. **Verify Kubernetes Resources**: Use `kubectl` to inspect the KubeVirt namespace and resources. +3. **Review Configuration**: Ensure all configuration options are correctly set in your Pulumi config or remove configuration to use defaults. + +--- + +## Additional Resources + +- **KubeVirt Documentation**: [https://kubevirt.io/docs/](https://kubevirt.io/docs/) +- **Kargo Kubevirt PaaS IaC Documentation**: Refer to the main [Kargo README](../README.md) for project usage. +- **Pulumi Kubernetes Provider**: [https://www.pulumi.com/docs/reference/pkg/kubernetes/](https://www.pulumi.com/docs/reference/pkg/kubernetes/) +- **Helm Charts**: [https://artifacthub.io/packages/helm/jetstack/cert-manager](https://artifacthub.io/packages/helm/jetstack/cert-manager) +- **Need Help?** If you have questions or need assistance, feel free to reach out to the community or maintainers on GitHub, Discord, or Twitter. From 408238f89f8046c8394c9cc1871e041f95cea993 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sat, 21 Sep 2024 14:37:29 -0700 Subject: [PATCH 27/43] tidy core --- pulumi/__main__.py | 1 - pulumi/core/README.md | 4 +-- pulumi/core/init.py | 3 +-- pulumi/core/metadata.py | 48 ++++++++++++++++++++++++++++++++++++ pulumi/core/utils.py | 54 ++++++++++++++++++++++++++--------------- pulumi/core/versions.py | 1 + 6 files changed, 87 insertions(+), 24 deletions(-) diff --git a/pulumi/__main__.py b/pulumi/__main__.py index 235a764..d62b6ed 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -1,5 +1,4 @@ # ./pulumi/__main__.py -# Description: from typing import List, Dict, Any diff --git a/pulumi/core/README.md b/pulumi/core/README.md index 80a8c12..a53a49f 100644 --- a/pulumi/core/README.md +++ b/pulumi/core/README.md @@ -45,7 +45,7 @@ Located in `pulumi/core/compliance.py`, this section deals with generating compl - **`ComplianceConfig`** Central configuration object for compliance settings. -- **`FismaConfig`, `NistConfig`, `ScipConfig`** +- **`FismaConfig`, `NistConfig`, and `ScipConfig`** Nested configuration types used within `ComplianceConfig`. ### Configuration @@ -137,7 +137,7 @@ Located in `pulumi/core/types.py`, this section defines shared configuration typ - **`NamespaceConfig`** Configuration object for Kubernetes namespaces. -- **`ComplianceConfig`, `FismaConfig`, `NistConfig`, `ScipConfig`** +- **`ComplianceConfig`, `FismaConfig`, `NistConfig`, and `ScipConfig`** Configuration objects for compliance settings. ### Utilities diff --git a/pulumi/core/init.py b/pulumi/core/init.py index 404d180..567f7f2 100644 --- a/pulumi/core/init.py +++ b/pulumi/core/init.py @@ -1,11 +1,10 @@ # ./pulumi/core/init.py # Description: Initializes Pulumi configuration, Kubernetes provider, and global resources. - import os import pulumi import pulumi_kubernetes as k8s from pulumi_kubernetes import Provider -from typing import Dict, List, Any +from typing import Dict, Any, List from .versions import load_default_versions from .metadata import ( diff --git a/pulumi/core/metadata.py b/pulumi/core/metadata.py index 6b260f8..32bb7d4 100644 --- a/pulumi/core/metadata.py +++ b/pulumi/core/metadata.py @@ -22,18 +22,48 @@ def __new__(cls, *args, **kwargs): return cls._instance def set_global_labels(labels: Dict[str, str]): + """ + Sets global labels. + + Args: + labels (Dict[str, str]): The global labels. + """ MetadataSingleton()._data["_global_labels"] = labels def set_global_annotations(annotations: Dict[str, str]): + """ + Sets global annotations. + + Args: + annotations (Dict[str, str]): The global annotations. + """ MetadataSingleton()._data["_global_annotations"] = annotations def get_global_labels() -> Dict[str, str]: + """ + Retrieves global labels. + + Returns: + Dict[str, str]: The global labels. + """ return MetadataSingleton()._data["_global_labels"] def get_global_annotations() -> Dict[str, str]: + """ + Retrieves global annotations. + + Returns: + Dict[str, str]: The global annotations. + """ return MetadataSingleton()._data["_global_annotations"] def collect_git_info() -> Dict[str, str]: + """ + Collects Git repository information. + + Returns: + Dict[str, str]: The Git information. + """ try: remote = subprocess.check_output(['git', 'config', '--get', 'remote.origin.url'], stderr=subprocess.STDOUT).strip().decode('utf-8') branch = subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], stderr=subprocess.STDOUT).strip().decode('utf-8') @@ -44,12 +74,30 @@ def collect_git_info() -> Dict[str, str]: return {'remote': 'N/A', 'branch': 'N/A', 'commit': 'N/A'} def generate_git_labels(git_info: Dict[str, str]) -> Dict[str, str]: + """ + Generates git-related labels. + + Args: + git_info (Dict[str, str]): The Git information. + + Returns: + Dict[str, str]: The git-related labels. + """ return { "git.branch": git_info.get("branch", ""), "git.commit": git_info.get("commit", "")[:7], } def generate_git_annotations(git_info: Dict[str, str]) -> Dict[str, str]: + """ + Generates git-related annotations. + + Args: + git_info (Dict[str, str]): The Git information. + + Returns: + Dict[str, str]: The git-related annotations. + """ return { "git.remote": git_info.get("remote", ""), "git.commit.full": git_info.get("commit", ""), diff --git a/pulumi/core/utils.py b/pulumi/core/utils.py index 99a9b85..8a0f813 100644 --- a/pulumi/core/utils.py +++ b/pulumi/core/utils.py @@ -6,45 +6,52 @@ import pulumi_kubernetes as k8s from typing import Optional, Dict, Any - def set_resource_metadata(metadata: Any, global_labels: Dict[str, str], global_annotations: Dict[str, str]): + """ + Updates resource metadata with global labels and annotations. + + Args: + metadata (Any): Metadata to update. + global_labels (Dict[str, str]): Global labels to apply. + global_annotations (Dict[str, str]): Global annotations to apply. + """ if isinstance(metadata, dict): - if metadata.get('labels') is None: - metadata['labels'] = {} metadata.setdefault('labels', {}).update(global_labels) - - if metadata.get('annotations') is None: - metadata['annotations'] = {} metadata.setdefault('annotations', {}).update(global_annotations) - elif isinstance(metadata, k8s.meta.v1.ObjectMetaArgs): if metadata.labels is None: metadata.labels = {} metadata.labels.update(global_labels) - if metadata.annotations is None: metadata.annotations = {} metadata.annotations.update(global_annotations) - def generate_global_transformations(global_labels: Dict[str, str], global_annotations: Dict[str, str]): + """ + Generates global transformations for resources. + + Args: + global_labels (Dict[str, str]): Global labels to apply. + global_annotations (Dict[str, str]): Global annotations to apply. + """ def global_transform(args: pulumi.ResourceTransformationArgs) -> Optional[pulumi.ResourceTransformationResult]: props = args.props - if 'metadata' in props: - metadata = props['metadata'] - set_resource_metadata(metadata, global_labels, global_annotations) - props['metadata'] = metadata - else: - props['metadata'] = { - 'labels': global_labels, - 'annotations': global_annotations - } + props.setdefault('metadata', {}) + set_resource_metadata(props['metadata'], global_labels, global_annotations) return pulumi.ResourceTransformationResult(props, args.opts) pulumi.runtime.register_stack_transformation(global_transform) - def sanitize_label_value(value: str) -> str: + """ + Sanitizes a label value to comply with Kubernetes naming conventions. + + Args: + value (str): The value to sanitize. + + Returns: + str: The sanitized value. + """ value = value.lower() sanitized = re.sub(r'[^a-z0-9_.-]', '-', value) sanitized = re.sub(r'^[^a-z0-9]+', '', sanitized) @@ -52,6 +59,15 @@ def sanitize_label_value(value: str) -> str: return sanitized[:63] def extract_repo_name(remote_url: str) -> str: + """ + Extracts the repository name from a Git remote URL. + + Args: + remote_url (str): The Git remote URL. + + Returns: + str: The repository name. + """ match = re.search(r'[:/]([^/:]+/[^/\.]+)(\.git)?$', remote_url) if match: return match.group(1) diff --git a/pulumi/core/versions.py b/pulumi/core/versions.py index 6c25b57..86b7500 100644 --- a/pulumi/core/versions.py +++ b/pulumi/core/versions.py @@ -3,6 +3,7 @@ import os import pulumi import requests +from typing import Dict #DEFAULT_VERSIONS_URL_TEMPLATE = 'https://github.com/containercraft/kargo/releases/latest/download/' # TODO: replace with official github releases artifact URLs when released From fc9b35712e3593969ac0b6011d8fd56de59da815 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sat, 21 Sep 2024 14:55:05 -0700 Subject: [PATCH 28/43] core names --- pulumi/__main__.py | 2 +- pulumi/core/{helm_chart_versions.py => helm.py} | 2 +- pulumi/core/{deploy_module.py => module.py} | 0 pulumi/modules/cert_manager/deploy.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename pulumi/core/{helm_chart_versions.py => helm.py} (97%) rename pulumi/core/{deploy_module.py => module.py} (100%) diff --git a/pulumi/__main__.py b/pulumi/__main__.py index d62b6ed..38284e4 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -7,7 +7,7 @@ from core.init import initialize_pulumi from core.config import export_results -from core.deploy_module import deploy_module +from core.module import deploy_module def main(): try: diff --git a/pulumi/core/helm_chart_versions.py b/pulumi/core/helm.py similarity index 97% rename from pulumi/core/helm_chart_versions.py rename to pulumi/core/helm.py index 019c342..c6c37e7 100644 --- a/pulumi/core/helm_chart_versions.py +++ b/pulumi/core/helm.py @@ -1,4 +1,4 @@ -# ./pulumi/core/helm_chart_versions.py +# ./pulumi/core/helm.py # Description: import requests diff --git a/pulumi/core/deploy_module.py b/pulumi/core/module.py similarity index 100% rename from pulumi/core/deploy_module.py rename to pulumi/core/module.py diff --git a/pulumi/modules/cert_manager/deploy.py b/pulumi/modules/cert_manager/deploy.py index 7c6a800..8c03bd7 100644 --- a/pulumi/modules/cert_manager/deploy.py +++ b/pulumi/modules/cert_manager/deploy.py @@ -7,7 +7,7 @@ from pulumi_kubernetes.apiextensions.CustomResource import CustomResource from core.namespace import create_namespace -from core.helm_chart_versions import get_latest_helm_chart_version +from core.helm import get_latest_helm_chart_version from core.types import NamespaceConfig from core.metadata import get_global_annotations, get_global_labels from core.utils import set_resource_metadata From 965740bf8d5330a1bead9a73e2d8c0c8dbf8762c Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sat, 21 Sep 2024 16:44:33 -0700 Subject: [PATCH 29/43] refactoring core module --- pulumi/__main__.py | 17 +- pulumi/core/README.md | 218 ++++++++++++-------------- pulumi/core/compliance.py | 48 ------ pulumi/core/config.py | 98 +++++++++++- pulumi/core/deployment.py | 211 +++++++++++++++++++++++++ pulumi/core/helm.py | 44 ------ pulumi/core/init.py | 82 ---------- pulumi/core/introspection.py | 39 ----- pulumi/core/metadata.py | 72 ++++++++- pulumi/core/module.py | 89 ----------- pulumi/core/namespace.py | 52 ------ pulumi/core/types.py | 7 +- pulumi/core/utils.py | 68 ++++++-- pulumi/core/versions.py | 96 ------------ pulumi/modules/cert_manager/deploy.py | 106 +++++++++---- pulumi/modules/kubevirt/deploy.py | 113 ++++++++----- 16 files changed, 702 insertions(+), 658 deletions(-) delete mode 100644 pulumi/core/compliance.py create mode 100644 pulumi/core/deployment.py delete mode 100644 pulumi/core/helm.py delete mode 100644 pulumi/core/init.py delete mode 100644 pulumi/core/introspection.py delete mode 100644 pulumi/core/module.py delete mode 100644 pulumi/core/namespace.py delete mode 100644 pulumi/core/versions.py diff --git a/pulumi/__main__.py b/pulumi/__main__.py index 38284e4..324f136 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -1,13 +1,12 @@ -# ./pulumi/__main__.py +# pulumi/__main__.py from typing import List, Dict, Any import pulumi from pulumi_kubernetes import Provider -from core.init import initialize_pulumi +from core.deployment import initialize_pulumi, deploy_module from core.config import export_results -from core.module import deploy_module def main(): try: @@ -22,7 +21,15 @@ def main(): modules_to_deploy = ["cert_manager", "kubevirt"] - deploy_modules(modules_to_deploy, config, default_versions, global_depends_on, k8s_provider, versions, configurations) + deploy_modules( + modules_to_deploy, + config, + default_versions, + global_depends_on, + k8s_provider, + versions, + configurations, + ) compliance_config = init.get("compliance_config", {}) export_results(versions, configurations, compliance_config) @@ -38,7 +45,7 @@ def deploy_modules( global_depends_on: List[pulumi.Resource], k8s_provider: Provider, versions: Dict[str, str], - configurations: Dict[str, Dict[str, Any]] + configurations: Dict[str, Dict[str, Any]], ) -> None: for module_name in modules: diff --git a/pulumi/core/README.md b/pulumi/core/README.md index a53a49f..cfdb1c9 100644 --- a/pulumi/core/README.md +++ b/pulumi/core/README.md @@ -1,190 +1,174 @@ -# Core Module Development Guide +# Kargo Module Developmer Guide -This document provides an in-depth guide for developers, contributors, triage maintainers, and project maintainers working with the `core` module in the Kargo KubeVirt Kubernetes PaaS project. It includes a thorough overview of available functions, types, classes, and other core infrastructure details essential for productive module development. +This document provides an in-depth guide for developers, contributors, triage maintainers, and project maintainers utilizing the `core` module in the Kargo KubeVirt Kubernetes PaaS project. It includes a thorough overview of available functions, types, classes, and other core infrastructure details essential for productive module development. ## Table of Contents - [Introduction](#introduction) -- [Core Components](#core-components) - - [Compliance](#compliance) - - [Configuration](#configuration) - - [Deployment](#deployment) - - [Helm Chart Versions](#helm-chart-versions) - - [Initialization](#initialization) - - [Introspection](#introspection) - - [Metadata](#metadata) - - [Namespace Management](#namespace-management) - - [Types](#types) - - [Utilities](#utilities) - - [Version Management](#version-management) +- [Core Module Structure](#core-module-structure) + - [config.py](#configpy) + - [deployment.py](#deploymentpy) + - [metadata.py](#metadatapy) + - [utils.py](#utilspy) + - [types.py](#typespy) - [Best Practices](#best-practices) - [Troubleshooting](#troubleshooting) - [Contributing Guidelines](#contributing-guidelines) - [Additional Resources](#additional-resources) +--- + ## Introduction The `core` module is the backbone of the Kargo KubeVirt Kubernetes PaaS project. It houses essential functions, types, classes, and globals that facilitate the development and deployment of modules. This guide aims to equip you with the necessary knowledge to extend or maintain the core functionality effectively. -## Core Components - -The core module is a collection of utilities and infrastructure necessary for the consistent and efficient deployment of modules in the Kargo platform. Below, we detail the key components and their usage. - -### Compliance - -Located in `pulumi/core/compliance.py`, this section deals with generating compliance-related labels and annotations. - -#### Functions - -- **`generate_compliance_labels(compliance_config: ComplianceConfig) -> Dict[str, str]`** - Generates compliance labels. -- **`generate_compliance_annotations(compliance_config: ComplianceConfig) -> Dict[str, str]`** - Generates compliance annotations. - -#### Key Types - -- **`ComplianceConfig`** - Central configuration object for compliance settings. -- **`FismaConfig`, `NistConfig`, and `ScipConfig`** - Nested configuration types used within `ComplianceConfig`. +## Core Module Structure -### Configuration +The `core` module structure: -Located in `pulumi/core/config.py`, this section handles module configuration parsing and loading. +``` +pulumi/core/ +├── __init__.py +├── README.md +├── config.py +├── deployment.py +├── metadata.py +├── utils.py +└── types.py +``` -#### Functions +### config.py -- **`get_module_config(module_name: str, config: pulumi.Config, default_versions: Dict[str, Any]) -> Tuple[Dict[str, Any], bool]`** - Retrieves and prepares the configuration for a module. -- **`export_results(versions: Dict[str, str], configurations: Dict[str, Dict[str, Any]], compliance: Dict[str, Any])`** - Exports deployment results including versions, configurations, and compliance information. +**Responsibilities:** -### Deployment +- Handle all configuration-related functionalities. +- Load and merge user configurations with defaults. +- Load default versions for modules. +- Export deployment results. -Located in `pulumi/core/deploy_module.py`, this section assists in deploying individual modules based on configuration. +**Key Functions:** -#### Functions +- `get_module_config(module_name, config, default_versions)`: Retrieves and prepares the configuration for a module. +- `load_default_versions(config, force_refresh=False)`: Loads the default versions for modules based on the specified configuration settings. +- `export_results(versions, configurations, compliance)`: Exports the results of the deployment processes including versions, configurations, and compliance information. -- **`deploy_module(module_name: str, config: pulumi.Config, default_versions: Dict[str, Any], global_depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, versions: Dict[str, str], configurations: Dict[str, Dict[str, Any]]) -> None`** - Helper function to deploy a module based on configuration. +**Key Types:** -### Helm Chart Versions +- `ComplianceConfig`: Central configuration object for compliance settings (imported from `types.py`). -Located in `pulumi/core/helm_chart_versions.py`, this section provides functionality to manage Helm chart versions. +--- -#### Functions +### deployment.py -- **`is_stable_version(version_str: str) -> bool`** - Determines if a version string represents a stable version. -- **`get_latest_helm_chart_version(url: str, chart_name: str) -> str`** - Fetches the latest stable version of a Helm chart from a given URL. +**Responsibilities:** -### Initialization +- Manage the deployment orchestration of modules. +- Initialize Pulumi and Kubernetes providers. +- Deploy individual modules based on configuration. +- Discover module-specific configuration classes and deploy functions. -Located in `pulumi/core/init.py`, this section initializes Pulumi configuration, Kubernetes provider, and global resources. +**Key Functions:** -#### Functions +- `initialize_pulumi()`: Initializes Pulumi configuration, Kubernetes provider, and global resources. +- `deploy_module(module_name, config, default_versions, global_depends_on, k8s_provider, versions, configurations)`: Helper function to deploy a module based on configuration. +- `discover_config_class(module_name)`: Discovers and returns the configuration class from the module's `types.py`. +- `discover_deploy_function(module_name)`: Discovers and returns the deploy function from the module's `deploy.py`. -- **`initialize_pulumi() -> Dict[str, Any]`** - Initializes Pulumi configuration, Kubernetes provider, and global resources. +--- -### Introspection +### metadata.py -Located in `pulumi/core/introspection.py`, this section provides functionality for discovering configuration classes and deploy functions. +**Responsibilities:** -#### Functions +- Manage global metadata, labels, and annotations. +- Generate compliance and Git-related metadata. +- Sanitize label values to comply with Kubernetes naming conventions. -- **`discover_config_class(module_name: str) -> Type`** - Discovers and returns the configuration class from a module's `types.py`. -- **`discover_deploy_function(module_name: str) -> Callable`** - Discovers and returns the deploy function from a module's `deploy.py`. +**Key Classes and Functions:** -### Metadata +- `MetadataSingleton`: Singleton class to store global labels and annotations. +- `set_global_labels(labels)`: Sets global labels. +- `set_global_annotations(annotations)`: Sets global annotations. +- `get_global_labels()`: Retrieves global labels. +- `get_global_annotations()`: Retrieves global annotations. +- `collect_git_info()`: Collects Git repository information. +- `generate_git_labels(git_info)`: Generates Git-related labels. +- `generate_git_annotations(git_info)`: Generates Git-related annotations. +- `generate_compliance_labels(compliance_config)`: Generates compliance labels. +- `generate_compliance_annotations(compliance_config)`: Generates compliance annotations. +- `sanitize_label_value(value)`: Sanitizes a label value to comply with Kubernetes naming conventions. -Located in `pulumi/core/metadata.py`, this section manages global metadata such as labels and annotations. +--- -#### Functions +### utils.py -- **`set_global_labels(labels: Dict[str, str])`** - Sets global labels. -- **`set_global_annotations(annotations: Dict[str, str])`** - Sets global annotations. -- **`get_global_labels() -> Dict[str, str]`** - Retrieves global labels. -- **`get_global_annotations() -> Dict[str, str]`** - Retrieves global annotations. -- **`collect_git_info() -> Dict[str, str]`** - Collects Git repository information. -- **`generate_git_labels(git_info: Dict[str, str]) -> Dict[str, str]`** - Generates git-related labels. -- **`generate_git_annotations(git_info: Dict[str, str]) -> Dict[str, str]`** - Generates git-related annotations. +**Responsibilities:** -### Namespace Management +- Provide utility functions that are generic and reusable. +- Handle tasks like resource transformations and Helm interactions. +- Extract repository names from Git URLs. -Located in `pulumi/core/namespace.py`, this section handles the creation and management of Kubernetes namespaces. +**Key Functions:** -#### Functions +- `set_resource_metadata(metadata, global_labels, global_annotations)`: Updates resource metadata with global labels and annotations. +- `generate_global_transformations(global_labels, global_annotations)`: Generates global transformations for resources. +- `get_latest_helm_chart_version(url, chart_name)`: Fetches the latest stable version of a Helm chart from the given URL. +- `is_stable_version(version_str)`: Determines if a version string represents a stable version. +- `extract_repo_name(remote_url)`: Extracts the repository name from a Git remote URL. -- **`create_namespace(config: NamespaceConfig, k8s_provider: k8s.Provider, depends_on: Optional[List[pulumi.Resource]] = None) -> k8s.core.v1.Namespace`** - Creates a Kubernetes namespace with the provided configuration. +--- -### Types +### types.py -Located in `pulumi/core/types.py`, this section defines shared configuration types that are utilized by various modules. +**Responsibilities:** -#### Key Types +- Define all shared data classes and types used across modules. -- **`NamespaceConfig`** - Configuration object for Kubernetes namespaces. -- **`ComplianceConfig`, `FismaConfig`, `NistConfig`, and `ScipConfig`** - Configuration objects for compliance settings. +**Key Data Classes:** -### Utilities +- `NamespaceConfig`: Configuration object for Kubernetes namespaces. +- `FismaConfig`: Configuration for FISMA compliance settings. +- `NistConfig`: Configuration for NIST compliance settings. +- `ScipConfig`: Configuration for SCIP compliance settings. +- `ComplianceConfig`: Central configuration object for compliance settings. -Located in `pulumi/core/utils.py`, this section provides various utility functions for sanitizing labels, handling metadata, and more. +**Methods:** -#### Functions +- `ComplianceConfig.merge(user_config)`: Merges user-provided compliance configuration with default configuration. -- **`set_resource_metadata(metadata: Any, global_labels: Dict[str, str], global_annotations: Dict[str, str])`** - Sets metadata for a given resource. -- **`generate_global_transformations(global_labels: Dict[str, str], global_annotations: Dict[str, str])`** - Generates global transformations for resources. -- **`sanitize_label_value(value: str) -> str`** - Sanitizes a label value to ensure compliance with Kubernetes naming conventions. -- **`extract_repo_name(remote_url: str) -> str`** - Extracts the repository name from a Git remote URL. - -### Version Management - -Located in `pulumi/core/versions.py`, this section loads default versions for modules based on specified configuration settings. - -#### Functions - -- **`load_default_versions(config: pulumi.Config, force_refresh=False) -> dict`** - Loads the default versions for modules based on configuration settings. +--- ## Best Practices - **Consistent Naming**: Follow consistent naming conventions for functions and variables. - **Documentation**: Use detailed docstrings and comments to document your code. -- **Type Annotations**: Use type annotations to ensure readability and type safety. +- **Type Annotations**: Use type annotations to enhance readability and type safety. - **Reusable Code**: Centralize reusable code in the `core` module to ensure consistency across modules. - **Version Control**: Manage component versions to maintain consistency and avoid conflicts. +--- + ## Troubleshooting - **Error Logging**: Use Pulumi's logging functions (`pulumi.log.info`, `pulumi.log.warn`, `pulumi.log.error`) to provide meaningful error messages. - **Configuration Issues**: Ensure all configuration options are correctly set and validate input using type checks. - **Module Dependencies**: Verify dependencies between modules are correctly resolved using Pulumi's dependency management features. +- **Resource Conflicts**: Be cautious of naming collisions in Kubernetes resources; use namespaces and labels appropriately. + +--- ## Contributing Guidelines - **Submit Issues**: Report bugs or request features via GitHub issues. - **Pull Requests**: Submit pull requests with detailed descriptions and follow the project's coding standards. - **Code Reviews**: Participate in code reviews to maintain code quality. +- **Testing**: Write unit tests for new functions and ensure existing tests pass. + +--- ## Additional Resources - **Kargo KubeVirt Documentation**: [GitHub Repository](https://github.com/containercraft/kargo) -- **Pulumi Documentation**: [Pulumi Documentation](https://www.pulumi.com/docs/) +- **Pulumi Documentation**: [Pulumi Docs](https://www.pulumi.com/docs/) +- **Kubernetes API Reference**: [Kubernetes API](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/) +- **Python Dataclasses**: [Dataclasses Documentation](https://docs.python.org/3/library/dataclasses.html) diff --git a/pulumi/core/compliance.py b/pulumi/core/compliance.py deleted file mode 100644 index 329a036..0000000 --- a/pulumi/core/compliance.py +++ /dev/null @@ -1,48 +0,0 @@ -# ./pulumi/core/compliance.py - -import json -from typing import Dict -from .types import ComplianceConfig -from .utils import sanitize_label_value - -def generate_compliance_labels(compliance_config: ComplianceConfig) -> Dict[str, str]: - """ - Generates compliance labels based on the given compliance configuration. - - Args: - compliance_config (ComplianceConfig): The compliance configuration object. - - Returns: - Dict[str, str]: A dictionary of compliance labels. - """ - labels = {} - if compliance_config.fisma.enabled: - labels['compliance.fisma.enabled'] = 'true' - if compliance_config.nist.enabled: - labels['compliance.nist.enabled'] = 'true' - if compliance_config.scip.environment: - labels['compliance.scip.environment'] = sanitize_label_value(compliance_config.scip.environment) - return labels - -def generate_compliance_annotations(compliance_config: ComplianceConfig) -> Dict[str, str]: - """ - Generates compliance annotations based on the given compliance configuration. - - Args: - compliance_config (ComplianceConfig): The compliance configuration object. - - Returns: - Dict[str, str]: A dictionary of compliance annotations. - """ - annotations = {} - if compliance_config.fisma.level: - annotations['compliance.fisma.level'] = compliance_config.fisma.level - if compliance_config.fisma.ato: - annotations['compliance.fisma.ato'] = json.dumps(compliance_config.fisma.ato) # Store as JSON - if compliance_config.nist.controls: - annotations['compliance.nist.controls'] = json.dumps(compliance_config.nist.controls) # Store as JSON array - if compliance_config.nist.auxiliary: - annotations['compliance.nist.auxiliary'] = json.dumps(compliance_config.nist.auxiliary) - if compliance_config.nist.exceptions: - annotations['compliance.nist.exceptions'] = json.dumps(compliance_config.nist.exceptions) - return annotations diff --git a/pulumi/core/config.py b/pulumi/core/config.py index 681c9c2..03e7376 100644 --- a/pulumi/core/config.py +++ b/pulumi/core/config.py @@ -1,5 +1,4 @@ -# ./pulumi/core/config.py -# Description: Module Configuration Parsing & Loading +# pulumi/core/config.py """ Configuration Management Module @@ -9,8 +8,15 @@ and maintainability. """ -from typing import Any, Dict, Tuple +import json +import os import pulumi +import requests +from typing import Any, Dict, Tuple +from .types import ComplianceConfig + +# Default versions URL template +DEFAULT_VERSIONS_URL_TEMPLATE = 'https://raw.githubusercontent.com/ContainerCraft/Kargo/rerefactor/pulumi/' def get_module_config( module_name: str, @@ -33,6 +39,92 @@ def get_module_config( module_config['version'] = module_config.get('version', default_versions.get(module_name)) return module_config, module_enabled +def load_default_versions(config: pulumi.Config, force_refresh=False) -> dict: + """ + Loads the default versions for modules based on the specified configuration settings. + + This function attempts to load version information from multiple sources in order of precedence: + 1. User-specified source via Pulumi config (`default_versions.source`). + 2. Stack-specific versions file (`./versions/$STACK_NAME.json`) if `versions.stack_name` is set to true. + 3. Local default versions file (`./default_versions.json`). + 4. Remote versions based on the specified channel (`versions.channel`). + + Args: + config: The Pulumi configuration object. + + Returns: + A dictionary containing the default versions for modules. + + Raises: + Exception: If default versions cannot be loaded from any source. + """ + cache_file = '/tmp/default_versions.json' + if not force_refresh and os.path.exists(cache_file): + try: + with open(cache_file) as f: + return json.load(f) + except Exception as e: + pulumi.log.warn(f"Error reading cache file: {e}") + + stack_name = pulumi.get_stack() + default_versions_source = config.get('default_versions.source') + versions_channel = config.get('versions.channel') or 'stable' + versions_stack_name = config.get_bool('versions.stack_name') or False + default_versions = {} + + def load_versions_from_file(file_path: str) -> dict: + try: + with open(file_path, 'r') as f: + versions = json.load(f) + pulumi.log.info(f"Loaded default versions from file: {file_path}") + return versions + except (FileNotFoundError, json.JSONDecodeError) as e: + pulumi.log.warn(f"Error loading versions from file {file_path}: {e}") + return {} + + def load_versions_from_url(url: str) -> dict: + try: + response = requests.get(url) + response.raise_for_status() + versions = response.json() + pulumi.log.info(f"Loaded default versions from URL: {url}") + return versions + except (requests.RequestException, json.JSONDecodeError) as e: + pulumi.log.warn(f"Error loading versions from URL {url}: {e}") + return {} + + if default_versions_source: + if default_versions_source.startswith(('http://', 'https://')): + default_versions = load_versions_from_url(default_versions_source) + else: + default_versions = load_versions_from_file(default_versions_source) + + if not default_versions: + raise Exception(f"Failed to load default versions from specified source: {default_versions_source}") + + else: + if versions_stack_name: + current_dir = os.path.dirname(os.path.abspath(__file__)) + stack_versions_path = os.path.join(current_dir, '..', 'versions', f'{stack_name}.json') + default_versions = load_versions_from_file(stack_versions_path) + + if not default_versions: + current_dir = os.path.dirname(os.path.abspath(__file__)) + default_versions_path = os.path.join(current_dir, '..', 'default_versions.json') + default_versions = load_versions_from_file(default_versions_path) + + if not default_versions: + versions_url = f'{DEFAULT_VERSIONS_URL_TEMPLATE}{versions_channel}_versions.json' + default_versions = load_versions_from_url(versions_url) + + if not default_versions: + raise Exception("Cannot proceed without default versions.") + + with open(cache_file, 'w') as f: + json.dump(default_versions, f) + + return default_versions + def export_results( versions: Dict[str, str], configurations: Dict[str, Dict[str, Any]], diff --git a/pulumi/core/deployment.py b/pulumi/core/deployment.py new file mode 100644 index 0000000..b33ba62 --- /dev/null +++ b/pulumi/core/deployment.py @@ -0,0 +1,211 @@ +# pulumi/core/deployment.py + +""" +Deployment Management Module + +This module manages the deployment orchestration of modules, +initializes Pulumi and Kubernetes providers, and handles module deployments. +""" + +import os +import pulumi +import pulumi_kubernetes as k8s +from pulumi_kubernetes import Provider +from typing import Dict, Any, List, Type, Callable +import inspect +import importlib + +from .config import get_module_config, load_default_versions +from .metadata import ( + collect_git_info, + generate_git_labels, + generate_git_annotations, + set_global_labels, + set_global_annotations, + generate_compliance_labels, + generate_compliance_annotations +) +from .utils import generate_global_transformations +from .types import ComplianceConfig + +def initialize_pulumi() -> Dict[str, Any]: + """ + Initializes Pulumi configuration, Kubernetes provider, and global resources. + + Returns: + Dict[str, Any]: A dictionary containing initialized components. + """ + config = pulumi.Config() + stack_name = pulumi.get_stack() + project_name = pulumi.get_project() + + try: + default_versions = load_default_versions(config) + versions: Dict[str, str] = {} + configurations: Dict[str, Dict[str, Any]] = {} + global_depends_on: List[pulumi.Resource] = [] + + kubernetes_config = config.get_object("kubernetes") or {} + kubeconfig = kubernetes_config.get("kubeconfig") or os.getenv('KUBECONFIG') + kubernetes_context = kubernetes_config.get("context") + + pulumi.log.info(f"Kubeconfig: {kubeconfig}") + pulumi.log.info(f"Kubernetes context: {kubernetes_context}") + + k8s_provider = Provider( + "k8sProvider", + kubeconfig=kubeconfig, + context=kubernetes_context, + ) + + git_info = collect_git_info() + configurations["source_repository"] = { + "remote": git_info["remote"], + "branch": git_info["branch"], + "commit": git_info["commit"] + } + + compliance_config_dict = config.get_object('compliance') or {} + compliance_config = ComplianceConfig.merge(compliance_config_dict) + compliance_labels = generate_compliance_labels(compliance_config) + compliance_annotations = generate_compliance_annotations(compliance_config) + + git_labels = generate_git_labels(git_info) + git_annotations = generate_git_annotations(git_info) + global_labels = {**compliance_labels, **git_labels} + global_annotations = {**compliance_annotations, **git_annotations} + + set_global_labels(global_labels) + set_global_annotations(global_annotations) + generate_global_transformations(global_labels, global_annotations) + + return { + "config": config, + "stack_name": stack_name, + "project_name": project_name, + "default_versions": default_versions, + "versions": versions, + "configurations": configurations, + "global_depends_on": global_depends_on, + "k8s_provider": k8s_provider, + "git_info": git_info, + "compliance_config": compliance_config, + "global_labels": global_labels, + "global_annotations": global_annotations, + } + except Exception as e: + pulumi.log.error(f"Initialization error: {str(e)}") + raise + +def deploy_module( + module_name: str, + config: pulumi.Config, + default_versions: Dict[str, Any], + global_depends_on: List[pulumi.Resource], + k8s_provider: k8s.Provider, + versions: Dict[str, str], + configurations: Dict[str, Dict[str, Any]] +) -> None: + """ + Helper function to deploy a module based on configuration. + + Args: + module_name (str): Name of the module. + config (pulumi.Config): Pulumi configuration object. + default_versions (Dict[str, Any]): Default versions for modules. + global_depends_on (List[pulumi.Resource]): Global dependencies. + k8s_provider (k8s.Provider): Kubernetes provider. + versions (Dict[str, str]): Dictionary to store versions of deployed modules. + configurations (Dict[str, Dict[str, Any]]): Dictionary to store configurations of deployed modules. + + Raises: + TypeError: If any arguments have incorrect types. + ValueError: If any module-specific errors occur. + """ + if not isinstance(module_name, str): + raise TypeError("module_name must be a string") + if not isinstance(config, pulumi.Config): + raise TypeError("config must be an instance of pulumi.Config") + if not isinstance(default_versions, dict): + raise TypeError("default_versions must be a dictionary") + if not isinstance(global_depends_on, list): + raise TypeError("global_depends_on must be a list") + if not isinstance(k8s_provider, k8s.Provider): + raise TypeError("k8s_provider must be an instance of pulumi_kubernetes.Provider") + if not isinstance(versions, dict): + raise TypeError("versions must be a dictionary") + if not isinstance(configurations, dict): + raise TypeError("configurations must be a dictionary") + + module_config_dict, module_enabled = get_module_config(module_name, config, default_versions) + + if module_enabled: + ModuleConfigClass = discover_config_class(module_name) + deploy_func = discover_deploy_function(module_name) + + config_obj = ModuleConfigClass.merge(module_config_dict) + + deploy_func_args = inspect.signature(deploy_func).parameters.keys() + config_arg_name = list(deploy_func_args)[0] + + try: + result = deploy_func( + **{config_arg_name: config_obj}, + global_depends_on=global_depends_on, + k8s_provider=k8s_provider, + ) + + if isinstance(result, tuple) and len(result) == 3: + version, release, exported_value = result + elif isinstance(result, tuple) and len(result) == 2: + version, release = result + exported_value = None + else: + raise ValueError(f"Unexpected return value structure from {module_name} deploy function") + + versions[module_name] = version + configurations[module_name] = {"enabled": module_enabled} + + if exported_value: + pulumi.export(f"{module_name}_exported_value", exported_value) + + global_depends_on.append(release) + + except Exception as e: + pulumi.log.error(f"Deployment failed for module {module_name}: {str(e)}") + raise + else: + pulumi.log.info(f"Module {module_name} is not enabled.") + +def discover_config_class(module_name: str) -> Type: + """ + Discovers and returns the configuration class from the module's types.py. + + Args: + module_name (str): The name of the module. + + Returns: + Type: The configuration class. + """ + types_module = importlib.import_module(f"modules.{module_name}.types") + for name, obj in inspect.getmembers(types_module): + if inspect.isclass(obj) and hasattr(obj, "__dataclass_fields__"): + return obj + raise ValueError(f"No dataclass found in modules.{module_name}.types") + +def discover_deploy_function(module_name: str) -> Callable: + """ + Discovers and returns the deploy function from the module's deploy.py. + + Args: + module_name (str): The name of the module. + + Returns: + Callable: The deploy function. + """ + deploy_module = importlib.import_module(f"modules.{module_name}.deploy") + function_name = f"deploy_{module_name}_module" + deploy_function = getattr(deploy_module, function_name, None) + if not deploy_function: + raise ValueError(f"No deploy function named '{function_name}' found in modules.{module_name}.deploy") + return deploy_function diff --git a/pulumi/core/helm.py b/pulumi/core/helm.py deleted file mode 100644 index c6c37e7..0000000 --- a/pulumi/core/helm.py +++ /dev/null @@ -1,44 +0,0 @@ -# ./pulumi/core/helm.py -# Description: - -import requests -import logging -import yaml -from packaging.version import parse as parse_version, InvalidVersion, Version -from typing import Dict, Any - -# Set up basic logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - -HELM_CHART_URL_TEMPLATE = "https://github.com/containercraft/kargo/releases/latest/download/" -CHART_NOT_FOUND = "Chart not found" - -def is_stable_version(version_str: str) -> bool: - try: - parsed_version = parse_version(version_str) - return isinstance(parsed_version, Version) and not parsed_version.is_prerelease and not parsed_version.is_devrelease - except InvalidVersion: - return False - -def get_latest_helm_chart_version(url: str, chart_name: str) -> str: - try: - logging.info(f"Fetching URL: {url}") - response = requests.get(url) - response.raise_for_status() - - index = yaml.safe_load(response.content) - if chart_name in index['entries']: - chart_versions = index['entries'][chart_name] - stable_versions = [v for v in chart_versions if is_stable_version(v['version'])] - if not stable_versions: - logging.info(f"No stable versions found for chart '{chart_name}'.") - return CHART_NOT_FOUND - latest_chart = max(stable_versions, key=lambda x: parse_version(x['version'])) - return latest_chart['version'] - else: - logging.info(f"No chart named '{chart_name}' found in repository.") - return CHART_NOT_FOUND - - except requests.RequestException as e: - logging.error(f"Error fetching data: {e}") - return f"Error fetching data: {e}" diff --git a/pulumi/core/init.py b/pulumi/core/init.py deleted file mode 100644 index 567f7f2..0000000 --- a/pulumi/core/init.py +++ /dev/null @@ -1,82 +0,0 @@ -# ./pulumi/core/init.py -# Description: Initializes Pulumi configuration, Kubernetes provider, and global resources. -import os -import pulumi -import pulumi_kubernetes as k8s -from pulumi_kubernetes import Provider -from typing import Dict, Any, List - -from .versions import load_default_versions -from .metadata import ( - collect_git_info, - generate_git_labels, - generate_git_annotations, - set_global_labels, - set_global_annotations -) -from .compliance import generate_compliance_labels, generate_compliance_annotations -from .types import ComplianceConfig -from .utils import generate_global_transformations - -def initialize_pulumi() -> Dict[str, Any]: - config = pulumi.Config() - stack_name = pulumi.get_stack() - project_name = pulumi.get_project() - - try: - default_versions = load_default_versions(config) - versions: Dict[str, str] = {} - configurations: Dict[str, Dict[str, Any]] = {} - global_depends_on: List[pulumi.Resource] = [] - - kubernetes_config = config.get_object("kubernetes") or {} - kubeconfig = kubernetes_config.get("kubeconfig") or os.getenv('KUBECONFIG') - kubernetes_context = kubernetes_config.get("context") - - pulumi.log.info(f"Kubeconfig: {kubeconfig}") - pulumi.log.info(f"Kubernetes context: {kubernetes_context}") - - k8s_provider = Provider( - "k8sProvider", - kubeconfig=kubeconfig, - context=kubernetes_context, - ) - - git_info = collect_git_info() - configurations["source_repository"] = { - "remote": git_info["remote"], - "branch": git_info["branch"], - "commit": git_info["commit"] - } - - compliance_config_dict = config.get_object('compliance') or {} - compliance_config = ComplianceConfig.merge(compliance_config_dict) - compliance_labels = generate_compliance_labels(compliance_config) - compliance_annotations = generate_compliance_annotations(compliance_config) - - git_labels = generate_git_labels(git_info) - git_annotations = generate_git_annotations(git_info) - global_labels = {**compliance_labels, **git_labels} - global_annotations = {**compliance_annotations, **git_annotations} - - set_global_labels(global_labels) - set_global_annotations(global_annotations) - generate_global_transformations(global_labels, global_annotations) - - return { - "config": config, - "stack_name": stack_name, - "project_name": project_name, - "default_versions": default_versions, - "versions": versions, - "configurations": configurations, - "global_depends_on": global_depends_on, - "k8s_provider": k8s_provider, - "git_info": git_info, - "compliance_config": compliance_config, - "global_labels": global_labels, - "global_annotations": global_annotations, - } - except Exception as e: - pulumi.log.error(f"Initialization error: {str(e)}") - raise diff --git a/pulumi/core/introspection.py b/pulumi/core/introspection.py deleted file mode 100644 index 14a7dc3..0000000 --- a/pulumi/core/introspection.py +++ /dev/null @@ -1,39 +0,0 @@ -# ./pulumi/core/introspection.py -# Description: - -import inspect -import importlib -from typing import Type, Callable - -def discover_config_class(module_name: str) -> Type: - """ - Discovers and returns the configuration class from the module's types.py. - - Args: - module_name (str): The name of the module. - - Returns: - Type: The configuration class. - """ - types_module = importlib.import_module(f"modules.{module_name}.types") - for name, obj in inspect.getmembers(types_module): - if inspect.isclass(obj) and hasattr(obj, "__dataclass_fields__"): - return obj - raise ValueError(f"No dataclass found in modules.{module_name}.types") - -def discover_deploy_function(module_name: str) -> Callable: - """ - Discovers and returns the deploy function from the module's deploy.py. - - Args: - module_name (str): The name of the module. - - Returns: - Callable: The deploy function. - """ - deploy_module = importlib.import_module(f"modules.{module_name}.deploy") - function_name = f"deploy_{module_name}_module" - deploy_function = getattr(deploy_module, function_name, None) - if not deploy_function: - raise ValueError(f"No deploy function named '{function_name}' found in modules.{module_name}.deploy") - return deploy_function diff --git a/pulumi/core/metadata.py b/pulumi/core/metadata.py index 32bb7d4..4ef2de4 100644 --- a/pulumi/core/metadata.py +++ b/pulumi/core/metadata.py @@ -1,13 +1,23 @@ -# ./pulumi/core/metadata.py +# pulumi/core/metadata.py # Description: # TODO: enhance with support for propagation of labels annotations on AWS resources # TODO: enhance by adding additional data to global tags / labels / annotation metadata # - git release tag +""" +Metadata Management Module + +This module manages global metadata, labels, and annotations. +It includes functions to generate compliance and Git-related metadata. +""" + import subprocess import pulumi import threading -from typing import Dict +from typing import Dict, Any +import json +from .types import ComplianceConfig +import re class MetadataSingleton: _instance = None @@ -103,3 +113,61 @@ def generate_git_annotations(git_info: Dict[str, str]) -> Dict[str, str]: "git.commit.full": git_info.get("commit", ""), "git.branch": git_info.get("branch", "") } + +def generate_compliance_labels(compliance_config: ComplianceConfig) -> Dict[str, str]: + """ + Generates compliance labels based on the given compliance configuration. + + Args: + compliance_config (ComplianceConfig): The compliance configuration object. + + Returns: + Dict[str, str]: A dictionary of compliance labels. + """ + labels = {} + if compliance_config.fisma.enabled: + labels['compliance.fisma.enabled'] = 'true' + if compliance_config.nist.enabled: + labels['compliance.nist.enabled'] = 'true' + if compliance_config.scip.environment: + labels['compliance.scip.environment'] = sanitize_label_value(compliance_config.scip.environment) + return labels + +def generate_compliance_annotations(compliance_config: ComplianceConfig) -> Dict[str, str]: + """ + Generates compliance annotations based on the given compliance configuration. + + Args: + compliance_config (ComplianceConfig): The compliance configuration object. + + Returns: + Dict[str, str]: A dictionary of compliance annotations. + """ + annotations = {} + if compliance_config.fisma.level: + annotations['compliance.fisma.level'] = compliance_config.fisma.level + if compliance_config.fisma.ato: + annotations['compliance.fisma.ato'] = json.dumps(compliance_config.fisma.ato) # Store as JSON + if compliance_config.nist.controls: + annotations['compliance.nist.controls'] = json.dumps(compliance_config.nist.controls) # Store as JSON array + if compliance_config.nist.auxiliary: + annotations['compliance.nist.auxiliary'] = json.dumps(compliance_config.nist.auxiliary) + if compliance_config.nist.exceptions: + annotations['compliance.nist.exceptions'] = json.dumps(compliance_config.nist.exceptions) + return annotations + +def sanitize_label_value(value: str) -> str: + """ + Sanitizes a label value to comply with Kubernetes naming conventions. + + Args: + value (str): The value to sanitize. + + Returns: + str: The sanitized value. + """ + value = value.lower() + sanitized = re.sub(r'[^a-z0-9_.-]', '-', value) + sanitized = re.sub(r'^[^a-z0-9]+', '', sanitized) + sanitized = re.sub(r'[^a-z0-9]+$', '', sanitized) + return sanitized[:63] diff --git a/pulumi/core/module.py b/pulumi/core/module.py deleted file mode 100644 index f95a556..0000000 --- a/pulumi/core/module.py +++ /dev/null @@ -1,89 +0,0 @@ -# ./pulumi/core/deploy_module.py -# Description: - -import inspect -import pulumi -import pulumi_kubernetes as k8s -from typing import Any, Dict, List, Type -from .introspection import discover_config_class, discover_deploy_function -from .config import get_module_config - -def deploy_module( - module_name: str, - config: pulumi.Config, - default_versions: Dict[str, Any], - global_depends_on: List[pulumi.Resource], - k8s_provider: k8s.Provider, - versions: Dict[str, str], - configurations: Dict[str, Dict[str, Any]] -) -> None: - """ - Helper function to deploy a module based on configuration. - - Args: - module_name (str): Name of the module. - config (pulumi.Config): Pulumi configuration object. - default_versions (Dict[str, Any]): Default versions for modules. - global_depends_on (List[pulumi.Resource]): Global dependencies. - k8s_provider (k8s.Provider): Kubernetes provider. - versions (Dict[str, str]): Dictionary to store versions of deployed modules. - configurations (Dict[str, Dict[str, Any]]): Dictionary to store configurations of deployed modules. - - Raises: - TypeError: If any arguments have incorrect types. - ValueError: If any module-specific errors occur. - """ - if not isinstance(module_name, str): - raise TypeError("module_name must be a string") - if not isinstance(config, pulumi.Config): - raise TypeError("config must be an instance of pulumi.Config") - if not isinstance(default_versions, dict): - raise TypeError("default_versions must be a dictionary") - if not isinstance(global_depends_on, list): - raise TypeError("global_depends_on must be a list") - if not isinstance(k8s_provider, k8s.Provider): - raise TypeError("k8s_provider must be an instance of pulumi_kubernetes.Provider") - if not isinstance(versions, dict): - raise TypeError("versions must be a dictionary") - if not isinstance(configurations, dict): - raise TypeError("configurations must be a dictionary") - - module_config_dict, module_enabled = get_module_config(module_name, config, default_versions) - - if module_enabled: - ModuleConfigClass = discover_config_class(module_name) - deploy_func = discover_deploy_function(module_name) - - config_obj = ModuleConfigClass.merge(module_config_dict) - - deploy_func_args = inspect.signature(deploy_func).parameters.keys() - config_arg_name = list(deploy_func_args)[0] - - try: - result = deploy_func( - **{config_arg_name: config_obj}, - global_depends_on=global_depends_on, - k8s_provider=k8s_provider, - ) - - if isinstance(result, tuple) and len(result) == 3: - version, release, exported_value = result - elif isinstance(result, tuple) and len(result) == 2: - version, release = result - exported_value = None - else: - raise ValueError(f"Unexpected return value structure from {module_name} deploy function") - - versions[module_name] = version - configurations[module_name] = {"enabled": module_enabled} - - if exported_value: - pulumi.export(f"{module_name}_exported_value", exported_value) - - global_depends_on.append(release) - - except Exception as e: - pulumi.log.error(f"Deployment failed for module {module_name}: {str(e)}") - raise - else: - pulumi.log.info(f"Module {module_name} is not enabled.") diff --git a/pulumi/core/namespace.py b/pulumi/core/namespace.py deleted file mode 100644 index 7fb7a21..0000000 --- a/pulumi/core/namespace.py +++ /dev/null @@ -1,52 +0,0 @@ -# ./pulumi/core/namespace.py -# Description: - -import pulumi -import pulumi_kubernetes as k8s -from typing import List, Optional -from .types import NamespaceConfig - -def create_namespace( - config: NamespaceConfig, - k8s_provider: k8s.Provider, - depends_on: Optional[List[pulumi.Resource]] = None, -) -> k8s.core.v1.Namespace: - """ - Creates a Kubernetes namespace with the provided configuration. - - Args: - config (NamespaceConfig): The configuration object for the namespace. - k8s_provider (k8s.Provider): The Kubernetes provider. - depends_on (Optional[List[pulumi.Resource]]): List of resources this namespace depends on. - - Returns: - k8s.core.v1.Namespace: The created namespace resource. - """ - if depends_on is None: - depends_on = [] - - namespace_resource = k8s.core.v1.Namespace( - config.name, - metadata={ - "name": config.name, - "labels": config.labels, - "annotations": config.annotations, - }, - spec={ - "finalizers": config.finalizers, - }, - opts=pulumi.ResourceOptions( - protect=config.protect, - retain_on_delete=config.retain_on_delete, - provider=k8s_provider, - depends_on=depends_on, - ignore_changes=config.ignore_changes, - custom_timeouts=pulumi.CustomTimeouts( - create=config.custom_timeouts.get("create", "5m"), - update=config.custom_timeouts.get("update", "10m"), - delete=config.custom_timeouts.get("delete", "10m"), - ), - ), - ) - - return namespace_resource diff --git a/pulumi/core/types.py b/pulumi/core/types.py index e928428..1a07153 100644 --- a/pulumi/core/types.py +++ b/pulumi/core/types.py @@ -1,13 +1,12 @@ -# ./pulumi/core/types.py +# pulumi/core/types.py """ -Types and data structures used across Kargo modules. +Types and Data Structures Module -This module defines shared configuration types that are utilized by various modules +This module defines all shared data classes and types used across modules within the Kargo PaaS platform. """ -import pulumi from dataclasses import dataclass, field from typing import Optional, List, Dict, Any diff --git a/pulumi/core/utils.py b/pulumi/core/utils.py index 8a0f813..eae6361 100644 --- a/pulumi/core/utils.py +++ b/pulumi/core/utils.py @@ -1,10 +1,23 @@ -# ./pulumi/core/utils.py -# Description: Utility functions for sanitizing compliance metadata labels. +# pulumi/core/utils.py + +""" +Utility Functions Module + +This module provides generic, reusable utility functions. +It includes resource transformations, Helm interactions, and miscellaneous helpers. +""" import re import pulumi import pulumi_kubernetes as k8s from typing import Optional, Dict, Any +import requests +import logging +import yaml +from packaging.version import parse as parse_version, InvalidVersion, Version + +# Set up basic logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') def set_resource_metadata(metadata: Any, global_labels: Dict[str, str], global_annotations: Dict[str, str]): """ @@ -42,21 +55,54 @@ def global_transform(args: pulumi.ResourceTransformationArgs) -> Optional[pulumi pulumi.runtime.register_stack_transformation(global_transform) -def sanitize_label_value(value: str) -> str: +def get_latest_helm_chart_version(url: str, chart_name: str) -> str: + """ + Fetches the latest stable version of a Helm chart from the given URL. + + Args: + url (str): The URL of the Helm repository index. + chart_name (str): The name of the Helm chart. + + Returns: + str: The latest stable version of the chart. + """ + try: + logging.info(f"Fetching URL: {url}") + response = requests.get(url) + response.raise_for_status() + + index = yaml.safe_load(response.content) + if chart_name in index['entries']: + chart_versions = index['entries'][chart_name] + stable_versions = [v for v in chart_versions if is_stable_version(v['version'])] + if not stable_versions: + logging.info(f"No stable versions found for chart '{chart_name}'.") + return "Chart not found" + latest_chart = max(stable_versions, key=lambda x: parse_version(x['version'])) + return latest_chart['version'] + else: + logging.info(f"No chart named '{chart_name}' found in repository.") + return "Chart not found" + + except requests.RequestException as e: + logging.error(f"Error fetching data: {e}") + return f"Error fetching data: {e}" + +def is_stable_version(version_str: str) -> bool: """ - Sanitizes a label value to comply with Kubernetes naming conventions. + Determines if a version string represents a stable version. Args: - value (str): The value to sanitize. + version_str (str): The version string to check. Returns: - str: The sanitized value. + bool: True if the version is stable, False otherwise. """ - value = value.lower() - sanitized = re.sub(r'[^a-z0-9_.-]', '-', value) - sanitized = re.sub(r'^[^a-z0-9]+', '', sanitized) - sanitized = re.sub(r'[^a-z0-9]+$', '', sanitized) - return sanitized[:63] + try: + parsed_version = parse_version(version_str) + return isinstance(parsed_version, Version) and not parsed_version.is_prerelease and not parsed_version.is_devrelease + except InvalidVersion: + return False def extract_repo_name(remote_url: str) -> str: """ diff --git a/pulumi/core/versions.py b/pulumi/core/versions.py deleted file mode 100644 index 86b7500..0000000 --- a/pulumi/core/versions.py +++ /dev/null @@ -1,96 +0,0 @@ -# ./pulumi/core/versions.py -import json -import os -import pulumi -import requests -from typing import Dict - -#DEFAULT_VERSIONS_URL_TEMPLATE = 'https://github.com/containercraft/kargo/releases/latest/download/' -# TODO: replace with official github releases artifact URLs when released -DEFAULT_VERSIONS_URL_TEMPLATE = 'https://raw.githubusercontent.com/ContainerCraft/Kargo/rerefactor/pulumi/' - -def load_default_versions(config: pulumi.Config, force_refresh=False) -> dict: - """ - Loads the default versions for modules based on the specified configuration settings. - - This function attempts to load version information from multiple sources in order of precedence: - 1. User-specified source via Pulumi config (`default_versions.source`). - 2. Stack-specific versions file (`./versions/$STACK_NAME.json`) if `versions.stack_name` is set to true. - 3. Local default versions file (`./default_versions.json`). - 4. Remote versions based on the specified channel (`versions.channel`). - - Args: - config: The Pulumi configuration object. - - Returns: - A dictionary containing the default versions for modules. - - Raises: - Exception: If default versions cannot be loaded from any source. - """ - cache_file = '/tmp/default_versions.json' - if not force_refresh and os.path.exists(cache_file): - try: - with open(cache_file) as f: - return json.load(f) - except Exception as e: - pulumi.log.warn(f"Error reading cache file: {e}") - - stack_name = pulumi.get_stack() - default_versions_source = config.get('default_versions.source') - versions_channel = config.get('versions.channel') or 'stable' - versions_stack_name = config.get_bool('versions.stack_name') or False - default_versions = {} - - def load_versions_from_file(file_path: str) -> dict: - try: - with open(file_path, 'r') as f: - versions = json.load(f) - pulumi.log.info(f"Loaded default versions from file: {file_path}") - return versions - except (FileNotFoundError, json.JSONDecodeError) as e: - pulumi.log.warn(f"Error loading versions from file {file_path}: {e}") - return {} - - def load_versions_from_url(url: str) -> dict: - try: - response = requests.get(url) - response.raise_for_status() - versions = response.json() - pulumi.log.info(f"Loaded default versions from URL: {url}") - return versions - except (requests.RequestException, json.JSONDecodeError) as e: - pulumi.log.warn(f"Error loading versions from URL {url}: {e}") - return {} - - if default_versions_source: - if default_versions_source.startswith(('http://', 'https://')): - default_versions = load_versions_from_url(default_versions_source) - else: - default_versions = load_versions_from_file(default_versions_source) - - if not default_versions: - raise Exception(f"Failed to load default versions from specified source: {default_versions_source}") - - else: - if versions_stack_name: - current_dir = os.path.dirname(os.path.abspath(__file__)) - stack_versions_path = os.path.join(current_dir, '..', 'versions', f'{stack_name}.json') - default_versions = load_versions_from_file(stack_versions_path) - - if not default_versions: - current_dir = os.path.dirname(os.path.abspath(__file__)) - default_versions_path = os.path.join(current_dir, '..', 'default_versions.json') - default_versions = load_versions_from_file(default_versions_path) - - if not default_versions: - versions_url = f'{DEFAULT_VERSIONS_URL_TEMPLATE}{versions_channel}_versions.json' - default_versions = load_versions_from_url(versions_url) - - if not default_versions: - raise Exception("Cannot proceed without default versions.") - - with open(cache_file, 'w') as f: - json.dump(default_versions, f) - - return default_versions diff --git a/pulumi/modules/cert_manager/deploy.py b/pulumi/modules/cert_manager/deploy.py index 8c03bd7..85dcd49 100644 --- a/pulumi/modules/cert_manager/deploy.py +++ b/pulumi/modules/cert_manager/deploy.py @@ -1,16 +1,17 @@ -# ./pulumi/modules/cert_manager/deploy.py -# Description: Deploys the CertManager module using Helm with labels and annotations. +# pulumi/modules/cert_manager/deploy.py + +""" +Deploys the CertManager module using Helm with labels and annotations. +""" from typing import List, Dict, Any, Tuple, Optional import pulumi import pulumi_kubernetes as k8s from pulumi_kubernetes.apiextensions.CustomResource import CustomResource -from core.namespace import create_namespace -from core.helm import get_latest_helm_chart_version from core.types import NamespaceConfig from core.metadata import get_global_annotations, get_global_labels -from core.utils import set_resource_metadata +from core.utils import set_resource_metadata, get_latest_helm_chart_version from .types import CertManagerConfig def deploy_cert_manager_module( @@ -18,30 +19,33 @@ def deploy_cert_manager_module( global_depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, ) -> Tuple[str, pulumi.Resource, str]: + cert_manager_version, release, ca_cert_b64 = deploy_cert_manager( config_cert_manager=config_cert_manager, depends_on=global_depends_on, k8s_provider=k8s_provider, ) - global_depends_on.append(release) pulumi.export("cert_manager_selfsigned_cert", ca_cert_b64) + global_depends_on.append(release) return cert_manager_version, release, ca_cert_b64 def deploy_cert_manager( config_cert_manager: CertManagerConfig, depends_on: List[pulumi.Resource], - k8s_provider: k8s.Provider + k8s_provider: k8s.Provider, ) -> Tuple[str, k8s.helm.v3.Release, str]: + namespace = config_cert_manager.namespace version = config_cert_manager.version cluster_issuer_name = config_cert_manager.cluster_issuer install_crds = config_cert_manager.install_crds - namespace_config = NamespaceConfig(name=namespace) - namespace_resource = create_namespace(namespace_config, k8s_provider, depends_on) + # Create Namespace + namespace_resource = create_namespace(namespace, k8s_provider, depends_on) + # Get Helm Chart Version chart_name = "cert-manager" chart_url = "https://charts.jetstack.io" version = get_helm_chart_version(chart_url, chart_name, version) @@ -70,10 +74,10 @@ def helm_transform(args: pulumi.ResourceTransformationArgs): opts=pulumi.ResourceOptions( provider=k8s_provider, parent=namespace_resource, - depends_on=[namespace_resource] + (depends_on or []), + depends_on=[namespace_resource] + depends_on, transformations=[helm_transform], - custom_timeouts=pulumi.CustomTimeouts(create="8m", update="4m", delete="4m") - ) + custom_timeouts=pulumi.CustomTimeouts(create="8m", update="4m", delete="4m"), + ), ) cluster_issuer_root, cluster_issuer_ca_certificate, cluster_issuer = create_cluster_issuers( @@ -83,7 +87,11 @@ def helm_transform(args: pulumi.ResourceTransformationArgs): ca_secret = k8s.core.v1.Secret( "cluster-selfsigned-issuer-ca-secret", metadata={"name": "cluster-selfsigned-issuer-ca", "namespace": namespace}, - opts=pulumi.ResourceOptions(provider=k8s_provider, parent=cluster_issuer, depends_on=[cluster_issuer]) + opts=pulumi.ResourceOptions( + provider=k8s_provider, + parent=cluster_issuer, + depends_on=[cluster_issuer], + ), ) ca_data_tls_crt_b64 = ca_secret.data.apply(lambda data: data["tls.crt"]) @@ -96,8 +104,8 @@ def generate_helm_values(config_cert_manager: CertManagerConfig) -> Dict[str, An 'installCRDs': config_cert_manager.install_crds, 'resources': { 'limits': {'cpu': '500m', 'memory': '1024Mi'}, - 'requests': {'cpu': '250m', 'memory': '512Mi'} - } + 'requests': {'cpu': '250m', 'memory': '512Mi'}, + }, } def get_helm_chart_version(chart_url: str, chart_name: str, version: Optional[str]) -> str: @@ -108,7 +116,41 @@ def get_helm_chart_version(chart_url: str, chart_name: str, version: Optional[st pulumi.log.info(f"Using Helm release version: {chart_name}/{version}") return version -def create_cluster_issuers(cluster_issuer_name: str, namespace: str, k8s_provider: k8s.Provider, release: pulumi.Resource) -> Tuple[CustomResource, CustomResource, CustomResource]: +def create_namespace( + namespace_name: str, + k8s_provider: k8s.Provider, + depends_on: List[pulumi.Resource], + ) -> k8s.core.v1.Namespace: + """ + Creates a Kubernetes namespace with the provided configuration. + """ + namespace_config = NamespaceConfig(name=namespace_name) + namespace_resource = k8s.core.v1.Namespace( + namespace_name, + metadata={ + "name": namespace_config.name, + "labels": namespace_config.labels, + "annotations": namespace_config.annotations, + }, + opts=pulumi.ResourceOptions( + provider=k8s_provider, + depends_on=depends_on, + custom_timeouts=pulumi.CustomTimeouts( + create=namespace_config.custom_timeouts.get("create", "5m"), + update=namespace_config.custom_timeouts.get("update", "10m"), + delete=namespace_config.custom_timeouts.get("delete", "10m"), + ), + ), + ) + return namespace_resource + +def create_cluster_issuers( + cluster_issuer_name: str, + namespace: str, + k8s_provider: k8s.Provider, + release: pulumi.Resource, + ) -> Tuple[CustomResource, CustomResource, CustomResource]: + cluster_issuer_root = CustomResource( "cluster-selfsigned-issuer-root", api_version="cert-manager.io/v1", @@ -116,9 +158,11 @@ def create_cluster_issuers(cluster_issuer_name: str, namespace: str, k8s_provide metadata={"name": "cluster-selfsigned-issuer-root"}, spec={"selfSigned": {}}, opts=pulumi.ResourceOptions( - provider=k8s_provider, parent=release, depends_on=[release], - custom_timeouts=pulumi.CustomTimeouts(create="5m", update="10m", delete="10m") - ) + provider=k8s_provider, + parent=release, + depends_on=[release], + custom_timeouts=pulumi.CustomTimeouts(create="5m", update="10m", delete="10m"), + ), ) cluster_issuer_ca_certificate = CustomResource( @@ -130,15 +174,21 @@ def create_cluster_issuers(cluster_issuer_name: str, namespace: str, k8s_provide "commonName": "cluster-selfsigned-issuer-ca", "duration": "2160h0m0s", "isCA": True, - "issuerRef": {"group": "cert-manager.io", "kind": "ClusterIssuer", "name": "cluster-selfsigned-issuer-root"}, + "issuerRef": { + "group": "cert-manager.io", + "kind": "ClusterIssuer", + "name": "cluster-selfsigned-issuer-root", + }, "privateKey": {"algorithm": "ECDSA", "size": 256}, "renewBefore": "360h0m0s", - "secretName": "cluster-selfsigned-issuer-ca" + "secretName": "cluster-selfsigned-issuer-ca", }, opts=pulumi.ResourceOptions( - provider=k8s_provider, parent=cluster_issuer_root, depends_on=[cluster_issuer_root], - custom_timeouts=pulumi.CustomTimeouts(create="5m", update="10m", delete="10m") - ) + provider=k8s_provider, + parent=cluster_issuer_root, + depends_on=[cluster_issuer_root], + custom_timeouts=pulumi.CustomTimeouts(create="5m", update="10m", delete="10m"), + ), ) cluster_issuer = CustomResource( @@ -148,9 +198,11 @@ def create_cluster_issuers(cluster_issuer_name: str, namespace: str, k8s_provide metadata={"name": cluster_issuer_name}, spec={"ca": {"secretName": "cluster-selfsigned-issuer-ca"}}, opts=pulumi.ResourceOptions( - provider=k8s_provider, parent=cluster_issuer_ca_certificate, depends_on=[cluster_issuer_ca_certificate], - custom_timeouts=pulumi.CustomTimeouts(create="4m", update="4m", delete="4m") - ) + provider=k8s_provider, + parent=cluster_issuer_ca_certificate, + depends_on=[cluster_issuer_ca_certificate], + custom_timeouts=pulumi.CustomTimeouts(create="4m", update="4m", delete="4m"), + ), ) return cluster_issuer_root, cluster_issuer_ca_certificate, cluster_issuer diff --git a/pulumi/modules/kubevirt/deploy.py b/pulumi/modules/kubevirt/deploy.py index ef7ee39..abecb4c 100644 --- a/pulumi/modules/kubevirt/deploy.py +++ b/pulumi/modules/kubevirt/deploy.py @@ -1,5 +1,8 @@ -# ./pulumi/modules/kubevirt/deploy.py -# Description: +# pulumi/modules/kubevirt/deploy.py + +""" +Deploys the KubeVirt module. +""" import requests import yaml @@ -13,10 +16,8 @@ from pulumi_kubernetes.meta.v1 import ObjectMetaArgs from core.types import NamespaceConfig -from core.namespace import create_namespace from core.metadata import get_global_labels, get_global_annotations from core.utils import set_resource_metadata - from .types import KubeVirtConfig def deploy_kubevirt_module( @@ -24,30 +25,32 @@ def deploy_kubevirt_module( global_depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, ) -> Tuple[Optional[str], Optional[pulumi.Resource]]: - namespace_config = NamespaceConfig(name=config_kubevirt.namespace) + namespace_resource = create_namespace( - config=namespace_config, - k8s_provider=k8s_provider, - depends_on=global_depends_on + config_kubevirt.namespace, + k8s_provider, + global_depends_on, ) - kubevirt_version, kubevirt_operator = deploy_kubevirt( + # Combine dependencies + depends_on = global_depends_on + [namespace_resource] + + kubevirt_version, kubevirt_resource = deploy_kubevirt( config_kubevirt=config_kubevirt, - depends_on=[namespace_resource], + depends_on=depends_on, k8s_provider=k8s_provider, - namespace_resource=namespace_resource ) - global_depends_on.append(kubevirt_operator) + global_depends_on.append(kubevirt_resource) - return kubevirt_version, kubevirt_operator + return kubevirt_version, kubevirt_resource def deploy_kubevirt( config_kubevirt: KubeVirtConfig, depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, - namespace_resource: pulumi.Resource - ) -> Tuple[str, k8s.yaml.ConfigFile]: + ) -> Tuple[str, pulumi.Resource]: + namespace = config_kubevirt.namespace version = config_kubevirt.version use_emulation = config_kubevirt.use_emulation @@ -55,22 +58,14 @@ def deploy_kubevirt( annotations = config_kubevirt.annotations if version == 'latest' or version is None: - kubevirt_stable_version_url = 'https://storage.googleapis.com/kubevirt-prow/release/kubevirt/kubevirt/stable.txt' - response = requests.get(kubevirt_stable_version_url) - if response.status_code != 200: - raise Exception(f"Failed to fetch latest KubeVirt version from {kubevirt_stable_version_url}") - version = response.text.strip().lstrip("v") + version = get_latest_kubevirt_version() pulumi.log.info(f"Setting KubeVirt release version to latest: {version}") else: pulumi.log.info(f"Using KubeVirt version: {version}") - kubevirt_operator_url = f'https://github.com/kubevirt/kubevirt/releases/download/v{version}/kubevirt-operator.yaml' - response = requests.get(kubevirt_operator_url) - if response.status_code != 200: - raise Exception(f"Failed to download KubeVirt operator YAML from {kubevirt_operator_url}") - kubevirt_yaml = yaml.safe_load_all(response.text) + kubevirt_operator_yaml = download_kubevirt_operator_yaml(version) - transformed_yaml = _transform_yaml(kubevirt_yaml, namespace) + transformed_yaml = _transform_yaml(kubevirt_operator_yaml, namespace) def kubevirt_transform(obj: Dict[str, Any], opts: pulumi.ResourceOptions) -> pulumi.ResourceTransformationResult: obj.setdefault("metadata", {}) @@ -91,17 +86,16 @@ def kubevirt_transform(obj: Dict[str, Any], opts: pulumi.ResourceOptions) -> pul transformations=[kubevirt_transform], opts=pulumi.ResourceOptions( provider=k8s_provider, - parent=None, depends_on=depends_on, custom_timeouts=pulumi.CustomTimeouts( create="10m", update="5m", - delete="5m" - ) - ) + delete="5m", + ), + ), ) finally: - pulumi.Output.all().apply(lambda _: os.unlink(temp_file_path)) + os.unlink(temp_file_path) if use_emulation: pulumi.log.info("KVM Emulation enabled for KubeVirt.") @@ -113,16 +107,16 @@ def kubevirt_transform(obj: Dict[str, Any], opts: pulumi.ResourceOptions) -> pul "featureGates": [ "HostDevices", "ExpandDisks", - "AutoResourceLimitsGate" - ] + "AutoResourceLimitsGate", + ], }, "smbios": { "sku": "kargo-kc2", "version": version, "manufacturer": "ContainerCraft", "product": "Kargo", - "family": "CCIO" - } + "family": "CCIO", + }, } } @@ -134,17 +128,58 @@ def kubevirt_transform(obj: Dict[str, Any], opts: pulumi.ResourceOptions) -> pul name="kubevirt", labels=labels, namespace=namespace, - annotations=annotations + annotations=annotations, ), spec=kubevirt_custom_resource_spec, opts=pulumi.ResourceOptions( provider=k8s_provider, - parent=namespace_resource, depends_on=[operator], - ) + ), ) - return version, operator + return version, kubevirt + +def create_namespace( + namespace_name: str, + k8s_provider: k8s.Provider, + depends_on: List[pulumi.Resource], + ) -> k8s.core.v1.Namespace: + """ + Creates a Kubernetes namespace with the provided configuration. + """ + namespace_config = NamespaceConfig(name=namespace_name) + namespace_resource = k8s.core.v1.Namespace( + namespace_name, + metadata={ + "name": namespace_config.name, + "labels": namespace_config.labels, + "annotations": namespace_config.annotations, + }, + opts=pulumi.ResourceOptions( + provider=k8s_provider, + depends_on=depends_on, + custom_timeouts=pulumi.CustomTimeouts( + create=namespace_config.custom_timeouts.get("create", "5m"), + update=namespace_config.custom_timeouts.get("update", "10m"), + delete=namespace_config.custom_timeouts.get("delete", "10m"), + ), + ), + ) + return namespace_resource + +def get_latest_kubevirt_version() -> str: + url = 'https://storage.googleapis.com/kubevirt-prow/release/kubevirt/kubevirt/stable.txt' + response = requests.get(url) + if response.status_code != 200: + raise Exception(f"Failed to fetch latest KubeVirt version from {url}") + return response.text.strip().lstrip("v") + +def download_kubevirt_operator_yaml(version: str) -> Any: + url = f'https://github.com/kubevirt/kubevirt/releases/download/v{version}/kubevirt-operator.yaml' + response = requests.get(url) + if response.status_code != 200: + raise Exception(f"Failed to download KubeVirt operator YAML from {url}") + return yaml.safe_load_all(response.text) def _transform_yaml(yaml_data: Any, namespace: str) -> List[Dict[str, Any]]: transformed = [] From 31c05804d5135d332d97efb5305e4a822d771fb4 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sat, 21 Sep 2024 23:02:06 -0700 Subject: [PATCH 30/43] adopt new resource_helpers for metadata propagation compliance --- pulumi/core/README.md | 259 +++++++++++++-------- pulumi/core/resource_helpers.py | 309 ++++++++++++++++++++++++++ pulumi/core/types.py | 1 + pulumi/core/utils.py | 9 +- pulumi/modules/cert_manager/README.md | 172 ++++++++------ pulumi/modules/cert_manager/deploy.py | 211 ++++++++++-------- pulumi/modules/kubevirt/README.md | 189 ++++++++-------- pulumi/modules/kubevirt/deploy.py | 153 ++++++------- 8 files changed, 874 insertions(+), 429 deletions(-) create mode 100644 pulumi/core/resource_helpers.py diff --git a/pulumi/core/README.md b/pulumi/core/README.md index cfdb1c9..2af0724 100644 --- a/pulumi/core/README.md +++ b/pulumi/core/README.md @@ -1,30 +1,68 @@ -# Kargo Module Developmer Guide +# Core Module Developer Guide -This document provides an in-depth guide for developers, contributors, triage maintainers, and project maintainers utilizing the `core` module in the Kargo KubeVirt Kubernetes PaaS project. It includes a thorough overview of available functions, types, classes, and other core infrastructure details essential for productive module development. +Welcome to the **Core Module** of the Kargo KubeVirt Kubernetes PaaS project! This guide is designed to help both newcomers to DevOps and experienced module developers navigate and contribute to the core functionalities of the Kargo platform. Whether you're looking to understand the basics or dive deep into the module development, this guide has got you covered. + +--- ## Table of Contents - [Introduction](#introduction) -- [Core Module Structure](#core-module-structure) +- [Getting Started](#getting-started) +- [Core Module Overview](#core-module-overview) + - [Module Structure](#module-structure) + - [Key Components](#key-components) +- [Detailed Explanation of Core Files](#detailed-explanation-of-core-files) - [config.py](#configpy) - [deployment.py](#deploymentpy) - [metadata.py](#metadatapy) - - [utils.py](#utilspy) + - [resource_helpers.py](#resource_helperspy) - [types.py](#typespy) + - [utils.py](#utilspy) - [Best Practices](#best-practices) -- [Troubleshooting](#troubleshooting) -- [Contributing Guidelines](#contributing-guidelines) +- [Troubleshooting and FAQs](#troubleshooting-and-faqs) +- [Contributing to the Core Module](#contributing-to-the-core-module) - [Additional Resources](#additional-resources) --- ## Introduction -The `core` module is the backbone of the Kargo KubeVirt Kubernetes PaaS project. It houses essential functions, types, classes, and globals that facilitate the development and deployment of modules. This guide aims to equip you with the necessary knowledge to extend or maintain the core functionality effectively. +The Core Module is the heart of the Kargo KubeVirt Kubernetes PaaS project. It provides essential functionalities that facilitate the development, deployment, and management of modules within the Kargo ecosystem. This guide aims to make core concepts accessible to everyone, regardless of their experience level in DevOps. -## Core Module Structure +--- -The `core` module structure: +## Getting Started + +If you're new to Kargo or DevOps, start here! + +- **Prerequisites**: + - Basic understanding of Python and Kubernetes. + - [Pulumi CLI](https://www.pulumi.com/docs/get-started/) installed. + - Access to a Kubernetes cluster (minikube, kind, or cloud-based). + +- **Setup Steps**: + 1. **Clone the Repository**: + ```bash + git clone https://github.com/ContainerCraft/Kargo.git + cd Kargo/pulumi + ``` + 2. **Install Dependencies**: + ```bash + pip install -r requirements.txt + ``` + 3. **Configure Pulumi**: + ```bash + pulumi login + pulumi stack init dev + ``` + +--- + +## Core Module Overview + +### Module Structure + +The Core Module is organized as follows: ``` pulumi/core/ @@ -33,142 +71,187 @@ pulumi/core/ ├── config.py ├── deployment.py ├── metadata.py -├── utils.py -└── types.py +├── resource_helpers.py +├── types.py +└── utils.py ``` +### Key Components + +- **Configuration Management**: Handles loading and merging of user configurations. +- **Deployment Orchestration**: Manages the deployment of modules and resources. +- **Metadata Management**: Generates and applies global labels and annotations. +- **Utility Functions**: Provides helper functions for common tasks. +- **Type Definitions**: Contains shared data structures used across modules. + +--- + +## Detailed Explanation of Core Files + ### config.py -**Responsibilities:** +**Purpose**: Manages configuration settings for modules, including loading defaults and exporting deployment results. -- Handle all configuration-related functionalities. -- Load and merge user configurations with defaults. -- Load default versions for modules. -- Export deployment results. +**Key Functions**: -**Key Functions:** +- `get_module_config(module_name, config, default_versions)`: Retrieves and merges the configuration for a specific module. +- `load_default_versions(config, force_refresh=False)`: Loads default module versions, prioritizing user-specified sources. +- `export_results(versions, configurations, compliance)`: Exports deployment outputs for reporting and auditing. -- `get_module_config(module_name, config, default_versions)`: Retrieves and prepares the configuration for a module. -- `load_default_versions(config, force_refresh=False)`: Loads the default versions for modules based on the specified configuration settings. -- `export_results(versions, configurations, compliance)`: Exports the results of the deployment processes including versions, configurations, and compliance information. +**Usage Example**: -**Key Types:** +```python +from core.config import get_module_config -- `ComplianceConfig`: Central configuration object for compliance settings (imported from `types.py`). +module_config, is_enabled = get_module_config('cert_manager', config, default_versions) +if is_enabled: + # Proceed with deployment +``` --- ### deployment.py -**Responsibilities:** +**Purpose**: Orchestrates the deployment of modules, initializing providers and handling dependencies. + +**Key Functions**: -- Manage the deployment orchestration of modules. -- Initialize Pulumi and Kubernetes providers. -- Deploy individual modules based on configuration. -- Discover module-specific configuration classes and deploy functions. +- `initialize_pulumi()`: Sets up Pulumi configurations and Kubernetes provider. +- `deploy_module(module_name, config, ...)`: Deploys a specified module, handling its configuration and dependencies. -**Key Functions:** +**Usage Example**: -- `initialize_pulumi()`: Initializes Pulumi configuration, Kubernetes provider, and global resources. -- `deploy_module(module_name, config, default_versions, global_depends_on, k8s_provider, versions, configurations)`: Helper function to deploy a module based on configuration. -- `discover_config_class(module_name)`: Discovers and returns the configuration class from the module's `types.py`. -- `discover_deploy_function(module_name)`: Discovers and returns the deploy function from the module's `deploy.py`. +```python +from core.deployment import initialize_pulumi, deploy_module + +init = initialize_pulumi() +deploy_module('kubevirt', init['config'], ...) +``` --- ### metadata.py -**Responsibilities:** +**Purpose**: Manages global metadata, such as labels and annotations, ensuring consistency across resources. + +**Key Components**: -- Manage global metadata, labels, and annotations. -- Generate compliance and Git-related metadata. -- Sanitize label values to comply with Kubernetes naming conventions. +- **Singleton Pattern**: Ensures a single source of truth for metadata. +- **Metadata Functions**: + - `set_global_labels(labels)` + - `set_global_annotations(annotations)` + - `get_global_labels()` + - `get_global_annotations()` -**Key Classes and Functions:** +**Usage Example**: -- `MetadataSingleton`: Singleton class to store global labels and annotations. -- `set_global_labels(labels)`: Sets global labels. -- `set_global_annotations(annotations)`: Sets global annotations. -- `get_global_labels()`: Retrieves global labels. -- `get_global_annotations()`: Retrieves global annotations. -- `collect_git_info()`: Collects Git repository information. -- `generate_git_labels(git_info)`: Generates Git-related labels. -- `generate_git_annotations(git_info)`: Generates Git-related annotations. -- `generate_compliance_labels(compliance_config)`: Generates compliance labels. -- `generate_compliance_annotations(compliance_config)`: Generates compliance annotations. -- `sanitize_label_value(value)`: Sanitizes a label value to comply with Kubernetes naming conventions. +```python +from core.metadata import set_global_labels + +set_global_labels({'app': 'kargo', 'env': 'production'}) +``` --- -### utils.py +### resource_helpers.py + +**Purpose**: Provides helper functions for creating Kubernetes resources with consistent metadata. + +**Key Functions**: -**Responsibilities:** +- `create_namespace(name, labels, annotations, ...)` +- `create_custom_resource(name, args, ...)` +- `create_helm_release(name, args, ...)` -- Provide utility functions that are generic and reusable. -- Handle tasks like resource transformations and Helm interactions. -- Extract repository names from Git URLs. +**Usage Example**: -**Key Functions:** +```python +from core.resource_helpers import create_namespace -- `set_resource_metadata(metadata, global_labels, global_annotations)`: Updates resource metadata with global labels and annotations. -- `generate_global_transformations(global_labels, global_annotations)`: Generates global transformations for resources. -- `get_latest_helm_chart_version(url, chart_name)`: Fetches the latest stable version of a Helm chart from the given URL. -- `is_stable_version(version_str)`: Determines if a version string represents a stable version. -- `extract_repo_name(remote_url)`: Extracts the repository name from a Git remote URL. +namespace = create_namespace('kargo-system', labels={'app': 'kargo'}) +``` --- ### types.py -**Responsibilities:** +**Purpose**: Defines shared data structures and configurations used across modules. + +**Key Data Classes**: + +- `NamespaceConfig` +- `FismaConfig` +- `NistConfig` +- `ScipConfig` +- `ComplianceConfig` + +**Usage Example**: + +```python +from core.types import ComplianceConfig + +compliance_settings = ComplianceConfig(fisma=FismaConfig(enabled=True)) +``` + +--- + +### utils.py -- Define all shared data classes and types used across modules. +**Purpose**: Contains utility functions for common tasks such as version checking and resource transformations. -**Key Data Classes:** +**Key Functions**: -- `NamespaceConfig`: Configuration object for Kubernetes namespaces. -- `FismaConfig`: Configuration for FISMA compliance settings. -- `NistConfig`: Configuration for NIST compliance settings. -- `ScipConfig`: Configuration for SCIP compliance settings. -- `ComplianceConfig`: Central configuration object for compliance settings. +- `set_resource_metadata(metadata, global_labels, global_annotations)` +- `get_latest_helm_chart_version(url, chart_name)` +- `is_stable_version(version_str)` -**Methods:** +**Usage Example**: -- `ComplianceConfig.merge(user_config)`: Merges user-provided compliance configuration with default configuration. +```python +from core.utils import get_latest_helm_chart_version + +latest_version = get_latest_helm_chart_version('https://charts.jetstack.io', 'cert-manager') +``` --- ## Best Practices -- **Consistent Naming**: Follow consistent naming conventions for functions and variables. -- **Documentation**: Use detailed docstrings and comments to document your code. -- **Type Annotations**: Use type annotations to enhance readability and type safety. -- **Reusable Code**: Centralize reusable code in the `core` module to ensure consistency across modules. -- **Version Control**: Manage component versions to maintain consistency and avoid conflicts. +- **Consistency**: Use the core functions and types to ensure consistency across modules. +- **Modularity**: Keep module-specific logic separate from core functionalities. +- **Documentation**: Document your code and configurations to aid future developers. +- **Error Handling**: Use appropriate error handling and logging for better debugging. --- -## Troubleshooting +## Troubleshooting and FAQs + +**Q1: I get a `ConnectionError` when deploying modules. What should I do?** -- **Error Logging**: Use Pulumi's logging functions (`pulumi.log.info`, `pulumi.log.warn`, `pulumi.log.error`) to provide meaningful error messages. -- **Configuration Issues**: Ensure all configuration options are correctly set and validate input using type checks. -- **Module Dependencies**: Verify dependencies between modules are correctly resolved using Pulumi's dependency management features. -- **Resource Conflicts**: Be cautious of naming collisions in Kubernetes resources; use namespaces and labels appropriately. +- **A**: Ensure your Kubernetes context is correctly configured and that you have network access to the cluster. + +**Q2: How do I add a new module?** + +- **A**: Create a new directory under `pulumi/modules/`, define your `deploy.py` and `types.py`, and update the main deployment script. + +**Q3: The deployment hangs during resource creation.** + +- **A**: Check for resource conflicts or namespace issues. Use `kubectl` to inspect the current state. --- -## Contributing Guidelines +## Contributing to the Core Module + +We welcome contributions from the community! -- **Submit Issues**: Report bugs or request features via GitHub issues. -- **Pull Requests**: Submit pull requests with detailed descriptions and follow the project's coding standards. -- **Code Reviews**: Participate in code reviews to maintain code quality. -- **Testing**: Write unit tests for new functions and ensure existing tests pass. +- **Reporting Issues**: Use the GitHub issues page to report bugs or request features. +- **Submitting Pull Requests**: Follow the project's coding standards and ensure all tests pass. +- **Code Reviews**: Participate in reviews to maintain high code quality. --- ## Additional Resources -- **Kargo KubeVirt Documentation**: [GitHub Repository](https://github.com/containercraft/kargo) -- **Pulumi Documentation**: [Pulumi Docs](https://www.pulumi.com/docs/) -- **Kubernetes API Reference**: [Kubernetes API](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/) -- **Python Dataclasses**: [Dataclasses Documentation](https://docs.python.org/3/library/dataclasses.html) +- **Kargo Project Documentation**: [Kargo GitHub Repository](https://github.com/ContainerCraft/Kargo) +- **Pulumi Documentation**: [Pulumi Official Docs](https://www.pulumi.com/docs/) +- **Kubernetes API Reference**: [Kubernetes API](https://kubernetes.io/docs/reference/generated/kubernetes-api/) diff --git a/pulumi/core/resource_helpers.py b/pulumi/core/resource_helpers.py new file mode 100644 index 0000000..e359436 --- /dev/null +++ b/pulumi/core/resource_helpers.py @@ -0,0 +1,309 @@ +# pulumi/core/resource_helpers.py + +import pulumi +import pulumi_kubernetes as k8s +from typing import Optional, Dict, Any, List, Callable +from .metadata import get_global_labels, get_global_annotations +from .utils import set_resource_metadata +from .types import NamespaceConfig + +def create_namespace( + name: str, + labels: Optional[Dict[str, str]] = None, + annotations: Optional[Dict[str, str]] = None, + finalizers: Optional[List[str]] = None, + custom_timeouts: Optional[Dict[str, str]] = None, + opts: Optional[pulumi.ResourceOptions] = None, + k8s_provider: Optional[k8s.Provider] = None, + depends_on: Optional[List[pulumi.Resource]] = None, +) -> k8s.core.v1.Namespace: + """ + Creates a Kubernetes Namespace with global labels and annotations. + + Args: + name (str): The name of the namespace. + labels (Optional[Dict[str, str]]): Additional labels to apply. + annotations (Optional[Dict[str, str]]): Additional annotations to apply. + finalizers (Optional[List[str]]): Finalizers for the namespace. + custom_timeouts (Optional[Dict[str, str]]): Custom timeouts for resource operations. + opts (Optional[pulumi.ResourceOptions]): Pulumi resource options. + k8s_provider (Optional[k8s.Provider]): Kubernetes provider. + depends_on (Optional[List[pulumi.Resource]]): Resources this resource depends on. + + Returns: + k8s.core.v1.Namespace: The created Namespace resource. + """ + if opts is None: + opts = pulumi.ResourceOptions() + if labels is None: + labels = {} + if annotations is None: + annotations = {} + if custom_timeouts is None: + custom_timeouts = {} + if depends_on is None: + depends_on = [] + + global_labels = get_global_labels() + global_annotations = get_global_annotations() + labels.update(global_labels) + annotations.update(global_annotations) + + metadata = { + "name": name, + "labels": labels, + "annotations": annotations, + } + + spec = {} + if finalizers: + spec["finalizers"] = finalizers + + opts = pulumi.ResourceOptions.merge( + opts, + pulumi.ResourceOptions( + provider=k8s_provider, + depends_on=depends_on, + custom_timeouts=pulumi.CustomTimeouts( + create=custom_timeouts.get("create", "5m"), + update=custom_timeouts.get("update", "10m"), + delete=custom_timeouts.get("delete", "10m"), + ), + ), + ) + + return k8s.core.v1.Namespace( + name, + metadata=metadata, + spec=spec, + opts=opts, + ) + +def create_custom_resource( + name: str, + args: Dict[str, Any], + opts: Optional[pulumi.ResourceOptions] = None, + k8s_provider: Optional[k8s.Provider] = None, + depends_on: Optional[List[pulumi.Resource]] = None, +) -> k8s.apiextensions.CustomResource: + if opts is None: + opts = pulumi.ResourceOptions() + if depends_on is None: + depends_on = [] + + if 'kind' not in args or 'apiVersion' not in args: + raise ValueError("The 'args' dictionary must include 'kind' and 'apiVersion' keys.") + + global_labels = get_global_labels() + global_annotations = get_global_annotations() + + def custom_resource_transform(resource_args: pulumi.ResourceTransformationArgs): + props = resource_args.props + if 'metadata' in props: + set_resource_metadata(props['metadata'], global_labels, global_annotations) + return pulumi.ResourceTransformationResult(props, resource_args.opts) + + opts = pulumi.ResourceOptions.merge( + opts, + pulumi.ResourceOptions( + provider=k8s_provider, + depends_on=depends_on, + transformations=[custom_resource_transform], + ), + ) + + # Extract required fields from args + api_version = args['apiVersion'] + kind = args['kind'] + metadata = args.get('metadata', None) + spec = args.get('spec', None) + + # Corrected constructor call + return k8s.apiextensions.CustomResource( + resource_name=name, + api_version=api_version, + kind=kind, + metadata=metadata, + spec=spec, + opts=opts + ) + +def create_helm_release( + name: str, + args: k8s.helm.v3.ReleaseArgs, + opts: Optional[pulumi.ResourceOptions] = None, + transformations: Optional[List[Callable[[pulumi.ResourceTransformationArgs], Optional[pulumi.ResourceTransformationResult]]]] = None, + k8s_provider: Optional[k8s.Provider] = None, + depends_on: Optional[List[pulumi.Resource]] = None, +) -> k8s.helm.v3.Release: + """ + Creates a Helm Release with global labels and annotations. + + Args: + name (str): The release name. + args (k8s.helm.v3.ReleaseArgs): Arguments for the Helm release. + opts (Optional[pulumi.ResourceOptions]): Pulumi resource options. + transformations (Optional[List[Callable]]): Additional transformations. + k8s_provider (Optional[k8s.Provider]): Kubernetes provider. + depends_on (Optional[List[pulumi.Resource]]): Resources this release depends on. + + Returns: + k8s.helm.v3.Release: The created Helm release. + """ + if opts is None: + opts = pulumi.ResourceOptions() + if transformations is None: + transformations = [] + if depends_on is None: + depends_on = [] + + global_labels = get_global_labels() + global_annotations = get_global_annotations() + + def helm_resource_transform(resource_args: pulumi.ResourceTransformationArgs): + props = resource_args.props + if 'metadata' in props: + set_resource_metadata(props['metadata'], global_labels, global_annotations) + elif 'spec' in props and isinstance(props['spec'], dict): + if 'metadata' in props['spec']: + set_resource_metadata(props['spec']['metadata'], global_labels, global_annotations) + return pulumi.ResourceTransformationResult(props, resource_args.opts) + + transformations.append(helm_resource_transform) + + opts = pulumi.ResourceOptions.merge( + opts, + pulumi.ResourceOptions( + provider=k8s_provider, + depends_on=depends_on, + transformations=transformations, + ), + ) + + return k8s.helm.v3.Release(name, args, opts=opts) + +def create_secret( + name: str, + args: Dict[str, Any], + opts: Optional[pulumi.ResourceOptions] = None, + k8s_provider: Optional[k8s.Provider] = None, + depends_on: Optional[List[pulumi.Resource]] = None, +) -> k8s.core.v1.Secret: + if opts is None: + opts = pulumi.ResourceOptions() + if depends_on is None: + depends_on = [] + + # Merge global labels and annotations (if any) + global_labels = get_global_labels() + global_annotations = get_global_annotations() + + def secret_resource_transform(resource_args: pulumi.ResourceTransformationArgs): + props = resource_args.props + if 'metadata' in props: + set_resource_metadata(props['metadata'], global_labels, global_annotations) + return pulumi.ResourceTransformationResult(props, resource_args.opts) + + # Merge resource options + opts = pulumi.ResourceOptions.merge( + opts, + pulumi.ResourceOptions( + provider=k8s_provider, + depends_on=depends_on, + transformations=[secret_resource_transform], + ), + ) + + # Constructor call + return k8s.core.v1.Secret(name, opts, **args) + +def create_config_file( + name: str, + file: str, + opts: Optional[pulumi.ResourceOptions] = None, + transformations: Optional[List[Callable[[pulumi.ResourceTransformationArgs], Optional[pulumi.ResourceTransformationResult]]]] = None, + k8s_provider: Optional[k8s.Provider] = None, + depends_on: Optional[List[pulumi.Resource]] = None, +) -> k8s.yaml.ConfigFile: + """ + Creates Kubernetes resources from a YAML config file with global labels and annotations. + + Args: + name (str): The resource name. + file (str): The path to the YAML file. + opts (Optional[pulumi.ResourceOptions]): Pulumi resource options. + transformations (Optional[List[Callable]]): Additional transformations. + k8s_provider (Optional[k8s.Provider]): Kubernetes provider. + depends_on (Optional[List[pulumi.Resource]]): Resources these resources depend on. + + Returns: + k8s.yaml.ConfigFile: The created resources. + """ + if opts is None: + opts = pulumi.ResourceOptions() + if transformations is None: + transformations = [] + if depends_on is None: + depends_on = [] + + global_labels = get_global_labels() + global_annotations = get_global_annotations() + + def config_file_transform(resource_args: pulumi.ResourceTransformationArgs): + props = resource_args.props + if 'metadata' in props: + set_resource_metadata(props['metadata'], global_labels, global_annotations) + elif 'spec' in props and isinstance(props['spec'], dict): + if 'metadata' in props['spec']: + set_resource_metadata(props['spec']['metadata'], global_labels, global_annotations) + return pulumi.ResourceTransformationResult(props, resource_args.opts) + + transformations.append(config_file_transform) + + opts = pulumi.ResourceOptions.merge( + opts, + pulumi.ResourceOptions( + provider=k8s_provider, + depends_on=depends_on, + transformations=transformations, + ), + ) + + return k8s.yaml.ConfigFile(name, file, opts=opts) + +def create_meta_objectmeta( + name: str, + labels: Optional[Dict[str, str]] = None, + annotations: Optional[Dict[str, str]] = None, + namespace: Optional[str] = None, + **kwargs, +) -> k8s.meta.v1.ObjectMetaArgs: + """ + Creates an ObjectMetaArgs with global labels and annotations. + + Args: + name (str): The resource name. + labels (Optional[Dict[str, str]]): Additional labels to apply. + annotations (Optional[Dict[str, str]]): Additional annotations to apply. + namespace (Optional[str]): Namespace for the resource. + + Returns: + k8s.meta.v1.ObjectMetaArgs: The metadata arguments. + """ + if labels is None: + labels = {} + if annotations is None: + annotations = {} + + global_labels = get_global_labels() + global_annotations = get_global_annotations() + labels.update(global_labels) + annotations.update(global_annotations) + + return k8s.meta.v1.ObjectMetaArgs( + name=name, + labels=labels, + annotations=annotations, + namespace=namespace, + **kwargs, + ) diff --git a/pulumi/core/types.py b/pulumi/core/types.py index 1a07153..2377db5 100644 --- a/pulumi/core/types.py +++ b/pulumi/core/types.py @@ -7,6 +7,7 @@ within the Kargo PaaS platform. """ +import pulumi from dataclasses import dataclass, field from typing import Optional, List, Dict, Any diff --git a/pulumi/core/utils.py b/pulumi/core/utils.py index eae6361..b4e4762 100644 --- a/pulumi/core/utils.py +++ b/pulumi/core/utils.py @@ -49,8 +49,13 @@ def generate_global_transformations(global_labels: Dict[str, str], global_annota """ def global_transform(args: pulumi.ResourceTransformationArgs) -> Optional[pulumi.ResourceTransformationResult]: props = args.props - props.setdefault('metadata', {}) - set_resource_metadata(props['metadata'], global_labels, global_annotations) + + if 'metadata' in props: + set_resource_metadata(props['metadata'], global_labels, global_annotations) + elif 'spec' in props and isinstance(props['spec'], dict): + if 'metadata' in props['spec']: + set_resource_metadata(props['spec']['metadata'], global_labels, global_annotations) + return pulumi.ResourceTransformationResult(props, args.opts) pulumi.runtime.register_stack_transformation(global_transform) diff --git a/pulumi/modules/cert_manager/README.md b/pulumi/modules/cert_manager/README.md index 22b3fff..f3caa66 100644 --- a/pulumi/modules/cert_manager/README.md +++ b/pulumi/modules/cert_manager/README.md @@ -1,27 +1,25 @@ -# Cert Manager Module +# Cert Manager Module Guide -ContainerCraft Kargo Kubevirt PaaS Cert Manager module. - -The `cert_manager` module automates SSL certificates and certificate issuers in your platform using [cert-manager](https://cert-manager.io/). Cert Manager is a dependency of many components in Kargo Kubevirt PaaS including Containerized Data Importer, Kubevirt, Hostpath Provisioner, and more. Cert Manager improves the simplicity of issuing and renewing SSL/TLS certificates making PKI (Private Key Infrastructure) an integrated and automated feature of the platform. +Welcome to the **Cert Manager Module** for the Kargo KubeVirt Kubernetes PaaS! This guide is tailored for both newcomers to DevOps and experienced developers, providing a comprehensive overview of how to deploy and configure the Cert Manager module within the Kargo platform. --- ## Table of Contents - [Introduction](#introduction) -- [Prerequisites](#prerequisites) -- [Features](#features) +- [Why Use Cert Manager?](#why-use-cert-manager) +- [Getting Started](#getting-started) - [Enabling the Module](#enabling-the-module) -- [Configuration](#configuration) - - [Default Configuration](#default-configuration) - - [Custom Configuration](#custom-configuration) -- [Module Components](#module-components) +- [Configuration Options](#configuration-options) + - [Default Settings](#default-settings) + - [Customizing Your Deployment](#customizing-your-deployment) +- [Module Components Explained](#module-components-explained) - [Namespace Creation](#namespace-creation) - [Helm Chart Deployment](#helm-chart-deployment) - - [Self-Signed Cluster Issuer](#self-signed-cluster-issuer) -- [Integration with Kargo PaaS](#integration-with-kargo-paas) -- [Best Practices](#best-practices) -- [Troubleshooting](#troubleshooting) + - [Self-Signed Cluster Issuer Setup](#self-signed-cluster-issuer-setup) +- [Using the Module](#using-the-module) + - [Example Usage](#example-usage) +- [Troubleshooting and FAQs](#troubleshooting-and-faqs) - [Additional Resources](#additional-resources) - [Conclusion](#conclusion) @@ -29,128 +27,162 @@ The `cert_manager` module automates SSL certificates and certificate issuers in ## Introduction -This guide provides an overview of the Cert Manager module, instructions on how to enable and configure it, and explanations of its functionality within the Kargo PaaS platform. Whether you're new to Kubernetes, cert-manager, or ContainerCraft Kargo PaaS, this guide will help you get started. +The Cert Manager module automates the management of SSL/TLS certificates in your Kubernetes cluster using [cert-manager](https://cert-manager.io/). It simplifies the process of obtaining, renewing, and managing certificates, enhancing the security of your applications without manual intervention. --- -## Features +## Why Use Cert Manager? -- **Automated Deployment**: Installs cert-manager using Helm charts. -- **Version Management**: Supports explicit version pinning or accepts `latest`. -- **Namespace Isolation**: Deploys cert-manager in a dedicated namespace. -- **Self-Signed Certificates**: Sets up a self-signed ClusterIssuer for certificate management. -- **Customizable Configuration**: Allows setting overrides for customization. +- **Automation**: Automatically provisions and renews certificates. +- **Integration**: Works seamlessly with Kubernetes Ingress resources and other services. +- **Security**: Enhances security by ensuring certificates are always up-to-date. +- **Compliance**: Helps meet compliance requirements by managing PKI effectively. + +--- + +## Getting Started + +### Prerequisites + +- **Kubernetes Cluster**: Ensure you have access to a Kubernetes cluster. +- **Pulumi CLI**: Install the Pulumi CLI and configure it. +- **Kubeconfig**: Your kubeconfig file should be properly set up. + +### Setup Steps + +1. **Navigate to the Kargo Pulumi Directory**: + ```bash + cd Kargo/pulumi + ``` +2. **Install Dependencies**: + ```bash + pip install -r requirements.txt + ``` +3. **Initialize Pulumi Stack**: + ```bash + pulumi stack init dev + ``` --- ## Enabling the Module -The Cert Manager module is enabled by default and executes with sane defaults. Customization can be configured in the Pulumi Stack Configuration. +The Cert Manager module is enabled by default. To verify or modify its enabled status, adjust your Pulumi configuration. -### Example Pulumi Configuration +### Verifying Module Enablement ```yaml # Pulumi..yaml config: cert_manager: - enabled: true # (default: true) + enabled: true # Set to false to disable ``` -Alternatively, you can set configuration values via the Pulumi CLI: +Alternatively, use the Pulumi CLI: ```bash -pulumi config set --path cert_manager.key value +pulumi config set --path cert_manager.enabled true ``` --- -## Configuration +## Configuration Options -### Default Configuration +### Default Settings -The module comes with sensible defaults to simplify deployment: +The module is designed to work out-of-the-box with default settings: - **Namespace**: `cert-manager` -- **Version**: Uses the default version specified in `default_versions.json` +- **Version**: Defined in `default_versions.json` - **Cluster Issuer Name**: `cluster-selfsigned-issuer` - **Install CRDs**: `true` -### Custom Configuration +### Customizing Your Deployment -You can customize the module's behavior by providing additional configuration options. +You can tailor the module to fit your specific needs by customizing its configuration. -#### Available Configuration Options +#### Available Configuration Parameters -- **namespace** *(string)*: The namespace where cert-manager will be deployed. -- **version** *(string)*: The version of the cert-manager Helm chart to deploy. Use `'latest'` to fetch the latest version. -- **cluster_issuer** *(string)*: The name of the ClusterIssuer to create. -- **install_crds** *(bool)*: Whether to install Custom Resource Definitions (CRDs). +- **enabled** *(bool)*: Enable or disable the module. +- **namespace** *(string)*: Kubernetes namespace for cert-manager. +- **version** *(string)*: Helm chart version to deploy. Use `'latest'` to fetch the most recent stable version. +- **cluster_issuer** *(string)*: Name of the ClusterIssuer resource. +- **install_crds** *(bool)*: Whether to install Custom Resource Definitions. #### Example Custom Configuration ```yaml -# Pulumi..yaml -# Default values are shown for reference - config: cert_manager: enabled: true + namespace: "my-cert-manager" version: "1.15.3" cluster_issuer: "my-cluster-issuer" - namespace: "custom-cert-manager" install_crds: true ``` --- -## Module Components +## Module Components Explained ### Namespace Creation -The module creates a dedicated namespace for cert-manager to ensure isolation and better management. +A dedicated namespace is created to isolate cert-manager resources. -- **Namespace**: Configurable via the `namespace` parameter. -- **Labels and Annotations**: Applied as per best practices for identification. +- **Why?**: Ensures better organization and avoids conflicts. +- **Customizable**: Change the namespace using the `namespace` parameter. ### Helm Chart Deployment -The module deploys cert-manager using the official Helm chart from Jetstack. +Deploys cert-manager using Helm. + +- **Chart Repository**: `https://charts.jetstack.io` +- **Version Management**: Specify a version or use `'latest'`. +- **Custom Values**: Resource requests and limits are set for optimal performance. -- **Chart Name**: `cert-manager` -- **Repository**: `https://charts.jetstack.io` -- **Custom Values**: Resources and replica counts are configured for optimal performance. -- **Version**: Configurable; defaults to the version specified in `default_versions.json`. +### Self-Signed Cluster Issuer Setup -### Self-Signed Cluster Issuer +Sets up a self-signed ClusterIssuer for certificate provisioning. + +- **Root ClusterIssuer**: Creates a root issuer. +- **CA Certificate**: Generates a CA certificate stored in a Kubernetes Secret. +- **Primary ClusterIssuer**: Issues certificates for your applications using the CA certificate. +- **Exported Values**: CA certificate data is exported for use in other modules. + +--- + +## Using the Module + +### Example Usage + +After enabling and configuring the module, deploy it using Pulumi: + +```bash +pulumi up +``` + +--- -To enable self signed local certificates, the module includes a self-signed ClusterIssuer chain of trust. +## Troubleshooting and FAQs -- **Root ClusterIssuer**: `cluster-selfsigned-issuer-root` -- **CA Certificate**: Generated and stored in a Kubernetes Secret. -- **Primary ClusterIssuer**: Uses the CA certificate to issue certificates for your applications. -- **Secret Export**: The CA certificate data is exported and available for other modules or applications. +**Q1: Cert-manager pods are not running.** -## Troubleshooting +- **A**: Check the namespace and ensure that CRDs are installed. Verify the Kubernetes version compatibility. -### Common Issues +**Q2: Certificates are not being issued.** -- **Connection Errors**: Ensure your `kubeconfig` and Kubernetes context are correctly configured. -- **Version Conflicts**: If deployment fails due to version issues, verify the specified version is available in the Helm repository. Alternatively use `'latest'` and Kargo will fetch the latest version. -- **CRD Issues**: If `install_crds` is set to false and are not otherwise installed, cert-manager components may fail install or function properly. +- **A**: Ensure that the ClusterIssuer is correctly configured and that your Ingress resources reference it. -### Debugging Steps +**Q3: How do I update cert-manager to a newer version?** -1. **Check Pulumi Logs**: Look for error messages during deployment. -2. **Verify Kubernetes Resources**: Use `kubectl` to inspect the cert-manager namespace and resources. -3. **Review Configuration**: Ensure all configuration options are correctly set in your Pulumi config or remove configuration to use defaults. +- **A**: Update the `version` parameter in your configuration and run `pulumi up`. --- ## Additional Resources -- **cert-manager Documentation**: [https://cert-manager.io/docs/](https://cert-manager.io/docs/) -- **Kargo Kubevirt PaaS IaC Documentation**: Refer to the main [Kargo README](../README.md) for developer guidelines. -- **Pulumi Kubernetes Provider**: [https://www.pulumi.com/docs/reference/pkg/kubernetes/](https://www.pulumi.com/docs/reference/pkg/kubernetes/) -- **Helm Charts**: [https://artifacthub.io/packages/helm/cert-manager/cert-manager](https://artifacthub.io/packages/helm/cert-manager/cert-manager) -- **Need Help?** If you have questions or need assistance, feel free to reach out to the community or maintainers on GitHub, Discord, or Twitter. +- **cert-manager Documentation**: [cert-manager.io/docs](https://cert-manager.io/docs/) +- **Kargo Project**: [Kargo GitHub Repository](https://github.com/ContainerCraft/Kargo) +- **Pulumi Kubernetes Provider**: [Pulumi Kubernetes Docs](https://www.pulumi.com/docs/reference/pkg/kubernetes/) +- **Helm Charts Repository**: [Artifact Hub - cert-manager](https://artifacthub.io/packages/helm/cert-manager/cert-manager) diff --git a/pulumi/modules/cert_manager/deploy.py b/pulumi/modules/cert_manager/deploy.py index 85dcd49..7891fbf 100644 --- a/pulumi/modules/cert_manager/deploy.py +++ b/pulumi/modules/cert_manager/deploy.py @@ -4,14 +4,19 @@ Deploys the CertManager module using Helm with labels and annotations. """ -from typing import List, Dict, Any, Tuple, Optional import pulumi import pulumi_kubernetes as k8s -from pulumi_kubernetes.apiextensions.CustomResource import CustomResource +from typing import List, Dict, Any, Tuple, Optional from core.types import NamespaceConfig -from core.metadata import get_global_annotations, get_global_labels -from core.utils import set_resource_metadata, get_latest_helm_chart_version +from core.metadata import get_global_labels, get_global_annotations +from core.utils import get_latest_helm_chart_version +from core.resource_helpers import ( + create_namespace, + create_helm_release, + create_custom_resource, + create_secret, +) from .types import CertManagerConfig def deploy_cert_manager_module( @@ -19,14 +24,20 @@ def deploy_cert_manager_module( global_depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, ) -> Tuple[str, pulumi.Resource, str]: - + """ + Deploys the CertManager module and returns the version, release resource, and CA certificate. + """ + # Deploy Cert Manager cert_manager_version, release, ca_cert_b64 = deploy_cert_manager( config_cert_manager=config_cert_manager, depends_on=global_depends_on, k8s_provider=k8s_provider, ) + # Export the CA certificate pulumi.export("cert_manager_selfsigned_cert", ca_cert_b64) + + # Update global dependencies global_depends_on.append(release) return cert_manager_version, release, ca_cert_b64 @@ -36,34 +47,33 @@ def deploy_cert_manager( depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, ) -> Tuple[str, k8s.helm.v3.Release, str]: - + """ + Deploys Cert Manager using Helm and sets up cluster issuers. + """ namespace = config_cert_manager.namespace version = config_cert_manager.version cluster_issuer_name = config_cert_manager.cluster_issuer install_crds = config_cert_manager.install_crds - # Create Namespace - namespace_resource = create_namespace(namespace, k8s_provider, depends_on) + # Create Namespace using the helper function + namespace_resource = create_namespace( + name=namespace, + k8s_provider=k8s_provider, + depends_on=depends_on, + ) # Get Helm Chart Version chart_name = "cert-manager" chart_url = "https://charts.jetstack.io" version = get_helm_chart_version(chart_url, chart_name, version) + # Generate Helm values helm_values = generate_helm_values(config_cert_manager) - global_labels = get_global_labels() - global_annotations = get_global_annotations() - - def helm_transform(args: pulumi.ResourceTransformationArgs): - metadata = args.props.get('metadata', {}) - set_resource_metadata(metadata, global_labels, global_annotations) - args.props['metadata'] = metadata - return pulumi.ResourceTransformationResult(args.props, args.opts) - - release = k8s.helm.v3.Release( - chart_name, - k8s.helm.v3.ReleaseArgs( + # Create Helm Release using the helper function + release = create_helm_release( + name=chart_name, + args=k8s.helm.v3.ReleaseArgs( chart=chart_name, version=version, namespace=namespace, @@ -72,33 +82,53 @@ def helm_transform(args: pulumi.ResourceTransformationArgs): values=helm_values, ), opts=pulumi.ResourceOptions( - provider=k8s_provider, parent=namespace_resource, - depends_on=[namespace_resource] + depends_on, - transformations=[helm_transform], custom_timeouts=pulumi.CustomTimeouts(create="8m", update="4m", delete="4m"), ), + k8s_provider=k8s_provider, + depends_on=[namespace_resource] + depends_on, ) + # Create Cluster Issuers using the helper function cluster_issuer_root, cluster_issuer_ca_certificate, cluster_issuer = create_cluster_issuers( cluster_issuer_name, namespace, k8s_provider, release ) - ca_secret = k8s.core.v1.Secret( - "cluster-selfsigned-issuer-ca-secret", - metadata={"name": "cluster-selfsigned-issuer-ca", "namespace": namespace}, + # Create Secret using the helper function + #ca_secret = k8s.core.v1.Secret.get( + # "cluster-selfsigned-issuer-ca-secret", + # id=f"{namespace}/cluster-selfsigned-issuer-ca", + # opts=pulumi.ResourceOptions( + # parent=cluster_issuer, + # depends_on=[cluster_issuer], + # provider=k8s_provider, + # ) + #) + ca_secret = create_secret( + name="cluster-selfsigned-issuer-ca-secret", + args={ + "metadata": { + "name": "cluster-selfsigned-issuer-ca", + "namespace": namespace, + }, + }, opts=pulumi.ResourceOptions( - provider=k8s_provider, parent=cluster_issuer, - depends_on=[cluster_issuer], + custom_timeouts=pulumi.CustomTimeouts(create="2m", update="2m", delete="2m"), ), + k8s_provider=k8s_provider, + depends_on=[cluster_issuer], ) + # Extract the CA certificate from the secret ca_data_tls_crt_b64 = ca_secret.data.apply(lambda data: data["tls.crt"]) return version, release, ca_data_tls_crt_b64 def generate_helm_values(config_cert_manager: CertManagerConfig) -> Dict[str, Any]: + """ + Generates Helm values for the CertManager deployment. + """ return { 'replicaCount': 1, 'installCRDs': config_cert_manager.install_crds, @@ -109,6 +139,9 @@ def generate_helm_values(config_cert_manager: CertManagerConfig) -> Dict[str, An } def get_helm_chart_version(chart_url: str, chart_name: str, version: Optional[str]) -> str: + """ + Retrieves the Helm chart version. + """ if version == 'latest' or version is None: version = get_latest_helm_chart_version(f"{chart_url}/index.yaml", chart_name).lstrip("v") pulumi.log.info(f"Setting Helm release version to latest: {chart_name}/{version}") @@ -116,93 +149,85 @@ def get_helm_chart_version(chart_url: str, chart_name: str, version: Optional[st pulumi.log.info(f"Using Helm release version: {chart_name}/{version}") return version -def create_namespace( - namespace_name: str, - k8s_provider: k8s.Provider, - depends_on: List[pulumi.Resource], - ) -> k8s.core.v1.Namespace: - """ - Creates a Kubernetes namespace with the provided configuration. - """ - namespace_config = NamespaceConfig(name=namespace_name) - namespace_resource = k8s.core.v1.Namespace( - namespace_name, - metadata={ - "name": namespace_config.name, - "labels": namespace_config.labels, - "annotations": namespace_config.annotations, - }, - opts=pulumi.ResourceOptions( - provider=k8s_provider, - depends_on=depends_on, - custom_timeouts=pulumi.CustomTimeouts( - create=namespace_config.custom_timeouts.get("create", "5m"), - update=namespace_config.custom_timeouts.get("update", "10m"), - delete=namespace_config.custom_timeouts.get("delete", "10m"), - ), - ), - ) - return namespace_resource - def create_cluster_issuers( cluster_issuer_name: str, namespace: str, k8s_provider: k8s.Provider, - release: pulumi.Resource, - ) -> Tuple[CustomResource, CustomResource, CustomResource]: - - cluster_issuer_root = CustomResource( - "cluster-selfsigned-issuer-root", - api_version="cert-manager.io/v1", - kind="ClusterIssuer", - metadata={"name": "cluster-selfsigned-issuer-root"}, - spec={"selfSigned": {}}, + release: pulumi.Resource + ) -> Tuple[k8s.apiextensions.CustomResource, k8s.apiextensions.CustomResource, k8s.apiextensions.CustomResource]: + """ + Creates cluster issuers required for CertManager. + """ + # Create ClusterIssuer root using the helper function + cluster_issuer_root = create_custom_resource( + name="cluster-selfsigned-issuer-root", + args={ + "apiVersion": "cert-manager.io/v1", + "kind": "ClusterIssuer", + "metadata": { + "name": "cluster-selfsigned-issuer-root", + }, + "spec": {"selfSigned": {}}, + }, opts=pulumi.ResourceOptions( - provider=k8s_provider, parent=release, - depends_on=[release], custom_timeouts=pulumi.CustomTimeouts(create="5m", update="10m", delete="10m"), ), + k8s_provider=k8s_provider, + depends_on=[release], ) - cluster_issuer_ca_certificate = CustomResource( - "cluster-selfsigned-issuer-ca", - api_version="cert-manager.io/v1", - kind="Certificate", - metadata={"name": "cluster-selfsigned-issuer-ca", "namespace": namespace}, - spec={ - "commonName": "cluster-selfsigned-issuer-ca", - "duration": "2160h0m0s", - "isCA": True, - "issuerRef": { - "group": "cert-manager.io", - "kind": "ClusterIssuer", - "name": "cluster-selfsigned-issuer-root", + # Create ClusterIssuer CA Certificate using the helper function + cluster_issuer_ca_certificate = create_custom_resource( + name="cluster-selfsigned-issuer-ca", + args={ + "apiVersion": "cert-manager.io/v1", + "kind": "Certificate", + "metadata": { + "name": "cluster-selfsigned-issuer-ca", + "namespace": namespace, + }, + "spec": { + "commonName": "cluster-selfsigned-issuer-ca", + "duration": "2160h0m0s", + "isCA": True, + "issuerRef": { + "group": "cert-manager.io", + "kind": "ClusterIssuer", + "name": "cluster-selfsigned-issuer-root", + }, + "privateKey": {"algorithm": "ECDSA", "size": 256}, + "renewBefore": "360h0m0s", + "secretName": "cluster-selfsigned-issuer-ca", }, - "privateKey": {"algorithm": "ECDSA", "size": 256}, - "renewBefore": "360h0m0s", - "secretName": "cluster-selfsigned-issuer-ca", }, opts=pulumi.ResourceOptions( - provider=k8s_provider, parent=cluster_issuer_root, - depends_on=[cluster_issuer_root], custom_timeouts=pulumi.CustomTimeouts(create="5m", update="10m", delete="10m"), ), + k8s_provider=k8s_provider, + depends_on=[cluster_issuer_root], ) - cluster_issuer = CustomResource( - cluster_issuer_name, - api_version="cert-manager.io/v1", - kind="ClusterIssuer", - metadata={"name": cluster_issuer_name}, - spec={"ca": {"secretName": "cluster-selfsigned-issuer-ca"}}, + # Create ClusterIssuer using the helper function + cluster_issuer = create_custom_resource( + name=cluster_issuer_name, + args={ + "apiVersion": "cert-manager.io/v1", + "kind": "ClusterIssuer", + "metadata": { + "name": cluster_issuer_name, + }, + "spec": { + "ca": {"secretName": "cluster-selfsigned-issuer-ca"}, + }, + }, opts=pulumi.ResourceOptions( - provider=k8s_provider, parent=cluster_issuer_ca_certificate, - depends_on=[cluster_issuer_ca_certificate], custom_timeouts=pulumi.CustomTimeouts(create="4m", update="4m", delete="4m"), ), + k8s_provider=k8s_provider, + depends_on=[cluster_issuer_ca_certificate], ) return cluster_issuer_root, cluster_issuer_ca_certificate, cluster_issuer diff --git a/pulumi/modules/kubevirt/README.md b/pulumi/modules/kubevirt/README.md index a9ba7b3..2869321 100644 --- a/pulumi/modules/kubevirt/README.md +++ b/pulumi/modules/kubevirt/README.md @@ -1,181 +1,186 @@ -# KubeVirt Module +# KubeVirt Module Guide -ContainerCraft Kargo Kubevirt PaaS KubeVirt module. - -The `kubevirt` module automates the deployment and configuration of KubeVirt in your Kubernetes environment using [KubeVirt](https://kubevirt.io/). KubeVirt is a Kubernetes-native solution to run and manage virtual machines alongside container workloads. This module simplifies the setup, configuration, and management of KubeVirt components for a seamless virtualization experience within Kubernetes. +Welcome to the **KubeVirt Module** for the Kargo KubeVirt Kubernetes PaaS! This guide is intended to help both newcomers and experienced developers understand, deploy, and customize the KubeVirt module within the Kargo platform. --- ## Table of Contents - [Introduction](#introduction) -- [Prerequisites](#prerequisites) -- [Features](#features) +- [Why Use KubeVirt?](#why-use-kubevirt) +- [Getting Started](#getting-started) - [Enabling the Module](#enabling-the-module) -- [Configuration](#configuration) - - [Default Configuration](#default-configuration) - - [Custom Configuration](#custom-configuration) -- [Module Components](#module-components) +- [Configuration Options](#configuration-options) + - [Default Settings](#default-settings) + - [Customizing Your Deployment](#customizing-your-deployment) +- [Module Components Explained](#module-components-explained) - [Namespace Creation](#namespace-creation) - - [KubeVirt Deployment](#kubevirt-deployment) - - [Custom Resource Creation](#custom-resource-creation) -- [Integration with Kargo PaaS](#integration-with-kargo-paas) -- [Best Practices](#best-practices) -- [Troubleshooting](#troubleshooting) + - [Operator Deployment](#operator-deployment) + - [Custom Resource Configuration](#custom-resource-configuration) +- [Using the Module](#using-the-module) + - [Example Usage](#example-usage) +- [Troubleshooting and FAQs](#troubleshooting-and-faqs) - [Additional Resources](#additional-resources) +- [Conclusion](#conclusion) --- ## Introduction -This guide provides an overview of the KubeVirt module, instructions on how to enable and configure it, and explanations of its functionality within the Kargo PaaS platform. Whether you're new to Kubernetes, KubeVirt, or ContainerCraft Kargo PaaS, this guide will help you get started. +The KubeVirt module enables you to run virtual machines (VMs) within your Kubernetes cluster using [KubeVirt](https://kubevirt.io/). It bridges the gap between containerized applications and traditional VM workloads, providing a unified platform for all your infrastructure needs. -## Prerequisites +--- -Before deploying the module, ensure the following prerequisites are met: +## Why Use KubeVirt? -- [Pulumi CLI](https://www.pulumi.com/docs/get-started/) -- Python 3.6 or higher -- Python dependencies (install via `pip install -r requirements.txt`) -- Kubernetes cluster (with `kubectl` configured) -- Properly configured `kubeconfig` file +- **Unified Platform**: Manage containers and VMs in a single Kubernetes cluster. +- **Flexibility**: Run legacy applications alongside cloud-native ones. +- **Scalability**: Leverage Kubernetes scaling features for VMs. +- **Ecosystem Integration**: Use Kubernetes tools and practices for VM management. --- -## Features +## Getting Started + +### Prerequisites + +- **Kubernetes Cluster**: Access to a cluster with appropriate resources. +- **Pulumi CLI**: Installed and configured. +- **Kubeconfig**: Properly set up for cluster access. -- **Automated Deployment**: Deploys KubeVirt using the official operator YAMLs. -- **Version Management**: Supports explicit version pinning or accepts `latest`. -- **Namespace Isolation**: Deploys KubeVirt in a dedicated namespace. -- **Customizable Configuration**: Allows setting overrides for customization, including emulation and feature gates. -- **Metadata Propagation**: Applies global labels and annotations consistently across resources. +### Setup Steps + +1. **Navigate to the Kargo Pulumi Directory**: + ```bash + cd Kargo/pulumi + ``` +2. **Install Dependencies**: + ```bash + pip install -r requirements.txt + ``` +3. **Initialize Pulumi Stack**: + ```bash + pulumi stack init dev + ``` --- ## Enabling the Module -The KubeVirt module is enabled by default. Customization can be configured in the Pulumi Stack Configuration. +The KubeVirt module is enabled by default. To confirm or adjust its status, modify your Pulumi configuration. -### Example Pulumi Configuration +### Verifying Module Enablement ```yaml # Pulumi..yaml config: kubevirt: - enabled: true # (default: true) + enabled: true # Set to false to disable ``` -Alternatively, you can set configuration values via the Pulumi CLI: +Alternatively, use the Pulumi CLI: ```bash -pulumi config set --path kubevirt.key value +pulumi config set --path kubevirt.enabled true ``` --- -## Configuration - -### Default Configuration +## Configuration Options -The module comes with sensible defaults to simplify deployment: +### Default Settings - **Namespace**: `kubevirt` -- **Version**: Uses the default version specified in `default_versions.json` -- **Use Emulation**: `false` -- **Labels and Annotations**: Derived from global configurations +- **Version**: Defined in `default_versions.json` +- **Use Emulation**: `false` (suitable for bare-metal environments) -### Custom Configuration +### Customizing Your Deployment -You can customize the module's behavior by providing additional configuration options. +#### Available Configuration Parameters -#### Available Configuration Options - -- **namespace** *(string)*: The namespace where KubeVirt will be deployed. -- **version** *(string)*: The version of the KubeVirt operator YAML to deploy. Use `'latest'` to fetch the latest version. -- **use_emulation** *(bool)*: Whether to enable KVM emulation. -- **labels** *(dict)*: Custom labels to apply to resources. -- **annotations** *(dict)*: Custom annotations to apply to resources. +- **enabled** *(bool)*: Enable or disable the module. +- **namespace** *(string)*: Kubernetes namespace for KubeVirt. +- **version** *(string)*: Specific version to deploy. Use `'latest'` for the most recent stable version. +- **use_emulation** *(bool)*: Enable if running in a nested virtualization environment. +- **labels** *(dict)*: Custom labels for resources. +- **annotations** *(dict)*: Custom annotations for resources. #### Example Custom Configuration ```yaml -# Pulumi..yaml -# Default values are shown for reference - config: kubevirt: enabled: true - version: "0.43.0" - namespace: "custom-kubevirt" + namespace: "kubevirt" + version: "1.3.1" use_emulation: true labels: - app.kubernetes.io/name: "kubevirt" + app: "kubevirt" annotations: - organization: "ContainerCraft" + owner: "dev-team" ``` --- -## Module Components +## Module Components Explained ### Namespace Creation -The module creates a dedicated namespace for KubeVirt to ensure isolation and better management. +A dedicated namespace is created for KubeVirt. -- **Namespace**: Configurable via the `namespace` parameter. -- **Labels and Annotations**: Applied as per best practices for identification. +- **Purpose**: Isolates KubeVirt resources for better management. +- **Customization**: Change using the `namespace` parameter. -### KubeVirt Deployment +### Operator Deployment -The module deploys KubeVirt using the official operator YAML from the KubeVirt GitHub repository. +Deploys the KubeVirt operator. -- **Operator YAML**: Downloaded from the specified version or the latest release. -- **Custom Values**: Includes configurations for namespaces, labels, and annotations. -- **Version**: Configurable; defaults to the version specified in `default_versions.json`. +- **Source**: Official KubeVirt operator YAML. +- **Version Management**: Specify a version or use `'latest'`. +- **Transformation**: YAML is adjusted to fit the specified namespace. -### Custom Resource Creation +### Custom Resource Configuration -To manage KubeVirt operational settings, the module creates custom resources with specified configurations. +Defines the KubeVirt CustomResource to configure KubeVirt settings. -- **KubeVirt CR**: Customizes KubeVirt to enable emulation and additional feature gates. -- **SMBIOS Configuration**: Adds specific values to the SMBIOS configuration for virtual machines. -- **Namespace and Resources**: The custom resource is applied in the specified namespace with the appropriate labels and annotations. +- **Emulation Mode**: Controlled by `use_emulation`. +- **Feature Gates**: Enables additional features like `HostDevices` and `ExpandDisks`. +- **SMBIOS Configuration**: Sets metadata for virtual machines. -## Integration with Kargo PaaS +--- -KubeVirt integrates seamlessly with the Kargo Kubevirt PaaS, making it simple to manage and run virtual machines alongside container workloads. The deployment process is optimized for consistency and simplicity, ensuring a smooth user experience within the Kubernetes ecosystem. +## Using the Module ---- +### Example Usage -## Best Practices +Deploy the module with your custom configuration: -- **Namespace Isolation**: Always deploy KubeVirt in a dedicated namespace to avoid conflicts. -- **Version Pinning**: Pin specific versions in production to avoid unintended issues with new releases. -- **Emulation Enablement**: Enable `use_emulation` only if you intend to run KubeVirt on non-bare-metal setups. +```bash +pulumi up +``` --- -## Troubleshooting +## Troubleshooting and FAQs + +**Q1: Virtual machines are not starting.** + +- **A**: Ensure that your nodes support virtualization. If running in a VM without the `/dev/kvm` device, set `use_emulation` to `true`. -### Common Issues +**Q2: Deployment fails with version errors.** -- **Connection Errors**: Ensure your `kubeconfig` and Kubernetes context are correctly configured. -- **Version Conflicts**: If deployment fails due to version issues, verify the specified version is available in the KubeVirt repository. Alternatively use `'latest'` and Kargo will fetch the latest version. -- **Namespace Issues**: Ensure the specified namespace is unique or does not conflict with existing namespaces. +- **A**: Verify that the specified version exists. Use `'latest'` to automatically fetch the latest stable version. -### Debugging Steps +**Q3: How do I enable additional feature gates?** -1. **Check Pulumi Logs**: Look for error messages during deployment. -2. **Verify Kubernetes Resources**: Use `kubectl` to inspect the KubeVirt namespace and resources. -3. **Review Configuration**: Ensure all configuration options are correctly set in your Pulumi config or remove configuration to use defaults. +- **A**: Modify the `featureGates` section in the `deploy.py` or submit a feature request to expose this via configuration. --- ## Additional Resources -- **KubeVirt Documentation**: [https://kubevirt.io/docs/](https://kubevirt.io/docs/) -- **Kargo Kubevirt PaaS IaC Documentation**: Refer to the main [Kargo README](../README.md) for project usage. -- **Pulumi Kubernetes Provider**: [https://www.pulumi.com/docs/reference/pkg/kubernetes/](https://www.pulumi.com/docs/reference/pkg/kubernetes/) -- **Helm Charts**: [https://artifacthub.io/packages/helm/jetstack/cert-manager](https://artifacthub.io/packages/helm/jetstack/cert-manager) -- **Need Help?** If you have questions or need assistance, feel free to reach out to the community or maintainers on GitHub, Discord, or Twitter. +- **KubeVirt Documentation**: [kubevirt.io/docs](https://kubevirt.io/docs/) +- **Kargo Project**: [Kargo GitHub Repository](https://github.com/ContainerCraft/Kargo) +- **Pulumi Kubernetes Provider**: [Pulumi Kubernetes Docs](https://www.pulumi.com/docs/reference/pkg/kubernetes/) +- **KubeVirt Releases**: [KubeVirt GitHub Releases](https://github.com/kubevirt/kubevirt/releases) diff --git a/pulumi/modules/kubevirt/deploy.py b/pulumi/modules/kubevirt/deploy.py index abecb4c..e9a19ae 100644 --- a/pulumi/modules/kubevirt/deploy.py +++ b/pulumi/modules/kubevirt/deploy.py @@ -12,12 +12,13 @@ import pulumi import pulumi_kubernetes as k8s -from pulumi_kubernetes.apiextensions.CustomResource import CustomResource -from pulumi_kubernetes.meta.v1 import ObjectMetaArgs -from core.types import NamespaceConfig from core.metadata import get_global_labels, get_global_annotations -from core.utils import set_resource_metadata +from core.resource_helpers import ( + create_namespace, + create_custom_resource, + create_config_file, +) from .types import KubeVirtConfig def deploy_kubevirt_module( @@ -25,22 +26,27 @@ def deploy_kubevirt_module( global_depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, ) -> Tuple[Optional[str], Optional[pulumi.Resource]]: - + """ + Deploys the KubeVirt module and returns the version and the deployed resource. + """ + # Create Namespace using the helper function namespace_resource = create_namespace( - config_kubevirt.namespace, - k8s_provider, - global_depends_on, + name=config_kubevirt.namespace, + k8s_provider=k8s_provider, + depends_on=global_depends_on, ) # Combine dependencies depends_on = global_depends_on + [namespace_resource] + # Deploy KubeVirt kubevirt_version, kubevirt_resource = deploy_kubevirt( config_kubevirt=config_kubevirt, depends_on=depends_on, k8s_provider=k8s_provider, ) + # Update global dependencies global_depends_on.append(kubevirt_resource) return kubevirt_version, kubevirt_resource @@ -50,49 +56,43 @@ def deploy_kubevirt( depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, ) -> Tuple[str, pulumi.Resource]: - + """ + Deploys KubeVirt operator and creates the KubeVirt CustomResource. + """ namespace = config_kubevirt.namespace version = config_kubevirt.version use_emulation = config_kubevirt.use_emulation - labels = config_kubevirt.labels - annotations = config_kubevirt.annotations + # Determine KubeVirt version if version == 'latest' or version is None: version = get_latest_kubevirt_version() pulumi.log.info(f"Setting KubeVirt release version to latest: {version}") else: pulumi.log.info(f"Using KubeVirt version: {version}") + # Download and transform KubeVirt operator YAML kubevirt_operator_yaml = download_kubevirt_operator_yaml(version) - transformed_yaml = _transform_yaml(kubevirt_operator_yaml, namespace) - def kubevirt_transform(obj: Dict[str, Any], opts: pulumi.ResourceOptions) -> pulumi.ResourceTransformationResult: - obj.setdefault("metadata", {}) - set_resource_metadata(obj["metadata"], labels, annotations) - if "spec" in obj and "template" in obj["spec"]: - template_meta = obj["spec"]["template"].setdefault("metadata", {}) - set_resource_metadata(template_meta, labels, annotations) - return pulumi.ResourceTransformationResult(obj, opts) - + # Write transformed YAML to a temporary file with tempfile.NamedTemporaryFile(delete=False, mode='w') as temp_file: yaml.dump_all(transformed_yaml, temp_file) temp_file_path = temp_file.name try: - operator = k8s.yaml.ConfigFile( - 'kubevirt-operator', + # Deploy KubeVirt operator using the helper function + operator = create_config_file( + name='kubevirt-operator', file=temp_file_path, - transformations=[kubevirt_transform], opts=pulumi.ResourceOptions( - provider=k8s_provider, - depends_on=depends_on, custom_timeouts=pulumi.CustomTimeouts( create="10m", update="5m", delete="5m", ), ), + k8s_provider=k8s_provider, + depends_on=depends_on, ) finally: os.unlink(temp_file_path) @@ -100,74 +100,53 @@ def kubevirt_transform(obj: Dict[str, Any], opts: pulumi.ResourceOptions) -> pul if use_emulation: pulumi.log.info("KVM Emulation enabled for KubeVirt.") - kubevirt_custom_resource_spec = { - "configuration": { - "developerConfiguration": { - "useEmulation": use_emulation, - "featureGates": [ - "HostDevices", - "ExpandDisks", - "AutoResourceLimitsGate", - ], + # Create KubeVirt CustomResource using the helper function + kubevirt_resource = create_custom_resource( + name="kubevirt", + args={ + "apiVersion": "kubevirt.io/v1", + "kind": "KubeVirt", + "metadata": { + "name": "kubevirt", + "namespace": namespace, }, - "smbios": { - "sku": "kargo-kc2", - "version": version, - "manufacturer": "ContainerCraft", - "product": "Kargo", - "family": "CCIO", + "spec": { + "configuration": { + "developerConfiguration": { + "useEmulation": use_emulation, + "featureGates": [ + "HostDevices", + "ExpandDisks", + "AutoResourceLimitsGate", + ], + }, + "smbios": { + "sku": "kargo-kc2", + "version": version, + "manufacturer": "ContainerCraft", + "product": "Kargo", + "family": "CCIO", + }, + }, }, - } - } - - kubevirt = CustomResource( - "kubevirt", - api_version="kubevirt.io/v1", - kind="KubeVirt", - metadata=ObjectMetaArgs( - name="kubevirt", - labels=labels, - namespace=namespace, - annotations=annotations, - ), - spec=kubevirt_custom_resource_spec, - opts=pulumi.ResourceOptions( - provider=k8s_provider, - depends_on=[operator], - ), - ) - - return version, kubevirt - -def create_namespace( - namespace_name: str, - k8s_provider: k8s.Provider, - depends_on: List[pulumi.Resource], - ) -> k8s.core.v1.Namespace: - """ - Creates a Kubernetes namespace with the provided configuration. - """ - namespace_config = NamespaceConfig(name=namespace_name) - namespace_resource = k8s.core.v1.Namespace( - namespace_name, - metadata={ - "name": namespace_config.name, - "labels": namespace_config.labels, - "annotations": namespace_config.annotations, }, opts=pulumi.ResourceOptions( - provider=k8s_provider, - depends_on=depends_on, custom_timeouts=pulumi.CustomTimeouts( - create=namespace_config.custom_timeouts.get("create", "5m"), - update=namespace_config.custom_timeouts.get("update", "10m"), - delete=namespace_config.custom_timeouts.get("delete", "10m"), + create="5m", + update="5m", + delete="5m", ), ), + k8s_provider=k8s_provider, + depends_on=[operator], ) - return namespace_resource + + return version, kubevirt_resource def get_latest_kubevirt_version() -> str: + """ + Retrieves the latest stable version of KubeVirt. + """ url = 'https://storage.googleapis.com/kubevirt-prow/release/kubevirt/kubevirt/stable.txt' response = requests.get(url) if response.status_code != 200: @@ -175,13 +154,19 @@ def get_latest_kubevirt_version() -> str: return response.text.strip().lstrip("v") def download_kubevirt_operator_yaml(version: str) -> Any: + """ + Downloads the KubeVirt operator YAML for the specified version. + """ url = f'https://github.com/kubevirt/kubevirt/releases/download/v{version}/kubevirt-operator.yaml' response = requests.get(url) if response.status_code != 200: raise Exception(f"Failed to download KubeVirt operator YAML from {url}") - return yaml.safe_load_all(response.text) + return list(yaml.safe_load_all(response.text)) def _transform_yaml(yaml_data: Any, namespace: str) -> List[Dict[str, Any]]: + """ + Transforms the YAML data to set the namespace and exclude Namespace resources. + """ transformed = [] for resource in yaml_data: if resource.get('kind') == 'Namespace': From 4c23f1c374b646d58d679306ae95084d27ea90b0 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sat, 21 Sep 2024 23:07:43 -0700 Subject: [PATCH 31/43] adopt new resource_helpers for metadata propagation compliance --- pulumi/README.md | 585 ++++++++++++++++++++++++++++------------------- 1 file changed, 346 insertions(+), 239 deletions(-) diff --git a/pulumi/README.md b/pulumi/README.md index c0854a3..38792ef 100644 --- a/pulumi/README.md +++ b/pulumi/README.md @@ -1,189 +1,275 @@ -# Kargo Kubevirt Kubernetes PaaS - Pulumi Python Infrastructure as Code (IaC) +# Kargo KubeVirt Kubernetes PaaS - Pulumi Python Infrastructure as Code (IaC) -## **Developer & Architecture Ethos** +Welcome to the **Kargo KubeVirt Kubernetes PaaS Pulumi Infrastructure as Code (IaC) project**! This guide is designed to help both newcomers to DevOps and experienced module developers navigate, contribute to, and get the most out of the Kargo platform. Whether you're setting up your environment for the first time or looking to develop new modules, this guide provides comprehensive instructions and best practices. -> **Prime Directive:** "Features are nice. Quality is paramount." -> -> Quality is not just about the product or code. Enjoyable developer experience is imperative to the survival of FOSS. +--- + +## Table of Contents + +- [Introduction](#introduction) +- [Developer & Architecture Ethos](#developer--architecture-ethos) + - [Prime Directive](#prime-directive) + - [Developer Directives](#developer-directives) +- [Getting Started](#getting-started) + - [Prerequisites](#prerequisites) + - [Setting Up Your Environment](#setting-up-your-environment) +- [Developer Imperatives](#developer-imperatives) + - [Detailed Breakdown](#detailed-breakdown) +- [Developing New Modules](#developing-new-modules) + - [Directory Structure](#directory-structure) + - [Creating a New Module](#creating-a-new-module) +- [Common Utilities](#common-utilities) +- [Version Control](#version-control) +- [Contributing to the Project](#contributing-to-the-project) +- [Additional Resources](#additional-resources) +- [Conclusion](#conclusion) + +--- + +## Introduction + +The Kargo KubeVirt Kubernetes PaaS project leverages Pulumi and Python to manage your Kubernetes infrastructure as code. Our goal is to provide an enjoyable developer experience (DX) and user experience (UX) by simplifying the deployment and management of Kubernetes resources, including KubeVirt virtual machines and other essential components. + +This guide aims to make core concepts accessible to everyone, regardless of their experience level in DevOps. + +--- + +## Developer & Architecture Ethos + +### Prime Directive + +> **"Features are nice. Quality is paramount."** + +Quality is not just about the product or code; it's about creating an enjoyable developer and user experience. At ContainerCraft, we believe that the success of open-source projects depends on the happiness and satisfaction of the community developers and users. + +### Developer Directives -ContainerCraft is a Developer Experience (DX) and User Experience (UX) obsessed project. As part of the CCIO Open Source education and skill development ecosystem, the Kargo project's survival is dependent on the happiness of community developers and users. +1. **Improve Code Maintainability**: Write code that is structured, organized, and easy to understand. Prioritize readability, reusability, and extensibility. -### **Developer Directives** +2. **Optimize Performance**: Ensure that the code performs efficiently and respects configurations. Avoid executing inactive or unnecessary code. -- **Improve Code Maintainability**: Enhance structure and organization. Prioritize readable, reusable, and extensible code. -- **Optimize Performance**: Improve code execution performance. Honor configuration. Do not execute inactive code. -- **Establish Standard Practices**: Develop a consistent approach to configuration handling, module deployment, and code organization to guide future development. +3. **Establish Standard Practices**: Develop consistent approaches to configuration handling, module deployment, and code organization to guide future development. --- -## **Getting Started** +## Getting Started -### **Prerequisites** +### Prerequisites -- [Pulumi CLI](https://www.pulumi.com/docs/get-started/) -- Python 3.6 or higher -- Python dependencies (install via `pip install -r requirements.txt`) -- Kubernetes cluster (with `kubectl` configured) -- Helm CLI (for Helm-related operations) +Before you begin, make sure you have the following installed: -### **Setting Up Your Environment** +- **Pulumi CLI**: [Install Pulumi](https://www.pulumi.com/docs/get-started/) +- **Python 3.6+**: Ensure you have Python installed on your system. +- **Python Dependencies**: Install required Python packages using `pip install -r requirements.txt` +- **Kubernetes Cluster**: Access to a Kubernetes cluster with `kubectl` configured. +- **Helm CLI**: [Install Helm](https://helm.sh/docs/intro/install/) if you plan to work with Helm charts. -1. **Clone the repository:** +### Setting Up Your Environment - ```sh - git clone https://github.com/containercraft/kargo.git - cd kargo - ``` +Follow these steps to set up your environment: -2. **Configure your Pulumi stack:** +1. **Clone the Repository** - ```sh - pulumi stack init - ``` + ```bash + git clone https://github.com/ContainerCraft/Kargo.git + cd Kargo/pulumi + ``` -3. **Set up your Pulumi configuration:** +2. **Install Python Dependencies** - ```sh - pulumi config set kubernetes:kubeconfig - ``` + ```bash + pip install -r requirements.txt + ``` -4. **Install dependencies:** +3. **Initialize Pulumi Stack** - ```sh - pip install -r requirements.txt - ``` + ```bash + pulumi stack init dev + ``` -5. **Deploy the stack:** +4. **Configure Pulumi** - ```sh - pulumi up - ``` + Set your Kubernetes context and any necessary configuration options. + + ```bash + pulumi config set kubernetes:kubeconfig + # Set other configuration options as needed + ``` + +5. **Deploy the Stack** + + Preview and deploy your changes. + + ```bash + pulumi up + ``` + + Follow the prompts to confirm the deployment. --- -## **Developer Imperatives** +## Developer Imperatives ### Detailed Breakdown 1. **User Experience (UX)** - - **Clear Error Messages:** Provide meaningful error messages that guide users to resolve issues. - - **Uniform Logging:** Use consistent logging practices to make debugging easier. - ```python - pulumi.log.info(f"Deploying module: {module_name}") - ``` + + - **Clear Error Messages**: Provide meaningful error messages to help users resolve issues. + - **Uniform Logging**: Use consistent logging practices to make debugging easier. + + ```python + pulumi.log.info(f"Deploying module: {module_name}") + ``` 2. **Developer Experience (DX)** - - **Documentation:** Add comprehensive docstrings and comments. - ```python - def deploy_module(...): - """ - Helper function to deploy a module based on configuration. - ... - """ - ``` - - **Examples:** Include example configurations and usage in the documentation. + + - **Documentation**: Include comprehensive docstrings and comments in your code. + + ```python + def deploy_module(...): + """ + Deploys a module based on configuration. + + Args: + module_name (str): Name of the module. + config (pulumi.Config): Pulumi configuration object. + ... + + Returns: + None + """ + ``` + + - **Examples**: Provide example configurations and usage in the documentation to help others understand how to use your code. 3. **Configurable Modules** - - **Pulumi Stack Configuration:** Use the Pulumi config object to roll out custom module configurations. - ```python - module_config = config.get_object("module_name") or {} - ``` + + - **Pulumi Stack Configuration**: Use the Pulumi config object to allow users to customize module configurations. + + ```python + module_config = config.get_object("module_name") or {} + ``` 4. **Module Data Classes** - - **Typed Data Classes:** Encapsulate configurations clearly using `dataclass`. - ```python - from dataclasses import dataclass - @dataclass - class KubeVirtConfig: - namespace: str = "default" - ``` + + - **Typed Data Classes**: Use `dataclass` to encapsulate configurations clearly. + + ```python + from dataclasses import dataclass + + @dataclass + class KubeVirtConfig: + namespace: str = "default" + ``` 5. **Sane Defaults in Data Classes** - - **Sensible Defaults:** Set reasonable default values to ensure minimal user configuration. - ```python - @dataclass - class CertManagerConfig: - namespace: str = "cert-manager" - install_crds: bool = True - ``` + + - **Sensible Defaults**: Set reasonable default values to minimize the need for user configuration. + + ```python + @dataclass + class CertManagerConfig: + namespace: str = "cert-manager" + install_crds: bool = True + ``` 6. **User Configuration Handling** - - **Merge Configurations:** Combine user-provided configurations with defaults. - ```python - @staticmethod - def merge(user_config: Dict[str, Any]) -> 'CertManagerConfig': - default_config = CertManagerConfig() - for key, value in user_config.items(): - if hasattr(default_config, key): - setattr(default_config, key, value) - return default_config - ``` + + - **Merge Configurations**: Combine user-provided configurations with defaults to ensure all necessary parameters are set. + + ```python + @staticmethod + def merge(user_config: Dict[str, Any]) -> 'CertManagerConfig': + default_config = CertManagerConfig() + for key, value in user_config.items(): + if hasattr(default_config, key): + setattr(default_config, key, value) + else: + pulumi.log.warn(f"Unknown configuration key '{key}' in cert_manager config.") + return default_config + ``` 7. **Simple Function Signatures** - - **Reduce Parameters:** Keep function signatures minimal by encapsulating configurations. - ```python - def deploy_module(module_config: ModuleConfig) - ``` + + - **Reduce Parameters**: Keep function signatures minimal by encapsulating configurations within data classes. + + ```python + def deploy_module(config_module: ModuleConfig, ...) + ``` 8. **Type Annotations** - - **Enhance Readability:** - ```python - def deploy_module(module_name: str, config: pulumi.Config) -> None: - ``` + + - **Enhance Readability**: Use type annotations to clarify expected parameter types and return values. + + ```python + def deploy_module(module_name: str, config: pulumi.Config) -> None: + ``` 9. **Safe Function Signatures** - - **Type Safety:** Use consistent type checks and raise meaningful errors. - ```python - if not hasattr(default_config, key): - pulumi.log.warn(f"Unknown configuration key '{key}' in config.") - ``` + + - **Type Safety**: Use consistent type checks and raise meaningful errors when types don't match expectations. + + ```python + if not isinstance(module_name, str): + raise TypeError("module_name must be a string") + ``` 10. **Streamlined Entrypoint** - - **Minimize Top-Level Code:** Encapsulate logic within the module and related files. + + - **Encapsulate Logic**: Keep the top-level code minimal and encapsulate logic within functions. + ```python if __name__ == "__main__": main() ``` -11. **Reuse + Dedupe Code** - - **Central Utilities:** Use common patterns by placing reusable code in `core/utils.py`. +11. **Reuse and Deduplicate Code** + + - **Central Utilities**: Place reusable code in the `core` module to maintain consistency and reduce duplication. + ```python from core.utils import sanitize_label_value, extract_repo_name ``` 12. **Version Control Dependencies** - - **Manage Versions:** Control component versions within `versions.py` to maintain consistency. + + - **Manage Versions**: Control component versions within configuration files to maintain consistency across deployments. + ```python default_versions = load_default_versions(config) ``` 13. **Transparency** - - **Informative Outputs:** Export configuration and version information. + + - **Informative Outputs**: Export configuration and version information for visibility and auditing. + ```python pulumi.export("versions", versions) ``` 14. **Conditional Execution** - - **Avoid Unnecessary Execution:** Load and execute only the necessary modules. + + - **Avoid Unnecessary Execution**: Only load and execute modules that are enabled in the configuration. + ```python if module_enabled: deploy_func(...) ``` 15. **Remove Deprecated Code** - - **Eliminate Obsolete Features:** Keep the codebase clean and update features as required. + + - **Maintain a Clean Codebase**: Remove obsolete features and update code to align with current best practices. --- -## **Developing New Modules** +## Developing New Modules -### **Directory Structure** +### Directory Structure -Maintain a consistent directory structure for new modules. Below is an example structure: +Maintain a consistent directory structure for new modules: ``` kargo/ -README.md pulumi/ __main__.py requirements.txt @@ -196,157 +282,161 @@ README.md __init__.py deploy.py types.py + README.md ... ``` -### **Creating a New Module** - -1. **Define Configuration:** - - Add a new `types.py` file under your module directory to define the configuration dataclass: - - ```python - from dataclasses import dataclass, field - from typing import Optional, Dict, Any - - @dataclass - class NewModuleConfig: - version: Optional[str] = None - param1: str = "default_value" - labels: Dict[str, str] = field(default_factory=dict) - annotations: Dict[str, Any] = field(default_factory=dict) - - @staticmethod - def merge(user_config: Dict[str, Any]) -> 'NewModuleConfig': - default_config = NewModuleConfig() - for key, value in user_config.items(): - if hasattr(default_config, key): - setattr(default_config, key, value) - else: - pulumi.log.warn(f"Unknown configuration key '{key}' in new_module config.") - return default_config - ``` - -2. **Deploy Function:** - - Define the deployment logic in `deploy.py`: - - ```python - from typing import List, Dict, Any, Tuple, Optional - import pulumi - import pulumi_kubernetes as k8s - - from core.types import NamespaceConfig - from core.namespace import create_namespace - from core.metadata import get_global_annotations, get_global_labels - from core.utils import set_resource_metadata - from .types import NewModuleConfig - - def deploy_new_module( - config_new_module: NewModuleConfig, - global_depends_on: List[pulumi.Resource], - k8s_provider: k8s.Provider, - ) -> Tuple[Optional[str], Optional[pulumi.Resource]]: - # Define deployment logic here - namespace_config = NamespaceConfig(name=config_new_module.namespace) - namespace_resource = create_namespace(namespace_config, k8s_provider, global_depends_on) - - # Implement specific resource creation and transformation logic - ... +### Creating a New Module + +1. **Define Configuration** + + Create a `types.py` file in your module directory to define the configuration data class: + + ```python + from dataclasses import dataclass, field + from typing import Optional, Dict, Any + + @dataclass + class NewModuleConfig: + version: Optional[str] = None + namespace: str = "default" + labels: Dict[str, str] = field(default_factory=dict) + annotations: Dict[str, Any] = field(default_factory=dict) + + @staticmethod + def merge(user_config: Dict[str, Any]) -> 'NewModuleConfig': + default_config = NewModuleConfig() + for key, value in user_config.items(): + if hasattr(default_config, key): + setattr(default_config, key, value) + else: + pulumi.log.warn(f"Unknown configuration key '{key}' in new_module config.") + return default_config + ``` + +2. **Implement Deployment Logic** + + Define the deployment logic in `deploy.py`: + + ```python + import pulumi + import pulumi_kubernetes as k8s + from typing import List, Dict, Any, Tuple, Optional + + from core.metadata import get_global_labels, get_global_annotations + from core.resource_helpers import create_namespace + from .types import NewModuleConfig + + def deploy_new_module( + config_new_module: NewModuleConfig, + global_depends_on: List[pulumi.Resource], + k8s_provider: k8s.Provider, + ) -> Tuple[Optional[str], Optional[pulumi.Resource]]: + # Create Namespace + namespace_resource = create_namespace( + name=config_new_module.namespace, + labels=config_new_module.labels, + annotations=config_new_module.annotations, + k8s_provider=k8s_provider, + depends_on=global_depends_on, + ) + + # Implement specific resource creation logic + # ... + + return config_new_module.version, namespace_resource + ``` - return version, deployed_resource - ``` +3. **Update `__main__.py`** -3. **Update `__main__.py`:** + Include your module in the main deployment script: - Ensure your module is included in the main deployment script: + ```python + from typing import List, Dict, Any + import pulumi + from pulumi_kubernetes import Provider - ```python - from typing import List, Dict, Any + from core.deployment import initialize_pulumi, deploy_module + from core.config import export_results - import pulumi - from pulumi_kubernetes import Provider + def main(): + try: + init = initialize_pulumi() - from core.init import initialize_pulumi - from core.config import export_results - from core.deploy_module import deploy_module + config = init["config"] + k8s_provider = init["k8s_provider"] + versions = init["versions"] + configurations = init["configurations"] + default_versions = init["default_versions"] + global_depends_on = init["global_depends_on"] - def main(): - try: - init = initialize_pulumi() + modules_to_deploy = ["cert_manager", "kubevirt", "new_module"] # Add your module here - config = init["config"] - k8s_provider = init["k8s_provider"] - versions = init["versions"] - configurations = init["configurations"] - default_versions = init["default_versions"] - global_depends_on = init["global_depends_on"] + deploy_modules( + modules=modules_to_deploy, + config=config, + default_versions=default_versions, + global_depends_on=global_depends_on, + k8s_provider=k8s_provider, + versions=versions, + configurations=configurations, + ) - modules_to_deploy = ["cert_manager", "kubevirt", "new_module"] # Add your module here + compliance_config = init.get("compliance_config", {}) + export_results(versions, configurations, compliance_config) - deploy_modules(modules_to_deploy, config, default_versions, global_depends_on, k8s_provider, versions, configurations) + except Exception as e: + pulumi.log.error(f"Deployment failed: {str(e)}") + raise - compliance_config = init.get("compliance_config", {}) - export_results(versions, configurations, compliance_config) + if __name__ == "__main__": + main() + ``` - except Exception as e: - pulumi.log.error(f"Deployment failed: {str(e)}") - raise +4. **Document Your Module** - def deploy_modules( - modules: List[str], - config: pulumi.Config, - default_versions: Dict[str, Any], - global_depends_on: List[pulumi.Resource], - k8s_provider: Provider, - versions: Dict[str, str], - configurations: Dict[str, Dict[str, Any]] - ) -> None: + Create a `README.md` file in your module directory to document its purpose, configuration options, and usage instructions. - for module_name in modules: - pulumi.log.info(f"Deploying module: {module_name}") - deploy_module( - module_name=module_name, - config=config, - default_versions=default_versions, - global_depends_on=global_depends_on, - k8s_provider=k8s_provider, - versions=versions, - configurations=configurations, - ) + ```markdown + # New Module - if __name__ == "__main__": - main() - ``` + Description of your module. -### **Common Utilities** + ## Configuration -Refer to `core/utils.py` for common helper functions. For example, `set_resource_metadata` to apply global labels and annotations: + - **version** *(string)*: The version of the module to deploy. + - **namespace** *(string)*: The Kubernetes namespace where the module will be deployed. + - **labels** *(dict)*: Custom labels to apply to resources. + - **annotations** *(dict)*: Custom annotations to apply to resources. + + ## Usage + + Example of how to configure and deploy the module. + + ## Additional Information + + Any additional details or resources. + ``` + +--- + +## Common Utilities + +Refer to `core/utils.py` for common helper functions, such as applying global labels and annotations to resources. ```python -from typing import Optional, Dict, Any import re import pulumi import pulumi_kubernetes as k8s +from typing import Dict, Any def set_resource_metadata(metadata: Any, global_labels: Dict[str, str], global_annotations: Dict[str, str]): if isinstance(metadata, dict): - if metadata.get('labels') is None: - metadata['labels'] = {} metadata.setdefault('labels', {}).update(global_labels) - - if metadata.get('annotations') is None: - metadata['annotations'] = {} metadata.setdefault('annotations', {}).update(global_annotations) - elif isinstance(metadata, k8s.meta.v1.ObjectMetaArgs): - if metadata.labels is None: - metadata.labels = {} - metadata.labels.update(global_labels) - - if metadata.annotations is None: - metadata.annotations = {} - metadata.annotations.update(global_annotations) + metadata.labels = {**metadata.labels or {}, **global_labels} + metadata.annotations = {**metadata.annotations or {}, **global_annotations} def sanitize_label_value(value: str) -> str: value = value.lower() @@ -356,20 +446,37 @@ def sanitize_label_value(value: str) -> str: return sanitized[:63] ``` -### **Version Control** - -Manage module versions within `core/versions.py`: +--- -```python -import json -import os -import pulumi -import requests +## Version Control -DEFAULT_VERSIONS_URL_TEMPLATE = 'https://raw.githubusercontent.com/ContainerCraft/Kargo/rerefactor/pulumi/' +Manage module versions and dependencies within configuration files, such as `default_versions.json`, to ensure consistency across deployments. -def load_default_versions(config: pulumi.Config, force_refresh=False) -> dict: - ... - # Function implementation - ... +```json +{ + "cert_manager": "1.15.3", + "kubevirt": "1.3.1", + "new_module": "0.1.0" +} ``` + +--- + +## Contributing to the Project + +We welcome contributions from the community! Here's how you can help: + +- **Report Issues**: If you encounter any bugs or have feature requests, please open an issue on GitHub. + +- **Submit Pull Requests**: If you'd like to contribute code, fork the repository and submit a pull request. + +- **Improve Documentation**: Help us enhance this guide and other documentation to make it more accessible. + +--- + +## Additional Resources + +- **Kargo Project Repository**: [ContainerCraft Kargo on GitHub](https://github.com/ContainerCraft/Kargo) +- **Pulumi Documentation**: [Pulumi Official Docs](https://www.pulumi.com/docs/) +- **Kubernetes Documentation**: [Kubernetes Official Docs](https://kubernetes.io/docs/home/) +- **KubeVirt Documentation**: [KubeVirt Official Docs](https://kubevirt.io/docs/) From d606183e3d60cf84af6c98bd59ba524d2ddcf058 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sat, 21 Sep 2024 23:08:58 -0700 Subject: [PATCH 32/43] adopt new resource_helpers for metadata propagation compliance --- pulumi/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pulumi/README.md b/pulumi/README.md index 38792ef..a9daf92 100644 --- a/pulumi/README.md +++ b/pulumi/README.md @@ -92,7 +92,7 @@ Follow these steps to set up your environment: Set your Kubernetes context and any necessary configuration options. ```bash - pulumi config set kubernetes:kubeconfig + pulumi config set --path kubernetes.kubeconfig # Set other configuration options as needed ``` From a3e0396b56b88ef100fe1de4da2619ebc12e9086 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sun, 22 Sep 2024 11:16:08 -0700 Subject: [PATCH 33/43] add default enabled config --- .github/bin/delete-namespace | 51 ++++++++---- Pulumi.yaml | 104 ++++++++++++------------ pulumi/core/config.py | 14 +++- pulumi/stacks/Pulumi.optiplexprime.yaml | 8 +- 4 files changed, 104 insertions(+), 73 deletions(-) diff --git a/.github/bin/delete-namespace b/.github/bin/delete-namespace index b6cb589..aeb5489 100755 --- a/.github/bin/delete-namespace +++ b/.github/bin/delete-namespace @@ -1,6 +1,8 @@ #!/bin/bash -# Delete all resources in namespace +set -euo pipefail + +# Function to delete all resources in a namespace delete_resources_in_namespace() { local namespace=$1 echo "Deleting all resources in namespace: $namespace" @@ -8,14 +10,20 @@ delete_resources_in_namespace() { | xargs -n 1 kubectl delete --all -n "$namespace" --ignore-not-found --wait } -# Remove finalizers and delete stuck namespace -delete_namespace() { +# Function to remove finalizers from a namespace +remove_finalizers() { local namespace=$1 - echo "Removing finalizers and deleting namespace: $namespace" + echo "Removing finalizers from namespace: $namespace" kubectl get namespace "$namespace" -o json \ | jq 'del(.spec.finalizers)' \ | kubectl replace --raw "/api/v1/namespaces/$namespace/finalize" -f - - kubectl delete namespace "$namespace" +} + +# Function to delete a namespace +delete_namespace() { + local namespace=$1 + echo "Deleting namespace: $namespace" + kubectl delete namespace "$namespace" --ignore-not-found --wait echo "Waiting for namespace $namespace to be deleted..." while kubectl get namespace "$namespace" &>/dev/null; do sleep 1 @@ -23,7 +31,7 @@ delete_namespace() { echo "Namespace $namespace has been deleted." } -# Delete CustomResourceDefinitions (CRDs) associated with cert-manager and kubevirt +# Function to delete CustomResourceDefinitions (CRDs) delete_crds() { local crds=("certificaterequests.cert-manager.io" "certificates.cert-manager.io" "challenges.acme.cert-manager.io" \ "orders.acme.cert-manager.io" "clusterissuers.cert-manager.io" "issuers.cert-manager.io" \ @@ -35,17 +43,24 @@ delete_crds() { done } -# Check for at least one namespace provided as argument -if [ $# -eq 0 ]; then - echo "Usage: $0 ... " - exit 1 -fi +# Main deletion process +main() { + if [ $# -eq 0 ]; then + echo "Usage: $0 ... " + exit 1 + fi + + for namespace in "$@"; do + delete_resources_in_namespace "$namespace" + remove_finalizers "$namespace" || echo "Failed to remove finalizers for namespace: $namespace" + delete_namespace "$namespace" || { + echo "Namespace $namespace is stuck. Attempting to force removal." + remove_finalizers "$namespace" + delete_namespace "$namespace" + } + done -# Loop through and delete list of namespaces -for namespace in "$@"; do - delete_resources_in_namespace "$namespace" - delete_namespace "$namespace" -done + delete_crds +} -# Delete CRDs -delete_crds +main "$@" diff --git a/Pulumi.yaml b/Pulumi.yaml index 6b9ed04..dc40dd2 100644 --- a/Pulumi.yaml +++ b/Pulumi.yaml @@ -8,55 +8,55 @@ runtime: name: python options: virtualenv: venv -config: - pulumi:tags: - value: - pulumi:template: ccio-kargo-python - vm: - value: - enabled: false - namespace: default - instance_name: ubuntu - image_name: docker.io/containercraft/ubuntu:22.04 - node_port: 30590 - ssh_user: kc2 - ssh_password: kc2 - kubernetes: - value: - context: kargo - kubeconfig: .kube/config - cert_manager: - value: - enabled: true - version: 1.15.3 - kubevirt: - value: - enabled: true - version: 1.3.1 - hostpath_provisioner: - value: - enabled: true - version: 0.20.0 - default_storage_class: true - cdi: - value: - enabled: true - version: 1.60.2 - multus: - value: - enabled: true - cnao: - value: - enabled: false - version: 0.94.2 - prometheus: - value: - enabled: false - version: 61.3.2 - kubernetes_dashboard: - value: - enabled: false - version: 7.4.0 - cilium: - value: - enabled: false +#config: +# pulumi:tags: +# value: +# pulumi:template: ccio-kargo-python +# vm: +# value: +# enabled: false +# namespace: default +# instance_name: ubuntu +# image_name: docker.io/containercraft/ubuntu:22.04 +# node_port: 30590 +# ssh_user: kc2 +# ssh_password: kc2 +# kubernetes: +# value: +# context: kargo +# kubeconfig: .kube/config +# cert_manager: +# value: +# enabled: true +# version: 1.15.3 +# kubevirt: +# value: +# enabled: true +# version: 1.3.1 +# hostpath_provisioner: +# value: +# enabled: true +# version: 0.20.0 +# default_storage_class: true +# cdi: +# value: +# enabled: true +# version: 1.60.2 +# multus: +# value: +# enabled: true +# cnao: +# value: +# enabled: false +# version: 0.94.2 +# prometheus: +# value: +# enabled: false +# version: 61.3.2 +# kubernetes_dashboard: +# value: +# enabled: false +# version: 7.4.0 +# cilium: +# value: +# enabled: false diff --git a/pulumi/core/config.py b/pulumi/core/config.py index 03e7376..668c081 100644 --- a/pulumi/core/config.py +++ b/pulumi/core/config.py @@ -18,6 +18,14 @@ # Default versions URL template DEFAULT_VERSIONS_URL_TEMPLATE = 'https://raw.githubusercontent.com/ContainerCraft/Kargo/rerefactor/pulumi/' +# Default module enabled settings +DEFAULT_ENABLED_CONFIG = { + "cert_manager": True, + "kubevirt": True, + "multus": False, +} + + def get_module_config( module_name: str, config: pulumi.Config, @@ -35,10 +43,14 @@ def get_module_config( Tuple[Dict[str, Any], bool]: A tuple containing the module's configuration dictionary and a boolean indicating if the module is enabled. """ module_config = config.get_object(module_name) or {} - module_enabled = str(module_config.pop('enabled', 'false')).lower() == "true" + + # Retrieve enabled status from configuration or defaults to defined default setting + module_enabled = str(module_config.pop('enabled', DEFAULT_ENABLED_CONFIG.get(module_name, False))).lower() == "true" + module_config['version'] = module_config.get('version', default_versions.get(module_name)) return module_config, module_enabled + def load_default_versions(config: pulumi.Config, force_refresh=False) -> dict: """ Loads the default versions for modules based on the specified configuration settings. diff --git a/pulumi/stacks/Pulumi.optiplexprime.yaml b/pulumi/stacks/Pulumi.optiplexprime.yaml index 25207a3..a594a8e 100644 --- a/pulumi/stacks/Pulumi.optiplexprime.yaml +++ b/pulumi/stacks/Pulumi.optiplexprime.yaml @@ -1,12 +1,14 @@ config: + kargo:cert_manager: + version: latest kargo:compliance: fisma: - enforcing: warn # accepts: strict, warn, disabled - enabled: true ato: authorized: "2025-03-27T00:00:00Z" renew: "2026-03-27T00:00:00Z" review: "2028-03-27T00:00:00Z" + enabled: true + enforcing: warn # accepts: strict, warn, disabled level: moderate nist: auxiliary: @@ -42,6 +44,8 @@ config: version: v2.87.11 kargo:kubernetes: context: usrbinkat-optiplexprime # Kubernetes context for the stack + kargo:kubevirt: + version: latest kargo:talos: controlplane: cpu_cores: 1 # Controlplane CPU cores From ca878a1e220537a0e638cc5bfc7e388282062904 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sun, 22 Sep 2024 11:18:17 -0700 Subject: [PATCH 34/43] add default enabled config --- pulumi/core/config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pulumi/core/config.py b/pulumi/core/config.py index 668c081..e32eb3a 100644 --- a/pulumi/core/config.py +++ b/pulumi/core/config.py @@ -22,7 +22,6 @@ DEFAULT_ENABLED_CONFIG = { "cert_manager": True, "kubevirt": True, - "multus": False, } From 4dd658c4f11eb90cf3c71ecdd2d18ce4c5c9a872 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Sun, 22 Sep 2024 22:37:51 -0700 Subject: [PATCH 35/43] refactoring centralized metadata propagation enforcement + parent/depends_on graph --- pulumi/core/config.py | 2 +- pulumi/core/resource_helpers.py | 111 ++++++---- pulumi/core/utils.py | 132 +++++++++-- pulumi/modules/cert_manager/deploy.py | 304 ++++++++++++++------------ pulumi/modules/kubevirt/deploy.py | 58 +++-- pulumi/mypy.ini | 7 + 6 files changed, 398 insertions(+), 216 deletions(-) create mode 100644 pulumi/mypy.ini diff --git a/pulumi/core/config.py b/pulumi/core/config.py index e32eb3a..4fffc2e 100644 --- a/pulumi/core/config.py +++ b/pulumi/core/config.py @@ -18,7 +18,7 @@ # Default versions URL template DEFAULT_VERSIONS_URL_TEMPLATE = 'https://raw.githubusercontent.com/ContainerCraft/Kargo/rerefactor/pulumi/' -# Default module enabled settings +# Module enabled defaults: Setting a module to True enables the module by default DEFAULT_ENABLED_CONFIG = { "cert_manager": True, "kubevirt": True, diff --git a/pulumi/core/resource_helpers.py b/pulumi/core/resource_helpers.py index e359436..a7b508f 100644 --- a/pulumi/core/resource_helpers.py +++ b/pulumi/core/resource_helpers.py @@ -5,7 +5,6 @@ from typing import Optional, Dict, Any, List, Callable from .metadata import get_global_labels, get_global_annotations from .utils import set_resource_metadata -from .types import NamespaceConfig def create_namespace( name: str, @@ -15,6 +14,7 @@ def create_namespace( custom_timeouts: Optional[Dict[str, str]] = None, opts: Optional[pulumi.ResourceOptions] = None, k8s_provider: Optional[k8s.Provider] = None, + parent: Optional[pulumi.Resource] = None, depends_on: Optional[List[pulumi.Resource]] = None, ) -> k8s.core.v1.Namespace: """ @@ -43,6 +43,8 @@ def create_namespace( custom_timeouts = {} if depends_on is None: depends_on = [] + if parent is None: + parent = [] global_labels = get_global_labels() global_annotations = get_global_annotations() @@ -64,6 +66,7 @@ def create_namespace( pulumi.ResourceOptions( provider=k8s_provider, depends_on=depends_on, + parent=parent, custom_timeouts=pulumi.CustomTimeouts( create=custom_timeouts.get("create", "5m"), update=custom_timeouts.get("update", "10m"), @@ -86,47 +89,62 @@ def create_custom_resource( k8s_provider: Optional[k8s.Provider] = None, depends_on: Optional[List[pulumi.Resource]] = None, ) -> k8s.apiextensions.CustomResource: - if opts is None: - opts = pulumi.ResourceOptions() - if depends_on is None: - depends_on = [] + """ + Creates a Kubernetes CustomResource with global labels and annotations. - if 'kind' not in args or 'apiVersion' not in args: - raise ValueError("The 'args' dictionary must include 'kind' and 'apiVersion' keys.") + Args: + name (str): The name of the custom resource. + args (Dict[str, Any]): Arguments for creating the custom resource. + opts (Optional[pulumi.ResourceOptions]): Pulumi resource options. + k8s_provider (Optional[k8s.Provider]): Kubernetes provider. + depends_on (Optional[List[pulumi.Resource]]): Resources this custom resource depends on. - global_labels = get_global_labels() - global_annotations = get_global_annotations() + Returns: + k8s.apiextensions.CustomResource: The created CustomResource. + """ + try: + if 'kind' not in args or 'apiVersion' not in args: + raise ValueError("The 'args' dictionary must include 'kind' and 'apiVersion' keys.") + + if opts is None: + opts = pulumi.ResourceOptions() + if depends_on is None: + depends_on = [] + + global_labels = get_global_labels() + global_annotations = get_global_annotations() + + def custom_resource_transform(resource_args: pulumi.ResourceTransformationArgs): + props = resource_args.props + if 'metadata' in props: + set_resource_metadata(props['metadata'], global_labels, global_annotations) + return pulumi.ResourceTransformationResult(props, resource_args.opts) + + opts = pulumi.ResourceOptions.merge( + opts, + pulumi.ResourceOptions( + provider=k8s_provider, + depends_on=depends_on, + transformations=[custom_resource_transform], + ), + ) - def custom_resource_transform(resource_args: pulumi.ResourceTransformationArgs): - props = resource_args.props - if 'metadata' in props: - set_resource_metadata(props['metadata'], global_labels, global_annotations) - return pulumi.ResourceTransformationResult(props, resource_args.opts) + # Ensure metadata and spec are included if specified + metadata = args.get('metadata', {}) + spec = args.get('spec', {}) - opts = pulumi.ResourceOptions.merge( - opts, - pulumi.ResourceOptions( - provider=k8s_provider, - depends_on=depends_on, - transformations=[custom_resource_transform], - ), - ) + return k8s.apiextensions.CustomResource( + resource_name=name, + api_version=args['apiVersion'], + kind=args['kind'], + metadata=metadata, + spec=spec, + opts=opts, + ) - # Extract required fields from args - api_version = args['apiVersion'] - kind = args['kind'] - metadata = args.get('metadata', None) - spec = args.get('spec', None) - - # Corrected constructor call - return k8s.apiextensions.CustomResource( - resource_name=name, - api_version=api_version, - kind=kind, - metadata=metadata, - spec=spec, - opts=opts - ) + except Exception as e: + pulumi.log.error(f"Failed to create custom resource '{name}': {e}") + raise def create_helm_release( name: str, @@ -189,6 +207,19 @@ def create_secret( k8s_provider: Optional[k8s.Provider] = None, depends_on: Optional[List[pulumi.Resource]] = None, ) -> k8s.core.v1.Secret: + """ + Creates a Kubernetes Secret with global labels and annotations. + + Args: + name (str): The name of the secret. + args (Dict[str, Any]): Arguments for creating the secret. + opts (Optional[pulumi.ResourceOptions]): Pulumi resource options. + k8s_provider (Optional[k8s.Provider]): Kubernetes provider. + depends_on (Optional[List[pulumi.Resource]]): Resources this secret depends on. + + Returns: + k8s.core.v1.Secret: The created Secret. + """ if opts is None: opts = pulumi.ResourceOptions() if depends_on is None: @@ -279,13 +310,13 @@ def create_meta_objectmeta( **kwargs, ) -> k8s.meta.v1.ObjectMetaArgs: """ - Creates an ObjectMetaArgs with global labels and annotations. + Creates a Kubernetes ObjectMetaArgs with global labels and annotations. Args: - name (str): The resource name. + name (str): The name of the resource. labels (Optional[Dict[str, str]]): Additional labels to apply. annotations (Optional[Dict[str, str]]): Additional annotations to apply. - namespace (Optional[str]): Namespace for the resource. + namespace (Optional[str]): The namespace of the resource. Returns: k8s.meta.v1.ObjectMetaArgs: The metadata arguments. diff --git a/pulumi/core/utils.py b/pulumi/core/utils.py index b4e4762..0a7eee6 100644 --- a/pulumi/core/utils.py +++ b/pulumi/core/utils.py @@ -8,25 +8,23 @@ """ import re +import os +import tempfile import pulumi import pulumi_kubernetes as k8s -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, List import requests import logging import yaml from packaging.version import parse as parse_version, InvalidVersion, Version + # Set up basic logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') def set_resource_metadata(metadata: Any, global_labels: Dict[str, str], global_annotations: Dict[str, str]): """ Updates resource metadata with global labels and annotations. - - Args: - metadata (Any): Metadata to update. - global_labels (Dict[str, str]): Global labels to apply. - global_annotations (Dict[str, str]): Global annotations to apply. """ if isinstance(metadata, dict): metadata.setdefault('labels', {}).update(global_labels) @@ -42,10 +40,6 @@ def set_resource_metadata(metadata: Any, global_labels: Dict[str, str], global_a def generate_global_transformations(global_labels: Dict[str, str], global_annotations: Dict[str, str]): """ Generates global transformations for resources. - - Args: - global_labels (Dict[str, str]): Global labels to apply. - global_annotations (Dict[str, str]): Global annotations to apply. """ def global_transform(args: pulumi.ResourceTransformationArgs) -> Optional[pulumi.ResourceTransformationResult]: props = args.props @@ -60,20 +54,22 @@ def global_transform(args: pulumi.ResourceTransformationArgs) -> Optional[pulumi pulumi.runtime.register_stack_transformation(global_transform) -def get_latest_helm_chart_version(url: str, chart_name: str) -> str: +def get_latest_helm_chart_version(repo_url: str, chart_name: str) -> str: """ - Fetches the latest stable version of a Helm chart from the given URL. + Fetches the latest stable version of a Helm chart from the given repository URL. Args: - url (str): The URL of the Helm repository index. + repo_url (str): The base URL of the Helm repository. chart_name (str): The name of the Helm chart. Returns: str: The latest stable version of the chart. """ try: - logging.info(f"Fetching URL: {url}") - response = requests.get(url) + index_url = repo_url.rstrip('/') + '/index.yaml' + + logging.info(f"Fetching Helm repository index from URL: {index_url}") + response = requests.get(index_url) response.raise_for_status() index = yaml.safe_load(response.content) @@ -84,14 +80,17 @@ def get_latest_helm_chart_version(url: str, chart_name: str) -> str: logging.info(f"No stable versions found for chart '{chart_name}'.") return "Chart not found" latest_chart = max(stable_versions, key=lambda x: parse_version(x['version'])) - return latest_chart['version'] + return latest_chart['version'].lstrip('v') else: logging.info(f"No chart named '{chart_name}' found in repository.") return "Chart not found" except requests.RequestException as e: - logging.error(f"Error fetching data: {e}") + logging.error(f"Error fetching Helm repository index: {e}") return f"Error fetching data: {e}" + except yaml.YAMLError as e: + logging.error(f"Error parsing Helm repository index YAML: {e}") + return f"Error parsing YAML: {e}" def is_stable_version(version_str: str) -> bool: """ @@ -123,3 +122,102 @@ def extract_repo_name(remote_url: str) -> str: if match: return match.group(1) return remote_url + + +# Set up basic logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +def wait_for_crds(crd_names: List[str], k8s_provider: k8s.Provider, depends_on: List[pulumi.Resource], parent: pulumi.Resource) -> List[pulumi.Resource]: + """ + Waits for the specified CRDs to be present and ensures dependencies. + + Args: + crd_names (List[str]): A list of CRD names. + k8s_provider (k8s.Provider): The Kubernetes provider. + depends_on (List[pulumi.Resource]): A list of dependencies. + parent (pulumi.Resource): The parent resource. + + Returns: + List[pulumi.Resource]: The CRD resources or an empty list during preview. + """ + crds = [] + + for crd_name in crd_names: + try: + crd = k8s.apiextensions.v1.CustomResourceDefinition.get( + resource_name=crd_name, + id=crd_name, + opts=pulumi.ResourceOptions( + provider=k8s_provider, + depends_on=depends_on, + parent=parent, + ), + ) + crds.append(crd) + except Exception: + pulumi.log.info(f"CRD {crd_name} not found, creating dummy CRD.") + dummy_crd = create_dummy_crd(crd_name, k8s_provider, depends_on, parent) + if dummy_crd: + crds.append(dummy_crd) + + return crds + +def create_dummy_crd(crd_name: str, k8s_provider: k8s.Provider, depends_on: List[pulumi.Resource], parent: pulumi.Resource) -> Optional[k8s.yaml.ConfigFile]: + """ + Create a dummy CRD definition to use during preview runs. + + Args: + crd_name (str): The name of the CRD. + k8s_provider (k8s.Provider): The Kubernetes provider. + depends_on (List[pulumi.Resource]): A list of dependencies. + parent (pulumi.Resource): The parent resource. + + Returns: + Optional[k8s.yaml.ConfigFile]: The dummy CRD resource. + """ + parts = crd_name.split('.') + plural = parts[0] + group = '.'.join(parts[1:]) + kind = ''.join(word.title() for word in plural.split('_')) + + dummy_crd_yaml_template = """ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: {metadata_name} +spec: + group: {group} + names: + plural: {plural} + kind: {kind} + scope: Namespaced + versions: + - name: v1 + served: true + storage: true +""" + + dummy_crd_yaml = dummy_crd_yaml_template.format( + metadata_name=f"{plural}.{group}", + group=group, + plural=plural, + kind=kind, + ) + + try: + with tempfile.NamedTemporaryFile(delete=False, mode='w') as temp_file: + temp_file.write(dummy_crd_yaml) + temp_file_path = temp_file.name + + dummy_crd = k8s.yaml.ConfigFile( + "dummy-crd-{}".format(crd_name), + file=temp_file_path, + opts=pulumi.ResourceOptions( + parent=parent, + depends_on=depends_on, + provider=k8s_provider, + ) + ) + return dummy_crd + finally: + os.unlink(temp_file_path) diff --git a/pulumi/modules/cert_manager/deploy.py b/pulumi/modules/cert_manager/deploy.py index 7891fbf..20222d9 100644 --- a/pulumi/modules/cert_manager/deploy.py +++ b/pulumi/modules/cert_manager/deploy.py @@ -1,16 +1,16 @@ # pulumi/modules/cert_manager/deploy.py """ -Deploys the CertManager module using Helm with labels and annotations. +Deploys the cert-manager module with proper dependency management. """ +# Necessary imports +import logging import pulumi import pulumi_kubernetes as k8s -from typing import List, Dict, Any, Tuple, Optional - +from typing import List, Dict, Any, Tuple, Optional, cast from core.types import NamespaceConfig -from core.metadata import get_global_labels, get_global_annotations -from core.utils import get_latest_helm_chart_version +from core.utils import get_latest_helm_chart_version, wait_for_crds from core.resource_helpers import ( create_namespace, create_helm_release, @@ -19,36 +19,39 @@ ) from .types import CertManagerConfig + def deploy_cert_manager_module( config_cert_manager: CertManagerConfig, global_depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, - ) -> Tuple[str, pulumi.Resource, str]: + ) -> Tuple[str, k8s.helm.v3.Release, str]: """ - Deploys the CertManager module and returns the version, release resource, and CA certificate. + Deploys the cert-manager module and returns the version, release resource, and CA certificate. """ - # Deploy Cert Manager + # add k8s_provider to module dependencies + # TODO: Create module specific dependencies object to avoid blocking global resources on k8s_provider or other module specific dependencies + + # Deploy cert-manager cert_manager_version, release, ca_cert_b64 = deploy_cert_manager( config_cert_manager=config_cert_manager, - depends_on=global_depends_on, + depends_on=global_depends_on, # Correctly pass the global dependencies k8s_provider=k8s_provider, ) - # Export the CA certificate - pulumi.export("cert_manager_selfsigned_cert", ca_cert_b64) - # Update global dependencies global_depends_on.append(release) return cert_manager_version, release, ca_cert_b64 + def deploy_cert_manager( config_cert_manager: CertManagerConfig, depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, ) -> Tuple[str, k8s.helm.v3.Release, str]: """ - Deploys Cert Manager using Helm and sets up cluster issuers. + Deploys cert-manager using Helm and sets up cluster issuers, + ensuring that CRDs are available before creating custom resources. """ namespace = config_cert_manager.namespace version = config_cert_manager.version @@ -59,13 +62,19 @@ def deploy_cert_manager( namespace_resource = create_namespace( name=namespace, k8s_provider=k8s_provider, + parent=k8s_provider, depends_on=depends_on, ) # Get Helm Chart Version chart_name = "cert-manager" - chart_url = "https://charts.jetstack.io" - version = get_helm_chart_version(chart_url, chart_name, version) + chart_repo_url = "https://charts.jetstack.io" + + if version == 'latest' or version is None: + version = get_latest_helm_chart_version(chart_repo_url, chart_name) + pulumi.log.info(f"Setting cert-manager chart version to latest: {version}") + else: + pulumi.log.info(f"Using cert-manager chart version: {version}") # Generate Helm values helm_values = generate_helm_values(config_cert_manager) @@ -78,7 +87,7 @@ def deploy_cert_manager( version=version, namespace=namespace, skip_await=False, - repository_opts=k8s.helm.v3.RepositoryOptsArgs(repo=chart_url), + repository_opts=k8s.helm.v3.RepositoryOptsArgs(repo=chart_repo_url), values=helm_values, ), opts=pulumi.ResourceOptions( @@ -89,145 +98,166 @@ def deploy_cert_manager( depends_on=[namespace_resource] + depends_on, ) - # Create Cluster Issuers using the helper function - cluster_issuer_root, cluster_issuer_ca_certificate, cluster_issuer = create_cluster_issuers( - cluster_issuer_name, namespace, k8s_provider, release + # Wait for the CRDs to be registered + crds = wait_for_crds( + crd_names=[ + "certificaterequests.cert-manager.io", + "certificates.cert-manager.io", + "challenges.acme.cert-manager.io", + "clusterissuers.cert-manager.io", + "issuers.cert-manager.io", + "orders.acme.cert-manager.io", + ], + k8s_provider=k8s_provider, + depends_on=[release], + parent=release ) - # Create Secret using the helper function - #ca_secret = k8s.core.v1.Secret.get( - # "cluster-selfsigned-issuer-ca-secret", - # id=f"{namespace}/cluster-selfsigned-issuer-ca", - # opts=pulumi.ResourceOptions( - # parent=cluster_issuer, - # depends_on=[cluster_issuer], - # provider=k8s_provider, - # ) - #) - ca_secret = create_secret( - name="cluster-selfsigned-issuer-ca-secret", - args={ - "metadata": { - "name": "cluster-selfsigned-issuer-ca", - "namespace": namespace, - }, - }, - opts=pulumi.ResourceOptions( - parent=cluster_issuer, - custom_timeouts=pulumi.CustomTimeouts(create="2m", update="2m", delete="2m"), - ), - k8s_provider=k8s_provider, - depends_on=[cluster_issuer], + # Create Cluster Issuers using the helper function + cluster_issuer_root, cluster_issuer_ca_certificate, cluster_issuer, ca_secret = create_cluster_issuers( + cluster_issuer_name, namespace, release, crds, k8s_provider ) # Extract the CA certificate from the secret - ca_data_tls_crt_b64 = ca_secret.data.apply(lambda data: data["tls.crt"]) + if ca_secret: + ca_data_tls_crt_b64 = ca_secret.data.apply(lambda data: data["tls.crt"]) + else: + ca_data_tls_crt_b64 = "" return version, release, ca_data_tls_crt_b64 -def generate_helm_values(config_cert_manager: CertManagerConfig) -> Dict[str, Any]: - """ - Generates Helm values for the CertManager deployment. - """ - return { - 'replicaCount': 1, - 'installCRDs': config_cert_manager.install_crds, - 'resources': { - 'limits': {'cpu': '500m', 'memory': '1024Mi'}, - 'requests': {'cpu': '250m', 'memory': '512Mi'}, - }, - } - -def get_helm_chart_version(chart_url: str, chart_name: str, version: Optional[str]) -> str: - """ - Retrieves the Helm chart version. - """ - if version == 'latest' or version is None: - version = get_latest_helm_chart_version(f"{chart_url}/index.yaml", chart_name).lstrip("v") - pulumi.log.info(f"Setting Helm release version to latest: {chart_name}/{version}") - else: - pulumi.log.info(f"Using Helm release version: {chart_name}/{version}") - return version - def create_cluster_issuers( cluster_issuer_name: str, namespace: str, + release: k8s.helm.v3.Release, + crds: List[pulumi.Resource], k8s_provider: k8s.Provider, - release: pulumi.Resource - ) -> Tuple[k8s.apiextensions.CustomResource, k8s.apiextensions.CustomResource, k8s.apiextensions.CustomResource]: - """ - Creates cluster issuers required for CertManager. +) -> Tuple[ + Optional[k8s.apiextensions.CustomResource], + Optional[k8s.apiextensions.CustomResource], + Optional[k8s.apiextensions.CustomResource], + Optional[k8s.core.v1.Secret], +]: """ - # Create ClusterIssuer root using the helper function - cluster_issuer_root = create_custom_resource( - name="cluster-selfsigned-issuer-root", - args={ - "apiVersion": "cert-manager.io/v1", - "kind": "ClusterIssuer", - "metadata": { - "name": "cluster-selfsigned-issuer-root", - }, - "spec": {"selfSigned": {}}, - }, - opts=pulumi.ResourceOptions( - parent=release, - custom_timeouts=pulumi.CustomTimeouts(create="5m", update="10m", delete="10m"), - ), - k8s_provider=k8s_provider, - depends_on=[release], - ) + Creates cluster issuers required for cert-manager, ensuring dependencies on CRDs. - # Create ClusterIssuer CA Certificate using the helper function - cluster_issuer_ca_certificate = create_custom_resource( - name="cluster-selfsigned-issuer-ca", - args={ - "apiVersion": "cert-manager.io/v1", - "kind": "Certificate", - "metadata": { - "name": "cluster-selfsigned-issuer-ca", - "namespace": namespace, - }, - "spec": { - "commonName": "cluster-selfsigned-issuer-ca", - "duration": "2160h0m0s", - "isCA": True, - "issuerRef": { - "group": "cert-manager.io", - "kind": "ClusterIssuer", + Args: + cluster_issuer_name (str): The name of the cluster issuer. + namespace (str): The Kubernetes namespace. + release (k8s.helm.v3.Release): The Helm release resource. + crds (List[pulumi.Resource]): List of CRDs. + k8s_provider (k8s.Provider): Kubernetes provider. + + Returns: + Tuple containing: + - ClusterIssuer for the self-signed root. + - ClusterIssuer's CA certificate. + - Primary ClusterIssuer. + - The secret resource containing the CA certificate. + """ + try: + # SelfSigned Root Issuer + cluster_issuer_root = create_custom_resource( + name="cluster-selfsigned-issuer-root", + args={ + "apiVersion": "cert-manager.io/v1", + "kind": "ClusterIssuer", + "metadata": { "name": "cluster-selfsigned-issuer-root", }, - "privateKey": {"algorithm": "ECDSA", "size": 256}, - "renewBefore": "360h0m0s", - "secretName": "cluster-selfsigned-issuer-ca", + "spec": {"selfSigned": {}}, }, - }, - opts=pulumi.ResourceOptions( - parent=cluster_issuer_root, - custom_timeouts=pulumi.CustomTimeouts(create="5m", update="10m", delete="10m"), - ), - k8s_provider=k8s_provider, - depends_on=[cluster_issuer_root], - ) + opts=pulumi.ResourceOptions( + parent=release, + provider=k8s_provider, + depends_on=crds, + custom_timeouts=pulumi.CustomTimeouts(create="5m", update="5m", delete="5m"), + ), + ) - # Create ClusterIssuer using the helper function - cluster_issuer = create_custom_resource( - name=cluster_issuer_name, - args={ - "apiVersion": "cert-manager.io/v1", - "kind": "ClusterIssuer", - "metadata": { - "name": cluster_issuer_name, + # CA Certificate Issuer + cluster_issuer_ca_certificate = create_custom_resource( + name="cluster-selfsigned-issuer-ca", + args={ + "apiVersion": "cert-manager.io/v1", + "kind": "Certificate", + "metadata": { + "name": "cluster-selfsigned-issuer-ca", + "namespace": namespace, + }, + "spec": { + "commonName": "cluster-selfsigned-issuer-ca", + "duration": "2160h0m0s", + "isCA": True, + "issuerRef": { + "group": "cert-manager.io", + "kind": "ClusterIssuer", + "name": "cluster-selfsigned-issuer-root", + }, + "privateKey": {"algorithm": "RSA", "size": 2048}, + "renewBefore": "360h0m0s", + "secretName": "cluster-selfsigned-issuer-ca", + }, }, - "spec": { - "ca": {"secretName": "cluster-selfsigned-issuer-ca"}, + opts=pulumi.ResourceOptions( + parent=cluster_issuer_root, + provider=k8s_provider, + depends_on=[cluster_issuer_root], + custom_timeouts=pulumi.CustomTimeouts(create="5m", update="5m", delete="10m"), + ), + ) + + # Main Cluster Issuer + cluster_issuer = create_custom_resource( + name=cluster_issuer_name, + args={ + "apiVersion": "cert-manager.io/v1", + "kind": "ClusterIssuer", + "metadata": { + "name": cluster_issuer_name, + }, + "spec": { + "ca": {"secretName": "cluster-selfsigned-issuer-ca"}, + }, }, - }, - opts=pulumi.ResourceOptions( - parent=cluster_issuer_ca_certificate, - custom_timeouts=pulumi.CustomTimeouts(create="4m", update="4m", delete="4m"), - ), - k8s_provider=k8s_provider, - depends_on=[cluster_issuer_ca_certificate], - ) + opts=pulumi.ResourceOptions( + parent=cluster_issuer_ca_certificate, + provider=k8s_provider, + depends_on=[cluster_issuer_ca_certificate], + custom_timeouts=pulumi.CustomTimeouts(create="5m", update="5m", delete="5m"), + ), + ) + + # Fetch CA Secret if not in dry-run + if not pulumi.runtime.is_dry_run(): + ca_secret = k8s.core.v1.Secret.get( + resource_name="cluster-selfsigned-issuer-ca", + id=f"{namespace}/cluster-selfsigned-issuer-ca", + opts=pulumi.ResourceOptions( + parent=cluster_issuer_ca_certificate, + provider=k8s_provider, + depends_on=[cluster_issuer_ca_certificate], + ) + ) + else: + ca_secret = None + + return cluster_issuer_root, cluster_issuer_ca_certificate, cluster_issuer, ca_secret - return cluster_issuer_root, cluster_issuer_ca_certificate, cluster_issuer + except Exception as e: + pulumi.log.error(f"Error during the creation of cluster issuers: {str(e)}") + return None, None, None, None + + +def generate_helm_values(config_cert_manager: CertManagerConfig) -> Dict[str, Any]: + """ + Generates Helm values for the CertManager deployment. + """ + return { + 'replicaCount': 1, + 'installCRDs': config_cert_manager.install_crds, + 'resources': { + 'limits': {'cpu': '500m', 'memory': '1024Mi'}, + 'requests': {'cpu': '250m', 'memory': '512Mi'}, + }, + } diff --git a/pulumi/modules/kubevirt/deploy.py b/pulumi/modules/kubevirt/deploy.py index e9a19ae..799ad9e 100644 --- a/pulumi/modules/kubevirt/deploy.py +++ b/pulumi/modules/kubevirt/deploy.py @@ -4,6 +4,7 @@ Deploys the KubeVirt module. """ +# Import necessary modules import requests import yaml import tempfile @@ -13,6 +14,7 @@ import pulumi import pulumi_kubernetes as k8s +from core.utils import wait_for_crds from core.metadata import get_global_labels, get_global_annotations from core.resource_helpers import ( create_namespace, @@ -25,29 +27,20 @@ def deploy_kubevirt_module( config_kubevirt: KubeVirtConfig, global_depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, - ) -> Tuple[Optional[str], Optional[pulumi.Resource]]: + ) -> Tuple[Optional[str], k8s.apiextensions.CustomResource]: """ Deploys the KubeVirt module and returns the version and the deployed resource. """ - # Create Namespace using the helper function - namespace_resource = create_namespace( - name=config_kubevirt.namespace, - k8s_provider=k8s_provider, - depends_on=global_depends_on, - ) - - # Combine dependencies - depends_on = global_depends_on + [namespace_resource] - # Deploy KubeVirt kubevirt_version, kubevirt_resource = deploy_kubevirt( config_kubevirt=config_kubevirt, - depends_on=depends_on, + depends_on=global_depends_on, k8s_provider=k8s_provider, ) - # Update global dependencies - global_depends_on.append(kubevirt_resource) + # Update global dependencies if not None + if kubevirt_resource: + global_depends_on.append(kubevirt_resource) return kubevirt_version, kubevirt_resource @@ -55,10 +48,21 @@ def deploy_kubevirt( config_kubevirt: KubeVirtConfig, depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, - ) -> Tuple[str, pulumi.Resource]: + ) -> Tuple[str, Optional[pulumi.Resource]]: """ - Deploys KubeVirt operator and creates the KubeVirt CustomResource. + Deploys KubeVirt operator and creates the KubeVirt CustomResource, + ensuring that the CRD is available before creating the CustomResource. """ + # Create Namespace using the helper function + namespace_resource = create_namespace( + name=config_kubevirt.namespace, + k8s_provider=k8s_provider, + parent=k8s_provider, + depends_on=depends_on, + ) + + # Combine dependencies + depends_on = depends_on + [namespace_resource] namespace = config_kubevirt.namespace version = config_kubevirt.version use_emulation = config_kubevirt.use_emulation @@ -79,12 +83,14 @@ def deploy_kubevirt( yaml.dump_all(transformed_yaml, temp_file) temp_file_path = temp_file.name + operator = None try: # Deploy KubeVirt operator using the helper function operator = create_config_file( name='kubevirt-operator', file=temp_file_path, opts=pulumi.ResourceOptions( + parent=namespace_resource, custom_timeouts=pulumi.CustomTimeouts( create="10m", update="5m", @@ -97,10 +103,18 @@ def deploy_kubevirt( finally: os.unlink(temp_file_path) - if use_emulation: - pulumi.log.info("KVM Emulation enabled for KubeVirt.") + # Wait for the CRDs to be registered + crds = wait_for_crds( + crd_names=[ + "kubevirts.kubevirt.io", + # Add other required CRD names here if needed + ], + k8s_provider=k8s_provider, + depends_on=depends_on, + parent=operator + ) - # Create KubeVirt CustomResource using the helper function + # Create the KubeVirt resource always kubevirt_resource = create_custom_resource( name="kubevirt", args={ @@ -131,18 +145,20 @@ def deploy_kubevirt( }, }, opts=pulumi.ResourceOptions( + provider=k8s_provider, + parent=operator, + depends_on=depends_on + crds, custom_timeouts=pulumi.CustomTimeouts( create="5m", update="5m", delete="5m", ), ), - k8s_provider=k8s_provider, - depends_on=[operator], ) return version, kubevirt_resource + def get_latest_kubevirt_version() -> str: """ Retrieves the latest stable version of KubeVirt. diff --git a/pulumi/mypy.ini b/pulumi/mypy.ini new file mode 100644 index 0000000..d0a1d65 --- /dev/null +++ b/pulumi/mypy.ini @@ -0,0 +1,7 @@ +[mypy] +python_version = 3.9 +disallow_untyped_calls = True +disallow_untyped_defs = True +check_untyped_defs = True +warn_unused_ignores = True + From 000b711be283e01f23ee922806092fb54974952e Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Mon, 23 Sep 2024 10:34:30 -0700 Subject: [PATCH 36/43] refactor / bring multus online in new code architecture --- pulumi/__main__.py | 2 +- pulumi/core/config.py | 1 + pulumi/modules/multus/deploy.py | 171 +++++++++++++++++++------------- pulumi/modules/multus/types.py | 27 +++++ 4 files changed, 133 insertions(+), 68 deletions(-) create mode 100644 pulumi/modules/multus/types.py diff --git a/pulumi/__main__.py b/pulumi/__main__.py index 324f136..35f850c 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -19,7 +19,7 @@ def main(): default_versions = init["default_versions"] global_depends_on = init["global_depends_on"] - modules_to_deploy = ["cert_manager", "kubevirt"] + modules_to_deploy = ["cert_manager", "kubevirt", "multus"] deploy_modules( modules_to_deploy, diff --git a/pulumi/core/config.py b/pulumi/core/config.py index 4fffc2e..5ecf1c9 100644 --- a/pulumi/core/config.py +++ b/pulumi/core/config.py @@ -22,6 +22,7 @@ DEFAULT_ENABLED_CONFIG = { "cert_manager": True, "kubevirt": True, + "multus": True, } diff --git a/pulumi/modules/multus/deploy.py b/pulumi/modules/multus/deploy.py index 014786d..02ae5bb 100644 --- a/pulumi/modules/multus/deploy.py +++ b/pulumi/modules/multus/deploy.py @@ -1,46 +1,56 @@ -import pulumi -import pulumi_kubernetes as k8s +# pulumi/modules/multus/deploy.py -def transform_host_path(args): +""" +Deploys the Multus module. +""" - # Get the object from the arguments - obj = args.props - pulumi.log.debug(f"Object keys: {list(obj.keys())}") +from typing import List, Dict, Any, Tuple, Optional +import pulumi +import pulumi_kubernetes as k8s +from core.metadata import get_global_labels, get_global_annotations +from core.resource_helpers import create_namespace, create_custom_resource +from core.utils import get_latest_helm_chart_version +from .types import MultusConfig - # Transform DaemonSet named 'kube-multus-ds' - if obj.get('kind', '') == 'DaemonSet' and obj.get('metadata', {}).get('name', '') == 'kube-multus-ds': - # Ensure spec is present - if 'spec' in obj: - # Transform paths in containers - containers = obj['spec']['template']['spec'].get('containers', []) - for container in containers: - volume_mounts = container.get('volumeMounts', []) - for vm in volume_mounts: - # Normalize path before checking to handle potential trailing slash - current_path = vm.get('mountPath', '').rstrip('/') - if current_path == '/run/netns': - vm['mountPath'] = '/var/run/netns' +def deploy_multus_module( + config_multus: MultusConfig, + global_depends_on: List[pulumi.Resource], + k8s_provider: k8s.Provider, + ) -> Tuple[str, Optional[pulumi.Resource]]: + """ + Deploys the Multus module and returns the version and the deployed resource. + """ + multus_version, multus_resource = deploy_multus( + config_multus=config_multus, + depends_on=global_depends_on, + k8s_provider=k8s_provider, + ) - # Transform paths in volumes - volumes = obj['spec']['template']['spec'].get('volumes', []) - for vol in volumes: - if 'hostPath' in vol: - # Normalize path before checking to handle potential trailing slash - current_path = vol['hostPath'].get('path', '').rstrip('/') - if current_path == '/run/netns': - vol['hostPath']['path'] = '/var/run/netns' + # Update global dependencies + global_depends_on.append(multus_resource) - # Return the modified object - return pulumi.ResourceTransformationResult(props=obj, opts=args.opts) + return multus_version, multus_resource def deploy_multus( - depends: pulumi.Input[list], - version: str, - bridge_name: str, - k8s_provider: k8s.Provider - ): + config_multus: MultusConfig, + depends_on: List[pulumi.Resource], + k8s_provider: k8s.Provider, + ) -> Tuple[str, Optional[pulumi.Resource]]: + """ + Deploys Multus using YAML manifest and creates a NetworkAttachmentDefinition, + ensuring proper paths for host mounts. + """ + namespace_resource = create_namespace( + name=config_multus.namespace, + labels=config_multus.labels, + annotations=config_multus.annotations, + k8s_provider=k8s_provider, + parent=k8s_provider, + ) - resource_name = f"k8snetworkplumbingwg-multus-daemonset-thick" + # Deploy Multus DaemonSet + resource_name = f"multus-daemonset" + version = config_multus.version or "master" manifest_url = f"https://raw.githubusercontent.com/k8snetworkplumbingwg/multus-cni/{version}/deployments/multus-daemonset-thick.yml" multus = k8s.yaml.ConfigFile( @@ -48,7 +58,7 @@ def deploy_multus( file=manifest_url, opts=pulumi.ResourceOptions( provider=k8s_provider, - depends_on=depends, + parent=namespace_resource, transformations=[transform_host_path], custom_timeouts=pulumi.CustomTimeouts( create="8m", @@ -58,43 +68,70 @@ def deploy_multus( ) ) - # Pulumi Kubernetes resource for NetworkAttachmentDefinition - network_attachment_definition = k8s.apiextensions.CustomResource( - "kargo-net-attach-def", - api_version="k8s.cni.cncf.io/v1", - kind="NetworkAttachmentDefinition", - metadata={ - "name": f"{bridge_name}", - "namespace": "default" - }, - spec={ - "config": pulumi.Output.all(bridge_name, bridge_name).apply(lambda args: f''' - {{ - "cniVersion": "0.3.1", - "name": "{bridge_name}", - "plugins": [ - {{ - "type": "bridge", - "bridge": "{bridge_name}", - "ipam": {{}} - }}, - {{ - "type": "tuning" - }} - ] - }}''') + # Create NetworkAttachmentDefinition + network_attachment_definition = create_custom_resource( + name=config_multus.bridge_name, + args={ + "apiVersion": "k8s.cni.cncf.io/v1", + "kind": "NetworkAttachmentDefinition", + "metadata": { + "name": config_multus.bridge_name, + "namespace": config_multus.namespace, + }, + "spec": { + "config": pulumi.Output.all(config_multus.bridge_name, config_multus.bridge_name).apply(lambda args: f''' + {{ + "cniVersion": "0.3.1", + "name": "{args[0]}", + "plugins": [ + {{ + "type": "bridge", + "bridge": "{args[1]}", + "ipam": {{}} + }}, + {{ + "type": "tuning" + }} + ] + }}''') + }, }, opts=pulumi.ResourceOptions( - depends_on=multus, provider=k8s_provider, + parent=multus, + depends_on=namespace_resource, custom_timeouts=pulumi.CustomTimeouts( create="5m", update="5m", - delete="5m" - ) - )) + delete="5m", + ), + ), + ) - # Export the name of the resource pulumi.export('network_attachment_definition', network_attachment_definition.metadata['name']) - return "master", multus + return config_multus.version, multus + +def transform_host_path(args: pulumi.ResourceTransformationArgs) -> pulumi.ResourceTransformationResult: + """ + Transforms the host paths in the Multus DaemonSet. + """ + obj = args.props + + if obj.get('kind', '') == 'DaemonSet' and obj.get('metadata', {}).get('name', '') == 'kube-multus-ds': + containers = obj['spec']['template']['spec'].get('containers', []) + for container in containers: + volume_mounts = container.get('volumeMounts', []) + for vm in volume_mounts: + current_path = vm.get('mountPath', '').rstrip('/') + if current_path == '/run/netns': + vm['mountPath'] = '/var/run/netns' + + volumes = obj['spec']['template']['spec'].get('volumes', []) + for vol in volumes: + if 'hostPath' in vol: + current_path = vol['hostPath'].get('path', '').rstrip('/') + if current_path == '/run/netns': + vol['hostPath']['path'] = '/var/run/netns' + + return pulumi.ResourceTransformationResult(props=obj, opts=args.opts) diff --git a/pulumi/modules/multus/types.py b/pulumi/modules/multus/types.py new file mode 100644 index 0000000..413d3e1 --- /dev/null +++ b/pulumi/modules/multus/types.py @@ -0,0 +1,27 @@ +# pulumi/modules/multus/types.py + +""" +Defines the data structure for the Multus module configuration. +""" + +from dataclasses import dataclass, field +from typing import Optional, Dict, Any +import pulumi + +@dataclass +class MultusConfig: + version: str = "master" + namespace: str = "multus" + bridge_name: str = "br0" + labels: Dict[str, str] = field(default_factory=dict) + annotations: Dict[str, Any] = field(default_factory=dict) + + @staticmethod + def merge(user_config: Dict[str, Any]) -> 'MultusConfig': + default_config = MultusConfig() + for key, value in user_config.items(): + if hasattr(default_config, key): + setattr(default_config, key, value) + else: + pulumi.log.warn(f"Unknown configuration key '{key}' in multus config.") + return default_config From 01aa4e79720f474b3299e9fe06fe0cd5f125e915 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Mon, 23 Sep 2024 13:51:52 -0700 Subject: [PATCH 37/43] refactor multus + hostpath_provisioner --- pulumi/__main__.py | 2 +- pulumi/core/config.py | 1 + pulumi/core/utils.py | 15 +- pulumi/default_versions.json | 1 + pulumi/modules/hostpath_provisioner/deploy.py | 369 +++++++++++------- pulumi/modules/hostpath_provisioner/types.py | 28 ++ pulumi/stacks/Pulumi.optiplexprime.yaml | 3 + 7 files changed, 262 insertions(+), 157 deletions(-) create mode 100644 pulumi/modules/hostpath_provisioner/types.py diff --git a/pulumi/__main__.py b/pulumi/__main__.py index 35f850c..ea29558 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -19,7 +19,7 @@ def main(): default_versions = init["default_versions"] global_depends_on = init["global_depends_on"] - modules_to_deploy = ["cert_manager", "kubevirt", "multus"] + modules_to_deploy = ["cert_manager", "kubevirt", "multus", "hostpath_provisioner"] deploy_modules( modules_to_deploy, diff --git a/pulumi/core/config.py b/pulumi/core/config.py index 5ecf1c9..01081d8 100644 --- a/pulumi/core/config.py +++ b/pulumi/core/config.py @@ -23,6 +23,7 @@ "cert_manager": True, "kubevirt": True, "multus": True, + "hostpath_provisioner": False, } diff --git a/pulumi/core/utils.py b/pulumi/core/utils.py index 0a7eee6..7c3cb76 100644 --- a/pulumi/core/utils.py +++ b/pulumi/core/utils.py @@ -140,12 +140,14 @@ def wait_for_crds(crd_names: List[str], k8s_provider: k8s.Provider, depends_on: Returns: List[pulumi.Resource]: The CRD resources or an empty list during preview. """ - crds = [] + + # Instantiate crds list to store retrieved CRD resources with enforced type safety for k8s.apiextensions.v1.CustomResourceDefinition + crds: List[pulumi.Resource] = [] for crd_name in crd_names: try: crd = k8s.apiextensions.v1.CustomResourceDefinition.get( - resource_name=crd_name, + resource_name=f"crd-{crd_name}", id=crd_name, opts=pulumi.ResourceOptions( provider=k8s_provider, @@ -155,10 +157,11 @@ def wait_for_crds(crd_names: List[str], k8s_provider: k8s.Provider, depends_on: ) crds.append(crd) except Exception: - pulumi.log.info(f"CRD {crd_name} not found, creating dummy CRD.") - dummy_crd = create_dummy_crd(crd_name, k8s_provider, depends_on, parent) - if dummy_crd: - crds.append(dummy_crd) + if pulumi.runtime.is_dry_run(): + pulumi.log.info(f"CRD {crd_name} not found, creating dummy CRD.") + dummy_crd = create_dummy_crd(crd_name, k8s_provider, depends_on, parent) + if dummy_crd: + crds.append(dummy_crd) return crds diff --git a/pulumi/default_versions.json b/pulumi/default_versions.json index 317c436..ce2bb7d 100644 --- a/pulumi/default_versions.json +++ b/pulumi/default_versions.json @@ -1,4 +1,5 @@ { "cert_manager": "1.15.3", + "hostpath_provisioner": "0.20.0", "kubevirt": "1.3.1" } diff --git a/pulumi/modules/hostpath_provisioner/deploy.py b/pulumi/modules/hostpath_provisioner/deploy.py index 3b27c0d..0b50fed 100644 --- a/pulumi/modules/hostpath_provisioner/deploy.py +++ b/pulumi/modules/hostpath_provisioner/deploy.py @@ -1,181 +1,250 @@ +# pulumi/modules/hostpath_provisioner/deploy.py + import requests import pulumi -from pulumi import ResourceOptions import pulumi_kubernetes as k8s -from pulumi_kubernetes.apiextensions import CustomResource -from pulumi_kubernetes.meta.v1 import ObjectMetaArgs -from pulumi_kubernetes.storage.v1 import StorageClass -from src.lib.namespace import create_namespace - -def deploy( - depends: pulumi.Output[list], - version: str, - ns_name: str, - hostpath: str, - default: bool, - k8s_provider: k8s.Provider, - ): - - # If version is not supplied, fetch the latest stable version - if version is None: - tag_url = 'https://github.com/kubevirt/hostpath-provisioner-operator/releases/latest' - tag = requests.get(tag_url, allow_redirects=False).headers.get('location') - version = tag.split('/')[-1] if tag else '0.17.0' - version = version.lstrip('v') - pulumi.log.info(f"Setting helm release version to latest stable: hostpath-provisioner/{version}") - else: - pulumi.log.info(f"Using helm release version: hostpath-provisioner/{version}") - - # Create namespace - ns_retain = True - ns_protect = False - ns_annotations = {} - ns_labels = { - "kubevirt.io": "", - "kubernetes.io/metadata.name": ns_name, - "pod-security.kubernetes.io/enforce": "privileged" - } - namespace = create_namespace( - depends, - ns_name, - ns_retain, - ns_protect, - k8s_provider, - ns_labels, - ns_annotations +from typing import List, Dict, Any, Tuple, Optional +from core.resource_helpers import create_namespace, create_custom_resource, create_config_file +from core.utils import wait_for_crds +from .types import HostPathProvisionerConfig + + +def deploy_hostpath_provisioner_module( + config_hostpath_provisioner: HostPathProvisionerConfig, + global_depends_on: List[pulumi.Resource], + k8s_provider: k8s.Provider, +) -> Tuple[str, k8s.yaml.ConfigFile]: + """ + Deploys the HostPath Provisioner module and returns the version and the deployed resource. + + Args: + config_hostpath_provisioner (HostPathProvisionerConfig): Configuration for the HostPath Provisioner. + global_depends_on (List[pulumi.Resource]): Global dependencies for all modules. + k8s_provider (k8s.Provider): The Kubernetes provider. + + Returns: + Tuple[str, k8s.yaml.ConfigFile]: The version deployed and the configured webhook resource. + """ + hostpath_version, hostpath_resource = deploy_hostpath_provisioner( + config_hostpath_provisioner=config_hostpath_provisioner, + depends_on=global_depends_on, + k8s_provider=k8s_provider, ) - # Function to add namespace to resource if not set - def add_namespace(args): - obj = args.props - - if 'metadata' in obj: - if isinstance(obj['metadata'], ObjectMetaArgs): - if not obj['metadata'].namespace: - obj['metadata'].namespace = ns_name - else: - if obj['metadata'] is None: - obj['metadata'] = {} - if not obj['metadata'].get('namespace'): - obj['metadata']['namespace'] = ns_name - else: - obj['metadata'] = {'namespace': ns_name} - - # Return the modified object wrapped in ResourceTransformationResult - return pulumi.ResourceTransformationResult(props=obj, opts=args.opts) + # Update global dependencies + global_depends_on.append(hostpath_resource) + + return hostpath_version, hostpath_resource + + +def deploy_hostpath_provisioner( + config_hostpath_provisioner: HostPathProvisionerConfig, + depends_on: List[pulumi.Resource], + k8s_provider: k8s.Provider, +) -> Tuple[str, k8s.yaml.ConfigFile]: + """ + Deploys the HostPath Provisioner and related resources. + + Args: + config_hostpath_provisioner (HostPathProvisionerConfig): Configuration for the HostPath Provisioner. + depends_on (List[pulumi.Resource]): Dependencies for this deployment. + k8s_provider (k8s.Provider): The Kubernetes provider. + + Returns: + Tuple[str, k8s.yaml.ConfigFile]: The version deployed and the configured webhook resource. + """ + name = "hostpath-provisioner" + namespace = config_hostpath_provisioner.namespace + + namespace_resource = create_namespace( + name=namespace, + labels=config_hostpath_provisioner.labels, + annotations=config_hostpath_provisioner.annotations, + k8s_provider=k8s_provider, + parent=k8s_provider, + depends_on=depends_on, + ) + + # Determine version to use + version = get_latest_version() if config_hostpath_provisioner.version == "latest" else config_hostpath_provisioner.version + + def enforce_namespace(resource_args: pulumi.ResourceTransformationArgs) -> pulumi.ResourceTransformationResult: + """ + Transformation function to enforce namespace on all resources. + """ + props = resource_args.props + namespace_conflict = False + + # Handle ObjectMetaArgs case + if isinstance(props.get('metadata'), k8s.meta.v1.ObjectMetaArgs): + meta = props['metadata'] + if meta.namespace and meta.namespace != namespace: + namespace_conflict = True + updated_meta = k8s.meta.v1.ObjectMetaArgs( + name=meta.name, + namespace=namespace, + labels=meta.labels, + annotations=meta.annotations + ) + props['metadata'] = updated_meta + + # Handle dictionary style metadata + elif isinstance(props.get('metadata'), dict): + meta = props['metadata'] + if 'namespace' in meta and meta['namespace'] != namespace: + namespace_conflict = True + meta['namespace'] = namespace + + if namespace_conflict: + raise ValueError("Resource namespace conflict detected.") + + return pulumi.ResourceTransformationResult(props, resource_args.opts) # Deploy the webhook - url_webhook = f'https://github.com/kubevirt/hostpath-provisioner-operator/releases/download/v{version}/webhook.yaml' - webhook = k8s.yaml.ConfigFile( - "hostpath-provisioner-webhook", - file=url_webhook, - opts=ResourceOptions( - parent=namespace, - depends_on=depends, + webhook_url = f'https://github.com/kubevirt/hostpath-provisioner-operator/releases/download/v{version}/webhook.yaml' + webhook = create_config_file( + name="hostpath-provisioner-webhook", + file=webhook_url, + opts=pulumi.ResourceOptions( provider=k8s_provider, - transformations=[add_namespace], - custom_timeouts=pulumi.CustomTimeouts( - create="1m", - update="1m", - delete="1m" - ) - ) + parent=namespace_resource, + depends_on=depends_on, + custom_timeouts=pulumi.CustomTimeouts(create="10m", update="5m", delete="5m"), + transformations=[enforce_namespace] + ), ) - # Deploy the operator with a namespace transformation - url_operator = f'https://github.com/kubevirt/hostpath-provisioner-operator/releases/download/v{version}/operator.yaml' - operator = k8s.yaml.ConfigFile( - "hostpath-provisioner-operator", - file=url_operator, - opts=ResourceOptions( - parent=webhook, - depends_on=depends, + # Deploy the operator + operator_url = f'https://github.com/kubevirt/hostpath-provisioner-operator/releases/download/v{version}/operator.yaml' + operator = create_config_file( + name="hostpath-provisioner-operator", + file=operator_url, + opts=pulumi.ResourceOptions( provider=k8s_provider, - transformations=[add_namespace], - custom_timeouts=pulumi.CustomTimeouts( - create="8m", - update="8m", - delete="2m" - ) - ) + parent=webhook, + depends_on=depends_on, + custom_timeouts=pulumi.CustomTimeouts(create="10m", update="5m", delete="5m"), + transformations=[enforce_namespace] + ), ) - # Ensure the CRDs are created before the HostPathProvisioner resource - # TODO: solve for the case where child resources are created before parent exists - crd = k8s.apiextensions.v1.CustomResourceDefinition.get( - "hostpathprovisioners", - id="hostpathprovisioners.hostpathprovisioner.kubevirt.io", - opts=ResourceOptions( - parent=operator, - depends_on=depends, - provider=k8s_provider, - custom_timeouts=pulumi.CustomTimeouts( - create="9m", - update="9m", - delete="2m" - ) - ) + # Ensure CRDs are created before HostPathProvisioner resource + crds = wait_for_crds( + crd_names=["hostpathprovisioners.hostpathprovisioner.kubevirt.io"], + k8s_provider=k8s_provider, + depends_on=depends_on, + parent=operator, ) - # Create a HostPathProvisioner resource - hostpath_provisioner = CustomResource( - "hostpath-provisioner-hpp", - api_version="hostpathprovisioner.kubevirt.io/v1beta1", - kind="HostPathProvisioner", - metadata={ - "name": "hostpath-provisioner-class-ssd", - "namespace": ns_name - }, - spec={ - "imagePullPolicy": "IfNotPresent", - "storagePools": [{ - "name": "ssd", - "path": hostpath - }], - "workload": { - "nodeSelector": { - "kubernetes.io/os": "linux" + # Create HostPathProvisioner resource + hostpath_provisioner = create_custom_resource( + name="hostpath-provisioner", + args={ + "apiVersion": "hostpathprovisioner.kubevirt.io/v1beta1", + "kind": "HostPathProvisioner", + "metadata": { + "name": "hostpath-provisioner", + "namespace": namespace, + }, + "spec": { + "imagePullPolicy": "IfNotPresent", + "storagePools": [{ + "name": "ssd", + "path": config_hostpath_provisioner.hostpath, + }], + "workload": { + "nodeSelector": { + "kubernetes.io/os": "linux" + } } - } + }, }, opts=pulumi.ResourceOptions( parent=operator, - depends_on=crd, + depends_on=depends_on + crds, provider=k8s_provider, - ignore_changes=["status"], - custom_timeouts=pulumi.CustomTimeouts( - create="8m", - update="8m", - delete="2m" - ) - ) + custom_timeouts=pulumi.CustomTimeouts(create="10m", update="5m", delete="5m") + ), ) # Define the StorageClass - storage_class = StorageClass( - "hostpath-storage-class-ssd", - metadata=ObjectMetaArgs( - name="ssd", + storage_class = create_storage_class( + name="hostpath-storage-class-ssd", + provisioner="kubevirt.io.hostpath-provisioner", + namespace=namespace, + default=config_hostpath_provisioner.default, + storage_pool="ssd", + parent=hostpath_provisioner, + k8s_provider=k8s_provider, + ) + + return version, webhook + + +def get_latest_version() -> str: + """ + Retrieves the latest stable version of HostPath Provisioner. + + Returns: + str: The latest version number. + """ + try: + tag_url = 'https://github.com/kubevirt/hostpath-provisioner-operator/releases/latest' + response = requests.get(tag_url, allow_redirects=False) + final_url = response.headers.get('location') + version = final_url.split('/')[-1].lstrip('v') if response.status_code == 302 else "0.17.0" + return version + except Exception as e: + pulumi.log.error(f"Error fetching the latest version: {e}") + return "0.17.0" + + +def create_storage_class( + name: str, + provisioner: str, + namespace: str, + default: bool, + storage_pool: str, + parent: pulumi.Resource, + k8s_provider: k8s.Provider, +) -> k8s.storage.v1.StorageClass: + """ + Creates a StorageClass resource specific to HostPath Provisioner. + + Args: + name (str): The name of the storage class. + provisioner (str): The provisioner to use. + namespace (str): The namespace to deploy into. + default (bool): Whether this storage class should be the default. + storage_pool (str): The name of the storage pool. + parent (pulumi.Resource): The parent resource. + k8s_provider (k8s.Provider): The Kubernetes provider. + + Returns: + k8s.storage.v1.StorageClass: The created StorageClass resource. + """ + if default: + is_default_storage_class = "true" + else: + is_default_storage_class = "false" + + return k8s.storage.v1.StorageClass( + resource_name=name, + metadata=k8s.meta.v1.ObjectMetaArgs( + name=name, annotations={ - "storageclass.kubernetes.io/is-default-class": "true" if default else "false" - } + "storageclass.kubernetes.io/is-default-class": is_default_storage_class, + }, ), + provisioner=provisioner, reclaim_policy="Delete", - provisioner="kubevirt.io.hostpath-provisioner", volume_binding_mode="WaitForFirstConsumer", parameters={ - "storagePool": "ssd", + "storagePool": storage_pool, }, - opts=ResourceOptions( - parent=hostpath_provisioner, - #depends_on=hostpath_provisioner, + opts=pulumi.ResourceOptions( + parent=parent, provider=k8s_provider, - custom_timeouts=pulumi.CustomTimeouts( - create="8m", - update="8m", - delete="2m" - ) - ) + custom_timeouts=pulumi.CustomTimeouts(create="5m", update="5m", delete="5m") + ), ) - - return version, webhook # operator diff --git a/pulumi/modules/hostpath_provisioner/types.py b/pulumi/modules/hostpath_provisioner/types.py new file mode 100644 index 0000000..4761027 --- /dev/null +++ b/pulumi/modules/hostpath_provisioner/types.py @@ -0,0 +1,28 @@ +# pulumi/modules/hostpath_provisioner/types.py + +""" +Defines the data structure for the HostPath Provisioner module configuration. +""" + +from dataclasses import dataclass, field +from typing import Optional, Dict, Any +import pulumi + +@dataclass +class HostPathProvisionerConfig: + version: Optional[str] = "latest" + namespace: str = "hostpath-provisioner" + hostpath: str = "/var/lib/hostpath-provisioner" + default: bool = True + labels: Dict[str, str] = field(default_factory=dict) + annotations: Dict[str, Any] = field(default_factory=dict) + + @staticmethod + def merge(user_config: Dict[str, Any]) -> 'HostPathProvisionerConfig': + default_config = HostPathProvisionerConfig() + for key, value in user_config.items(): + if hasattr(default_config, key): + setattr(default_config, key, value) + else: + pulumi.log.warn(f"Unknown configuration key '{key}' in hostpath_provisioner config.") + return default_config diff --git a/pulumi/stacks/Pulumi.optiplexprime.yaml b/pulumi/stacks/Pulumi.optiplexprime.yaml index a594a8e..0ecac62 100644 --- a/pulumi/stacks/Pulumi.optiplexprime.yaml +++ b/pulumi/stacks/Pulumi.optiplexprime.yaml @@ -42,6 +42,9 @@ config: - scip-east-1 - scip-lunar-2 version: v2.87.11 + kargo:hostpath_provisioner: + enabled: true + version: latest kargo:kubernetes: context: usrbinkat-optiplexprime # Kubernetes context for the stack kargo:kubevirt: From b55272c82d502793a442533bf5f1a8cc4adfa2b8 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Mon, 23 Sep 2024 14:19:25 -0700 Subject: [PATCH 38/43] return version --- pulumi/modules/multus/deploy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pulumi/modules/multus/deploy.py b/pulumi/modules/multus/deploy.py index 02ae5bb..e5509fd 100644 --- a/pulumi/modules/multus/deploy.py +++ b/pulumi/modules/multus/deploy.py @@ -110,7 +110,7 @@ def deploy_multus( pulumi.export('network_attachment_definition', network_attachment_definition.metadata['name']) - return config_multus.version, multus + return version, multus def transform_host_path(args: pulumi.ResourceTransformationArgs) -> pulumi.ResourceTransformationResult: """ From afa98f3cc50932a93325137cf5c6f9e89c9d83b1 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Mon, 23 Sep 2024 19:18:59 -0700 Subject: [PATCH 39/43] successful cdi deploy --- pulumi/__main__.py | 2 +- pulumi/core/config.py | 3 +- .../containerized_data_importer/deploy.py | 176 ++++++++++++------ .../containerized_data_importer/types.py | 25 +++ 4 files changed, 144 insertions(+), 62 deletions(-) create mode 100644 pulumi/modules/containerized_data_importer/types.py diff --git a/pulumi/__main__.py b/pulumi/__main__.py index ea29558..83f2750 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -19,7 +19,7 @@ def main(): default_versions = init["default_versions"] global_depends_on = init["global_depends_on"] - modules_to_deploy = ["cert_manager", "kubevirt", "multus", "hostpath_provisioner"] + modules_to_deploy = ["cert_manager", "kubevirt", "multus", "hostpath_provisioner", "containerized_data_importer"] deploy_modules( modules_to_deploy, diff --git a/pulumi/core/config.py b/pulumi/core/config.py index 01081d8..ecfb410 100644 --- a/pulumi/core/config.py +++ b/pulumi/core/config.py @@ -23,7 +23,8 @@ "cert_manager": True, "kubevirt": True, "multus": True, - "hostpath_provisioner": False, + "hostpath_provisioner": True, + "containerized_data_importer": True, } diff --git a/pulumi/modules/containerized_data_importer/deploy.py b/pulumi/modules/containerized_data_importer/deploy.py index 3b7d587..bbe77e0 100644 --- a/pulumi/modules/containerized_data_importer/deploy.py +++ b/pulumi/modules/containerized_data_importer/deploy.py @@ -1,74 +1,130 @@ +# pulumi/modules/containerized_data_importer/deploy.py + +""" +Enhanced deployment script for the Containerized Data Importer (CDI) module. +""" + import requests import pulumi import pulumi_kubernetes as k8s -from pulumi_kubernetes.apiextensions.CustomResource import CustomResource -from pulumi_kubernetes.meta.v1 import ObjectMetaArgs +from typing import List, Dict, Any, Tuple, Optional +from core.resource_helpers import create_namespace, create_custom_resource +from .types import CdiConfig -def deploy_cdi( - depends, - version: str, - k8s_provider: k8s.Provider - ): +def deploy_containerized_data_importer_module( + config_cdi: CdiConfig, + global_depends_on: List[pulumi.Resource], + k8s_provider: k8s.Provider, + ) -> Tuple[Optional[str], Optional[pulumi.Resource]]: + """ + Deploys the Containerized Data Importer (CDI) module and returns the version and the deployed resource. - # Fetch the latest stable version of CDI - if version is None: - tag_url = 'https://github.com/kubevirt/containerized-data-importer/releases/latest' - tag = requests.get(tag_url, allow_redirects=False).headers.get('location') - version = tag.split('/')[-1] - version = version.lstrip('v') - pulumi.log.info(f"Setting helm release version to latest stable: cdi/{version}") - else: - # Log the version override - pulumi.log.info(f"Using helm release version: cdi/{version}") + Args: + config_cdi (CdiConfig): Configuration for the CDI module. + global_depends_on (List[pulumi.Resource]): Global dependencies for all modules. + k8s_provider (k8s.Provider): The Kubernetes provider. + + Returns: + Tuple[Optional[str], Optional[pulumi.Resource]]: The version deployed and the deployed resource. + """ + try: + pulumi.log.info("Starting deployment of CDI module") - # Deploy the CDI operator - cdi_operator_url = f'https://github.com/kubevirt/containerized-data-importer/releases/download/v{version}/cdi-operator.yaml' - operator = k8s.yaml.ConfigFile( - 'cdi-operator', - file=cdi_operator_url, - opts=pulumi.ResourceOptions( - provider=k8s_provider + version = config_cdi.version if config_cdi.version and config_cdi.version != "latest" else fetch_latest_version() + pulumi.log.info(f"Using CDI version: {version}") + + # Create namespace + pulumi.log.info(f"Creating namespace: {config_cdi.namespace}") + namespace_resource = create_namespace( + name=config_cdi.namespace, + labels=config_cdi.labels, + annotations=config_cdi.annotations, + k8s_provider=k8s_provider, + parent=k8s_provider, + depends_on=global_depends_on, ) - ) - # Deploy the default CDI custom resource - cdi_resource = CustomResource( - "cdi", - api_version="cdi.kubevirt.io/v1beta1", - kind="CDI", - metadata={ - "name": "cdi", - "namespace": "cdi", - }, - spec={ - "config": { - "featureGates": [ - "HonorWaitForFirstConsumer", - ], - }, - "imagePullPolicy": "IfNotPresent", - "infra": { - "nodeSelector": { - "kubernetes.io/os": "linux", + # Deploy CDI operator + operator_url = f"https://github.com/kubevirt/containerized-data-importer/releases/download/v{version}/cdi-operator.yaml" + pulumi.log.info(f"Deploying CDI operator from URL: {operator_url}") + + operator_resource = k8s.yaml.ConfigFile( + "cdi-operator", + file=operator_url, + opts=pulumi.ResourceOptions( + provider=k8s_provider, + parent=namespace_resource, + ) + ) + + # Ensure dependencies on operator and namespace + depends_on = global_depends_on + [operator_resource] + + # Create CDI custom resource + pulumi.log.info("Creating CDI custom resource") + cdi_resource = create_custom_resource( + name="cdi", + args={ + "apiVersion": "cdi.kubevirt.io/v1beta1", + "kind": "CDI", + "metadata": { + "name": "cdi", + "namespace": config_cdi.namespace, }, - "tolerations": [ - { - "key": "CriticalAddonsOnly", - "operator": "Exists", + "spec": { + "config": { + "featureGates": [ + "HonorWaitForFirstConsumer", + ], + }, + "imagePullPolicy": "IfNotPresent", + "infra": { + "nodeSelector": { + "kubernetes.io/os": "linux", + }, + "tolerations": [ + { + "key": "CriticalAddonsOnly", + "operator": "Exists", + }, + ], + }, + "workload": { + "nodeSelector": { + "kubernetes.io/os": "linux", + }, }, - ], - }, - "workload": { - "nodeSelector": { - "kubernetes.io/os": "linux", }, }, - }, - opts=pulumi.ResourceOptions( - provider=k8s_provider, - parent=operator, - depends_on=depends + opts=pulumi.ResourceOptions( + parent=operator_resource, + depends_on=namespace_resource, + provider=k8s_provider, + custom_timeouts=pulumi.CustomTimeouts(create="1m", update="1m", delete="1m"), + ) ) - ) - return version, operator + pulumi.log.info("CDI module deployment complete") + return version, operator_resource + + except Exception as e: + pulumi.log.error(f"Deployment of CDI module failed: {str(e)}") + raise + +def fetch_latest_version() -> str: + """ + Fetches the latest stable version of CDI from GitHub releases. + + Returns: + str: Latest stable version string. + """ + try: + latest_release_url = 'https://github.com/kubevirt/containerized-data-importer/releases/latest' + tag = requests.get(latest_release_url, allow_redirects=False).headers.get('location') + version = tag.split('/')[-1] + version = version.lstrip('v') + pulumi.log.info(f"Fetched latest CDI version: {version}") + return version + except Exception as e: + pulumi.log.error(f"Error fetching the latest version: {e}") + return "latest" diff --git a/pulumi/modules/containerized_data_importer/types.py b/pulumi/modules/containerized_data_importer/types.py new file mode 100644 index 0000000..1643f37 --- /dev/null +++ b/pulumi/modules/containerized_data_importer/types.py @@ -0,0 +1,25 @@ +# pulumi/modules/containerized_data_importer/types.py + +""" +Defines the data structure for the Containerized Data Importer (CDI) module configuration. +""" + +from dataclasses import dataclass, field +from typing import Optional, Dict, Any + +@dataclass +class CdiConfig: + version: Optional[str] = "latest" + namespace: str = "cdi" + labels: Dict[str, str] = field(default_factory=lambda: {"app": "cdi"}) + annotations: Dict[str, Any] = field(default_factory=dict) + + @staticmethod + def merge(user_config: Dict[str, Any]) -> 'CdiConfig': + default_config = CdiConfig() + for key, value in user_config.items(): + if hasattr(default_config, key): + setattr(default_config, key, value) + else: + pulumi.log.warn(f"Unknown configuration key '{key}' in cdi config.") + return default_config From 40dbd8d84383616c46991c7d814791766dea89a7 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Mon, 23 Sep 2024 20:02:28 -0700 Subject: [PATCH 40/43] add prometheus --- pulumi/__main__.py | 33 +-- pulumi/core/config.py | 1 + pulumi/core/deployment.py | 22 ++ pulumi/modules/prometheus/deploy.py | 329 ++++++++++++++-------------- pulumi/modules/prometheus/types.py | 26 +++ 5 files changed, 221 insertions(+), 190 deletions(-) create mode 100644 pulumi/modules/prometheus/types.py diff --git a/pulumi/__main__.py b/pulumi/__main__.py index 83f2750..2a05442 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -5,7 +5,7 @@ import pulumi from pulumi_kubernetes import Provider -from core.deployment import initialize_pulumi, deploy_module +from core.deployment import initialize_pulumi, deploy_modules from core.config import export_results def main(): @@ -19,7 +19,14 @@ def main(): default_versions = init["default_versions"] global_depends_on = init["global_depends_on"] - modules_to_deploy = ["cert_manager", "kubevirt", "multus", "hostpath_provisioner", "containerized_data_importer"] + modules_to_deploy = [ + "cert_manager", + "kubevirt", + "multus", + "hostpath_provisioner", + "containerized_data_importer", + "prometheus" + ] deploy_modules( modules_to_deploy, @@ -38,27 +45,5 @@ def main(): pulumi.log.error(f"Deployment failed: {str(e)}") raise -def deploy_modules( - modules: List[str], - config: pulumi.Config, - default_versions: Dict[str, Any], - global_depends_on: List[pulumi.Resource], - k8s_provider: Provider, - versions: Dict[str, str], - configurations: Dict[str, Dict[str, Any]], - ) -> None: - - for module_name in modules: - pulumi.log.info(f"Deploying module: {module_name}") - deploy_module( - module_name=module_name, - config=config, - default_versions=default_versions, - global_depends_on=global_depends_on, - k8s_provider=k8s_provider, - versions=versions, - configurations=configurations, - ) - if __name__ == "__main__": main() diff --git a/pulumi/core/config.py b/pulumi/core/config.py index ecfb410..6c99769 100644 --- a/pulumi/core/config.py +++ b/pulumi/core/config.py @@ -25,6 +25,7 @@ "multus": True, "hostpath_provisioner": True, "containerized_data_importer": True, + "prometheus": True, } diff --git a/pulumi/core/deployment.py b/pulumi/core/deployment.py index b33ba62..25b15a7 100644 --- a/pulumi/core/deployment.py +++ b/pulumi/core/deployment.py @@ -209,3 +209,25 @@ def discover_deploy_function(module_name: str) -> Callable: if not deploy_function: raise ValueError(f"No deploy function named '{function_name}' found in modules.{module_name}.deploy") return deploy_function + +def deploy_modules( + modules: List[str], + config: pulumi.Config, + default_versions: Dict[str, Any], + global_depends_on: List[pulumi.Resource], + k8s_provider: Provider, + versions: Dict[str, str], + configurations: Dict[str, Dict[str, Any]], + ) -> None: + + for module_name in modules: + pulumi.log.info(f"Deploying module: {module_name}") + deploy_module( + module_name=module_name, + config=config, + default_versions=default_versions, + global_depends_on=global_depends_on, + k8s_provider=k8s_provider, + versions=versions, + configurations=configurations, + ) diff --git a/pulumi/modules/prometheus/deploy.py b/pulumi/modules/prometheus/deploy.py index c2becac..53e2ebf 100644 --- a/pulumi/modules/prometheus/deploy.py +++ b/pulumi/modules/prometheus/deploy.py @@ -1,193 +1,190 @@ +# pulumi/modules/prometheus/deploy.py + +""" +Deploys the Prometheus module following the shared design patterns. +""" + import pulumi import pulumi_kubernetes as k8s -from src.lib.namespace import create_namespace -from src.lib.helm_chart_versions import get_latest_helm_chart_version +from typing import List, Dict, Any, Tuple, Optional +from core.resource_helpers import create_namespace, create_helm_release +from core.utils import get_latest_helm_chart_version +from .types import PrometheusConfig -def deploy_prometheus( - depends: pulumi.Input[list], - ns_name: str, - version: str, +def deploy_prometheus_module( + config_prometheus: PrometheusConfig, + global_depends_on: List[pulumi.Resource], k8s_provider: k8s.Provider, - openunison_enabled: bool - ): - - # Create the monitoring Namespace - ns_retain = True - ns_protect = False - ns_annotations = {} - ns_labels = { - "kubevirt.io": "", - "kubernetes.io/metadata.name": ns_name, - "openshift.io/cluster-monitoring": "true", - "pod-security.kubernetes.io/enforce": "privileged" - } - namespace = create_namespace( - depends, - ns_name, - ns_retain, - ns_protect, - k8s_provider, - ns_labels, - ns_annotations + ) -> Tuple[str, Optional[pulumi.Resource]]: + """ + Deploys the Prometheus module and returns the version and the deployed resource. + + Args: + config_prometheus (PrometheusConfig): Configuration for the Prometheus module. + global_depends_on (List[pulumi.Resource]): Global dependencies for all modules. + k8s_provider (k8s.Provider): The Kubernetes provider. + + Returns: + Tuple[str, Optional[pulumi.Resource]]: The version deployed and the deployed resource. + """ + prometheus_version, prometheus_resource = deploy_prometheus( + config_prometheus=config_prometheus, + depends_on=global_depends_on, + k8s_provider=k8s_provider, ) - prometheus_helm_values = {} - if openunison_enabled: - prometheus_helm_values = { - "grafana": { - "grafana.ini": { - "users": { - "allow_sign_up": False, - "auto_assign_org": True, - "auto_assign_org_role": "Admin" - }, - "auth.proxy": { - "enabled": True, - "header_name": "X-WEBAUTH-USER", - "auto_sign_up": True, - "headers": "Groups:X-WEBAUTH-GROUPS" - } - } - } - } + # Update global dependencies + if prometheus_resource: + global_depends_on.append(prometheus_resource) + + return prometheus_version, prometheus_resource + +def deploy_prometheus( + config_prometheus: PrometheusConfig, + depends_on: List[pulumi.Resource], + k8s_provider: k8s.Provider, + ) -> Tuple[str, Optional[pulumi.Resource]]: + """ + Deploys Prometheus using Helm and sets up necessary services. + """ + namespace = config_prometheus.namespace + version = config_prometheus.version + openunison_enabled = config_prometheus.openunison_enabled + + # Create Namespace using the helper function + namespace_resource = create_namespace( + name=namespace, + labels=config_prometheus.labels, + annotations=config_prometheus.annotations, + k8s_provider=k8s_provider, + parent=k8s_provider, + depends_on=depends_on, + ) - # Fetch the latest version from the helm chart index chart_name = "kube-prometheus-stack" - chart_index_path = "index.yaml" chart_url = "https://prometheus-community.github.io/helm-charts" - if version is None: - chart_index_url = f"{chart_url}/{chart_index_path}" - version = get_latest_helm_chart_version(chart_index_url, chart_name) - pulumi.log.info(f"Setting helm release version to latest stable: {chart_name}/{version}") + if version is None or version == "latest": + version = get_latest_helm_chart_version(chart_url, chart_name) + pulumi.log.info(f"Setting Prometheus helm chart version to latest release: {version}") else: - pulumi.log.info(f"Using helm release version: {chart_name}/{version}") + pulumi.log.info(f"Using Prometheus helm release version: {version}") - release = k8s.helm.v3.Release( - 'helm-release-prometheus', - k8s.helm.v3.ReleaseArgs( + # Helm values customization based on OpenUnison integration + prometheus_helm_values = { + "grafana": { + "grafana.ini": { + "users": { + "allow_sign_up": False, + "auto_assign_org": True, + "auto_assign_org_role": "Admin" + }, + "auth.proxy": { + "enabled": True, + "header_name": "X-WEBAUTH-USER", + "auto_sign_up": True, + "headers": "Groups:X-WEBAUTH-GROUPS" + } + } + } + } if openunison_enabled else {} + + # Create the Helm Release + release = create_helm_release( + name=chart_name, + args=k8s.helm.v3.ReleaseArgs( chart=chart_name, version=version, - values=prometheus_helm_values, - namespace='monitoring', + namespace=namespace, skip_await=False, - repository_opts= k8s.helm.v3.RepositoryOptsArgs( - repo=chart_url - ), + repository_opts=k8s.helm.v3.RepositoryOptsArgs(repo=chart_url), + values=prometheus_helm_values, ), opts=pulumi.ResourceOptions( - provider = k8s_provider, - parent=namespace, - depends_on=depends, - custom_timeouts=pulumi.CustomTimeouts( - create="30m", - update="30m", - delete="30m" - ) - ) - ) - depends.append(release) - - # create services with predictable names - service_grafana = k8s.core.v1.Service( - "service-grafana", - metadata=k8s.meta.v1.ObjectMetaArgs( - name="grafana", - namespace=ns_name + parent=namespace_resource, ), - spec={ - "type":"ClusterIP", - "ports":[ - { - "name":"http-web", - "port": 80, - "protocol": "TCP", - "targetPort": 3000 + k8s_provider=k8s_provider, + depends_on=depends_on, + ) - } - ], - "selector":{ - "app.kubernetes.io/name":"grafana" - } - }, - opts=pulumi.ResourceOptions( - parent=namespace, - depends_on=depends, - retain_on_delete=False, - provider = k8s_provider, - custom_timeouts=pulumi.CustomTimeouts( - create="3m", - update="3m", - delete="3m" - ) - ) + # Create Services with predictable names + services = create_prometheus_services( + config_prometheus=config_prometheus, + k8s_provider=k8s_provider, + namespace=namespace, + parent=namespace_resource, + depends_on=[release], ) - service_alertmanager = k8s.core.v1.Service( - "service-alertmanager", - metadata=k8s.meta.v1.ObjectMetaArgs( - name="alertmanager", - namespace="monitoring" - ), - spec={ - "type":"ClusterIP", - "ports":[ - { - "name":"http-web", - "port": 9093, - "protocol": "TCP", - "targetPort": 9093 + return version, release - } - ], - "selector":{ - "app.kubernetes.io/name":"alertmanager" - } - }, - opts=pulumi.ResourceOptions( - parent=namespace, - depends_on=depends, - provider = k8s_provider, - retain_on_delete=False, - custom_timeouts=pulumi.CustomTimeouts( - create="3m", - update="3m", - delete="3m" - ) - ) - ) +def create_prometheus_services( + config_prometheus: PrometheusConfig, + k8s_provider: k8s.Provider, + namespace: str, + parent: pulumi.Resource, + depends_on: List[pulumi.Resource], + ) -> List[k8s.core.v1.Service]: + """ + Creates Prometheus, Grafana, and Alertmanager services. - service_prometheus = k8s.core.v1.Service( - "service-prometheus", - metadata=k8s.meta.v1.ObjectMetaArgs( - name="prometheus", - namespace="monitoring" - ), - spec={ - "type":"ClusterIP", - "ports":[ - { - "name":"http-web", - "port": 9090, - "protocol": "TCP", - "targetPort": 9090 + Args: + config_prometheus (PrometheusConfig): Configuration for the Prometheus module. + k8s_provider (k8s.Provider): The Kubernetes provider. + namespace (str): The namespace to deploy services in. + parent (pulumi.Resource): The parent resource. + depends_on (List[pulumi.Resource]): Dependencies for the services. - } - ], - "selector":{ - "app.kubernetes.io/name":"prometheus" - } + Returns: + List[k8s.core.v1.Service]: The created services. + """ + services = [] + + service_definitions = [ + { + "name": "grafana", + "port": 80, + "targetPort": 3000, + "selector": "app.kubernetes.io/name", }, - opts=pulumi.ResourceOptions( - parent=namespace, - depends_on=depends, - provider = k8s_provider, - retain_on_delete=False, - custom_timeouts=pulumi.CustomTimeouts( - create="3m", - update="3m", - delete="3m" + { + "name": "alertmanager", + "port": 9093, + "targetPort": 9093, + "selector": "app.kubernetes.io/name", + }, + { + "name": "prometheus", + "port": 9090, + "targetPort": 9090, + "selector": "app.kubernetes.io/name", + } + ] + + for service_def in service_definitions: + service = k8s.core.v1.Service( + f"service-{service_def['name']}", + metadata=k8s.meta.v1.ObjectMetaArgs( + name=service_def["name"], + namespace=namespace, + labels=config_prometheus.labels, + annotations=config_prometheus.annotations, + ), + spec=k8s.core.v1.ServiceSpecArgs( + type="ClusterIP", + ports=[k8s.core.v1.ServicePortArgs( + name="http-web", + port=service_def["port"], + protocol="TCP", + target_port=service_def["targetPort"], + )], + selector={service_def["selector"]: service_def["name"]}, + ), + opts=pulumi.ResourceOptions( + provider=k8s_provider, + parent=parent, + depends_on=depends_on, ) ) - ) + services.append(service) - return version, release + return services diff --git a/pulumi/modules/prometheus/types.py b/pulumi/modules/prometheus/types.py new file mode 100644 index 0000000..72a3956 --- /dev/null +++ b/pulumi/modules/prometheus/types.py @@ -0,0 +1,26 @@ +# pulumi/modules/prometheus/types.py + +""" +Defines the data structure for the Prometheus module configuration. +""" + +from dataclasses import dataclass, field +from typing import Optional, Dict, Any + +@dataclass +class PrometheusConfig: + version: Optional[str] = None + namespace: str = "monitoring" + openunison_enabled: bool = False + labels: Dict[str, str] = field(default_factory=dict) + annotations: Dict[str, Any] = field(default_factory=dict) + + @staticmethod + def merge(user_config: Dict[str, Any]) -> 'PrometheusConfig': + default_config = PrometheusConfig() + for key, value in user_config.items(): + if hasattr(default_config, key): + setattr(default_config, key, value) + else: + pulumi.log.warn(f"Unknown configuration key '{key}' in prometheus config.") + return default_config From 1d21cf35e917a685c3526af09860a9c533e4ecba Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Tue, 24 Sep 2024 09:05:37 -0700 Subject: [PATCH 41/43] add prometheus --- pulumi/default_versions.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pulumi/default_versions.json b/pulumi/default_versions.json index ce2bb7d..0a26831 100644 --- a/pulumi/default_versions.json +++ b/pulumi/default_versions.json @@ -1,5 +1,8 @@ { "cert_manager": "1.15.3", + "containerized_data_importer": "1.60.3", "hostpath_provisioner": "0.20.0", - "kubevirt": "1.3.1" + "kubevirt": "1.3.1", + "multus": "master", + "prometheus": "62.7.0" } From ee160e5e71252815282ba4c97b657bc50836f0db Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Tue, 24 Sep 2024 14:00:32 -0700 Subject: [PATCH 42/43] add developer comments, questions, planning, and TODOs --- pulumi/__main__.py | 29 +++++-- pulumi/core/config.py | 20 ++++- pulumi/core/deployment.py | 81 +++++++++++++++---- pulumi/core/metadata.py | 37 ++++++--- pulumi/core/resource_helpers.py | 14 ++++ pulumi/core/types.py | 3 +- pulumi/core/utils.py | 11 ++- pulumi/modules/cert_manager/deploy.py | 26 ++++-- pulumi/modules/cert_manager/types.py | 2 +- .../containerized_data_importer/deploy.py | 28 ++++--- pulumi/modules/hostpath_provisioner/deploy.py | 28 ++++++- pulumi/modules/kubevirt/deploy.py | 44 ++++++++-- pulumi/modules/kubevirt/types.py | 20 ++++- pulumi/modules/multus/deploy.py | 11 ++- pulumi/modules/prometheus/deploy.py | 46 +++++++---- 15 files changed, 315 insertions(+), 85 deletions(-) diff --git a/pulumi/__main__.py b/pulumi/__main__.py index 2a05442..20f1746 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -1,24 +1,31 @@ # pulumi/__main__.py -from typing import List, Dict, Any - -import pulumi -from pulumi_kubernetes import Provider - -from core.deployment import initialize_pulumi, deploy_modules +from pulumi import log from core.config import export_results +from core.deployment import initialize_pulumi, deploy_modules def main(): try: + # Initialize Pulumi init = initialize_pulumi() + # Extract the components from the initialization dictionary. + # TODO: + # - Refactor this to use dataclasses. + # - Relocate the dataclasses to a shared location. + # - Relocate module specific initialization logic into the pulumi/core/deployment.py module. config = init["config"] k8s_provider = init["k8s_provider"] versions = init["versions"] configurations = init["configurations"] default_versions = init["default_versions"] global_depends_on = init["global_depends_on"] + compliance_config = init.get("compliance_config", {}) + # Map of modules to deploy with default boolean value. + # TODO: + # - Refactor this as a map of module names and default enabled booleans. + # - Map of module:enabled pairs will depricate the DEFAULT_ENABLED_CONFIG list in config.py. modules_to_deploy = [ "cert_manager", "kubevirt", @@ -28,6 +35,9 @@ def main(): "prometheus" ] + # Deploy modules + # TODO: + # - Simplify deploy_modules signature after relocating the module:enabled map and init dictionary location. deploy_modules( modules_to_deploy, config, @@ -38,12 +48,15 @@ def main(): configurations, ) - compliance_config = init.get("compliance_config", {}) + # Export stack outputs. export_results(versions, configurations, compliance_config) except Exception as e: - pulumi.log.error(f"Deployment failed: {str(e)}") + log.error(f"Deployment failed: {str(e)}") raise +# Entry point for the Pulumi program. +# TODO: +# - Re-evaluate structure and best location for export_results function call. if __name__ == "__main__": main() diff --git a/pulumi/core/config.py b/pulumi/core/config.py index 6c99769..26bb601 100644 --- a/pulumi/core/config.py +++ b/pulumi/core/config.py @@ -19,6 +19,7 @@ DEFAULT_VERSIONS_URL_TEMPLATE = 'https://raw.githubusercontent.com/ContainerCraft/Kargo/rerefactor/pulumi/' # Module enabled defaults: Setting a module to True enables the module by default +# TODO: relocate to pulumi/__main__.py for better visibility DEFAULT_ENABLED_CONFIG = { "cert_manager": True, "kubevirt": True, @@ -28,7 +29,8 @@ "prometheus": True, } - +# Centralized Pulumi Config Retrieval +# Fetches the configuration for a module and determines if the module is enabled. def get_module_config( module_name: str, config: pulumi.Config, @@ -54,6 +56,17 @@ def get_module_config( return module_config, module_enabled +# Retrieve Module Component Version Control Configuration from external local or remote json file. +# Supports centralized component version control configuration via local or remote json objects including: +# - `latest` for dynamic retrieval and utilization of the latest version. +# - `v0.00.0` hard coded version in Pulumi Config for overrid-ing version control. +# - `{lts,stable,edge,latest}` for subscribing to remote version control channels. +# TODO: +# - Refactor function to use more obvious prescedence ordering and configuration loading. +# - Refactor function for easier module maintainer adoption. +# - Adopt remote stable channel as first default version source after first github releases are published. +# - Adopt local stable channel as exception fallback for centralized version configuration. +# - Adopt `latest` as default version for modules without remote or local or pulumi config version configuration. def load_default_versions(config: pulumi.Config, force_refresh=False) -> dict: """ Loads the default versions for modules based on the specified configuration settings. @@ -87,6 +100,10 @@ def load_default_versions(config: pulumi.Config, force_refresh=False) -> dict: versions_stack_name = config.get_bool('versions.stack_name') or False default_versions = {} + # Function to try loading default versions from file + # TODO: + # - Adopt standardized local file storage location `./pulumi/versions/{filename}.json` + # - Adopt file naming convention `default.json` `lts.json` `stable.json` `edge.json` `latest.json` def load_versions_from_file(file_path: str) -> dict: try: with open(file_path, 'r') as f: @@ -140,6 +157,7 @@ def load_versions_from_url(url: str) -> dict: return default_versions +# Function to export global deployment stack metadata def export_results( versions: Dict[str, str], configurations: Dict[str, Dict[str, Any]], diff --git a/pulumi/core/deployment.py b/pulumi/core/deployment.py index 25b15a7..2dc8812 100644 --- a/pulumi/core/deployment.py +++ b/pulumi/core/deployment.py @@ -8,12 +8,14 @@ """ import os +import inspect +import importlib +from typing import Dict, Any, List, Type, Callable + import pulumi import pulumi_kubernetes as k8s +from pulumi import log from pulumi_kubernetes import Provider -from typing import Dict, Any, List, Type, Callable -import inspect -import importlib from .config import get_module_config, load_default_versions from .metadata import ( @@ -28,6 +30,8 @@ from .utils import generate_global_transformations from .types import ComplianceConfig +# Function to perform all prerequisite configuration retrieval and variable initialization. +# TODO: Evaluate all code to further improve centralized configuration and variable init. def initialize_pulumi() -> Dict[str, Any]: """ Initializes Pulumi configuration, Kubernetes provider, and global resources. @@ -40,24 +44,41 @@ def initialize_pulumi() -> Dict[str, Any]: project_name = pulumi.get_project() try: + # Load global default versions and initialize variables from configuration. + # TODO: + # - Refactor this to utilize a dataclass for type safety and better organization. default_versions = load_default_versions(config) versions: Dict[str, str] = {} + + # Initialize empty global configuration and dependency list variables. configurations: Dict[str, Dict[str, Any]] = {} global_depends_on: List[pulumi.Resource] = [] + # Initialize the Kubernetes provider. kubernetes_config = config.get_object("kubernetes") or {} - kubeconfig = kubernetes_config.get("kubeconfig") or os.getenv('KUBECONFIG') kubernetes_context = kubernetes_config.get("context") - pulumi.log.info(f"Kubeconfig: {kubeconfig}") - pulumi.log.info(f"Kubernetes context: {kubernetes_context}") + # TODO: refactor to export kubeconfig as a secret for use by k8s_provider. + kubeconfig = kubernetes_config.get("kubeconfig") or os.getenv('KUBECONFIG') + # Initialize the Kubernetes provider. + # TODO: + # - Refactor to utilized kubeconfig from pulumi secret export object to reduce risk of loss or exposure. k8s_provider = Provider( "k8sProvider", kubeconfig=kubeconfig, context=kubernetes_context, ) + # Export k8s_provider as secret for use in other pulumi stacks. + k8s_provider_secret = pulumi.Output.secret(k8s_provider) + pulumi.export("k8s_provider", k8s_provider_secret) + + log.info(f"Kubeconfig: {kubeconfig}") + log.info(f"Kubernetes context: {kubernetes_context}") + + # Collect and store git information in the global configuration. + # Global Compliance Metadata for global transformations to propagate as resource tag / label / annotations git_info = collect_git_info() configurations["source_repository"] = { "remote": git_info["remote"], @@ -65,6 +86,9 @@ def initialize_pulumi() -> Dict[str, Any]: "commit": git_info["commit"] } + # Retrieve compliance metadata from pulumi configuration and generate global tags, labels, and annotations. + # TODO: + # - Evaluate for provider-specific structure and transformation to support Kubernetes, AWS, Azure, GCP, OpenStack, etc. compliance_config_dict = config.get_object('compliance') or {} compliance_config = ComplianceConfig.merge(compliance_config_dict) compliance_labels = generate_compliance_labels(compliance_config) @@ -79,6 +103,8 @@ def initialize_pulumi() -> Dict[str, Any]: set_global_annotations(global_annotations) generate_global_transformations(global_labels, global_annotations) + # Return the initialized components. + # TODO: Refactor this as a dataclas, namedtuple, or similar. return { "config": config, "stack_name": stack_name, @@ -94,9 +120,10 @@ def initialize_pulumi() -> Dict[str, Any]: "global_annotations": global_annotations, } except Exception as e: - pulumi.log.error(f"Initialization error: {str(e)}") + log.error(f"Initialization error: {str(e)}") raise +# Reusable function to deploy any IaC module with dynamic configuration and versioning. def deploy_module( module_name: str, config: pulumi.Config, @@ -122,6 +149,9 @@ def deploy_module( TypeError: If any arguments have incorrect types. ValueError: If any module-specific errors occur. """ + + # Validate input types. + # TODO: Evalute for better approach to type checking. if not isinstance(module_name, str): raise TypeError("module_name must be a string") if not isinstance(config, pulumi.Config): @@ -137,45 +167,65 @@ def deploy_module( if not isinstance(configurations, dict): raise TypeError("configurations must be a dictionary") + # Retrieve module configuration and enabled status. module_config_dict, module_enabled = get_module_config(module_name, config, default_versions) + # Deploy the module if enabled. if module_enabled: + + # Discover module configuration class and deploy function. ModuleConfigClass = discover_config_class(module_name) deploy_func = discover_deploy_function(module_name) + # Merge module's default and user supplied configuration values. config_obj = ModuleConfigClass.merge(module_config_dict) + # Validate the configuration object. deploy_func_args = inspect.signature(deploy_func).parameters.keys() config_arg_name = list(deploy_func_args)[0] + # Deploy the module. try: + + # Execute the module's deploy function. result = deploy_func( **{config_arg_name: config_obj}, global_depends_on=global_depends_on, k8s_provider=k8s_provider, ) + # Parse the result for version and release information. + # Accommodate for optional exported value. + # TODO: + # - Refactor this to be more robust and less restrictive. + # - Refactor to inherit value names from name string of returned key arguments. if isinstance(result, tuple) and len(result) == 3: - version, release, exported_value = result + version, release, module_aux_meta = result elif isinstance(result, tuple) and len(result) == 2: version, release = result - exported_value = None + module_aux_meta = None else: raise ValueError(f"Unexpected return value structure from {module_name} deploy function") + # Append the module's version and configuration to the global dictionaries. versions[module_name] = version configurations[module_name] = {"enabled": module_enabled} - if exported_value: - pulumi.export(f"{module_name}_exported_value", exported_value) + # adopt value export names from the returned key name argument string if possible, else solve for naming positional args. + if module_aux_meta: + pulumi.export(f"meta_{module_name}", module_aux_meta) + # Append the module to the global dependencies. + # TODO: Reevaluate optimization for global_depends_on and best location for appending each module's primary resource.' global_depends_on.append(release) except Exception as e: - pulumi.log.error(f"Deployment failed for module {module_name}: {str(e)}") + log.error(f"Deployment failed for module {module_name}: {str(e)}") raise + + # Report to log if module is not enabled. else: - pulumi.log.info(f"Module {module_name} is not enabled.") + log.info(f"Module {module_name} is not enabled.") def discover_config_class(module_name: str) -> Type: """ @@ -210,6 +260,9 @@ def discover_deploy_function(module_name: str) -> Callable: raise ValueError(f"No deploy function named '{function_name}' found in modules.{module_name}.deploy") return deploy_function +# TODO: +# - Refactor to use a more robust approach to type checking. +# - Reevaluate deploy_module signature and optimize on existing argument encapsulation and passing for simpler module developer experience. def deploy_modules( modules: List[str], config: pulumi.Config, @@ -221,7 +274,7 @@ def deploy_modules( ) -> None: for module_name in modules: - pulumi.log.info(f"Deploying module: {module_name}") + log.info(f"Deploying module: {module_name}") deploy_module( module_name=module_name, config=config, diff --git a/pulumi/core/metadata.py b/pulumi/core/metadata.py index 4ef2de4..adc137a 100644 --- a/pulumi/core/metadata.py +++ b/pulumi/core/metadata.py @@ -1,8 +1,8 @@ # pulumi/core/metadata.py -# Description: -# TODO: enhance with support for propagation of labels annotations on AWS resources -# TODO: enhance by adding additional data to global tags / labels / annotation metadata -# - git release tag +# TODO: +# - enhance with support for propagation of labels annotations on AWS resources +# - enhance by adding additional data to global tag / label / annotation metadata +# - support adding git release semver to global tag / label / annotation metadata """ Metadata Management Module @@ -11,14 +11,20 @@ It includes functions to generate compliance and Git-related metadata. """ -import subprocess -import pulumi +import re +import json import threading +import subprocess from typing import Dict, Any -import json + +import pulumi +from pulumi import log + from .types import ComplianceConfig -import re +# Singleton class to manage global metadata +# Globals are correctly chosen to enforce consistency across all modules and resources +# This class is thread-safe and used to store global labels and annotations class MetadataSingleton: _instance = None __lock = threading.Lock() @@ -67,6 +73,9 @@ def get_global_annotations() -> Dict[str, str]: """ return MetadataSingleton()._data["_global_annotations"] +# Function to collect Git repository information +# TODO: re-implement this function to use the GitPython library or other more pythonic approach +# TODO: add support for fetching and returning the latest git release semver def collect_git_info() -> Dict[str, str]: """ Collects Git repository information. @@ -80,7 +89,7 @@ def collect_git_info() -> Dict[str, str]: commit = subprocess.check_output(['git', 'rev-parse', 'HEAD'], stderr=subprocess.STDOUT).strip().decode('utf-8') return {'remote': remote, 'branch': branch, 'commit': commit} except subprocess.CalledProcessError as e: - pulumi.log.error(f"Error fetching git information: {e}") + log.error(f"Error fetching git information: {e}") return {'remote': 'N/A', 'branch': 'N/A', 'commit': 'N/A'} def generate_git_labels(git_info: Dict[str, str]) -> Dict[str, str]: @@ -143,19 +152,25 @@ def generate_compliance_annotations(compliance_config: ComplianceConfig) -> Dict Returns: Dict[str, str]: A dictionary of compliance annotations. """ + + # TODO: enhance if logic to improve efficiency, DRY, readability and maintainability annotations = {} if compliance_config.fisma.level: annotations['compliance.fisma.level'] = compliance_config.fisma.level if compliance_config.fisma.ato: - annotations['compliance.fisma.ato'] = json.dumps(compliance_config.fisma.ato) # Store as JSON + annotations['compliance.fisma.ato'] = json.dumps(compliance_config.fisma.ato) if compliance_config.nist.controls: - annotations['compliance.nist.controls'] = json.dumps(compliance_config.nist.controls) # Store as JSON array + annotations['compliance.nist.controls'] = json.dumps(compliance_config.nist.controls) if compliance_config.nist.auxiliary: annotations['compliance.nist.auxiliary'] = json.dumps(compliance_config.nist.auxiliary) if compliance_config.nist.exceptions: annotations['compliance.nist.exceptions'] = json.dumps(compliance_config.nist.exceptions) return annotations +# Function to sanitize a label value to comply with Kubernetes `label` naming conventions +# https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set +# TODO: +# - retool this feature as a more efficient implementation in `collect_git_info()` and related functions. def sanitize_label_value(value: str) -> str: """ Sanitizes a label value to comply with Kubernetes naming conventions. diff --git a/pulumi/core/resource_helpers.py b/pulumi/core/resource_helpers.py index a7b508f..2310392 100644 --- a/pulumi/core/resource_helpers.py +++ b/pulumi/core/resource_helpers.py @@ -33,6 +33,10 @@ def create_namespace( Returns: k8s.core.v1.Namespace: The created Namespace resource. """ + + # If the optional arguments are not provided, set them to default values. + # TODO: + # - refactor/simplify for better readability and maintainability if opts is None: opts = pulumi.ResourceOptions() if labels is None: @@ -61,6 +65,11 @@ def create_namespace( if finalizers: spec["finalizers"] = finalizers + # Set Global Pulumi Resource Options + # TODO: + # - Enhance core/config.py with a centralized default pulumi `opts` configuration + # - Support merging with custom opts + # - Adopt across project resources to improve consistency and DRYness opts = pulumi.ResourceOptions.merge( opts, pulumi.ResourceOptions( @@ -302,6 +311,11 @@ def config_file_transform(resource_args: pulumi.ResourceTransformationArgs): return k8s.yaml.ConfigFile(name, file, opts=opts) +# ------------------------------------------------------------------------------ +# Metadata +# ------------------------------------------------------------------------------ +# TODO: +# - Evaluate full codebase for wider utilization of create_meta_objectmeta() def create_meta_objectmeta( name: str, labels: Optional[Dict[str, str]] = None, diff --git a/pulumi/core/types.py b/pulumi/core/types.py index 2377db5..7b9b44c 100644 --- a/pulumi/core/types.py +++ b/pulumi/core/types.py @@ -7,9 +7,10 @@ within the Kargo PaaS platform. """ +from typing import Optional, List, Dict, Any + import pulumi from dataclasses import dataclass, field -from typing import Optional, List, Dict, Any @dataclass class NamespaceConfig: diff --git a/pulumi/core/utils.py b/pulumi/core/utils.py index 7c3cb76..28c9a9a 100644 --- a/pulumi/core/utils.py +++ b/pulumi/core/utils.py @@ -22,6 +22,7 @@ # Set up basic logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +# Function to update global resource tags, labels, and annotations from compliance config spec def set_resource_metadata(metadata: Any, global_labels: Dict[str, str], global_annotations: Dict[str, str]): """ Updates resource metadata with global labels and annotations. @@ -37,6 +38,7 @@ def set_resource_metadata(metadata: Any, global_labels: Dict[str, str], global_a metadata.annotations = {} metadata.annotations.update(global_annotations) +# Function to apply global resource tags, labels, and annotations to all yaml objects def generate_global_transformations(global_labels: Dict[str, str], global_annotations: Dict[str, str]): """ Generates global transformations for resources. @@ -54,6 +56,7 @@ def global_transform(args: pulumi.ResourceTransformationArgs) -> Optional[pulumi pulumi.runtime.register_stack_transformation(global_transform) +# Function to fetch the latest stable version of a Helm chart from a helm chart index.yaml url def get_latest_helm_chart_version(repo_url: str, chart_name: str) -> str: """ Fetches the latest stable version of a Helm chart from the given repository URL. @@ -92,6 +95,7 @@ def get_latest_helm_chart_version(repo_url: str, chart_name: str) -> str: logging.error(f"Error parsing Helm repository index YAML: {e}") return f"Error parsing YAML: {e}" +# Sanity check Helm chart versions for stable releases def is_stable_version(version_str: str) -> bool: """ Determines if a version string represents a stable version. @@ -108,6 +112,7 @@ def is_stable_version(version_str: str) -> bool: except InvalidVersion: return False +# Function to extract the repository name from a Git remote URL def extract_repo_name(remote_url: str) -> str: """ Extracts the repository name from a Git remote URL. @@ -124,9 +129,7 @@ def extract_repo_name(remote_url: str) -> str: return remote_url -# Set up basic logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - +# Function to wait for a list of CRDs to be present def wait_for_crds(crd_names: List[str], k8s_provider: k8s.Provider, depends_on: List[pulumi.Resource], parent: pulumi.Resource) -> List[pulumi.Resource]: """ Waits for the specified CRDs to be present and ensures dependencies. @@ -165,6 +168,8 @@ def wait_for_crds(crd_names: List[str], k8s_provider: k8s.Provider, depends_on: return crds +# HACK: Create a dummy CRD definition to use during pulumi dry_run / preview runs if CRDs are not found. +# TODO: Solve this in a more elegant way. def create_dummy_crd(crd_name: str, k8s_provider: k8s.Provider, depends_on: List[pulumi.Resource], parent: pulumi.Resource) -> Optional[k8s.yaml.ConfigFile]: """ Create a dummy CRD definition to use during preview runs. diff --git a/pulumi/modules/cert_manager/deploy.py b/pulumi/modules/cert_manager/deploy.py index 20222d9..ae59efd 100644 --- a/pulumi/modules/cert_manager/deploy.py +++ b/pulumi/modules/cert_manager/deploy.py @@ -4,11 +4,12 @@ Deploys the cert-manager module with proper dependency management. """ -# Necessary imports -import logging +from typing import List, Dict, Any, Tuple, Optional, cast + import pulumi import pulumi_kubernetes as k8s -from typing import List, Dict, Any, Tuple, Optional, cast +from pulumi import log + from core.types import NamespaceConfig from core.utils import get_latest_helm_chart_version, wait_for_crds from core.resource_helpers import ( @@ -17,6 +18,7 @@ create_custom_resource, create_secret, ) + from .types import CertManagerConfig @@ -28,7 +30,6 @@ def deploy_cert_manager_module( """ Deploys the cert-manager module and returns the version, release resource, and CA certificate. """ - # add k8s_provider to module dependencies # TODO: Create module specific dependencies object to avoid blocking global resources on k8s_provider or other module specific dependencies # Deploy cert-manager @@ -65,16 +66,19 @@ def deploy_cert_manager( parent=k8s_provider, depends_on=depends_on, ) + # TODO: consider adding k8s_provider to module_depends_on dependencies # Get Helm Chart Version + # TODO: set the chart name and repo URL as variables in the CertManagerConfig class to allow for user configuration chart_name = "cert-manager" chart_repo_url = "https://charts.jetstack.io" + # TODO: re-implement into the get_module_config function and adopt across all modules to reduce code duplication if version == 'latest' or version is None: version = get_latest_helm_chart_version(chart_repo_url, chart_name) - pulumi.log.info(f"Setting cert-manager chart version to latest: {version}") + log.info(f"Setting cert-manager chart version to latest: {version}") else: - pulumi.log.info(f"Using cert-manager chart version: {version}") + log.info(f"Using cert-manager chart version: {version}") # Generate Helm values helm_values = generate_helm_values(config_cert_manager) @@ -99,6 +103,8 @@ def deploy_cert_manager( ) # Wait for the CRDs to be registered + # TODO: re-evaluate effectiveness of approach to wait for CRDs and complete the wait_for_crds implementation until it's effective. + # The current implementation fails to wait for the CRDs to be registered before continuing with child and dependent resources. crds = wait_for_crds( crd_names=[ "certificaterequests.cert-manager.io", @@ -114,11 +120,17 @@ def deploy_cert_manager( ) # Create Cluster Issuers using the helper function + # TODO: + # - make self-signed-issuer configurable enabled/disabled from boolean set in cert_manager/types.py CertManagerConfig class, default to enabled. cluster_issuer_root, cluster_issuer_ca_certificate, cluster_issuer, ca_secret = create_cluster_issuers( cluster_issuer_name, namespace, release, crds, k8s_provider ) # Extract the CA certificate from the secret + # TODO: + # - re-evaluate relevance. IIRC this is used to return unwrapped secret values as b64 encoded strings for OpenUnison configuration. + # - consider maintaining the secret object as a return value for future use in other modules without exporting the secret values. + # - if user need requires the CA for client secret trust then consider documenting the use case and user instructions for utilization. if ca_secret: ca_data_tls_crt_b64 = ca_secret.data.apply(lambda data: data["tls.crt"]) else: @@ -245,7 +257,7 @@ def create_cluster_issuers( return cluster_issuer_root, cluster_issuer_ca_certificate, cluster_issuer, ca_secret except Exception as e: - pulumi.log.error(f"Error during the creation of cluster issuers: {str(e)}") + log.error(f"Error during the creation of cluster issuers: {str(e)}") return None, None, None, None diff --git a/pulumi/modules/cert_manager/types.py b/pulumi/modules/cert_manager/types.py index 7928626..c6ebd38 100644 --- a/pulumi/modules/cert_manager/types.py +++ b/pulumi/modules/cert_manager/types.py @@ -15,7 +15,7 @@ @dataclass class CertManagerConfig: - version: Optional[str] = None + version: Optional[str] = "latest" namespace: str = "cert-manager" cluster_issuer: str = "cluster-selfsigned-issuer" install_crds: bool = True diff --git a/pulumi/modules/containerized_data_importer/deploy.py b/pulumi/modules/containerized_data_importer/deploy.py index bbe77e0..e861f42 100644 --- a/pulumi/modules/containerized_data_importer/deploy.py +++ b/pulumi/modules/containerized_data_importer/deploy.py @@ -5,10 +5,14 @@ """ import requests +from typing import List, Dict, Any, Tuple, Optional + import pulumi import pulumi_kubernetes as k8s -from typing import List, Dict, Any, Tuple, Optional +from pulumi import log + from core.resource_helpers import create_namespace, create_custom_resource + from .types import CdiConfig def deploy_containerized_data_importer_module( @@ -28,13 +32,13 @@ def deploy_containerized_data_importer_module( Tuple[Optional[str], Optional[pulumi.Resource]]: The version deployed and the deployed resource. """ try: - pulumi.log.info("Starting deployment of CDI module") + log.info("Starting deployment of CDI module") version = config_cdi.version if config_cdi.version and config_cdi.version != "latest" else fetch_latest_version() - pulumi.log.info(f"Using CDI version: {version}") + log.info(f"Using CDI version: {version}") # Create namespace - pulumi.log.info(f"Creating namespace: {config_cdi.namespace}") + log.info(f"Creating namespace: {config_cdi.namespace}") namespace_resource = create_namespace( name=config_cdi.namespace, labels=config_cdi.labels, @@ -45,8 +49,9 @@ def deploy_containerized_data_importer_module( ) # Deploy CDI operator + # TODO: consider moving url variable to config via new value in ContainerizedDataImporterConfig class with default value, may require helper function in /types.py to support `latest` operator_url = f"https://github.com/kubevirt/containerized-data-importer/releases/download/v{version}/cdi-operator.yaml" - pulumi.log.info(f"Deploying CDI operator from URL: {operator_url}") + log.info(f"Deploying CDI operator from URL: {operator_url}") operator_resource = k8s.yaml.ConfigFile( "cdi-operator", @@ -58,10 +63,11 @@ def deploy_containerized_data_importer_module( ) # Ensure dependencies on operator and namespace + # TODO: re-evaluate implementing module specific module_depends_on and consistently adopting across all modules depends_on = global_depends_on + [operator_resource] # Create CDI custom resource - pulumi.log.info("Creating CDI custom resource") + log.info("Creating CDI custom resource") cdi_resource = create_custom_resource( name="cdi", args={ @@ -104,13 +110,15 @@ def deploy_containerized_data_importer_module( ) ) - pulumi.log.info("CDI module deployment complete") + log.info("CDI module deployment complete") return version, operator_resource except Exception as e: - pulumi.log.error(f"Deployment of CDI module failed: {str(e)}") + log.error(f"Deployment of CDI module failed: {str(e)}") raise +# Function to fetch the latest stable semantic version from GitHub releases +# TODO: consider making github latest release version fetching a shared utility function & adopting across all modules to reduce code duplication def fetch_latest_version() -> str: """ Fetches the latest stable version of CDI from GitHub releases. @@ -123,8 +131,8 @@ def fetch_latest_version() -> str: tag = requests.get(latest_release_url, allow_redirects=False).headers.get('location') version = tag.split('/')[-1] version = version.lstrip('v') - pulumi.log.info(f"Fetched latest CDI version: {version}") + log.info(f"Fetched latest CDI version: {version}") return version except Exception as e: - pulumi.log.error(f"Error fetching the latest version: {e}") + log.error(f"Error fetching the latest version: {e}") return "latest" diff --git a/pulumi/modules/hostpath_provisioner/deploy.py b/pulumi/modules/hostpath_provisioner/deploy.py index 0b50fed..5e610e2 100644 --- a/pulumi/modules/hostpath_provisioner/deploy.py +++ b/pulumi/modules/hostpath_provisioner/deploy.py @@ -1,14 +1,20 @@ # pulumi/modules/hostpath_provisioner/deploy.py import requests +from typing import List, Dict, Any, Tuple, Optional + import pulumi import pulumi_kubernetes as k8s -from typing import List, Dict, Any, Tuple, Optional +from pulumi import log + from core.resource_helpers import create_namespace, create_custom_resource, create_config_file from core.utils import wait_for_crds from .types import HostPathProvisionerConfig +# Function to call the deploy_hostpath_provisioner function and encapsulate any auxiliary logic like updating global dependencies +# TODO: standardize function signatures and common function names across all modules for deploy functions including adopting common naming conventions like using `config` parameter name instead of `config_` format. +# TODO: adopt a consistent naming convention for common function names across all modules. def deploy_hostpath_provisioner_module( config_hostpath_provisioner: HostPathProvisionerConfig, global_depends_on: List[pulumi.Resource], @@ -32,11 +38,14 @@ def deploy_hostpath_provisioner_module( ) # Update global dependencies + # TODO: re-evaluate global_depends_on usage, implementation, and hygene, and document strategy. Then adopt a consistent approach across all modules. global_depends_on.append(hostpath_resource) return hostpath_version, hostpath_resource +# Function to deploy the HostPath Provisioner +# TODO: standardize function signatures and common function names across all modules for deploy functions including adopting common naming conventions like using `config` parameter name instead of `config_` format. def deploy_hostpath_provisioner( config_hostpath_provisioner: HostPathProvisionerConfig, depends_on: List[pulumi.Resource], @@ -68,6 +77,8 @@ def deploy_hostpath_provisioner( # Determine version to use version = get_latest_version() if config_hostpath_provisioner.version == "latest" else config_hostpath_provisioner.version + # Transformation function to enforce namespace override on all resources + # TODO: consider implementing as a utility or resource helper function and adopting directly in core/resource_helpers.py in applicable functions. def enforce_namespace(resource_args: pulumi.ResourceTransformationArgs) -> pulumi.ResourceTransformationResult: """ Transformation function to enforce namespace on all resources. @@ -95,12 +106,15 @@ def enforce_namespace(resource_args: pulumi.ResourceTransformationArgs) -> pulum namespace_conflict = True meta['namespace'] = namespace + # TODO: document when/if this case is applicable and why this approach is used. if namespace_conflict: raise ValueError("Resource namespace conflict detected.") return pulumi.ResourceTransformationResult(props, resource_args.opts) # Deploy the webhook + # TODO: consider relocating url variable into the HostpathProvisionerConfig class as a property for better user configuration. + # TODO: consider supporting remote and local path webhook.yaml sources. webhook_url = f'https://github.com/kubevirt/hostpath-provisioner-operator/releases/download/v{version}/webhook.yaml' webhook = create_config_file( name="hostpath-provisioner-webhook", @@ -115,6 +129,8 @@ def enforce_namespace(resource_args: pulumi.ResourceTransformationArgs) -> pulum ) # Deploy the operator + # TODO: consider relocating url variable into the HostpathProvisionerConfig class as a property for better user configuration. + # TODO: consider supporting remote and local path operator.yaml sources. operator_url = f'https://github.com/kubevirt/hostpath-provisioner-operator/releases/download/v{version}/operator.yaml' operator = create_config_file( name="hostpath-provisioner-operator", @@ -129,6 +145,7 @@ def enforce_namespace(resource_args: pulumi.ResourceTransformationArgs) -> pulum ) # Ensure CRDs are created before HostPathProvisioner resource + # TODO: re-evaluate if this is functional and finish the implementation to ensure pulumi waits for CRDs to be created before creating the HostPathProvisioner resource. crds = wait_for_crds( crd_names=["hostpathprovisioners.hostpathprovisioner.kubevirt.io"], k8s_provider=k8s_provider, @@ -168,6 +185,7 @@ def enforce_namespace(resource_args: pulumi.ResourceTransformationArgs) -> pulum ) # Define the StorageClass + # TODO: make more user configurable and consider supporting multiple storage pools from a configuration map or array. storage_class = create_storage_class( name="hostpath-storage-class-ssd", provisioner="kubevirt.io.hostpath-provisioner", @@ -181,6 +199,8 @@ def enforce_namespace(resource_args: pulumi.ResourceTransformationArgs) -> pulum return version, webhook +# Function to retrieve the latest version of HostPath Provisioner from GitHub Releases +# TODO: consider relocating this function to a utility or resource helper module to reduce code duplication. def get_latest_version() -> str: """ Retrieves the latest stable version of HostPath Provisioner. @@ -192,13 +212,15 @@ def get_latest_version() -> str: tag_url = 'https://github.com/kubevirt/hostpath-provisioner-operator/releases/latest' response = requests.get(tag_url, allow_redirects=False) final_url = response.headers.get('location') - version = final_url.split('/')[-1].lstrip('v') if response.status_code == 302 else "0.17.0" + version = final_url.split('/')[-1].lstrip('v') return version except Exception as e: - pulumi.log.error(f"Error fetching the latest version: {e}") + log.error(f"Error fetching the latest version: {e}") return "0.17.0" +# Function to create a StorageClass resource +# TODO: consider supporting iterating over multiple storage pools from a configuration map or array. def create_storage_class( name: str, provisioner: str, diff --git a/pulumi/modules/kubevirt/deploy.py b/pulumi/modules/kubevirt/deploy.py index 799ad9e..af3e34b 100644 --- a/pulumi/modules/kubevirt/deploy.py +++ b/pulumi/modules/kubevirt/deploy.py @@ -2,6 +2,24 @@ """ Deploys the KubeVirt module. + +This module is responsible for deploying KubeVirt on the Kubernetes cluster. + +The configuration options are: + + namespace: str - The namespace where KubeVirt will be deployed. Default is 'kubevirt'. + version: Optional[str] - The version of KubeVirt to deploy. Default is None. + use_emulation: bool - Whether to use emulation or not. Default is False. + labels: Dict[str, str] - The labels to apply to the KubeVirt resources. Default is {}. + annotations: Dict[str, Any] - The annotations to apply to the KubeVirt resources. Default is {}. + global_depends_on: List[pulumi.Resource] - The list of resources that the KubeVirt module depends on. Default is []. + k8s_provider: k8s.Provider - The Kubernetes provider. Default is None. + + Returns: + Tuple[Optional[str], k8s.apiextensions.CustomResource] - The version of KubeVirt deployed and the deployed resource. + + Raises: + Exception: If the KubeVirt CRDs are not available. """ # Import necessary modules @@ -13,6 +31,7 @@ import pulumi import pulumi_kubernetes as k8s +from pulumi import log from core.utils import wait_for_crds from core.metadata import get_global_labels, get_global_annotations @@ -39,6 +58,8 @@ def deploy_kubevirt_module( ) # Update global dependencies if not None + # TODO: re-evaluate if global_depends_on should be updated here or in the calling function + # TODO: regardless, the if statement is not necessary as this code will not be executed if kubevirt module is not enabled if kubevirt_resource: global_depends_on.append(kubevirt_resource) @@ -53,7 +74,7 @@ def deploy_kubevirt( Deploys KubeVirt operator and creates the KubeVirt CustomResource, ensuring that the CRD is available before creating the CustomResource. """ - # Create Namespace using the helper function + # Create Namespace using the helper function from core/resource_helpers.py namespace_resource = create_namespace( name=config_kubevirt.namespace, k8s_provider=k8s_provider, @@ -61,18 +82,22 @@ def deploy_kubevirt( depends_on=depends_on, ) - # Combine dependencies + # Add the namespace to the dependencies + # TODO: reevaluate if this is necessary, helpful, and if a module scoped `module_depends_on` pattern should be adopted across modules depends_on = depends_on + [namespace_resource] - namespace = config_kubevirt.namespace + + # Extract config objects from config dictionary version = config_kubevirt.version + namespace = config_kubevirt.namespace use_emulation = config_kubevirt.use_emulation - # Determine KubeVirt version + # Determine latest version release from GitHub Releases + # TODO: reimplement into the get_module_config function and adopt across all modules to reduce code duplication if version == 'latest' or version is None: version = get_latest_kubevirt_version() - pulumi.log.info(f"Setting KubeVirt release version to latest: {version}") + log.info(f"Setting KubeVirt release version to latest: {version}") else: - pulumi.log.info(f"Using KubeVirt version: {version}") + log.info(f"Using KubeVirt version: {version}") # Download and transform KubeVirt operator YAML kubevirt_operator_yaml = download_kubevirt_operator_yaml(version) @@ -159,10 +184,13 @@ def deploy_kubevirt( return version, kubevirt_resource +# Function to get the latest KubeVirt version if the version is set to 'latest' or no version configuration is supplied def get_latest_kubevirt_version() -> str: """ Retrieves the latest stable version of KubeVirt. """ + + # TODO: relocate this URL to a default in the KubevirtConfig class and allow for an override url = 'https://storage.googleapis.com/kubevirt-prow/release/kubevirt/kubevirt/stable.txt' response = requests.get(url) if response.status_code != 200: @@ -173,12 +201,16 @@ def download_kubevirt_operator_yaml(version: str) -> Any: """ Downloads the KubeVirt operator YAML for the specified version. """ + + # TODO: relocate this URL to a default in the KubevirtConfig class and allow for an override + # TODO: support remote or local kubevirt-operator.yaml file url = f'https://github.com/kubevirt/kubevirt/releases/download/v{version}/kubevirt-operator.yaml' response = requests.get(url) if response.status_code != 200: raise Exception(f"Failed to download KubeVirt operator YAML from {url}") return list(yaml.safe_load_all(response.text)) +# Function to remove Namespace resources from the YAML data and replace other object namespaces with the specified namespace value as an override def _transform_yaml(yaml_data: Any, namespace: str) -> List[Dict[str, Any]]: """ Transforms the YAML data to set the namespace and exclude Namespace resources. diff --git a/pulumi/modules/kubevirt/types.py b/pulumi/modules/kubevirt/types.py index 872c9ee..a3fc64f 100644 --- a/pulumi/modules/kubevirt/types.py +++ b/pulumi/modules/kubevirt/types.py @@ -1,5 +1,21 @@ # ./pulumi/modules/kubevirt/types.py -# Description: +# TODO: +# - add the missing docstrings +# - re-evaluate type enforcement techniques + +""" +Defines the data structure for the KubeVirt module configuration. + +This module is responsible for deploying KubeVirt on the Kubernetes cluster. + +The configuration options are: + + namespace: str - The namespace where KubeVirt will be deployed. Default is 'kubevirt'. + version: Optional[str] - The version of KubeVirt to deploy. Default is None. + use_emulation: bool - Whether to use emulation or not. Default is False. + labels: Dict[str, str] - The labels to apply to the KubeVirt resources. Default is {}. + annotations: Dict[str, Any] - The annotations to apply to the KubeVirt resources. Default is {}. +""" from dataclasses import dataclass, field from typing import Optional, Dict, Any @@ -10,7 +26,7 @@ @dataclass class KubeVirtConfig: namespace: str = "kubevirt" - version: Optional[str] = None + version: Optional[str] = "latest" use_emulation: bool = False labels: Dict[str, str] = field(default_factory=dict) annotations: Dict[str, Any] = field(default_factory=dict) diff --git a/pulumi/modules/multus/deploy.py b/pulumi/modules/multus/deploy.py index e5509fd..b34f2a7 100644 --- a/pulumi/modules/multus/deploy.py +++ b/pulumi/modules/multus/deploy.py @@ -1,15 +1,19 @@ # pulumi/modules/multus/deploy.py +# TODO: enhance logging and error handling consistent with documented best practices and other modules. """ Deploys the Multus module. """ from typing import List, Dict, Any, Tuple, Optional + import pulumi import pulumi_kubernetes as k8s + +from core.utils import get_latest_helm_chart_version from core.metadata import get_global_labels, get_global_annotations from core.resource_helpers import create_namespace, create_custom_resource -from core.utils import get_latest_helm_chart_version + from .types import MultusConfig def deploy_multus_module( @@ -51,6 +55,7 @@ def deploy_multus( # Deploy Multus DaemonSet resource_name = f"multus-daemonset" version = config_multus.version or "master" + # TODO: consider moving url variable to config via new value in MultusConfig class with default value, may require helper function in multus/types.py to support `latest` manifest_url = f"https://raw.githubusercontent.com/k8snetworkplumbingwg/multus-cni/{version}/deployments/multus-daemonset-thick.yml" multus = k8s.yaml.ConfigFile( @@ -69,6 +74,7 @@ def deploy_multus( ) # Create NetworkAttachmentDefinition + # TODO: document the dependency on `br0` bridge interface, also consider making this configurable network_attachment_definition = create_custom_resource( name=config_multus.bridge_name, args={ @@ -115,6 +121,9 @@ def deploy_multus( def transform_host_path(args: pulumi.ResourceTransformationArgs) -> pulumi.ResourceTransformationResult: """ Transforms the host paths in the Multus DaemonSet. + + The Multus DaemonSet mounts the host path `/run/netns` to kubelet pod at `/var/run/netns`. + This transformation ensures the unique path required is utilized for Talos Linux compatibility. """ obj = args.props diff --git a/pulumi/modules/prometheus/deploy.py b/pulumi/modules/prometheus/deploy.py index 53e2ebf..a38a60c 100644 --- a/pulumi/modules/prometheus/deploy.py +++ b/pulumi/modules/prometheus/deploy.py @@ -4,11 +4,15 @@ Deploys the Prometheus module following the shared design patterns. """ +from typing import List, Dict, Any, Tuple, Optional + import pulumi import pulumi_kubernetes as k8s -from typing import List, Dict, Any, Tuple, Optional +from pulumi import log + from core.resource_helpers import create_namespace, create_helm_release from core.utils import get_latest_helm_chart_version + from .types import PrometheusConfig def deploy_prometheus_module( @@ -63,30 +67,36 @@ def deploy_prometheus( chart_name = "kube-prometheus-stack" chart_url = "https://prometheus-community.github.io/helm-charts" + + # Get the latest version of the Helm chart + # TODO: re-implement into the get_module_config function and adopt across all modules to reduce code duplication if version is None or version == "latest": version = get_latest_helm_chart_version(chart_url, chart_name) - pulumi.log.info(f"Setting Prometheus helm chart version to latest release: {version}") + log.info(f"Setting Prometheus helm chart version to latest release: {version}") else: - pulumi.log.info(f"Using Prometheus helm release version: {version}") + log.info(f"Using Prometheus helm release version: {version}") # Helm values customization based on OpenUnison integration - prometheus_helm_values = { - "grafana": { - "grafana.ini": { - "users": { - "allow_sign_up": False, - "auto_assign_org": True, - "auto_assign_org_role": "Admin" - }, - "auth.proxy": { - "enabled": True, - "header_name": "X-WEBAUTH-USER", - "auto_sign_up": True, - "headers": "Groups:X-WEBAUTH-GROUPS" + if openunison_enabled: + prometheus_helm_values = { + "grafana": { + "grafana.ini": { + "users": { + "allow_sign_up": False, + "auto_assign_org": True, + "auto_assign_org_role": "Admin" + }, + "auth.proxy": { + "enabled": True, + "header_name": "X-WEBAUTH-USER", + "auto_sign_up": True, + "headers": "Groups:X-WEBAUTH-GROUPS" + } } } } - } if openunison_enabled else {} + else : + prometheus_helm_values = {} # Create the Helm Release release = create_helm_release( @@ -160,6 +170,8 @@ def create_prometheus_services( } ] + # Create services from list of service definitions + # TODO: re-evaluate if this should be centralized into a pulumi/core/utils.py helper function for service_def in service_definitions: service = k8s.core.v1.Service( f"service-{service_def['name']}", From 296027437c2ef2445bd1ee11633834f01fd99078 Mon Sep 17 00:00:00 2001 From: Kat Morgan Date: Tue, 24 Sep 2024 14:19:26 -0700 Subject: [PATCH 43/43] add developer comments, questions, planning, and TODOs --- pulumi/CALL_TO_ACTION.md | 157 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 pulumi/CALL_TO_ACTION.md diff --git a/pulumi/CALL_TO_ACTION.md b/pulumi/CALL_TO_ACTION.md new file mode 100644 index 0000000..89fa502 --- /dev/null +++ b/pulumi/CALL_TO_ACTION.md @@ -0,0 +1,157 @@ +### Refactoring Enhancements to Consider + +**Modular Design**: + - Core functionalities are segregated into distinct files/modules, such as `config.py`, `deployment.py`, `resource_helpers.py`, etc. + - Each module follows a clear pattern with separate `types.py` and `deploy.py` files. + +**Configuration Management**: + - Centralize configuration management using `config.py` to handle global settings. + - Use data classes for module configurations to ensure type safety and defaults. + +**Global Metadata Handling**: + - Implementation of a singleton pattern for managing global metadata (labels and annotations). + - Functions to generate and apply global metadata. + +**Consistency and Readability**: + - The existing TODO comments highlight areas needing reorganization and refactoring. + - Some modules including `kubevirt`, `cert_manager`, `hostpath_provisioner` and others deploy differently in terms of resource creation and dependency management, look for ways to improve consistency. + +**Centralized Configuration Loading**: + - Configuration loading and merging logic vary across modules. + - There is redundancy in fetching the latest versions for modules (e.g., `kubevirt`, `cert_manager`). Look for ways to reduce version fetching redundancy. + +**Exception Handling**: + - Exception handling is partially implemented in some places, consistent and detailed error handling across all modules will improve reliability. + +**Resource Helper Centralization**: + - Several helper functions like `create_namespace`, `create_custom_resource`, etc., provide common functionality but could be standardized further. + - Handling dependencies and resource transformations could be more DRY (Don't Repeat Yourself). + +**Standardize Configuration Management**: + - Refactor configuration management to ensure consistency across all modules. + - Implement a common pattern for fetching the latest versions and configuration merging. + +**Refactor `initialize_pulumi` Function**: + - Use data classes or named tuples instead of dictionaries for initialization components. + - Centralize and streamline initialization logic to reduce redundancy. + +**Enhance Error Handling and Logging**: + - Implement structured logging and consistent error handling across all the modules. + - Ensure all relevant operations are logged, and errors are informative. + +**Simplify Function Signatures and Improve Type Safety**: + - Refactor function signatures to use data classes and named tuples. This will improve readability and maintainability. + +**Centralize Shared Logic**: + - Standardize and centralize shared logic like version fetching, resource transformation, and compliance metadata generation. + - Use utility functions from `utils.py` and refactor repetitive logic across `deploy.py` files. + +### Implementation Examples + +#### Centralize Configuration Handling + +Refactor the `load_default_versions` function and adopt it across all modules: + +```python +# centralize logic in core/config.py +def load_default_versions(config: pulumi.Config, force_refresh=False) -> dict: + ... + # reuse this function for fetching specific versions in modules + return default_versions + +# example usage in kubevirt/types.py +@staticmethod +def get_latest_version() -> str: + return load_default_versions(pulumi.Config()).get('kubevirt', 'latest') +``` + +#### Standardize Initialization Method + +Refactor `initialize_pulumi` in `deployment.py`: + +```python +from typing import NamedTuple + +class PulumiInit(NamedTuple): + config: pulumi.Config + stack_name: str + project_name: str + default_versions: Dict[str, str] + versions: Dict[str, str] + configurations: Dict[str, Dict[str, Any]] + global_depends_on: List[pulumi.Resource] + k8s_provider: k8s.Provider + git_info: Dict[str, str] + compliance_config: ComplianceConfig + global_labels: Dict[str, str] + global_annotations: Dict[str, str] + +def initialize_pulumi() -> PulumiInit: + ... + # use PulumiInit named tuple for returning components + return PulumiInit(...) +``` + +Update main entry (`__main__.py`) to use the tuple: + +```python +def main(): + try: + init = initialize_pulumi() + + # Use named tuple instead of dictionary + config = init.config + k8s_provider = init.k8s_provider + versions = init.versions + configurations = init.configurations + default_versions = init.default_versions + global_depends_on = init.global_depends_on + + ... + except Exception as e: + log.error(f"Deployment failed: {str(e)}") + raise +``` + +#### Enhance Logging and Error Handling + +Standardize logging across the modules: + +```python +import logging + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +def deploy_module(...): + try: + ... + except ValueError as ve: + log.error(f"Value error during deployment: {ve}") + raise + except Exception as e: + log.error(f"General error during deployment: {e}") + raise +``` + +#### Improve Reusability of Helper Functions + +Refactor `resource_helpers.py` to adopt utility functions for setting metadata and transformations: + +```python +def universal_resource_transform(resource_args: pulumi.ResourceTransformationArgs): + props = resource_args.props + set_resource_metadata(props.get('metadata', {}), get_global_labels(), get_global_annotations()) + return pulumi.ResourceTransformationResult(props, resource_args.opts) +``` + +### Adopt universal transforms in more places: + +```python +def create_custom_resource(..., transformations: Optional[List] = None, ...): + ... + opts = pulumi.ResourceOptions.merge( + ... # include universal transformations + transformations=[universal_resource_transform] + (transformations or []) + ) +``` +