diff --git a/api/azimuth/cluster_api/base.py b/api/azimuth/cluster_api/base.py index 2b9cb509..498829b0 100644 --- a/api/azimuth/cluster_api/base.py +++ b/api/azimuth/cluster_api/base.py @@ -367,6 +367,12 @@ def _build_cluster_spec(self, **options): spec = {} if "control_plane_size" in options: spec["controlPlaneMachineSize"] = options["control_plane_size"].name + if "control_plane_count" in options: + spec["controlPlaneMachineCount"] = options["control_plane_count"] + if "control_plane_etcd_volume_size" in options: + spec["controlPlaneEtcdVolumeSize"] = options[ + "control_plane_etcd_volume_size" + ] if "node_groups" in options: spec["nodeGroups"] = [ { @@ -406,6 +412,8 @@ def create_cluster( control_plane_size: cloud_dto.Size, node_groups: list[NodeGroupSpec], resources: scheduling_dto.PlatformResources, + control_plane_count: int = 3, + control_plane_etcd_volume_size: int = 20, autohealing_enabled: bool = True, ingress_enabled: bool = False, ingress_controller_load_balancer_ip: str | None = None, @@ -453,6 +461,8 @@ def create_cluster( # Build the cluster spec options = dict( control_plane_size=control_plane_size, + control_plane_count=control_plane_count, + control_plane_etcd_volume_size=control_plane_etcd_volume_size, node_groups=node_groups, autohealing_enabled=autohealing_enabled, ingress_enabled=ingress_enabled, diff --git a/api/azimuth/provider/openstack/provider.py b/api/azimuth/provider/openstack/provider.py index 41a88f18..f7ab7026 100644 --- a/api/azimuth/provider/openstack/provider.py +++ b/api/azimuth/provider/openstack/provider.py @@ -502,9 +502,10 @@ def _get_coral_quotas(self): active_allocation_list = list( filter( - lambda a: parse_time_and_correct_tz(a["start"], target_tz) - < current_time - and current_time < parse_time_and_correct_tz(a["end"], target_tz), + lambda a: ( + parse_time_and_correct_tz(a["start"], target_tz) < current_time + and current_time < parse_time_and_correct_tz(a["end"], target_tz) + ), account_allocations, ) ) diff --git a/api/azimuth/scheduling/calculator.py b/api/azimuth/scheduling/calculator.py index 3d7f6617..ba5df07d 100644 --- a/api/azimuth/scheduling/calculator.py +++ b/api/azimuth/scheduling/calculator.py @@ -78,22 +78,34 @@ def calculate( monitoring_enabled: bool, monitoring_metrics_volume_size: int, monitoring_logs_volume_size: int, + control_plane_count: int | None = None, + control_plane_etcd_volume_size: int | None = None, **kwargs, ) -> dto.PlatformResources: """ Calculates the resources required to make the specified Kubernetes cluster. """ resources = dto.PlatformResources() + # Use the user-selected count, otherwise fall back to the template value + cp_count = ( + control_plane_count + if control_plane_count is not None + else template.control_plane_count + ) # First, deal with the control plane - resources.add_machines(template.control_plane_count, control_plane_size) - if template.etcd_volume_size > 0: - resources.add_volumes( - template.control_plane_count, template.etcd_volume_size - ) + resources.add_machines(cp_count, control_plane_size) + # When control_plane_count is explicitly set, etcd is configured on a dedicated + # Cinder volume at the cluster level (cluster-values.yaml), overriding the + # template value. Otherwise fall back to the template's etcd volume size. + etcd_size = ( + (control_plane_etcd_volume_size or 20) + if control_plane_count is not None + else template.etcd_volume_size + ) + if etcd_size > 0: + resources.add_volumes(cp_count, etcd_size) if template.control_plane_root_volume_size > 0: - resources.add_volumes( - template.control_plane_count, template.control_plane_root_volume_size - ) + resources.add_volumes(cp_count, template.control_plane_root_volume_size) # Next, the node groups for ng in node_groups: # When autoscaling, make sure there is enough space for the max size of the diff --git a/api/azimuth/serializers.py b/api/azimuth/serializers.py index ce13c985..c4ae193c 100644 --- a/api/azimuth/serializers.py +++ b/api/azimuth/serializers.py @@ -937,6 +937,8 @@ class CreateKubernetesClusterSerializer( name = serializers.RegexField("^[a-z][a-z0-9-]+[a-z0-9]$") template = serializers.RegexField("^[a-z0-9-]+$") control_plane_size = serializers.RegexField(ID_REGEX) + control_plane_count = serializers.ChoiceField(choices=[1, 3], default=3) + control_plane_etcd_volume_size = serializers.IntegerField(min_value=1, default=20) node_groups = NodeGroupSpecSerializer(many=True) autohealing_enabled = serializers.BooleanField(default=True) ingress_enabled = serializers.BooleanField(default=False) diff --git a/chart/files/api/settings/07-disable-kubernetes.yaml b/chart/files/api/settings/07-disable-kubernetes.yaml deleted file mode 100644 index 19564192..00000000 --- a/chart/files/api/settings/07-disable-kubernetes.yaml +++ /dev/null @@ -1,4 +0,0 @@ -{{- if not .Values.tags.kubernetes -}} -AZIMUTH: - CLUSTER_API_PROVIDER: null -{{- end }} diff --git a/chart/files/api/settings/07-feature-flags.yaml b/chart/files/api/settings/07-feature-flags.yaml new file mode 100644 index 00000000..c804cbd2 --- /dev/null +++ b/chart/files/api/settings/07-feature-flags.yaml @@ -0,0 +1,9 @@ +{{- if or (not .Values.tags.kubernetes) (not .Values.tags.clusters) -}} +AZIMUTH: + {{- if not .Values.tags.kubernetes }} + CLUSTER_API_PROVIDER: null + {{- end }} + {{- if not .Values.tags.clusters }} + CLUSTER_ENGINE: null + {{- end }} +{{- end }} diff --git a/chart/files/api/settings/08-awx.yaml b/chart/files/api/settings/08-awx.yaml deleted file mode 100644 index 4c80c6b2..00000000 --- a/chart/files/api/settings/08-awx.yaml +++ /dev/null @@ -1,55 +0,0 @@ -{{- if and .Values.tags.clusters (eq .Values.clusterEngine.type "awx") -}} -AZIMUTH: - AWX: - ENABLED: true - URL: {{ required "clusterEngine.awx.url is required" .Values.clusterEngine.awx.url | quote }} - VERIFY_SSL: {{ .Values.clusterEngine.awx.verifySsl }} - USERNAME: {{ .Values.clusterEngine.awx.username }} - CREATE_TEAMS: {{ .Values.clusterEngine.awx.createTeams }} - CREATE_TEAM_ALLOW_ALL_PERMISSION: {{ .Values.clusterEngine.awx.createTeamAllowAllPermission }} - {{- with .Values.clusterEngine.awx.adminUsername }} - ADMIN_USERNAME: {{ . }} - {{- end }} - {{- if .Values.clusterEngine.terraformBackend.enabled }} - {{- if (eq .Values.clusterEngine.terraformBackend.type "consul") }} - EXTRA_CREDENTIALS: - - NAME: CaaS Consul - TYPE: Hashicorp Consul - INPUTS: - address: {{ required "clusterEngine.terraformBackend.consul.address" .Values.clusterEngine.terraformBackend.consul.address | quote }} - {{- else }} - {{- fail (printf "Unsupported terraform backend '%s'" .Values.clusterEngine.terraformBackend.type) }} - {{- end }} - {{- end }} - {{- with .Values.clusterEngine.awx.executionEnvironment }} - EXECUTION_ENVIRONMENT: - IMAGE: {{ .image }} - {{- if .alwaysPull }} - ALWAYS_PULL: true - {{- end }} - {{- with .credentials }} - CREDENTIALS: - HOST: {{ .host }} - USERNAME: {{ quote .username }} - TOKEN: {{ quote .token }} - {{- end }} - {{- end }} - {{- with .Values.clusterEngine.awx.defaultProjects }} - DEFAULT_PROJECTS: - {{- range . }} - - NAME: {{ .name | quote }} - GIT_URL: {{ .gitUrl }} - GIT_VERSION: {{ .gitVersion }} - METADATA_ROOT: {{ .metadataRoot }} - {{- if .alwaysUpdate }} - ALWAYS_UPDATE: true - {{- end }} - {{- with .playbooks }} - PLAYBOOKS: {{ toYaml . | nindent 14 }} - {{- end }} - {{- with .extraVars }} - EXTRA_VARS: {{ toYaml . | nindent 14 }} - {{- end }} - {{- end }} - {{- end }} -{{- end }} diff --git a/chart/files/api/settings/09-awx-passwords.py b/chart/files/api/settings/09-awx-passwords.py deleted file mode 100644 index b2f66d8e..00000000 --- a/chart/files/api/settings/09-awx-passwords.py +++ /dev/null @@ -1,8 +0,0 @@ -{{- if and .Values.tags.clusters (eq .Values.clusterEngine.type "awx") -}} -import os - -# Read the AWX password from an environment variable that is populated from the specified secret -AZIMUTH['AWX']['PASSWORD'] = os.environ['AWX_PASSWORD'] -if 'AWX_ADMIN_PASSWORD' in os.environ: - AZIMUTH['AWX']['ADMIN_PASSWORD'] = os.environ['AWX_ADMIN_PASSWORD'] -{{- end }} diff --git a/chart/files/api/settings/10-disable-caas.yaml b/chart/files/api/settings/10-disable-caas.yaml deleted file mode 100644 index 42e58b2c..00000000 --- a/chart/files/api/settings/10-disable-caas.yaml +++ /dev/null @@ -1,4 +0,0 @@ -{{- if not .Values.tags.clusters -}} -AZIMUTH: - CLUSTER_ENGINE: null -{{- end }} diff --git a/chart/templates/api/deployment.yaml b/chart/templates/api/deployment.yaml index 6e14b1f3..5c8e1b22 100644 --- a/chart/templates/api/deployment.yaml +++ b/chart/templates/api/deployment.yaml @@ -1,6 +1,5 @@ {{- $component := "api" -}} {{- $values := get .Values $component -}} -{{- $clusterEngine := .Values.clusterEngine -}} apiVersion: apps/v1 kind: Deployment metadata: @@ -57,23 +56,8 @@ spec: (eq .Values.authentication.type "oidc") (hasPrefix "http://" .Values.authentication.oidc.issuerUrl) ) - (and .Values.tags.clusters (eq $clusterEngine.type "awx")) }} env: - {{- if and .Values.tags.clusters (eq $clusterEngine.type "awx") }} - - name: AWX_PASSWORD - valueFrom: - secretKeyRef: - name: {{ required "clusterEngine.awx.passwordSecretName is required" $clusterEngine.awx.passwordSecretName | quote }} - key: password - {{- with $clusterEngine.awx.adminPasswordSecretName }} - - name: AWX_ADMIN_PASSWORD - valueFrom: - secretKeyRef: - name: {{ quote . }} - key: password - {{- end }} - {{- end }} {{- if $values.monitoring.enabled }} - name: GUNICORN_STATSD_HOST value: "localhost:9125" diff --git a/chart/templates/api/settings.yaml b/chart/templates/api/settings.yaml index 2be8b42c..824be99f 100644 --- a/chart/templates/api/settings.yaml +++ b/chart/templates/api/settings.yaml @@ -1,4 +1,4 @@ -{{- if not (has .Values.clusterEngine.type (list "crd" "awx")) -}} +{{- if ne .Values.clusterEngine.type "crd" -}} {{- fail (printf "Unsupported cluster engine '%s'" .Values.clusterEngine.type) }} {{- end }} @@ -23,14 +23,8 @@ data: {{- tpl (.Files.Get "files/api/settings/05-apps.yaml") . | b64enc | nindent 4 }} 06-ssh-key-store.yaml: | {{- tpl (.Files.Get "files/api/settings/06-ssh-key-store.yaml") . | b64enc | nindent 4 }} - 07-disable-kubernetes.yaml: | - {{- tpl (.Files.Get "files/api/settings/07-disable-kubernetes.yaml") . | b64enc | nindent 4 }} - 08-awx.yaml: | - {{- tpl (.Files.Get "files/api/settings/08-awx.yaml") . | b64enc | nindent 4 }} - 09-awx-passwords.py: | - {{- tpl (.Files.Get "files/api/settings/09-awx-passwords.py") . | b64enc | nindent 4 }} - 10-disable-caas.yaml: | - {{- tpl (.Files.Get "files/api/settings/10-disable-caas.yaml") . | b64enc | nindent 4 }} + 07-feature-flags.yaml: | + {{- tpl (.Files.Get "files/api/settings/07-feature-flags.yaml") . | b64enc | nindent 4 }} 11-scheduling.yaml: | {{- tpl (.Files.Get "files/api/settings/11-scheduling.yaml") . | b64enc | nindent 4 }} 12-apps-provider.yaml: | diff --git a/chart/tests/__snapshot__/snapshot_test.yaml.snap b/chart/tests/__snapshot__/snapshot_test.yaml.snap index 531e1675..af95746d 100755 --- a/chart/tests/__snapshot__/snapshot_test.yaml.snap +++ b/chart/tests/__snapshot__/snapshot_test.yaml.snap @@ -158,7 +158,7 @@ templated manifests should match snapshot: template: metadata: annotations: - azimuth.stackhpc.com/settings-checksum: ba9764bba470b2cacf65a4646a795dd5a62707a4879baac5749a8884af5dc0cd + azimuth.stackhpc.com/settings-checksum: 65ccb797a35210f0ae53ea1af4483f357627b78cc7c97b321de04fb78bdd6ed8 azimuth.stackhpc.com/theme-checksum: ec0f36322392deee39d80b7f77ecd634df60358857af9dc208077860c4e174ab kubectl.kubernetes.io/default-container: api labels: @@ -261,13 +261,7 @@ templated manifests should match snapshot: Cg== 06-ssh-key-store.yaml: | QVpJTVVUSDoKICBTU0hfS0VZX1NUT1JFOgogICAgRkFDVE9SWTogYXppbXV0aC5rZXlzdG9yZS5wcm92aWRlci5Qcm92aWRlcktleVN0b3JlCg== - 07-disable-kubernetes.yaml: | - Cg== - 08-awx.yaml: | - Cg== - 09-awx-passwords.py: | - Cg== - 10-disable-caas.yaml: | + 07-feature-flags.yaml: | QVpJTVVUSDoKICBDTFVTVEVSX0VOR0lORTogbnVsbAo= 11-scheduling.yaml: | QVpJTVVUSDoKICBTQ0hFRFVMSU5HOgogICAgRU5BQkxFRDogZmFsc2UK diff --git a/chart/values.yaml b/chart/values.yaml index f1974fa1..408d895b 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -314,85 +314,8 @@ apps: # The cluster engine to use for Cluster-as-a-Service support clusterEngine: - # The type of cluster engine to use - # Currently only CRD is functional, and AWX support is deprecated + # The type of cluster engine to use (only crd is supported) type: crd - # Parameters for the awx cluster engine - # By default, we use values that will work with the managed AWX - awx: - # The AWX or Ansible Tower URL - url: - # Whether to verify SSL with the AWX/Tower API - verifySsl: true - # The username to use for the AWX/Tower API - # This user must be at least an org admin for the CaaS organisation - username: admin - # The name of a secret to get the AWX password from - passwordSecretName: - # Indicates whether teams should be created automatically - createTeams: true - # Indicates whether automatically-created teams should be granted access - # to all cluster types - createTeamAllowAllPermission: true - # Indicates whether CaaS resources should be created in AWX - createResources: true - # Creating resources requires the admin user - # If the user specified above is not a global admin, a separate admin username and - # password secret should be specified - adminUsername: - adminPasswordSecretName: - # Definition of a custom execution environment for CaaS appliances - executionEnvironment: - # # The image to use for the execution environment (required) - # image: registry.example.com/azimuth/caas-ee: - # # Indicates if the image should be pulled for every job (defaults to false) - # # This can be useful in development, but in production the recommended method is - # # to set this to false and use immutable tags - # alwaysPull: false - # # Optional credentials if the image is in a private registry - # credentials: - # # The registry host - # host: registry.example.com - # # The username to use to authenticate - # username: - # # The token/password to use to authenticate - # token: - # Definition of the default projects and job templates. - # By default, the StackHPC demo appliances will be used - defaultProjects: - # - # The name of the project (required) - # name: My Site Appliances - # # The git URL of the project (required) - # gitUrl: https://github.com/myorg/site-appliances.git - # # The branch, tag or commit id to use (required) - # gitVersion: master - # # The base URL for cluster metadata files (required) - # metadataRoot: https://raw.githubusercontent.com/myorg/site-appliances/master/ui-meta - # ## - # # OPTIONAL KEYS - # ## - # # Indicates if the project should be updated to the latest version on each job launch - # # Can be useful during development but recommend to leave as false in production - # # Defaults to false if not given - # alwaysUpdate: true - # # List of playbooks to create job templates for - # # If not given, a job template is created for each playbook in the project - # playbooks: ['cluster1-playbook.yml', 'cluster2-playbook.yml'] - # # Map whose keys are the playbooks and whose values are maps of Ansible extra_vars for those playbooks - # extraVars: - # # This special key can be used to set common extra_vars for all playbooks in a project - # __ALL__: { var1: value1, var2: value2 } - # cluster1-playbook.yml: { var1: "override value1" } - # Configuration for the Terraform backend - terraformBackend: - # Indicates if the Terraform backend should be configured - enabled: true - # The type of the Terraform backend to use (currently only consul is supported) - type: consul - # Configuration for the consul backend - consul: - # The address, including port, of the Consul server to use - address: # The SSH key store to use # The portal asks this store for the SSH key for a user diff --git a/ui/src/components/pages/tenancy/platforms/kubernetes/form.js b/ui/src/components/pages/tenancy/platforms/kubernetes/form.js index 15542828..729d8951 100644 --- a/ui/src/components/pages/tenancy/platforms/kubernetes/form.js +++ b/ui/src/components/pages/tenancy/platforms/kubernetes/form.js @@ -229,6 +229,7 @@ const initialState = kubernetesCluster => { name: kubernetesCluster.name, template: kubernetesCluster.template.id, control_plane_size: kubernetesCluster.control_plane_size ? kubernetesCluster.control_plane_size.id : "", + control_plane_count: kubernetesCluster.control_plane_count || 3, node_groups: kubernetesCluster.node_groups.map(ng => ({ name: ng.name, machine_size: ng.machine_size ? ng.machine_size.id : "", @@ -251,6 +252,7 @@ const initialState = kubernetesCluster => { name: '', template: '', control_plane_size: '', + control_plane_count: 3, node_groups: [], autohealing_enabled: true, ingress_enabled: false, @@ -506,6 +508,30 @@ export const KubernetesClusterForm = ({ onChange={setStateKey('control_plane_size')} /> + +
+ setStateKey('control_plane_count')(1)} + /> + setStateKey('control_plane_count')(3)} + /> +
+
Node groups {formState.showWorkerCountMessage && workerCountMessage !== '' && (