diff --git a/backend/lib/routes/config.js b/backend/lib/routes/config.js index 302a4e07cf..a8b6fe850c 100644 --- a/backend/lib/routes/config.js +++ b/backend/lib/routes/config.js @@ -58,7 +58,8 @@ function sanitizeFrontendConfig (frontendConfig) { } = {}, vendorHints = [], resourceQuotaHelp = '', - controlPlaneHighAvailabilityHelp = '' + controlPlaneHighAvailabilityHelp = '', + customCloudProviders = {} } = sanitizedFrontendConfig convertAndSanitize(alert, 'message') @@ -92,5 +93,12 @@ function sanitizeFrontendConfig (frontendConfig) { convertAndSanitize(vendorHint, 'message') } + for (const key of Object.keys(customCloudProviders)) { + const secret = _.get(customCloudProviders, [key, 'secret']) + if (secret) { + convertAndSanitize(secret, 'help') + } + } + return sanitizedFrontendConfig } diff --git a/backend/lib/services/cloudprofiles.js b/backend/lib/services/cloudprofiles.js index 619ea77cf9..0c02429380 100644 --- a/backend/lib/services/cloudprofiles.js +++ b/backend/lib/services/cloudprofiles.js @@ -8,7 +8,6 @@ const { NotFound, Forbidden } = require('http-errors') const authorization = require('./authorization') -const logger = require('../logger') const _ = require('lodash') const { getCloudProfiles, getVisibleAndNotProtectedSeeds } = require('../cache') @@ -65,17 +64,7 @@ exports.list = async function ({ user }) { const cloudProfiles = getCloudProfiles() const seeds = getVisibleAndNotProtectedSeeds() - return _ - .chain(cloudProfiles) - .map(assignSeedsToCloudProfileIteratee(seeds)) - .filter(cloudProfile => { - if (!_.isEmpty(cloudProfile.data.seedNames)) { - return true - } - logger.info(`No matching seed for cloud profile with name ${cloudProfile.metadata.name} found`) - return false - }) - .value() + return _.map(cloudProfiles, assignSeedsToCloudProfileIteratee(seeds)) } exports.read = async function ({ user, name }) { diff --git a/backend/test/acceptance/__snapshots__/api.cloudprofiles.spec.js.snap b/backend/test/acceptance/__snapshots__/api.cloudprofiles.spec.js.snap index 612aaad4a8..468d29bb46 100644 --- a/backend/test/acceptance/__snapshots__/api.cloudprofiles.spec.js.snap +++ b/backend/test/acceptance/__snapshots__/api.cloudprofiles.spec.js.snap @@ -85,6 +85,27 @@ exports[`api cloudprofiles should return all cloudprofiles 2`] = ` "name": "infra1-profileName2", }, }, + { + "data": { + "kubernetes": { + "versions": [ + { + "version": "1.9.0", + }, + { + "version": "1.8.5", + }, + ], + }, + "seedSelector": {}, + "type": "infra2", + }, + "metadata": { + "cloudProviderKind": "infra2", + "displayName": "infra2-profileName", + "name": "infra2-profileName", + }, + }, { "data": { "kubernetes": { diff --git a/charts/__tests__/gardener-dashboard/runtime/dashboard/configmap.spec.js b/charts/__tests__/gardener-dashboard/runtime/dashboard/configmap.spec.js index e9df7cf1e6..92d3a00408 100644 --- a/charts/__tests__/gardener-dashboard/runtime/dashboard/configmap.spec.js +++ b/charts/__tests__/gardener-dashboard/runtime/dashboard/configmap.spec.js @@ -824,6 +824,115 @@ describe('gardener-dashboard', function () { }) }) + describe('cloudProviderList', function () { + it('should render the template with cloudProviderList', async function () { + const values = { + global: { + dashboard: { + frontendConfig: { + cloudProviderList: [ + 'foo', + 'bar' + ] + } + } + } + } + const documents = await renderTemplates(templates, values) + expect(documents).toHaveLength(1) + const [configMap] = documents + const config = yaml.load(configMap.data['config.yaml']) + expect(pick(config, ['frontend.cloudProviderList'])).toMatchSnapshot() + }) + }) + + describe('customCloudProviders', function () { + it('should render the template with customCloudProviders', async function () { + const values = { + global: { + dashboard: { + frontendConfig: { + customCloudProviders: { + fooProvider: { + zoned: false, + shoot: { + createFields: [ + { + key: 'selectFoo', + path: 'spec.provider', + hint: 'Select foo value', + label: 'Select Foo', + type: 'select', + validators: { + required: { + type: 'required' + } + }, + values: { + cloudprofilePath: 'data.foo', + key: 'name' + } + } + ], + specTemplate: { + provider: { + type: 'custom' + } + } + }, + secret: { + fields: [ + { + key: 'namespace', + hint: 'Enter a valid namespace', + label: 'Namespace', + type: 'text', + validators: { + required: { + type: 'required' + } + } + } + ], + help: '#Custom Cloud Provider\nfoo' + } + } + } + } + } + } + } + const documents = await renderTemplates(templates, values) + expect(documents).toHaveLength(1) + const [configMap] = documents + const config = yaml.load(configMap.data['config.yaml']) + expect(pick(config, ['frontend.customCloudProviders'])).toMatchSnapshot() + }) + }) + + describe('vendors', function () { + it('should render the template with vendors', async function () { + const values = { + global: { + dashboard: { + frontendConfig: { + vendors: { + foo: { + icon: 'foo_icon.svg' + } + } + } + } + } + } + const documents = await renderTemplates(templates, values) + expect(documents).toHaveLength(1) + const [configMap] = documents + const config = yaml.load(configMap.data['config.yaml']) + expect(pick(config, ['frontend.vendors'])).toMatchSnapshot() + }) + }) + describe('knownConditions', function () { it('should render the template with knownConditions markdown', async function () { const values = { diff --git a/charts/gardener-dashboard/charts/runtime/templates/dashboard/configmap-vendor-assets.yaml b/charts/gardener-dashboard/charts/runtime/templates/dashboard/configmap-vendor-assets.yaml new file mode 100644 index 0000000000..c8b426157d --- /dev/null +++ b/charts/gardener-dashboard/charts/runtime/templates/dashboard/configmap-vendor-assets.yaml @@ -0,0 +1,20 @@ +{{- if .Values.global.dashboard.enabled }} +{{- if .Values.global.dashboard.frontendConfig.vendorAssets }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: dashboard-vendor-assets + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: gardener-dashboard + app.kubernetes.io/component: dashboard + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + app.kubernetes.io/instance: "{{ .Release.Name }}" + app.kubernetes.io/managed-by: "{{ .Release.Service }}" +binaryData: +{{- range $file, $content := .Values.global.dashboard.frontendConfig.vendorAssets }} + {{ $file }}: | + {{ $content }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/gardener-dashboard/charts/runtime/templates/dashboard/configmap.yaml b/charts/gardener-dashboard/charts/runtime/templates/dashboard/configmap.yaml index b3654196d2..1c5db4f3cf 100644 --- a/charts/gardener-dashboard/charts/runtime/templates/dashboard/configmap.yaml +++ b/charts/gardener-dashboard/charts/runtime/templates/dashboard/configmap.yaml @@ -346,6 +346,18 @@ data: {{- if .Values.global.dashboard.frontendConfig.serviceAccountDefaultTokenExpiration }} serviceAccountDefaultTokenExpiration: {{ .Values.global.dashboard.frontendConfig.serviceAccountDefaultTokenExpiration }} {{- end }} + {{- if .Values.global.dashboard.frontendConfig.cloudProviderList }} + cloudProviderList: + {{- range .Values.global.dashboard.frontendConfig.cloudProviderList }} + - {{ . }} + {{- end }} + {{- end }} + {{- if .Values.global.dashboard.frontendConfig.customCloudProviders }} + customCloudProviders: {{ toYaml .Values.global.dashboard.frontendConfig.customCloudProviders | nindent 8 }} + {{- end }} + {{- if .Values.global.dashboard.frontendConfig.vendors }} + vendors: {{ toYaml .Values.global.dashboard.frontendConfig.vendors | nindent 8 }} + {{- end }} {{- if .Values.global.dashboard.frontendConfig.knownConditions }} knownConditions: {{ toYaml .Values.global.dashboard.frontendConfig.knownConditions | nindent 8 }} {{- end }} diff --git a/charts/gardener-dashboard/charts/runtime/templates/dashboard/deployment.yaml b/charts/gardener-dashboard/charts/runtime/templates/dashboard/deployment.yaml index 4a1dc78cd0..0df4b9a46c 100644 --- a/charts/gardener-dashboard/charts/runtime/templates/dashboard/deployment.yaml +++ b/charts/gardener-dashboard/charts/runtime/templates/dashboard/deployment.yaml @@ -81,6 +81,12 @@ spec: name: dashboard-assets defaultMode: 0444 {{- end }} + {{- if .Values.global.dashboard.frontendConfig.vendorAssets }} + - name: vendor-assets + configMap: + name: dashboard-vendor-assets + defaultMode: 0444 + {{- end }} {{- if .Values.global.dashboard.oidc }} - name: gardener-dashboard-oidc secret: @@ -206,6 +212,10 @@ spec: - name: assets mountPath: /app/public/static/assets {{- end }} + {{- if .Values.global.dashboard.frontendConfig.vendorAssets }} + - name: vendor-assets + mountPath: /app/public/static/vendor-assets + {{- end }} {{- if .Values.global.dashboard.serviceAccountTokenVolumeProjection.enabled }} - name: service-account-token mountPath: /var/run/secrets/projected/serviceaccount diff --git a/charts/gardener-dashboard/values.yaml b/charts/gardener-dashboard/values.yaml index e6cf9b3bd8..b2cae21e43 100644 --- a/charts/gardener-dashboard/values.yaml +++ b/charts/gardener-dashboard/values.yaml @@ -363,6 +363,52 @@ global: # resourceQuotaHelp: # text: Help text + # # cloudProviderList - configure available cloud providers. This allows to change order and to add additional (not built-in providers), see also customCloudProviders + # cloudProviderList: + # - aws + # - custom-provider + + # # customCloudProviders - configure additional cloud providers + # customCloudProviders: + # custom-provider: + # zoned: true + # shoot: + # specTemplate: # shoot template for this provider + # provider: + # type: custom-provider + # networking: + # nodes: ${workerCIDR} + # createFields: + # - key: foo + # path: spec.provider.extra + # hint: Enter Additional data + # label: Additional Information + # type: text + # validators: + # required: + # type: required + # secret: # secret dialog + # fields: + # - key: user + # hint: Enter a valid user + # label: User + # type: text + # validators: + # required: + # type: required + # validationErrors: + # required: User is required + # help: | # help text for secret dialog + # #Foo Cloud Provider + + # # vendors - configure additional or overwrite built-in vendor icons + # vendors: + # custom-provider: + # icon: custom-provider.svg + # + # vendorAssets: + # custom-provider.svg: | + # # controlPlaneHighAvailabilityHelp - configure help text for control plane high availability, control plane pricing etc. # controlPlaneHighAvailabilityHelp: # text: Help text diff --git a/docs/README.md b/docs/README.md index bbbacc92a5..ad0ae64328 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,6 +18,7 @@ * [Configure Access Restrictions](operations/access-restrictions.md) * [Theming and Branding](operations/customization.md) +* [Configure Vendors and Custom Cloud Providers](operations/vendors-custom-cps.md) * [Webterminals](operations/webterminals.md) ## Development diff --git a/docs/operations/vendors-custom-cps.md b/docs/operations/vendors-custom-cps.md new file mode 100644 index 0000000000..1c6d7c9a61 --- /dev/null +++ b/docs/operations/vendors-custom-cps.md @@ -0,0 +1,234 @@ +# Configure Vendors and Custom Cloud Providers + +## Motivation +Gardener landscape administrators should have the possibility to change icons and names of vendors (cloud providers, image vendors, etc.). This enables the administrator to adjust vendors and apply company or context-specific values. An example of use could be a locally hosted OpenStack infrastructure that should appear as `MyCustomCloud` with a custom icon in the Dashboard, so that the user immediately identifies it as `MyCustomCloud` as they might not be familiar with the term `OpenStack`. + +## Vendor Configuration +It is possible to change the name and icon of built-in vendors in the Gardener Dashboard (e.g., AWS, Ubuntu, etc.) when using the [helm chart](https://github.com/gardener/dashboard/blob/master/charts/gardener-dashboard) in the `frontend.vendors` map. You can add a key for each vendor that you want to customize. The key equals the vendor type. The following configuration properties are supported: + +| name | description | +| ---- | ----------- | +| `name` | Display name of the vendor | +| `icon` | Asset name as defined in the vendor asset map. You can also use the [data:](https://developer.mozilla.org/en-US/docs/web/http/basics_of_http/data_urls) scheme for development. For production, it is recommended to provide static assets | + +Besides overwriting built-in vendors, you can also add additional vendors that the Dashboard does not (yet) have built-in support for. For example, additional machine image vendors or a cloud provider that you have defined via [cloud provider configuration](#configure-available-cloud-providers). + +## Configure Available Cloud Providers +The Gardener Dashboard has a list of built-in cloud providers: +`'aws', 'azure', 'gcp', 'openstack', 'alicloud', 'metal', 'vsphere', 'hcloud', 'onmetal', 'local'` + +The Dashboard renders available cloud providers in the order defined by the list. +> **_NOTE:_** A cloud provider is considered `available` if at least one cloud profile is defined for a Gardener landscape. If you add custom cloud providers, they will only appear if you have configured a cloud profile in your landscape. + +You can overwrite the list by providing the `frontend.cloudProviderList` array using the [helm chart](https://github.com/gardener/dashboard/blob/master/charts/gardener-dashboard). +This way, you can change the order or hide cloud providers from the Gardener Dashboard by omitting them from the list. You can also add additional (custom) cloud providers. You can overwrite the appearance of the custom cloud providers via [vendor configuration](#vendor-configuration). For information on how to configure a custom cloud provider, see [custom cloud providers](#custom-cloud-providers). + +## Custom Cloud Providers +Custom Cloud Providers are provider types already suppored by Gardener (Gardener Provider Extension exists) but that are not (yet) built into the Dashboard. +You can add custom cloud providers and define the secret dialog input fields as well as add a default shoot spec where you can provide values that are required for the shoot resource. You can also define input fields that are added to the `Infrastructure Details` section of the create cluster page. These fields can be used if a user needs to provide infrastructure-specific data for a shoot cluster. All values for the custom cloud provider can be configured using the helm chart in the `frontend.customCloudProviders` map. You can add a key for each vendor that you want to customize. The key equals the custom cloud provider type. The following configuration properties are supported: + +| name | description | +| ---- | ----------- | +| `zoned` | Defines if this cloud provider is zoned. If not provided, defaults to `true` | +| `shoot.specTemplate` | Add default `shoot.spec` template. The provided yaml values are added to each new shoot resource. The worker CIDR can be used as a variable `${workerCIDR}` | +| `shoot.createFields` | Define infrastructure-specific input fields for the create cluster page. See below for more details. | +| `secret.fields` | Define secret input fields. See below for more details. If not provided, defaults to plain yaml object input | +| `secret.help` | `HTML` secret dialog help content | + +> **_NOTE:_** It is currently not possible to add custom DNS Providers. Only infrastructure providers are supported. + +### Custom Input Fields +It is possible to define input fields with data validation for the `Infrastructure Details` section on the create cluster page as well as for the infrastructure secret dialog. The array can contain field definitions with the following properties: +| name | description | +| ---- | ----------- | +| `key` | Unique key for the input field. Used as the secret data key if defined as part of infrastructure secret dialog. | +| `path` | Destination path in shoot spec for create fields. Only required for `Infrastructure Details` section. | +| `hint` | Input field hint. | +| `label` | Input field label. | +| `type` | Input field type. Supported values are detailed below. | +| `values` | List of values for the select input field. Details are provided below. | +| `validators` | Add a key for each validation. Each validator has a `type` property as well as additional properties depending on the type. See below for possible validation properties. | +| `validationErrors` | For each key defined in `validations`, add a key with an error message for the validation. | + +### Type +Supported input field types include: + +- `text`: Simple plain text input. +- `password`: Hidden secret data plain text input. +- `select`: List of values that the user can select. +- `select-multiple`: Allow to select multiple values from a list. +- `json`: Object data in JSON format. +- `yaml`: Object data in YAML input. + +### Values +For the `select` and `select-multiple` input field, the list of values can be: + +- `Array`: A simple list of values. +- `Object`: Use `{ cloudprofilePath, key }` - Source path in the cloud profile. If an object, use `key` to map values. + + +### Validators +| type | properties | description | +| ---- | ---------- | ----------- | +| `required` | `none` | Required input field | +| `requiredIf` | `not`: Optional array of field keys. Makes field required if one of the referenced input fields is empty. | Depends on other input field if required or not. Use `not` references to create *either or* scenarios, e.g., `username / password` *or* `token` is required | +| `regex` | `value`: JavaScript regex pattern | Input field must match the provided pattern. You can define arbitrary validations, e.g., length or specific syntax | +| `isValidObject` | `none` | Input value must be a valid object. Can be used for `json` and `yaml` field types to ensure valid data | + +You can add an optional `message` property to all validators to provide a custom error message. + +## Example +A complete custom cloud provider configuration example including required available cloud provider configuration as well as vendor configuration: + +```yaml +global: + dashboard: + frontendConfig: + cloudProviderList: + - aws + - my-cc + - gcp + customCloudProviders: + my-cc: + zoned: true + shoot: + createFields: + - key: extra + path: spec.provider.extra + hint: Enter Additional data + label: Additional Information + type: text + validators: + required: + type: required + isNamespace: + type: regex + value: .+.+--.+ + message: Must be valid extra data with format a--b + - key: cp_select + path: spec.provider + hint: Select a value from the cloud profile + label: Select Value + type: select + validators: + required: + type: required + values: + cloudprofilePath: data.foo.bar + key: name + - key: value_select + path: spec.provider + hint: Select at least one predefined value + label: Select Value + type: select-multiple + validators: + required: + type: required + values: + - foo + - bar + specTemplate: + provider: + type: my-cc + infrastructureConfig: + apiVersion: my-cc.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + vpc: + cidr: ${workerCIDR} + controlPlaneConfig: + apiVersion: my-cc.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + networking: + nodddes: ${workerCIDR} + secret: + fields: + - key: project + hint: Enter a valid project + label: Project + type: text + validators: + required: + type: required + isproject: + type: regex + value: .+.+--.+ + validationErrors: + required: Project is required + isproject: Must be a valid project with format a--b + - key: token + hint: Enter a valid token + label: Token + type: password + validators: + required: + type: requiredIf + not: + - user + - password + istoken: + type: regex + value: ^.{10}$ + validationErrors: + required: Token is required if no user / password provided + istoken: Must be a valid token with length 10 + - key: user + hint: Enter a valid User + label: User + type: text + validators: + required: + type: requiredIf + not: + - token + validationErrors: + required: User is required if no token provided + - key: password + hint: Enter a password + label: password + type: password + validators: + required: + type: requiredIf + not: + - token + validationErrors: + required: Password is required if no token provided + - key: json + hint: Enter additional Data as JSON + label: JSON + type: json + validators: + isJSON: + type: isValidObject + validationErrors: + isJSON: Data must be valid JSON + - key: yaml + hint: Enter additional Data as YAML + label: YAML + type: yaml + validators: + isYAML: + type: isValidObject + validationErrors: + isYAML: Data must be valid YAML + help: | + #My Custom Cloud Provider +
+ +
+ ## Project + Please enter a valid project with format a--b +
+ ## Token + Please enter a valid token with exactly 10 chars + vendors: + my-cc: + name: My CC + icon: cp_my-cc.jpg +``` + +## Vendor assets +In the above example, the vendor icon is referenced as static asset. Static vendor assets can be provided in the `static/vendor-assets` folder using in the `frontendConfig.vendorAssets` map. + +The files have to be encoded as base64 for the chart. For more information on how to provide assets using the chart, see [Logos and Icons](customization.md#logos-and-icons) section of customization documentation. diff --git a/frontend/src/components/GCloudProfile.vue b/frontend/src/components/GCloudProfile.vue index c001f17992..5e5a44954a 100644 --- a/frontend/src/components/GCloudProfile.vue +++ b/frontend/src/components/GCloudProfile.vue @@ -9,6 +9,7 @@ SPDX-License-Identifier: Apache-2.0 @@ -36,6 +39,8 @@ import { required } from '@vuelidate/validators' import { getErrorMessages } from '@/utils' import { withFieldName } from '@/utils/validators' +import { find } from '@/lodash' + export default { props: { modelValue: { @@ -74,6 +79,22 @@ export default { this.$emit('update:modelValue', value) }, }, + validators () { + return { + modelValue: { + required, + }, + } + }, + selectedCloudProfile () { + return find(this.cloudProfiles, { metadata: { name: this.modelValue } }) + }, + hint () { + if (this.selectedCloudProfile && !this.selectedCloudProfile.data.seedNames?.length) { + return 'This cloud profile does not have a matching seed. Gardener will not be able to schedule shoots using this cloud profile' + } + return '' + }, }, methods: { getErrorMessages, diff --git a/frontend/src/components/GGenericInputFields.vue b/frontend/src/components/GGenericInputFields.vue new file mode 100644 index 0000000000..4d5e9da0cf --- /dev/null +++ b/frontend/src/components/GGenericInputFields.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/frontend/src/components/GVendor.vue b/frontend/src/components/GVendor.vue index 29dc103d9c..aa4a2bed45 100644 --- a/frontend/src/components/GVendor.vue +++ b/frontend/src/components/GVendor.vue @@ -37,7 +37,7 @@ SPDX-License-Identifier: Apache-2.0 :icon="cloudProviderKind" class="mr-2" /> - {{ cloudProviderKind }} + {{ vendorName }} @@ -58,6 +58,10 @@ SPDX-License-Identifier: Apache-2.0 diff --git a/frontend/src/components/GVendorIcon.vue b/frontend/src/components/GVendorIcon.vue index 3a4e20c17c..f879436293 100644 --- a/frontend/src/components/GVendorIcon.vue +++ b/frontend/src/components/GVendorIcon.vue @@ -28,12 +28,19 @@ SPDX-License-Identifier: Apache-2.0 diff --git a/frontend/src/components/Secrets/GSecretDialogHCloud.vue b/frontend/src/components/Secrets/GSecretDialogHCloud.vue index b1c6989307..900c2d1ed7 100644 --- a/frontend/src/components/Secrets/GSecretDialogHCloud.vue +++ b/frontend/src/components/Secrets/GSecretDialogHCloud.vue @@ -10,8 +10,6 @@ SPDX-License-Identifier: Apache-2.0 :data="secretData" :secret-validations="v$" :secret="secret" - create-title="Add new Hetzner Cloud Secret" - replace-title="Replace Hetzner Cloud Secret" >