diff --git a/pkg/capi/components/CCVariables/Variable.vue b/pkg/capi/components/CCVariables/Variable.vue index 2359b4f..bd1d0f8 100644 --- a/pkg/capi/components/CCVariables/Variable.vue +++ b/pkg/capi/components/CCVariables/Variable.vue @@ -9,7 +9,7 @@ import LabeledSelect from '@shell/components/form/LabeledSelect'; import formRulesGenerator, { Validator } from '@shell/utils/validators/formRules'; import type { ClusterClassVariable } from '../../types/clusterClass'; -import { openAPIV3SchemaValidators } from '../../util/validators'; +import { isDefined, openAPIV3SchemaValidators } from '../../util/validators'; export default defineComponent({ name: 'CCVariable', @@ -98,17 +98,31 @@ export default defineComponent({ const required = this.variable?.required; if (required) { - out.push(formRulesGenerator(this.$store.getters['i18n/t'], { key: this.variable.name }).required as Validator); + // out.push(formRulesGenerator(this.$store.getters['i18n/t'], { key: this.variable.name }).required as Validator); + + out.push(val => !isDefined(val) ? this.$store.getters['i18n/t']('validation.required', { key: this.variable.name }) : null); } return out; }, isValid() { - return !this.validationRules.find((rule: Validator) => !!rule(this.value)); + return !this.validationErrors.length; }, - widerComponent() { + validationErrors() { + return this.validationRules.reduce((errs: string[], rule: Validator) => { + const message = rule(this.value); + + if (message) { + errs.push(message); + } + + return errs; + }, []); + }, + + listComponent() { return this.componentForType?.name === 'arraylist-var' || this.componentForType?.name === 'keyvalue-var'; } }, @@ -131,7 +145,7 @@ export default defineComponent({ @@ -153,4 +176,13 @@ export default defineComponent({ .align-center { align-self: 'center' } +.input-label{ + color: var(--input-label); + margin-bottom: 5px; + display: block; + width:100%; + .icon-warning{ + color: var(--error) + } +} diff --git a/pkg/capi/components/CCVariables/index.vue b/pkg/capi/components/CCVariables/index.vue index 6d1c64e..07a125e 100644 --- a/pkg/capi/components/CCVariables/index.vue +++ b/pkg/capi/components/CCVariables/index.vue @@ -5,6 +5,7 @@ import type { PropType } from 'vue'; import { randomStr } from '@shell/utils/string'; import { ClusterClassVariable } from '../../types/clusterClass'; import type { CapiClusterVariable } from '../../types/cluster.x-k8s.io.cluster'; +import { isDefined } from '../../util/validators'; import Variable from './Variable.vue'; export default defineComponent({ @@ -19,11 +20,25 @@ export default defineComponent({ }, // .spec.topology.variables + // OR .spec.topology.workers.machineDeployments[].variables.overrides + // OR .spec.topology.workers.machinePools[].variables.overrides value: { type: Array as PropType>, default: () => { return []; } + }, + + // if this and machinePoolClass are empty, ALL variables will be shown + // only 1 of machinePoolClass and machineDeploymentClass should be set + machineDeploymentClass: { + type: String, + default: null + }, + + machinePoolClass: { + type: String, + default: null } }, @@ -38,6 +53,7 @@ export default defineComponent({ this.$emit('validation-passed', !neu); }, 5), }, + variableDefinitions(neu, old) { this.updateVariableDefaults(neu, old); this.$nextTick(() => { @@ -52,9 +68,55 @@ export default defineComponent({ computed: { variableDefinitions() { - return this.clusterClass?.spec?.variables || []; + const allVariableDefinitions = this.clusterClass?.spec?.variables || []; + + if (!this.machineDeploymentClass && !this.machinePoolClass) { + return allVariableDefinitions; + } + const variableNames = this.machineScopedJsonPatches.reduce((names, patch) => { + const valueFromVariable = patch?.valueFrom?.variable; + + if (!valueFromVariable) { + return names; + } + + // the value here could be a field or index of the variable, defined . or [i] + const parsedName = valueFromVariable.split(/\.|\[/)[0]; + + if (parsedName !== 'builtin') { + names.push(parsedName); + } + + return names; + }, []); + + return allVariableDefinitions.filter((v: ClusterClassVariable) => variableNames.includes(v.name)); }, + machineScopedJsonPatches() { + if (!this.machineDeploymentClass && !this.machinePoolClass) { + return []; + } + const out = [] as any[]; + const matchName = this.machineDeploymentClass || this.machinePoolClass; + const matchKey = this.machineDeploymentClass ? 'machineDeploymentClass' : 'machinePoolClass'; + + const patches = this.clusterClass?.spec?.patches || []; + + patches.forEach((p) => { + const definitions = p?.definitions || []; + + definitions.forEach((definition: any) => { + const matchMachines = definition?.selector?.matchResources?.[matchKey]?.names || []; + + if (matchMachines.includes(matchName)) { + out.push(...definition.jsonPatches); + } + }); + }); + + return out; + }, }, methods: { @@ -90,12 +152,15 @@ export default defineComponent({ } const oldDefault = (old || []).find(d => d.name === existingVar.name)?.schema?.openAPIV3Schema?.default; - if (oldDefault && existingVar.value === oldDefault) { + if (isDefined(oldDefault) && existingVar.value === oldDefault) { delete existingVar.value; } - const newDefault = neuDef.schema?.openAPIV3Schema?.default; + let newDefault = neuDef.schema?.openAPIV3Schema?.default; - if (newDefault && !existingVar.value) { + if (neuDef.schema?.openAPIV3Schema?.type === 'boolean' && !newDefault) { + newDefault = false; + } + if (isDefined(newDefault) && !existingVar.value) { existingVar.value = newDefault; } acc.push(existingVar); @@ -104,9 +169,12 @@ export default defineComponent({ }, []); neu.forEach((def) => { - const newDefault = def.schema?.openAPIV3Schema?.default; + let newDefault = def.schema?.openAPIV3Schema?.default; - if (newDefault && !out.find(v => v.name === def.name)) { + if (def.schema?.openAPIV3Schema?.type === 'boolean' && !newDefault) { + newDefault = false; + } + if (isDefined(newDefault) && !out.find(v => v.name === def.name)) { out.push({ name: def.name, value: newDefault }); } }); diff --git a/pkg/capi/edit/ClusterConfig.vue b/pkg/capi/edit/ClusterConfig.vue new file mode 100644 index 0000000..d1f677d --- /dev/null +++ b/pkg/capi/edit/ClusterConfig.vue @@ -0,0 +1,451 @@ + + diff --git a/pkg/capi/edit/ControlPlaneEndpointSection.vue b/pkg/capi/edit/ControlPlaneEndpointSection.vue new file mode 100644 index 0000000..e54cad1 --- /dev/null +++ b/pkg/capi/edit/ControlPlaneEndpointSection.vue @@ -0,0 +1,59 @@ + + diff --git a/pkg/capi/edit/NetworkSection.vue b/pkg/capi/edit/NetworkSection.vue new file mode 100644 index 0000000..62f7f07 --- /dev/null +++ b/pkg/capi/edit/NetworkSection.vue @@ -0,0 +1,87 @@ + + diff --git a/pkg/capi/edit/WorkerItem.vue b/pkg/capi/edit/WorkerItem.vue new file mode 100644 index 0000000..38d8e04 --- /dev/null +++ b/pkg/capi/edit/WorkerItem.vue @@ -0,0 +1,253 @@ + + diff --git a/pkg/capi/edit/cluster.x-k8s.io.cluster.vue b/pkg/capi/edit/cluster.x-k8s.io.cluster.vue index 4fd6d84..8963714 100644 --- a/pkg/capi/edit/cluster.x-k8s.io.cluster.vue +++ b/pkg/capi/edit/cluster.x-k8s.io.cluster.vue @@ -1,58 +1,178 @@ diff --git a/pkg/capi/index.ts b/pkg/capi/index.ts index bac72d4..34b60d5 100644 --- a/pkg/capi/index.ts +++ b/pkg/capi/index.ts @@ -38,36 +38,36 @@ export default function(plugin: IPlugin): void { ); // add enable auto-import action to namespace table - plugin.addAction(ActionLocation.TABLE, - { path: [{ urlPath: '/c/local/explorer/projectsnamespaces', exact: true }, { urlPath: 'cluster.x-k8s.io.cluster', endsWith: true }] }, - { - labelKey: 'capi.autoImport.enableAction', - icon: 'icon-plus', - enabled(target: any) { - return target.metadata.labels[LABELS.AUTO_IMPORT] !== 'true'; - }, - invoke(opts, resources = []) { - resources.forEach((ns) => { - toggleAutoImport(ns); - }); - } - }); + // plugin.addAction(ActionLocation.TABLE, + // { path: [{ urlPath: '/c/local/explorer/projectsnamespaces', exact: true }, { urlPath: 'cluster.x-k8s.io.cluster', endsWith: true }] }, + // { + // labelKey: 'capi.autoImport.enableAction', + // icon: 'icon-plus', + // enabled(target: any) { + // return target.metadata.labels[LABELS.AUTO_IMPORT] !== 'true'; + // }, + // invoke(opts, resources = []) { + // resources.forEach((ns) => { + // toggleAutoImport(ns); + // }); + // } + // }); // add disable auto-import action to namespace table - plugin.addAction(ActionLocation.TABLE, - { path: [{ urlPath: '/c/local/explorer/projectsnamespaces', exact: true }, { urlPath: 'cluster.x-k8s.io.cluster', endsWith: true }] }, - { - labelKey: 'capi.autoImport.disableAction', - icon: 'icon-minus', - enabled(target: any) { - return target.metadata.labels[LABELS.AUTO_IMPORT] === 'true'; - }, - invoke(opts, resources = []) { - resources.forEach((ns) => { - toggleAutoImport(ns); - }); - } - }); + // plugin.addAction(ActionLocation.TABLE, + // { path: [{ urlPath: '/c/local/explorer/projectsnamespaces', exact: true }, { urlPath: 'cluster.x-k8s.io.cluster', endsWith: true }] }, + // { + // labelKey: 'capi.autoImport.disableAction', + // icon: 'icon-minus', + // enabled(target: any) { + // return target.metadata.labels[LABELS.AUTO_IMPORT] === 'true'; + // }, + // invoke(opts, resources = []) { + // resources.forEach((ns) => { + // toggleAutoImport(ns); + // }); + // } + // }); // add column to namespace table plugin.addTableColumn( diff --git a/pkg/capi/l10n/en-us.yaml b/pkg/capi/l10n/en-us.yaml index ef4d22e..f5ec018 100644 --- a/pkg/capi/l10n/en-us.yaml +++ b/pkg/capi/l10n/en-us.yaml @@ -12,6 +12,52 @@ capi: disableAction: Disable CAPI Auto-Import warnings: embeddedFeatureFlag: "It looks like the Rancher-managed cluster API feature is disabled. To provision and manage RKE2 clusters you must either enable the embedded-cluster-api feature flag or install the Rancher Turtles extension." + cluster: + steps: + basics: + title: Basics + label: Basics + subtext: + description: '' + configuration: + title: Configuration + label: Configuration + subtext: + description: '' + secret: + reuse: Use existing credential + create: Create new credential + controlPlane: + title: Control Plane Options + provisioner: Type + providerConfig: + title: Infrastructure + clusterClass: + title: Cluster Class + label: Cluster Class + description: Cluster Class Description + variables: + title: Variables + version: + title: Kubernetes Version + networking: + title: Networking + apiServerPort: API Server Port + serviceDomain: Service Domain + pods: Pod CIDR Blocks + services: Service VIP CIDR Blocks + controlPlaneEndpoint: + title: Control Plane Endpoint + host: Host + port: Port + workers: + title: Workers + class: Class + name: Name + machineDeployments: + title: Machine Deployments + machinePools: + title: Machine Pools nav: group: @@ -45,3 +91,4 @@ validation: minItems: '"{key}" must contain at least {minItems} items.' pattern: '"{key}" must match the pattern {pattern}.' uniqueItems: '"{key}" may not contain duplicate elements.' + version: Version format must match format for this provisioner. diff --git a/pkg/capi/types/capi.ts b/pkg/capi/types/capi.ts index f75f6fe..abbdf5e 100644 --- a/pkg/capi/types/capi.ts +++ b/pkg/capi/types/capi.ts @@ -8,3 +8,58 @@ export const CAPI = { CLUSTER_CLASS: 'cluster.x-k8s.io.clusterclass', PROVIDER: 'operator.cluster.x-k8s.io.infrastructureprovider', }; + +export const CP_VERSIONS = { + 'kubekey-k3s': ['k3s1', 'k3s2'], + rke2: ['rke2r1', 'rke2r2'] +}; + +export const CREDENTIALS_UPDATE_REQUIRED = ['aks']; +export const CREDENTIALS_NOT_REQUIRED = ['docker']; +export interface Worker { + name: String, + class: String +} + +export interface CAPIClusterTopology { + version: String, + class: String, + workers: { + machineDeployments: Worker[], + machinePools: Worker[] + } +} +export interface CAPIClusterCPEndpoint { + host: String, + port: Number +} + +export interface CAPIClusterNetwork { + apiServerPort?: Number, + pods?: { + cidrBlocks: String[] + }, + serviceDomain?: String, + services?: { + cidrBlocks: String[] + }, +} + +export interface ClusterClass { + metadata: { + name: String, + annotations: Object + }, + spec: { + infrastructure: Object, + workers: Object, + controlPlane: Object + } +} + +export interface InfrastructureProvider { + metadata: { + name: String, + annotations: Object + } +} diff --git a/pkg/capi/util/validators.ts b/pkg/capi/util/validators.ts index ee7717e..bb79b32 100644 --- a/pkg/capi/util/validators.ts +++ b/pkg/capi/util/validators.ts @@ -1,5 +1,7 @@ import { Validator, ValidationOptions } from '@shell/utils/validators/formRules'; import { Translation } from '@shell/types/t'; +import isEmpty from 'lodash/isEmpty'; +import { CP_VERSIONS } from '@pkg/capi/types/capi'; // const stringFormats = { // // this is a mongodb id - requires library to validate? @@ -117,3 +119,23 @@ export const openAPIV3SchemaValidators = function(t: Translation, { key = 'Value return out; }; + +export const isDefined = (val: any) => (val || val === false) && !isEmpty(val); +export const versionTest = function(t: Translation, type: String): RegExp { + let ending = ''; + + if (CP_VERSIONS[type]) { + ending = `\\+(${ CP_VERSIONS[type].join('|') })`; + } + + return new RegExp(`^v(\\d+.){2}\\d+${ ending }$`); +}; + +export const versionValidator = function(t: Translation, type: String): Validator[] { + const out = [] as any[]; + const test = versionTest(t, type); + + out.push((val: String) => val && !val.match(test) ? t('validation.version') : undefined); + + return out; +};