diff --git a/.images/cap-op-plugin-convert-to-configurable-templates.gif b/.images/cap-op-plugin-convert-to-configurable-templates.gif new file mode 100644 index 0000000..54df985 Binary files /dev/null and b/.images/cap-op-plugin-convert-to-configurable-templates.gif differ diff --git a/.images/cds-add-cap-operator-with-configurable-templates.gif b/.images/cds-add-cap-operator-with-configurable-templates.gif new file mode 100644 index 0000000..6854994 Binary files /dev/null and b/.images/cds-add-cap-operator-with-configurable-templates.gif differ diff --git a/README.md b/README.md index abc43cc..db87ef2 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,17 @@ To integrate the CAP Operator Plugin into your project, follow these steps: ``` ![](.images/cds-add-cap-operator-with-templates.gif) + * `--with-configurable-templates` + + With this option, the plugin adds a chart with configurable templates. This is required when applications need to utilize template functions in the CAP Operator resources. In this version of the chart, all the CAP Operator resource configurations are defined in `templates/cap-operator-cros.yaml`. If you choose this option, you can skip the `cds build` step since the chart already contains the `templates` folder. + + ```sh + cds add cap-operator --with-configurable-templates + ``` + ![](.images/cds-add-cap-operator-with-configurable-templates.gif) + + > If you have already added your chart and want to switch to `--with-configurable-templates`, you can use the plugin to convert the existing chart. For more information, see the [Converting to Configurable Templates](#converting-to-configurable-templates-chart) section. + * `--force` This option allows you to overwrite the existing chart folder. @@ -73,18 +84,23 @@ To integrate the CAP Operator Plugin into your project, follow these steps: 3. Once you've executed the command above, the basic chart folder or chart folder with templates is added to your project directory, depending on your choice. -## Configure the Plugin +## Configure Your Chart The generated `chart/values.yaml` contains two types of information: * Design-time deployment - - [serviceInstances](https://github.com/SAP/sap-btp-service-operator?tab=readme-ov-file#service-instance) - - [serviceBindings](https://github.com/SAP/sap-btp-service-operator?tab=readme-ov-file#service-binding) - - workloads - There are two types of workloads: - - [Deployment definition](https://sap.github.io/cap-operator/docs/usage/resources/capapplicationversion/#workloads-with-deploymentdefinition) - - [Job definition](https://sap.github.io/cap-operator/docs/usage/resources/capapplicationversion/#workloads-with-jobdefinition) - - [tenantOperations](https://sap.github.io/cap-operator/docs/usage/resources/capapplicationversion/#sequencing-tenant-operations) - - [contentJobs](https://sap.github.io/cap-operator/docs/usage/resources/capapplicationversion/#sequencing-content-jobs) + * Without option `--with-configurable-templates` + - [serviceInstances](https://github.com/SAP/sap-btp-service-operator?tab=readme-ov-file#service-instance) + - [serviceBindings](https://github.com/SAP/sap-btp-service-operator?tab=readme-ov-file#service-binding) + - workloads - There are two types of workloads: + - [Deployment definition](https://sap.github.io/cap-operator/docs/usage/resources/capapplicationversion/#workloads-with-deploymentdefinition) + - [Job definition](https://sap.github.io/cap-operator/docs/usage/resources/capapplicationversion/#workloads-with-jobdefinition) + - [tenantOperations](https://sap.github.io/cap-operator/docs/usage/resources/capapplicationversion/#sequencing-tenant-operations) + - [contentJobs](https://sap.github.io/cap-operator/docs/usage/resources/capapplicationversion/#sequencing-content-jobs) + * With option `--with-configurable-templates` + - [serviceInstances](https://github.com/SAP/sap-btp-service-operator?tab=readme-ov-file#service-instance) + - [serviceBindings](https://github.com/SAP/sap-btp-service-operator?tab=readme-ov-file#service-binding) + - workloads - With this option all the workload configuations are maintained in `templates/cap-operator-cros.yaml` and in the `values.yaml` you can only define the images for the workloads. * Runtime deployment - app @@ -111,7 +127,7 @@ The generated `chart/values.yaml` contains two types of information: ![](.images/cds-build.gif) - > If you've already added the `templates` folder during the initial plugin call using `--with-templates` option, you can skip this step as the Helm chart is already complete. + > If you've already added the `templates` folder during the initial plugin call using `--with-templates` or `--with-configurable-templates` option, you can skip this step as the Helm chart is already complete. 2. Up to this point, you've only filled in the design time information in the chart. But to deploy the application, you need to create a `runtime-values.yaml` file with all the runtime values, as mentioned in the section on configuration. You can generate the file using the plugin itself. @@ -175,6 +191,21 @@ The generated `chart/values.yaml` contains two types of information: helm upgrade -i -n /gen/chart --set-file serviceInstances.xsuaa.jsonParameters=/xs-security.json -f /chart/runtime-values.yaml ``` +## Converting to Configurable Templates Chart + +If you've already added the basic chart folder and want to switch to configurable templates chart, you can use the plugin to convert the chart. To do so, run the following command: + +```sh +npx cap-op-plugin convert-to-configurable-template-chart +``` + +If you want to convert the existing `runtime-values.yaml` as well to the new format, you can do so by passing the `runtime-values.yaml` path via the `--with-runtime-yaml` option: + +```sh +npx cap-op-plugin convert-to-configurable-template-chart --with-runtime-yaml /chart/runtime-values.yaml +``` +![](.images/cap-op-plugin-convert-to-configurable-templates.gif) + ## Example As a reference, check out the [CAP Operator Helm chart](https://github.com/cap-js/incidents-app/tree/cap-operator-plugin/chart) in the sample incident app. Also, take a look at the corresponding [runtime-values.yaml](https://github.com/cap-js/incidents-app/blob/cap-operator-plugin/chart/runtime-values.yaml) file. diff --git a/bin/cap-op-plugin.js b/bin/cap-op-plugin.js index d17b0cd..418919c 100755 --- a/bin/cap-op-plugin.js +++ b/bin/cap-op-plugin.js @@ -7,18 +7,35 @@ const yaml = require('@sap/cds-foss').yaml const Mustache = require('mustache') const { spawn } = require('child_process') -const { ask, mergeObj, isCAPOperatorChart } = require('../lib/util') +const { ask, mergeObj, isCAPOperatorChart, isConfigurableTemplateChart, transformValuesAndFillCapOpCroYaml } = require('../lib/util') -const SUPPORTED = { 'generate-runtime-values': ['--with-input-yaml'] } +const SUPPORTED = { 'generate-runtime-values': ['--with-input-yaml'], 'convert-to-configurable-template-chart': ['--with-runtime-yaml'] } -async function capOperatorPlugin(cmd, option, inputYamlPath) { +async function capOperatorPlugin(cmd, option, yamlPath) { try { if (!cmd) return _usage() if (!Object.keys(SUPPORTED).includes(cmd)) return _usage(`Unknown command ${cmd}.`) if (option && !SUPPORTED[cmd].includes(option)) return _usage(`Invalid option ${option}.`) - if (option === '--with-input-yaml' && !inputYamlPath) return _usage(`Input yaml path is missing.`) - if (cmd === 'generate-runtime-values') await generateRuntimeValues(option, inputYamlPath) + if (cmd === 'generate-runtime-values') { + if (option === '--with-input-yaml' && !yamlPath) + return _usage(`Input yaml path is missing.`) + + if (option === '--with-input-yaml' && !yamlPath && cds.utils.exists(cds.utils.path.join(cds.root,yamlPath))) + return _usage(`Input yaml path ${yamlPath} does not exist.`) + + await generateRuntimeValues(option, yamlPath) + } + + if (cmd === 'convert-to-configurable-template-chart') { + if (option === '--with-runtime-yaml' && !yamlPath) + return _usage(`Input runtime yaml path is missing.`) + + if (option === '--with-runtime-yaml' && !yamlPath && cds.utils.exists(cds.utils.path.join(cds.root,yamlPath))) + return _usage(`Input runtime yaml path ${yamlPath} does not exist.`) + + await convertToconfigurableTemplateChart(option, yamlPath) + } } catch (e) { if (isCli) { console.error(e.message) @@ -46,14 +63,74 @@ COMMANDS generate-runtime-values [--with-input-yaml ] Generate runtime-values.yaml file for the cap-operator chart + convert-to-configurable-template-chart [--with-runtime-yaml ] Convert existing chart to configurable template chart + EXAMPLES cap-op-plugin generate-runtime-values cap-op-plugin generate-runtime-values --with-input-yaml /path/to/input.yaml + + cap-op-plugin convert-to-configurable-template-chart + cap-op-plugin convert-to-configurable-template-chart --with-runtime-yaml /path/to/runtime.yaml ` ) } +async function transformRuntimeValues(runtimeYamlPath) { + console.log('Transforming runtime values file '+ cds.utils.path.join(cds.root,runtimeYamlPath) + ' to the configurable template chart format.') + let runtimeYaml = yaml.parse(await cds.utils.read(cds.utils.path.join(cds.root, runtimeYamlPath))) + if (runtimeYaml?.workloads?.server?.deploymentDefinition?.env) { + const index = runtimeYaml.workloads.server.deploymentDefinition.env.findIndex(e => e.name === 'CDS_CONFIG') + if (index > -1) { + const cdsConfigValueJson = JSON.parse(runtimeYaml.workloads.server.deploymentDefinition.env[index].value) + if (cdsConfigValueJson?.requires?.['cds.xt.DeploymentService']?.hdi?.create?.database_id){ + runtimeYaml['hanaInstanceId'] = cdsConfigValueJson.requires['cds.xt.DeploymentService'].hdi.create.database_id + delete runtimeYaml['workloads'] + await cds.utils.write(yaml.stringify(runtimeYaml)).to(cds.utils.path.join(cds.root, runtimeYamlPath)) + } + } + } +} + +async function isRuntimeValueAlreadyTransformed(runtimeYamlPath) { + let runtimeYaml = yaml.parse(await cds.utils.read(cds.utils.path.join(cds.root, runtimeYamlPath))) + return !!runtimeYaml['hanaInstanceId'] +} + +async function convertToconfigurableTemplateChart(option, runtimeYamlPath) { + if (!((cds.utils.exists('chart') && isCAPOperatorChart(cds.utils.path.join(cds.root,'chart'))))) + throw new Error("No CAP Operator chart found in the project. Please run 'cds add cap-operator --force' to add the CAP Operator chart folder.") + + if (isConfigurableTemplateChart(cds.utils.path.join(cds.root,'chart'))){ + console.log("Exisiting chart is already a configurable template chart. No need for conversion.") + if (option === '--with-runtime-yaml' && runtimeYamlPath && !(await isRuntimeValueAlreadyTransformed(runtimeYamlPath))) + await transformRuntimeValues(runtimeYamlPath) + else + console.log('Runtime values file '+ cds.utils.path.join(cds.root,runtimeYamlPath) + ' already in the configurable template chart format.') + return + } + + console.log('Converting chart '+cds.utils.path.join(cds.root,'chart')+' to configurable template chart.') + + // Copy templates + await cds.utils.copy(cds.utils.path.join(__dirname, '../files/configurableTemplatesChart/templates')).to(cds.utils.path.join(cds.root,'chart','templates')) + + // Copy values.schema.json + await cds.utils.copy(cds.utils.path.join(__dirname, '../files/configurableTemplatesChart/values.schema.json')).to(cds.utils.path.join(cds.root,'chart', 'values.schema.json')) + + // Add annotation to chart.yaml + const chartYaml = yaml.parse(await cds.utils.read(cds.utils.path.join(cds.root, 'chart/Chart.yaml'))) + chartYaml['annotations']['app.kubernetes.io/part-of'] = 'cap-operator-configurable-templates' + await cds.utils.write(yaml.stringify(chartYaml)).to(cds.utils.path.join(cds.root, 'chart/Chart.yaml')) + + // Transform + await transformValuesAndFillCapOpCroYaml() + + if (option === '--with-runtime-yaml' && runtimeYamlPath) { + await transformRuntimeValues(runtimeYamlPath) + } +} + async function generateRuntimeValues(option, inputYamlPath) { if (!((cds.utils.exists('chart') && isCAPOperatorChart(cds.utils.path.join(cds.root,'chart'))))) { throw new Error("No CAP Operator chart found in the project. Please run 'cds add cap-operator --force' to add the CAP Operator chart folder.") @@ -61,6 +138,7 @@ async function generateRuntimeValues(option, inputYamlPath) { let answerStruct = {} const { appName, appDescription } = getAppDetails() + const isConfigurableTempChart = isConfigurableTemplateChart(cds.utils.path.join(cds.root,'chart')) if (option === '--with-input-yaml' && inputYamlPath) { @@ -104,6 +182,17 @@ async function generateRuntimeValues(option, inputYamlPath) { if (!answerStruct['imagePullSecret']) delete runtimeValuesYaml['imagePullSecrets'] + if (isConfigurableTempChart && answerStruct['hanaInstanceId']) + runtimeValuesYaml['hanaInstanceId'] = answerStruct['hanaInstanceId'] + + if (!isConfigurableTempChart) + updateWorkloadEnv(runtimeValuesYaml, valuesYaml, answerStruct) + + await cds.utils.write(yaml.stringify(runtimeValuesYaml)).to(cds.utils.path.join(cds.root, 'chart/runtime-values.yaml')) + console.log("Generated 'runtime-values.yaml' file in the 'chart' folder.") +} + +function updateWorkloadEnv(runtimeValuesYaml, valuesYaml, answerStruct) { runtimeValuesYaml['workloads'] = {} for (const [workloadKey, workloadDetails] of Object.entries(valuesYaml.workloads)) { @@ -131,17 +220,10 @@ async function generateRuntimeValues(option, inputYamlPath) { // remove workload definition where env is empty for (const [workloadKey, workloadDetails] of Object.entries(runtimeValuesYaml.workloads)) { - if (workloadDetails?.deploymentDefinition?.env.length === 0) { - delete runtimeValuesYaml['workloads'][workloadKey] - } - - if (workloadDetails?.jobDefinition?.env.length === 0) { + if (workloadDetails?.deploymentDefinition?.env.length === 0 || workloadDetails?.jobDefinition?.env.length === 0) { delete runtimeValuesYaml['workloads'][workloadKey] } } - - await cds.utils.write(yaml.stringify(runtimeValuesYaml)).to(cds.utils.path.join(cds.root, 'chart/runtime-values.yaml')) - console.log("Generated 'runtime-values.yaml' file in the 'chart' folder.") } function getServiceInstanceKeyName(serviceInstances, offeringName) { @@ -193,8 +275,8 @@ async function getShootDomain() { } if (isCli) { - const [, , cmd, option, inputYamlPath] = process.argv; - (async () => await capOperatorPlugin(cmd, option, inputYamlPath ?? undefined))() + const [, , cmd, option, yamlPath] = process.argv; + (async () => await capOperatorPlugin(cmd, option, yamlPath ?? undefined))() } module.exports = { capOperatorPlugin } diff --git a/files/approuter.yaml.hbs b/files/approuter.yaml.hbs index 696c484..be76069 100644 --- a/files/approuter.yaml.hbs +++ b/files/approuter.yaml.hbs @@ -1,5 +1,5 @@ workloads: - app-router: + appRouter: name: app-router labels: sme.sap.com/app-type: {{appName}} diff --git a/files/chart/templates/_helpers.tpl b/files/chart/templates/_helpers.tpl index c0acba4..b13a09f 100644 --- a/files/chart/templates/_helpers.tpl +++ b/files/chart/templates/_helpers.tpl @@ -1,8 +1,12 @@ {{- define "capApplicationVersionName" -}} -{{ printf "cav-%s-%d" (include "appName" $) (.Release.Revision) }} +{{ printf "%s-%d" (include "appName" $) (.Release.Revision) }} {{- end -}} {{- define "appName" -}} -{{- $xsuaa := index .Values.serviceInstances "xsuaa" -}} -{{ printf "%s" $xsuaa.parameters.xsappname }} +{{- range $sik, $siv := .Values.serviceInstances}} + {{- if and (eq (get $siv "serviceOfferingName") "xsuaa") (eq (get $siv "servicePlanName") "broker") -}} + {{ printf "%s" $siv.parameters.xsappname }} + {{- break -}} + {{- end -}} +{{- end -}} {{- end -}} diff --git a/files/chart/templates/cap-operator-cros.yaml b/files/chart/templates/cap-operator-cros.yaml index f3ace41..880f65b 100644 --- a/files/chart/templates/cap-operator-cros.yaml +++ b/files/chart/templates/cap-operator-cros.yaml @@ -2,7 +2,7 @@ apiVersion: sme.sap.com/v1alpha1 kind: CAPApplication metadata: - name: cap-{{ include "appName" $ }} + name: {{ include "appName" $ }} spec: domains: primary: {{.Values.app.domains.primary}} @@ -18,10 +18,10 @@ spec: value: {{ $v | default "invalidValue"}} {{- end }} btpAppName: {{ include "appName" $ }} - globalAccountId: "{{.Values.btp.globalAccountId}}" + globalAccountId: {{.Values.btp.globalAccountId}} provider: - subDomain: "{{.Values.btp.provider.subdomain}}" - tenantId: "{{.Values.btp.provider.tenantId}}" + subDomain: {{.Values.btp.provider.subdomain}} + tenantId: {{.Values.btp.provider.tenantId}} btp: services: {{- $serviceInstances := .Values.serviceInstances }} @@ -51,8 +51,8 @@ metadata: helm.sh/resource-policy: keep name: {{ include "capApplicationVersionName" $ }} spec: - capApplicationInstance: "cap-{{ include "appName" $ }}" - version: "{{ .Release.Revision }}" + capApplicationInstance: {{ include "appName" $ }} + version: {{ .Release.Revision }} registrySecrets: {{- range .Values.imagePullSecrets }} - {{.}} diff --git a/files/configurableTemplatesChart/Chart.yaml.hbs b/files/configurableTemplatesChart/Chart.yaml.hbs new file mode 100644 index 0000000..b5e7a6a --- /dev/null +++ b/files/configurableTemplatesChart/Chart.yaml.hbs @@ -0,0 +1,9 @@ +apiVersion: v2 +name: {{appName}} +description: Helm chart to deploy {{appName}} using CAP Operator +type: application +version: 0.0.1 +appVersion: {{appVersion}} +annotations: + app.kubernetes.io/managed-by: cap-operator-plugin + app.kubernetes.io/part-of: cap-operator-configurable-templates diff --git a/files/configurableTemplatesChart/templates/_helpers.tpl b/files/configurableTemplatesChart/templates/_helpers.tpl new file mode 100644 index 0000000..ea3871b --- /dev/null +++ b/files/configurableTemplatesChart/templates/_helpers.tpl @@ -0,0 +1,41 @@ +{{- define "capApplicationVersionName" -}} +{{ printf "%s-%d" (include "appName" $) (.Release.Revision) }} +{{- end -}} + +{{- define "appName" -}} +{{- range $sik, $siv := .Values.serviceInstances}} + {{- if and (eq (get $siv "serviceOfferingName") "xsuaa") (eq (get $siv "servicePlanName") "broker") -}} + {{ printf "%s" $siv.parameters.xsappname }} + {{- break -}} + {{- end -}} +{{- end -}} +{{- end -}} + +{{- define "hasService" -}} +{{- $found := "false" -}} +{{- $offeringName := .offeringName -}} +{{- $planName := .planName -}} +{{- $si := .si -}} +{{- range $sik, $siv := $si}} + {{- if and (eq (get $siv "serviceOfferingName") $offeringName) (eq (get $siv "servicePlanName") $planName) -}} + {{- $found = "true" -}} + {{- end -}} +{{- end -}} +{{- $found -}} +{{- end -}} + +{{- define "domainPatterns" -}} + {{- if .Values.app.domains.secondary -}} + {{- $doms := list .Values.app.domains.primary -}} + {{- range .Values.app.domains.secondary -}} + {{- $doms = append $doms . -}} + {{- end -}} + {{- if gt (len $doms) 1 -}} + {{- join "|" $doms | printf "(%s)" -}} + {{- else -}} + {{- first $doms -}} + {{- end -}} + {{- else -}} + {{- printf "%s" .Values.app.domains.primary -}} + {{- end -}} +{{- end -}} diff --git a/files/configurableTemplatesChart/templates/cap-operator-cros.yaml b/files/configurableTemplatesChart/templates/cap-operator-cros.yaml new file mode 100644 index 0000000..d31b1d2 --- /dev/null +++ b/files/configurableTemplatesChart/templates/cap-operator-cros.yaml @@ -0,0 +1,126 @@ +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: {{ include "appName" $ }} +spec: + domains: + primary: {{.Values.app.domains.primary}} + {{- if .Values.app.domains.secondary }} + secondary: + {{- range .Values.app.domains.secondary }} + - {{.}} + {{- end }} + {{- end }} + istioIngressGatewayLabels: + {{- range $k, $v := .Values.app.istioIngressGatewayLabels }} + - name: {{ $k }} + value: {{ $v | default "invalidValue"}} + {{- end }} + btpAppName: {{ include "appName" $ }} + globalAccountId: {{.Values.btp.globalAccountId}} + provider: + subDomain: {{.Values.btp.provider.subdomain}} + tenantId: {{.Values.btp.provider.tenantId}} + btp: + services: + {{- $serviceInstances := .Values.serviceInstances }} + {{- range $k, $v := .Values.serviceBindings }} + {{- $serviceInstance := dict }} + {{- range $sik, $siv := $serviceInstances }} + {{- if eq $siv.name $v.serviceInstanceName }} + {{- $serviceInstance = $siv }} + {{- end }} + {{- end }} + {{- if hasKey $serviceInstance "serviceOfferingName" }} + - class: {{ get $serviceInstance "serviceOfferingName" | default "invalidValue" }} + {{- if $v.externalName }} + name: {{ $v.externalName | default "invalidValue" }} + {{- else }} + name: {{ $v.name | default "invalidValue" }} + {{- end }} + secret: {{ $v.secretName | default "invalidValue" }} + {{- end }} + {{- end }} +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/resource-policy: keep + name: {{ include "capApplicationVersionName" $ }} +spec: + capApplicationInstance: {{ include "appName" $ }} + version: {{ .Release.Revision }} + registrySecrets: + {{- range .Values.imagePullSecrets }} + - {{.}} + {{- end }} + workloads: + - name: server + labels: + sme.sap.com/app-type: {{ include "appName" $ }} + consumedBTPServices: + - {{ include "originalAppName" $ }}-uaa-bind + - {{ include "originalAppName" $ }}-saas-registry-bind + - {{ include "originalAppName" $ }}-service-manager-bind + deploymentDefinition: + type: CAP + image: {{ .Values.workloads.server.image }} + {{- if .Values.hanaInstanceId }} + env: + - name: CDS_CONFIG + value: '{"requires":{"cds.xt.DeploymentService":{"hdi": { "create": {"database_id": "{{.Values.hanaInstanceId}}"}}}}}' + {{- end }} + - name: app-router + labels: + sme.sap.com/app-type: {{ include "appName" $ }} + consumedBTPServices: + - {{ include "originalAppName" $ }}-uaa-bind + - {{ include "originalAppName" $ }}-saas-registry-bind + {{- if eq (include "hasService" (dict "si" .Values.serviceInstances "offeringName" "destination" "planName" "lite")) "true" }} + - {{ include "originalAppName" $ }}-destination-bind + {{- end}} + {{- if eq (include "hasService" (dict "si" .Values.serviceInstances "offeringName" "html5-apps-repo" "planName" "app-runtime")) "true" }} + - {{ include "originalAppName" $ }}-html5-repo-runtime-bind + {{- end }} + deploymentDefinition: + type: Router + image: {{ .Values.workloads.appRouter.image }} + env: + - name: TENANT_HOST_PATTERN + value: ^(.*).{{ template "domainPatterns" . }} + ports: + - name: router-port + port: 5000 + - name: tenant-job + labels: + sme.sap.com/app-type: {{ include "appName" $ }} + consumedBTPServices: + - {{ include "originalAppName" $ }}-uaa-bind + - {{ include "originalAppName" $ }}-saas-registry-bind + - {{ include "originalAppName" $ }}-service-manager-bind + jobDefinition: + type: TenantOperation + image: {{ .Values.workloads.tenantJob.image }} + {{- if .Values.hanaInstanceId }} + env: + - name: CDS_CONFIG + value: '{"requires":{"cds.xt.DeploymentService":{"hdi": { "create": {"database_id": "{{.Values.hanaInstanceId}}"}}}}}' + {{- end }} + - name: content-deploy + labels: + sme.sap.com/app-type: {{ include "appName" $ }} + consumedBTPServices: + - {{ include "originalAppName" $ }}-uaa-bind + - {{ include "originalAppName" $ }}-saas-registry-bind + {{- if eq (include "hasService" (dict "si" .Values.serviceInstances "offeringName" "html5-apps-repo" "planName" "app-host")) "true" }} + - {{ include "originalAppName" $ }}-html5-repo-host-bind + {{- end }} + {{- if eq (include "hasService" (dict "si" .Values.serviceInstances "offeringName" "content-agent" "planName" "application")) "true" }} + - {{ include "originalAppName" $ }}-content-agent-bind + {{- end }} + jobDefinition: + type: Content + image: {{ .Values.workloads.contentDeploy.image }} diff --git a/test/files/expectedChart/templates/service-binding.yaml b/files/configurableTemplatesChart/templates/service-binding.yaml similarity index 100% rename from test/files/expectedChart/templates/service-binding.yaml rename to files/configurableTemplatesChart/templates/service-binding.yaml diff --git a/test/files/expectedChart/templates/service-instance.yaml b/files/configurableTemplatesChart/templates/service-instance.yaml similarity index 100% rename from test/files/expectedChart/templates/service-instance.yaml rename to files/configurableTemplatesChart/templates/service-instance.yaml diff --git a/files/configurableTemplatesChart/values.schema.json b/files/configurableTemplatesChart/values.schema.json new file mode 100644 index 0000000..a29f90b --- /dev/null +++ b/files/configurableTemplatesChart/values.schema.json @@ -0,0 +1,318 @@ +{ + "$defs": { + "CredentialsRotationPolicy": { + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "rotatedBindingTTL": { + "type": "string" + }, + "rotationFrequency": { + "type": "string" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "Domains": { + "additionalProperties": false, + "properties": { + "primary": { + "type": "string" + }, + "secondary": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "primary", + "secondary" + ], + "type": "object" + }, + "ExtraValue": { + "items": { + "type": "string" + }, + "type": "array" + }, + "ParametersFromSource": { + "additionalProperties": false, + "properties": { + "secretKeyRef": { + "$ref": "#/$defs/SecretKeyReference" + } + }, + "type": "object" + }, + "RawExtension": { + "additionalProperties": true, + "properties": {}, + "type": "object" + }, + "SecretKeyReference": { + "additionalProperties": false, + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "name", + "key" + ], + "type": "object" + }, + "UserInfo": { + "additionalProperties": false, + "properties": { + "extra": { + "additionalProperties": { + "$ref": "#/$defs/ExtraValue" + }, + "type": "object" + }, + "groups": { + "items": { + "type": "string" + }, + "type": "array" + }, + "uid": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "app": { + "additionalProperties": false, + "properties": { + "domains": { + "$ref": "#/$defs/Domains" + }, + "istioIngressGatewayLabels": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + }, + "required": [ + "domains", + "istioIngressGatewayLabels" + ], + "type": "object" + }, + "btp": { + "additionalProperties": false, + "properties": { + "globalAccountId": { + "type": "string" + }, + "provider": { + "$ref": "#/$defs/provider" + } + }, + "required": [ + "globalAccountId", + "provider" + ], + "type": "object" + }, + "configurableChartValue": { + "additionalProperties": true, + "properties": { + "app": { + "$ref": "#/$defs/app" + }, + "btp": { + "$ref": "#/$defs/btp" + }, + "hanaInstanceId": { + "type": "string" + }, + "imagePullSecrets": { + "items": { + "type": "string" + }, + "type": "array" + }, + "serviceBindings": { + "additionalProperties": { + "$ref": "#/$defs/serviceBindingExt" + }, + "type": "object" + }, + "serviceInstances": { + "additionalProperties": { + "$ref": "#/$defs/serviceInstanceExt" + }, + "type": "object" + }, + "workloads": { + "additionalProperties": { + "$ref": "#/$defs/workloadDefinition" + }, + "type": "object" + } + }, + "required": [ + "app", + "btp", + "serviceInstances", + "serviceBindings", + "workloads" + ], + "type": "object" + }, + "provider": { + "additionalProperties": false, + "properties": { + "subdomain": { + "type": "string" + }, + "tenantId": { + "type": "string" + } + }, + "required": [ + "subdomain", + "tenantId" + ], + "type": "object" + }, + "serviceBindingExt": { + "additionalProperties": false, + "properties": { + "credentialsRotationPolicy": { + "$ref": "#/$defs/CredentialsRotationPolicy" + }, + "externalName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parameters": { + "$ref": "#/$defs/RawExtension" + }, + "parametersFrom": { + "items": { + "$ref": "#/$defs/ParametersFromSource" + }, + "type": "array" + }, + "secretKey": { + "type": "string" + }, + "secretName": { + "type": "string" + }, + "secretRootKey": { + "type": "string" + }, + "secretTemplate": { + "type": "string" + }, + "serviceInstanceName": { + "type": "string" + }, + "serviceInstanceNamespace": { + "type": "string" + }, + "userInfo": { + "$ref": "#/$defs/UserInfo" + } + }, + "required": [ + "name", + "serviceInstanceName", + "secretName" + ], + "type": "object" + }, + "serviceInstanceExt": { + "additionalProperties": false, + "properties": { + "btpAccessCredentialsSecret": { + "type": "string" + }, + "customTags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "dataCenter": { + "type": "string" + }, + "externalName": { + "type": "string" + }, + "jsonParameters": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parameters": { + "$ref": "#/$defs/RawExtension" + }, + "parametersFrom": { + "items": { + "$ref": "#/$defs/ParametersFromSource" + }, + "type": "array" + }, + "serviceOfferingName": { + "type": "string" + }, + "servicePlanID": { + "type": "string" + }, + "servicePlanName": { + "type": "string" + }, + "shared": { + "type": "boolean" + }, + "userInfo": { + "$ref": "#/$defs/UserInfo" + } + }, + "required": [ + "name", + "serviceOfferingName", + "servicePlanName" + ], + "type": "object" + }, + "workloadDefinition": { + "additionalProperties": true, + "properties": { + "image": { + "type": "string" + } + }, + "required": [ + "image" + ], + "type": "object" + } + }, + "$ref": "#/$defs/configurableChartValue", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/files/configurableTemplatesChart/values.yaml b/files/configurableTemplatesChart/values.yaml new file mode 100644 index 0000000..04880cc --- /dev/null +++ b/files/configurableTemplatesChart/values.yaml @@ -0,0 +1,42 @@ +serviceInstances: + +serviceBindings: + +app: + # -- BTP application name (xsappname) + domains: + # -- primary domain to expose application routes (usually a subdomain of the cluster domain). Ensure this is under 62 chars. + primary: + # -- Customer specific domains for which dns entries are created (these can be stable and transferred between clusters) + secondary: [] + # -- Labels used to identify the istio ingress-gateway component and its corresponding namespace + istioIngressGatewayLabels: + istio: ingressgateway + app: istio-ingressgateway + +btp: + # -- BTP Global account identifier where the application is hosted + globalAccountId: # mandatory let helm fail if not provided + provider: + # -- Subdomain of the provider sub-account where application services are created + subdomain: # mandatory let helm fail if not provided + # -- Tenant identifier for the provider sub-account (usually a guid) + tenantId: # mandatory let helm fail if not provided + +# -- Image pull secrets +imagePullSecrets: [] + +hanaInstanceId: + +workloads: + server: + image: + + appRouter: + image: + + tenantJob: + image: + + contentDeploy: + image: diff --git a/files/saas-registry.yaml.hbs b/files/saas-registry.yaml.hbs index 5fafab4..32e43f3 100644 --- a/files/saas-registry.yaml.hbs +++ b/files/saas-registry.yaml.hbs @@ -1,5 +1,5 @@ serviceInstances: - saas-registry: + saasRegistry: name: {{appName}}-saas-registry serviceOfferingName: saas-registry servicePlanName: application @@ -16,7 +16,7 @@ serviceInstances: category: "CAP" serviceBindings: - saas-registry: + saasRegistry: name: {{appName}}-saas-registry-bind serviceInstanceName: {{appName}}-saas-registry secretName: {{appName}}-saas-registry-bind-secret diff --git a/files/service-manager.yaml.hbs b/files/service-manager.yaml.hbs index 38e2b9d..4713a5d 100644 --- a/files/service-manager.yaml.hbs +++ b/files/service-manager.yaml.hbs @@ -1,12 +1,12 @@ serviceInstances: - service-manager: + serviceManager: name: {{appName}}-service-manager serviceOfferingName: service-manager servicePlanName: container parameters: {} serviceBindings: - service-manager: + serviceManager: name: {{appName}}-service-manager-bind serviceInstanceName: {{appName}}-service-manager secretName: {{appName}}-service-manager-bind-secret diff --git a/files/workloads.yaml.hbs b/files/workloads.yaml.hbs index ca0178e..a5a1b7c 100644 --- a/files/workloads.yaml.hbs +++ b/files/workloads.yaml.hbs @@ -15,7 +15,7 @@ workloads: type: CAP image: - content-deploy: + contentDeploy: name: content-deploy labels: sme.sap.com/app-type: {{appName}} @@ -33,7 +33,7 @@ workloads: type: Content image: - tenant-job: + tenantJob: name: tenant-job labels: sme.sap.com/app-type: {{appName}} diff --git a/hack/schema-generation.go b/hack/schema-generation.go index 6c93eee..1e14def 100644 --- a/hack/schema-generation.go +++ b/hack/schema-generation.go @@ -54,6 +54,20 @@ type chartValue struct { ContentJobs []string `json:"contentJobs,omitempty"` } +type workloadDefinition struct { + Image string `json:"image"` +} + +type configurableChartValue struct { + App app `json:"app"` + Btp btp `json:"btp"` + ImagePullSecrets []string `json:"imagePullSecrets,omitempty"` + HanaInstanceId string `json:"hanaInstanceId,omitempty"` + ServiceInstances map[string]serviceInstanceExt `json:"serviceInstances"` + ServiceBindings map[string]serviceBindingExt `json:"serviceBindings"` + Workloads map[string]workloadDefinition `json:"workloads"` +} + func updateProperties(data []byte) []byte { m := map[string]interface{}{} @@ -82,6 +96,32 @@ func updateProperties(data []byte) []byte { return data } +func updatePropertiesconfigurableChart(data []byte) []byte { + + m := map[string]interface{}{} + + json.Unmarshal(data, &m) + + rawExt := m["$defs"].(map[string]interface{})["RawExtension"].(map[string]interface{}) + rawExt["additionalProperties"] = true + m["$defs"].(map[string]interface{})["RawExtension"] = rawExt + + serviceBindingSpec := m["$defs"].(map[string]interface{})["serviceBindingExt"].(map[string]interface{}) + serviceBindingSpec["required"] = []string{"name", "serviceInstanceName", "secretName"} + + workloadDefinition := m["$defs"].(map[string]interface{})["workloadDefinition"].(map[string]interface{}) + workloadDefinition["additionalProperties"] = true + m["$defs"].(map[string]interface{})["workloadDefinition"] = workloadDefinition + + configurableChartValue := m["$defs"].(map[string]interface{})["configurableChartValue"].(map[string]interface{}) + configurableChartValue["additionalProperties"] = true + m["$defs"].(map[string]interface{})["configurableChartValue"] = configurableChartValue + + data, _ = json.Marshal(m) + + return data +} + func main() { s := jsonschema.Reflect(&chartValue{}) @@ -99,10 +139,20 @@ func main() { panic(err) } + // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + sV2 := jsonschema.Reflect(&configurableChartValue{}) + dataV2, errV2 := json.MarshalIndent(sV2, "", " ") + if errV2 != nil { + panic(errV2.Error()) + } + + dataV2 = updatePropertiesconfigurableChart(dataV2) + fmt.Println(string(dataV2)) + // write the whole body at once - err = os.WriteFile("../test/files/expectedChart/values.schema.json", data, 0644) + err = os.WriteFile("../files/configurableTemplatesChart/values.schema.json", dataV2, 0644) if err != nil { panic(err) } - } diff --git a/lib/add.js b/lib/add.js index df01a43..0d2162a 100644 --- a/lib/add.js +++ b/lib/add.js @@ -10,7 +10,7 @@ const yaml = require('@sap/cds-foss').yaml const md5 = data => require('crypto').createHash('md5').update(data).digest('hex') const MtaTransformer = require('./mta-transformer') -const { isCAPOperatorChart } = require('./util') +const { isCAPOperatorChart, isConfigurableTemplateChart, injectTemplateFunction, transformValuesAndFillCapOpCroYaml } = require('./util') const Mustache = require('mustache') @@ -22,6 +22,10 @@ module.exports = class CapOperatorAddPlugin extends cds.add.Plugin { type: 'boolean', help: 'To add the templates folder to the chart folder.' }, + 'with-configurable-templates': { + type: 'boolean', + help: 'To add a chart with configurable templates' + }, 'with-mta': { type: 'string', //help: 'Path to the mta.yaml file.' @@ -61,35 +65,56 @@ module.exports = class CapOperatorAddPlugin extends cds.add.Plugin { } async run() { - if (cds.cli.options.force) await rimraf('chart') - else if (exists('chart')) { - let isCAPOpChart = isCAPOperatorChart('chart') + if (cds.cli.options.force) { + await rimraf('chart') + } else if (cds.cli.options['with-configurable-templates'] && cds.cli.options['with-templates']) { + throw new Error(`Option '--with-templates' cannot be used with '--with-configurable-templates' option.`) + } else if (!cds.cli.options['with-mta'] && cds.cli.options['with-mta-extensions']) { + throw new Error(`mta YAML not provided. Please pass the mta YAML via option '--with-mta'.`) + } else if (exists('chart')) { + const isCAPOpChart = isCAPOperatorChart('chart') + const isConfigurableTempChart = isConfigurableTemplateChart('chart') + + if (!isCAPOpChart) { + throw new Error(`Existing 'chart' folder is not a CAP Operator helm chart. Run 'cds add cap-operator --force' to overwrite.`) + } - if(isCAPOpChart && !exists('chart/templates') && cds.cli.options['with-templates']){ + if (!exists('chart/templates') && cds.cli.options['with-templates']) { await copy(join(__dirname, '../files/chart/templates')).to('chart/templates') console.log("Added 'templates' folder to the 'chart' folder.") } - if (!isCAPOpChart) { - throw new Error(`Existing 'chart' folder is not a CAP Operator helm chart. Run 'cds add cap-operator --force' to overwrite.`) - } else if (isCAPOpChart && md5(JSON.stringify(await read('chart/values.schema.json'))) != md5(JSON.stringify(await read(join(__dirname, '../files/chart/values.schema.json'))))) { + if (!isConfigurableTempChart &&cds.cli.options['with-configurable-templates']) { + console.log("CAP Operator chart already present. If you want to convert the existing chart to a configurable template chart, run 'npx cap-op-plugin convert-to-configurable-template-chart'") + } + + const valuesSchemaPath = isConfigurableTempChart ? '../files/configurableTemplatesChart/values.schema.json' : '../files/chart/values.schema.json' + if (md5(JSON.stringify(await read('chart/values.schema.json'))) !== md5(JSON.stringify(await read(join(__dirname, valuesSchemaPath))))) { console.log("⚠️ 'values.schema.json' file is outdated. Run with '--force' to overwrite the file and accept the new changes.") } return } const project = cds.add.readProject() - await cds.add.merge(__dirname, '../files/chart/Chart.yaml.hbs').into('chart/Chart.yaml', { project }) - await copy(join(__dirname, '../files/chart/values.yaml')).to('chart/values.yaml') - await copy(join(__dirname, '../files/chart/values.schema.json')).to('chart/values.schema.json') - if (cds.cli.options['with-templates']) - await copy(join(__dirname, '../files/chart/templates')).to('chart/templates') + if (cds.cli.options['with-configurable-templates']) { + await copy(join(__dirname, '../files/configurableTemplatesChart/templates')).to('chart/templates') + await cds.add.merge(__dirname, '../files/configurableTemplatesChart/Chart.yaml.hbs').into('chart/Chart.yaml', { project }) + await copy(join(__dirname, '../files/configurableTemplatesChart/values.yaml')).to('chart/values.yaml') + await copy(join(__dirname, '../files/configurableTemplatesChart/values.schema.json')).to('chart/values.schema.json') - console.log("`chart` folder generated.") + const originalAppNameFunCode = `{{- define "originalAppName" -}}\n{{ print "`+ project['appName'] +`" }}\n{{- end -}}` + injectTemplateFunction(join(cds.root, 'chart/templates/_helpers.tpl'), originalAppNameFunCode) + } else { + await cds.add.merge(__dirname, '../files/chart/Chart.yaml.hbs').into('chart/Chart.yaml', { project }) + await copy(join(__dirname, '../files/chart/values.yaml')).to('chart/values.yaml') + await copy(join(__dirname, '../files/chart/values.schema.json')).to('chart/values.schema.json') - if (!cds.cli.options['with-mta'] && cds.cli.options['with-mta-extensions']) - throw new Error(`mta YAML not provided. Please pass the mta YAML via option '--with-mta'.`) + if (cds.cli.options['with-templates']) + await copy(join(__dirname, '../files/chart/templates')).to('chart/templates') + } + + console.log("`chart` folder generated.") if (cds.cli.options['with-mta']) { const { hasMta } = project @@ -104,9 +129,24 @@ module.exports = class CapOperatorAddPlugin extends cds.add.Plugin { updateValuesMap.set('serviceBindings', await mtaTransformer.getServiceBindings()) updateValuesMap.set('workloads', await mtaTransformer.getWorkloads()) await this.updateValuesYaml(updateValuesMap) + + if (isConfigurableTemplateChart('chart')) { + await transformValuesAndFillCapOpCroYaml() + } } else { console.log("Review and update the values.yaml file in the 'chart' folder as per your project's requirements.") } + + // Update xs-security.json + const { hasXsuaa } = project + if (hasXsuaa) { + await cds.add.merge(__dirname, '../files/xs-security.json.hbs').into('xs-security.json', { + project, + additions: [{ in: 'scopes', where: { name: '$XSAPPNAME.Callback' }}, + { in: 'scopes', where: { name: '$XSAPPNAME.mtcallback' }}] + }) + } + console.log("Once values.yaml is updated, run 'cds build' to generate the helm chart. You can find the generated chart in the 'gen' folder within your project directory.") } @@ -132,17 +172,6 @@ module.exports = class CapOperatorAddPlugin extends cds.add.Plugin { if (hasXsuaa) { const xsuaaaYaml = yaml.parse(Mustache.render( await read(join(__dirname, '../files/xsuaa.yaml.hbs')), project)) await cds.add.merge(xsuaaaYaml).into(valuesYaml) - - await cds.add.merge(__dirname, '../files/xs-security.json.hbs').into('xs-security.json', { - project, - additions: [{ in: 'scopes', where: { name: '$XSAPPNAME.Callback' }}, - { in: 'scopes', where: { name: '$XSAPPNAME.mtcallback' }}] - }) - } - - if (hasApprouter || exists('approuter')) { - const approuterYaml = yaml.parse(Mustache.render( await read(join(__dirname, '../files/approuter.yaml.hbs')), project)) - await cds.add.merge(approuterYaml).into(valuesYaml) } if (hasMultitenancy) { @@ -153,8 +182,15 @@ module.exports = class CapOperatorAddPlugin extends cds.add.Plugin { await cds.add.merge(serviceManagerYaml).into(valuesYaml) } - const workloadsYaml = yaml.parse(Mustache.render( await read(join(__dirname, '../files/workloads.yaml.hbs')), project)) - await cds.add.merge(workloadsYaml).into(valuesYaml) + if (!isConfigurableTemplateChart('chart')) { + if (hasApprouter || exists('approuter')) { + const approuterYaml = yaml.parse(Mustache.render( await read(join(__dirname, '../files/approuter.yaml.hbs')), project)) + await cds.add.merge(approuterYaml).into(valuesYaml) + } + + const workloadsYaml = yaml.parse(Mustache.render( await read(join(__dirname, '../files/workloads.yaml.hbs')), project)) + await cds.add.merge(workloadsYaml).into(valuesYaml) + } await write(yaml.stringify(valuesYaml)).to(join(cds.root, 'chart/values.yaml')) } diff --git a/lib/mta-transformer.js b/lib/mta-transformer.js index 9f0d629..eb10d36 100644 --- a/lib/mta-transformer.js +++ b/lib/mta-transformer.js @@ -7,7 +7,7 @@ const cds = require('@sap/cds-dk') const { join } = cds.utils.path const md5 = data => require('crypto').createHash('md5').update(data).digest('hex') -const { replacePlaceholders, mergeObj } = require('./util') +const { replacePlaceholders, mergeObj, convertHypenNameToCamelcase } = require('./util') module.exports = class MtaTransformer { @@ -99,7 +99,7 @@ module.exports = class MtaTransformer { let managedSvcs = this.mergedMta?.resources.filter(r => r.type === 'org.cloudfoundry.managed-service') || [] for(let svc of managedSvcs) { this.serviceInstances.set( - svc.parameters["service"] && svc.parameters["service-plan"] ? svc.parameters["service"] + "-" +svc.parameters["service-plan"] : null, + svc.parameters["service"] && svc.parameters["service-plan"] ? convertHypenNameToCamelcase(svc.parameters["service"] + "-" +svc.parameters["service-plan"]) : null, { name: svc.name, serviceOfferingName: svc.parameters["service"] ? svc.parameters["service"] : null, @@ -154,6 +154,7 @@ module.exports = class MtaTransformer { } let serviceBindingName = consumedBTPService.name + this.serviceBindingNameSuffix + let serviceBindingNameKey = convertHypenNameToCamelcase(serviceBindingName) let serviceBindingValue = { name: serviceBindingName, parameters: consumedBTPService.properties ?? {}, @@ -161,11 +162,12 @@ module.exports = class MtaTransformer { secretName: serviceBindingName + "-secret", serviceInstanceName: consumedBTPService.name, } - if (this.serviceBindings.has(serviceBindingName) && - md5(JSON.stringify(this.serviceBindings.get(serviceBindingName))) !== md5(JSON.stringify(serviceBindingValue))) { + if (this.serviceBindings.has(serviceBindingNameKey) && + md5(JSON.stringify(this.serviceBindings.get(serviceBindingNameKey))) !== md5(JSON.stringify(serviceBindingValue))) { let serviceBindingNewName = consumedBTPService.name +"-"+ module.name + this.serviceBindingNameSuffix - this.serviceBindings.set(serviceBindingNewName, { + let serviceBindingNewNameKey = convertHypenNameToCamelcase(serviceBindingNewName) + this.serviceBindings.set(serviceBindingNewNameKey, { name: serviceBindingNewName, parameters: consumedBTPService.properties ?? {}, secretKey: "credentials", @@ -173,15 +175,15 @@ module.exports = class MtaTransformer { serviceInstanceName: consumedBTPService.name, }) workloadServiceBindings.push(serviceBindingNewName) - } else if (this.serviceBindings.has(serviceBindingName) && - md5(JSON.stringify(this.serviceBindings.get(serviceBindingName))) === md5(JSON.stringify(serviceBindingValue))) { + } else if (this.serviceBindings.has(serviceBindingNameKey) && + md5(JSON.stringify(this.serviceBindings.get(serviceBindingNameKey))) === md5(JSON.stringify(serviceBindingValue))) { workloadServiceBindings.push(serviceBindingName) continue } else { workloadServiceBindings.push(serviceBindingName) - this.serviceBindings.set(serviceBindingName,serviceBindingValue) + this.serviceBindings.set(serviceBindingNameKey,serviceBindingValue) } } return workloadServiceBindings @@ -212,18 +214,36 @@ module.exports = class MtaTransformer { if (module.path.includes("gen/mtx/sidecar") || module.type === 'com.sap.application.content') { workload.jobDefinition = { type: module.type === 'com.sap.application.content'? "Content": "TenantOperation", - image: null, - env: module.properties ? this.getWorkloadEnv(module.properties) : [] + image: null } } else { workload.deploymentDefinition = { type: module.path.includes("approuter") || module.path.includes("router") ? "Router" : null, - image: null, - env: module.properties ? this.getWorkloadEnv(module.properties) : [] + image: null } } - this.workloads.set(module.name, workload) + + if (module.properties) { + let workloadEnv = this.getWorkloadEnv(module.properties) + if (workloadEnv.length > 0) + workload[workload.deploymentDefinition ? 'deploymentDefinition' : 'jobDefinition'].env = workloadEnv + } + + let tenantHostPattern = { name: 'TENANT_HOST_PATTERN', value: '^(.*).{{ template "domainPatterns" . }}' } + if (workload.deploymentDefinition?.type == "Router") { + if (workload.deploymentDefinition.env) { + const index = workload.deploymentDefinition.env.findIndex(e => e.name === tenantHostPattern.name) + if (index == -1) + workload.deploymentDefinition.env.push(tenantHostPattern) + else + workload.deploymentDefinition.env[index].value = tenantHostPattern.value + } else { + workload.deploymentDefinition.env = [tenantHostPattern] + } + } + + this.workloads.set(convertHypenNameToCamelcase(module.name), workload) } return this.workloads } diff --git a/lib/util.js b/lib/util.js index 38e0bc4..57e1750 100644 --- a/lib/util.js +++ b/lib/util.js @@ -4,7 +4,9 @@ SPDX-License-Identifier: Apache-2.0 */ const cds = require('@sap/cds-dk') +const yaml = require('@sap/cds-foss').yaml const readline = require('readline') +const fs = require('fs') function replacePlaceholders(obj, replacements) { if (typeof obj === "object") { @@ -63,6 +65,14 @@ function isCAPOperatorChart(chartFolderPath) { } } +function isConfigurableTemplateChart(chartFolderPath) { + try { + const chartYaml = cds.parse.yaml(cds.utils.fs.readFileSync(chartFolderPath + "/Chart.yaml").toString()) + return chartYaml.annotations?.["app.kubernetes.io/part-of"] === 'cap-operator-configurable-templates' || false + } catch (err) { + return false + } +} async function ask(...args) { const answers = [] const rl = readline.createInterface({ @@ -96,5 +106,110 @@ async function ask(...args) { return answers } +function injectTemplateFunction(templateFilePath, newFunctionCode) { + fs.readFile(templateFilePath, 'utf8', (err, data) => { + if (err) + throw new Error(`Error reading the file: ${err}; _helper.tpl modification failed`) + + // Append the new function to the content + const updatedContent = data + '\n' + newFunctionCode + + fs.writeFile(templateFilePath, updatedContent, 'utf8', (err) => { + if (err) + throw new Error(`Error writing to the file: ${err}; _helper.tpl modification failed`) + }) + }) +} + +async function transformValuesAndFillCapOpCroYaml() { + let valuesYaml = yaml.parse(await cds.utils.read(cds.utils.path.join(cds.root, 'chart/values.yaml'))) + const capOpCROYaml = cds.utils.fs.readFileSync(cds.utils.path.join(cds.root, 'chart/templates/cap-operator-cros.yaml'), 'utf8') + + // Update cap-operator-cro.yaml with existing values + let workloadArray = [] + let newWorkloadObj = {} + let tenantHostPattern = { name: 'TENANT_HOST_PATTERN', value: '^(.*).{{ template "domainPatterns" . }}' } + for (const [workloadKey, workloadDetails] of Object.entries(valuesYaml.workloads)) { + if (workloadDetails?.deploymentDefinition?.type === 'Router') { + if (!workloadDetails.deploymentDefinition.env) { + workloadDetails.deploymentDefinition.env = [tenantHostPattern] + } else if (!workloadDetails.deploymentDefinition.env.some(e => e.name === tenantHostPattern.name)) { + workloadDetails.deploymentDefinition.env.push(tenantHostPattern) + } + } else if (workloadDetails?.deploymentDefinition?.type === 'CAP' && workloadDetails.deploymentDefinition.env) { + const index = workloadDetails.deploymentDefinition.env.findIndex(e => e.name === 'CDS_CONFIG') + if (index > -1) { + const cdsConfigValueJson = JSON.parse(workloadDetails.deploymentDefinition.env[index].value) + if (cdsConfigValueJson?.requires?.['cds.xt.DeploymentService']?.hdi?.create?.database_id) { + valuesYaml['hanaInstanceId'] = cdsConfigValueJson.requires['cds.xt.DeploymentService'].hdi.create.database_id + cdsConfigValueJson.requires['cds.xt.DeploymentService'].hdi.create.database_id = '{{.Values.hanaInstanceId}}' + workloadDetails.deploymentDefinition.env[index].value = JSON.stringify(cdsConfigValueJson) + } + } + } else if (workloadDetails?.jobDefinition?.type === 'TenantOperation' && workloadDetails.jobDefinition.env) { + const index = workloadDetails.jobDefinition.env.findIndex(e => e.name === 'CDS_CONFIG') + if (index > -1) { + const cdsConfigValueJson = JSON.parse(workloadDetails.jobDefinition.env[index].value) + if (cdsConfigValueJson?.requires?.['cds.xt.DeploymentService']?.hdi?.create?.database_id) { + valuesYaml['hanaInstanceId'] = cdsConfigValueJson.requires['cds.xt.DeploymentService'].hdi.create.database_id + cdsConfigValueJson.requires['cds.xt.DeploymentService'].hdi.create.database_id = '{{.Values.hanaInstanceId}}' + workloadDetails.jobDefinition.env[index].value = JSON.stringify(cdsConfigValueJson) + } + } + } + + let workloadKeyCamelCase = convertHypenNameToCamelcase(workloadKey) + if (workloadDetails.deploymentDefinition) { + newWorkloadObj[workloadKeyCamelCase] = { "image": workloadDetails.deploymentDefinition.image } + workloadDetails.deploymentDefinition.image = '{{.Values.workloads.'+ workloadKeyCamelCase +'.image}}' + } else { + newWorkloadObj[workloadKeyCamelCase] = { "image": workloadDetails.jobDefinition.image } + workloadDetails.jobDefinition.image = '{{.Values.workloads.'+ workloadKeyCamelCase +'.image}}' + } + + workloadArray.push(workloadDetails) + } + + let updatedCapOpCROYaml = capOpCROYaml.replace( + /workloads:\n(.*\n)*?(?=\n\s{2,}- name|spec:|$)/gm, + yaml.stringify({ 'workloads': workloadArray }, { indent: 2 }) + ) + + if (valuesYaml['tenantOperations']){ + updatedCapOpCROYaml = updatedCapOpCROYaml.replace( + /spec:\n((?:.*\n)*?)(\n[^ ]|$)/gm, + (match, p1, p2) => `spec:\n${p1} ${yaml.stringify({ 'tenantOperations': valuesYaml['tenantOperations'] }, { indent: 4 })}${p2}` + ) + delete valuesYaml['tenantOperations'] + } + + if (valuesYaml['contentJobs']){ + updatedCapOpCROYaml = updatedCapOpCROYaml.replace( + /spec:\n((?:.*\n)*?)(\n[^ ]|$)/gm, + (match, p1, p2) => `spec:\n${p1} ${yaml.stringify({ 'contentJobs': valuesYaml['contentJobs'] }, { indent: 2 })}${p2}` + ) + delete valuesYaml['contentJobs'] + } + + cds.utils.fs.writeFileSync(cds.utils.path.join(cds.root, 'chart/templates/cap-operator-cros.yaml'), updatedCapOpCROYaml) + + valuesYaml['workloads'] = newWorkloadObj + await cds.utils.write(yaml.stringify(valuesYaml)).to(cds.utils.path.join(cds.root, 'chart/values.yaml')) +} + +function convertHypenNameToCamelcase(str) { + if (!str.includes('-')) { + return str + } + return str + .split('-') // Split the string into an array by the hyphen + .map((word, index) => { + // Capitalize the first letter of each word except the first word + if (index === 0) { + return word // Keep the first word in lowercase + } + return word.charAt(0).toUpperCase() + word.slice(1) + }).join('') // Join the words back together without spaces +} -module.exports = { replacePlaceholders, mergeObj, isCAPOperatorChart, ask } +module.exports = { replacePlaceholders, mergeObj, isCAPOperatorChart, isConfigurableTemplateChart, ask, injectTemplateFunction, transformValuesAndFillCapOpCroYaml, convertHypenNameToCamelcase } diff --git a/test/add.test.js b/test/add.test.js index ef714ec..729b234 100644 --- a/test/add.test.js +++ b/test/add.test.js @@ -1,6 +1,7 @@ const { join } = require('path') const { execSync } = require('child_process') const { expect } = require("chai") +const fs = require('fs') const TempUtil = require('./tempUtil') const tempUtil = new TempUtil(__filename, { local: true }) @@ -8,7 +9,7 @@ const tempUtil = new TempUtil(__filename, { local: true }) const { getFolderHash, getFileHash, updateDependency, setupHack, undoSetupHack } = require('./util') describe('cds add cap-operator', () => { - let temp, bookshop + let temp, bookshop, orignalXsSecurityJson before(async () => { await tempUtil.cleanUp() @@ -18,10 +19,11 @@ describe('cds add cap-operator', () => { updateDependency(bookshop) execSync(`npm install`, { cwd: bookshop }) setupHack(bookshop) + orignalXsSecurityJson = fs.readFileSync(bookshop+"/xs-security.json", 'utf8') }) afterEach(async () => { - execSync(`rm -r chart`, { cwd: bookshop }) + if (cds.utils.exists(join(bookshop, 'chart'))) execSync(`rm -r chart`, { cwd: bookshop }) }) after(async () => { @@ -33,7 +35,7 @@ describe('cds add cap-operator', () => { execSync(`cds add cap-operator`, { cwd: bookshop }) expect(getFileHash(join(__dirname,'files/expectedChart/Chart.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/Chart.yaml'))) - expect(getFileHash(join(__dirname,'files/expectedChart/values.schema.json'))).to.equal(getFileHash(join(bookshop, 'chart/values.schema.json'))) + expect(getFileHash(join(__dirname,'../files/chart/values.schema.json'))).to.equal(getFileHash(join(bookshop, 'chart/values.schema.json'))) expect(getFileHash(join(__dirname,'files/expectedChart/values.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/values.yaml'))) // Check changes to xs-security.json @@ -44,7 +46,7 @@ describe('cds add cap-operator', () => { execSync(`cds add cap-operator --force`, { cwd: bookshop }) expect(getFileHash(join(__dirname,'files/expectedChart/Chart.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/Chart.yaml'))) - expect(getFileHash(join(__dirname,'files/expectedChart/values.schema.json'))).to.equal(getFileHash(join(bookshop, 'chart/values.schema.json'))) + expect(getFileHash(join(__dirname,'../files/chart/values.schema.json'))).to.equal(getFileHash(join(bookshop, 'chart/values.schema.json'))) expect(getFileHash(join(__dirname,'files/expectedChart/values.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/values.yaml'))) }) @@ -53,8 +55,10 @@ describe('cds add cap-operator', () => { execSync(`cds add cap-operator --with-templates`, { cwd: bookshop }) expect(getFileHash(join(__dirname,'files/expectedChart/Chart.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/Chart.yaml'))) - expect(getFileHash(join(__dirname,'files/expectedChart/values.schema.json'))).to.equal(getFileHash(join(bookshop, 'chart/values.schema.json'))) + expect(getFileHash(join(__dirname,'../files/chart/values.schema.json'))).to.equal(getFileHash(join(bookshop, 'chart/values.schema.json'))) expect(getFileHash(join(__dirname,'files/expectedChart/values.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/values.yaml'))) + + expect(getFolderHash(join(__dirname,'../files/chart/templates'))).to.equal(getFolderHash(join(bookshop, 'chart/templates'))) }) it('Chart folder already added by `cds add helm` ', async () => { @@ -77,9 +81,9 @@ describe('cds add cap-operator', () => { execSync(`cds add cap-operator --with-templates`, { cwd: bookshop }) expect(getFileHash(join(__dirname,'files/expectedChart/Chart.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/Chart.yaml'))) - expect(getFileHash(join(__dirname,'files/expectedChart/values.schema.json'))).to.equal(getFileHash(join(bookshop, 'chart/values.schema.json'))) + expect(getFileHash(join(__dirname,'../files/chart/values.schema.json'))).to.equal(getFileHash(join(bookshop, 'chart/values.schema.json'))) expect(getFileHash(join(__dirname,'files/expectedChart/values.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/values.yaml'))) - expect(getFolderHash(join(__dirname,'files/expectedChart/templates'))).to.equal(getFolderHash(join(bookshop, 'chart/templates'))) + expect(getFolderHash(join(__dirname,'../files/chart/templates'))).to.equal(getFolderHash(join(bookshop, 'chart/templates'))) }) it('Add cap-operator chart with mta but mta.yaml is not present', async () => { @@ -93,12 +97,15 @@ describe('cds add cap-operator', () => { it('Add cap-operator chart with mta and mtaExtensions', async () => { await cds.utils.copy(join('test/files', 'mta.yaml'), join(bookshop, 'mta.yaml')) await cds.utils.copy(join('test/files', 'corrected_xsappname.mtaext'), join(bookshop, 'corrected_xsappname.mtaext')) + // revert xs-security to original value + fs.writeFileSync(bookshop+"/xs-security.json", orignalXsSecurityJson) execSync(`cds add cap-operator --with-mta mta.yaml --with-mta-extensions corrected_xsappname.mtaext`, { cwd: bookshop }) expect(getFileHash(join(__dirname,'files/expectedChart/Chart.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/Chart.yaml'))) - expect(getFileHash(join(__dirname,'files/expectedChart/values.schema.json'))).to.equal(getFileHash(join(bookshop, 'chart/values.schema.json'))) + expect(getFileHash(join(__dirname,'../files/chart/values.schema.json'))).to.equal(getFileHash(join(bookshop, 'chart/values.schema.json'))) expect(getFileHash(join(__dirname,'files/expectedChart/valuesWithMTA.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/values.yaml'))) + expect(getFileHash(join(__dirname,'files/xs-security.json'))).to.equal(getFileHash(join(bookshop, 'xs-security.json'))) }) it('Add cap-operator chart and add destination', async () => { @@ -106,7 +113,60 @@ describe('cds add cap-operator', () => { execSync(`cds add destination`, { cwd: bookshop }) expect(getFileHash(join(__dirname,'files/expectedChart/Chart.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/Chart.yaml'))) - expect(getFileHash(join(__dirname,'files/expectedChart/values.schema.json'))).to.equal(getFileHash(join(bookshop, 'chart/values.schema.json'))) + expect(getFileHash(join(__dirname,'../files/chart/values.schema.json'))).to.equal(getFileHash(join(bookshop, 'chart/values.schema.json'))) expect(getFileHash(join(__dirname,'files/expectedChart/valuesWithDestination.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/values.yaml'))) }) + + it('Add cap-operator configurable template chart', async () => { + // revert xs-security to original value + fs.writeFileSync(bookshop+"/xs-security.json", orignalXsSecurityJson) + + execSync(`cds add cap-operator --with-configurable-templates`, { cwd: bookshop }) + + expect(getFileHash(join(__dirname,'files/expectedConfigurableTemplatesChart/Chart.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/Chart.yaml'))) + expect(getFileHash(join(__dirname,'files/expectedConfigurableTemplatesChart/values.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/values.yaml'))) + expect(getFileHash(join(__dirname,'files/expectedConfigurableTemplatesChart/templates/_helpers.tpl'))).to.equal(getFileHash(join(bookshop, 'chart/templates/_helpers.tpl'))) + + expect(getFileHash(join(__dirname,'../files/configurableTemplatesChart/values.schema.json'))).to.equal(getFileHash(join(bookshop, 'chart/values.schema.json'))) + expect(getFileHash(join(__dirname,'../files/configurableTemplatesChart/templates/cap-operator-cros.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/templates/cap-operator-cros.yaml'))) + expect(getFileHash(join(__dirname,'../files/configurableTemplatesChart/templates/service-instance.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/templates/service-instance.yaml'))) + expect(getFileHash(join(__dirname,'../files/configurableTemplatesChart/templates/service-binding.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/templates/service-binding.yaml'))) + + // Check changes to xs-security.json + expect(getFileHash(join(__dirname,'files/xs-security.json'))).to.equal(getFileHash(join(bookshop, 'xs-security.json'))) + }) + + it('Add cap-operator configurable template chart with cap-operator chart already present', async () => { + execSync(`cds add cap-operator`, { cwd: bookshop }) + + const log = execSync(`cds add cap-operator --with-configurable-templates`, { cwd: bookshop }).toString() + expect(log).to.include("CAP Operator chart already present. If you want to convert the existing chart to a configurable template chart, run 'npx cap-op-plugin convert-to-configurable-template-chart'") + + }) + + it('Chart folder already added but values.schema.json changed ', async () => { + execSync(`cds add cap-operator --with-configurable-templates`, { cwd: bookshop }) + + // Copying a dummy change to mimic a difference. In real scenario, the chart/values.schema.json inside the plugin repo will be updated. + // We dont expect users to change the values.schema.json file manually. + await cds.utils.copy(join('test/files', 'updatedValues.schema.json'), join(bookshop, 'chart/values.schema.json')) + + const log = execSync(`cds add cap-operator --with-configurable-templates`, { cwd: bookshop }).toString() + expect(log).to.include('⚠️ \'values.schema.json\' file is outdated. Run with \'--force\' to overwrite the file and accept the new changes.') + }) + + it('Add cap-operator configurable template chart with mta and mtaExtensions', async () => { + await cds.utils.copy(join('test/files', 'mta.yaml'), join(bookshop, 'mta.yaml')) + await cds.utils.copy(join('test/files', 'corrected_xsappname.mtaext'), join(bookshop, 'corrected_xsappname.mtaext')) + // revert xs-security to original value + fs.writeFileSync(bookshop+"/xs-security.json", orignalXsSecurityJson) + + execSync(`cds add cap-operator --with-mta mta.yaml --with-mta-extensions corrected_xsappname.mtaext --with-configurable-templates`, { cwd: bookshop }) + + expect(getFileHash(join(__dirname,'files/expectedConfigurableTemplatesChart/Chart.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/Chart.yaml'))) + expect(getFileHash(join(__dirname,'files/expectedConfigurableTemplatesChart/valuesWithMTA.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/values.yaml'))) + expect(getFileHash(join(__dirname,'files/expectedConfigurableTemplatesChart/templates/cap-operator-cros-mta.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/templates/cap-operator-cros.yaml'))) + expect(getFileHash(join(__dirname,'files/expectedConfigurableTemplatesChart/templates/_helpers.tpl'))).to.equal(getFileHash(join(bookshop, 'chart/templates/_helpers.tpl'))) + expect(getFileHash(join(__dirname,'files/xs-security.json'))).to.equal(getFileHash(join(bookshop, 'xs-security.json'))) + }) }) diff --git a/test/build.test.js b/test/build.test.js index 3f72838..cca46ec 100644 --- a/test/build.test.js +++ b/test/build.test.js @@ -35,9 +35,9 @@ describe('cds build', () => { execSync(`cds build`, { cwd: bookshop }) expect(getFileHash(join(__dirname,'files/expectedChart/Chart.yaml'))).to.equal(getFileHash(join(bookshop, 'gen/chart/Chart.yaml'))) - expect(getFileHash(join(__dirname,'files/expectedChart/values.schema.json'))).to.equal(getFileHash(join(bookshop, 'gen/chart/values.schema.json'))) + expect(getFileHash(join(__dirname,'../files/chart/values.schema.json'))).to.equal(getFileHash(join(bookshop, 'gen/chart/values.schema.json'))) expect(getFileHash(join(__dirname,'files/expectedChart/values.yaml'))).to.equal(getFileHash(join(bookshop, 'gen/chart/values.yaml'))) - expect(getFolderHash(join(__dirname,'files/expectedChart/templates'))).to.equal(getFolderHash(join(bookshop, 'gen/chart/templates'))) + expect(getFolderHash(join(__dirname,'../files/chart/templates'))).to.equal(getFolderHash(join(bookshop, 'gen/chart/templates'))) }) it('Build cap-operator chart with modified templates', async () => { @@ -49,8 +49,8 @@ describe('cds build', () => { execSync(`cds build`, { cwd: bookshop }) expect(getFileHash(join(__dirname,'files/expectedChart/Chart.yaml'))).to.equal(getFileHash(join(bookshop, 'gen/chart/Chart.yaml'))) - expect(getFileHash(join(__dirname,'files/expectedChart/values.schema.json'))).to.equal(getFileHash(join(bookshop, 'gen/chart/values.schema.json'))) + expect(getFileHash(join(__dirname,'../files/chart/values.schema.json'))).to.equal(getFileHash(join(bookshop, 'gen/chart/values.schema.json'))) expect(getFileHash(join(__dirname,'files/expectedChart/values.yaml'))).to.equal(getFileHash(join(bookshop, 'gen/chart/values.yaml'))) - expect(getFolderHash(join(__dirname,'files/expectedChart/templates'))).to.not.equal(getFolderHash(join(bookshop, 'gen/chart/templates'))) + expect(getFolderHash(join(__dirname,'../files/chart/templates'))).to.not.equal(getFolderHash(join(bookshop, 'gen/chart/templates'))) }) }) diff --git a/test/cap-op-plugin.test.js b/test/cap-op-plugin.test.js index 95af4fb..b3f5733 100644 --- a/test/cap-op-plugin.test.js +++ b/test/cap-op-plugin.test.js @@ -36,7 +36,7 @@ describe('cap-op-plugin', () => { }) it('Generate runtime-values file', async () => { - await cds.utils.copy(join('test/files', 'input_values.yaml'), join(bookshop, 'input_values.yaml')) + await cds.utils.copy(join(__dirname, 'files', 'input_values.yaml'), join(bookshop, 'input_values.yaml')) execSync(`cds add cap-operator`, { cwd: bookshop }) execSync(`npx cap-op-plugin generate-runtime-values --with-input-yaml input_values.yaml`, { cwd: bookshop }) @@ -44,7 +44,7 @@ describe('cap-op-plugin', () => { }) it('Generate runtime-values file using wrong input_values.yaml', async () => { - await cds.utils.copy(join('test/files', 'input_values_wrong.yaml'), join(bookshop, 'input_values_wrong.yaml')) + await cds.utils.copy(join(__dirname, 'files', 'input_values_wrong.yaml'), join(bookshop, 'input_values_wrong.yaml')) execSync(`cds add cap-operator`, { cwd: bookshop }) expect(() => execSync(`npx cap-op-plugin generate-runtime-values --with-input-yaml input_values_wrong.yaml`, { cwd: bookshop })).to.throw(`'appName', 'capOperatorSubdomain', 'clusterDomain', 'globalAccountId', 'providerSubdomain' and 'tenantId' are mandatory fields in the input yaml file.`) @@ -64,10 +64,15 @@ COMMANDS generate-runtime-values [--with-input-yaml ] Generate runtime-values.yaml file for the cap-operator chart + convert-to-configurable-template-chart [--with-runtime-yaml ] Convert existing chart to configurable template chart + EXAMPLES cap-op-plugin generate-runtime-values cap-op-plugin generate-runtime-values --with-input-yaml /path/to/input.yaml + + cap-op-plugin convert-to-configurable-template-chart + cap-op-plugin convert-to-configurable-template-chart --with-runtime-yaml /path/to/runtime.yaml `) }) @@ -92,9 +97,84 @@ EXAMPLES cds.root = bookshop await capOperatorPlugin('generate-runtime-values') + sinon.restore() expect(getFileHash(join(__dirname, 'files/expectedChart/runtime-values.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/runtime-values.yaml'))) + }) + + it('Convert existing chart to configurable template chart', async () => { + execSync(`cds add cap-operator`, { cwd: bookshop }) + + // Copy filled values.yaml + await cds.utils.copy(join(__dirname, 'files', 'values-of-simple-chart-filled.yaml'), join(bookshop, 'chart/values.yaml')) + execSync(`npx cap-op-plugin convert-to-configurable-template-chart`, { cwd: bookshop }) + + expect(getFileHash(join(__dirname,'files/expectedConfigurableTemplatesChart/templates/cap-operator-cros-modified.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/templates/cap-operator-cros.yaml'))) + expect(getFileHash(join(__dirname,'files/expectedConfigurableTemplatesChart/values-modified.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/values.yaml'))) + }) + + it('Generate runtime-values via prompts for configurable template chart', async () => { + execSync(`cds add cap-operator --with-configurable-templates`, { cwd: bookshop }) + + rlQuestion = sinon.stub() + rlInterface = { + question: rlQuestion, + close: sinon.stub() + } + sinon.stub(readline, 'createInterface').returns(rlInterface) + + rlQuestion.onFirstCall().callsArgWith(1, 'bkshop') + rlQuestion.onSecondCall().callsArgWith(1, '') + rlQuestion.onThirdCall().callsArgWith(1, 'c-abc.kyma.ondemand.com') + rlQuestion.onCall(3).callsArgWith(1, 'dc94db56-asda-adssa-dada-123456789012') + rlQuestion.onCall(4).callsArgWith(1, 'bem-aad-sadad-123456789012') + rlQuestion.onCall(5).callsArgWith(1, 'dasdsd-1234-1234-1234-123456789012') + rlQuestion.onCall(6).callsArgWith(1, 'sdasd-4c4d-4d4d-4d4d-123456789012') + rlQuestion.onCall(7).callsArgWith(1, 'regcred') + cds.root = bookshop + await capOperatorPlugin('generate-runtime-values') sinon.restore() + + expect(getFileHash(join(__dirname, 'files/expectedConfigurableTemplatesChart/runtime-values.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/runtime-values.yaml'))) + }) + + it('Convert existing chart to configurable template chart with runtime-values.yaml', async () => { + execSync(`cds add cap-operator`, { cwd: bookshop }) + + // Copy filled values.yaml + await cds.utils.copy(join(__dirname, 'files', 'values-of-simple-chart-filled.yaml'), join(bookshop, 'chart/values.yaml')) + await cds.utils.copy(join(__dirname, 'files', 'runtime-values-of-simple-chart.yaml'), join(bookshop, 'chart/runtime-values.yaml')) + execSync(`npx cap-op-plugin convert-to-configurable-template-chart --with-runtime-yaml chart/runtime-values.yaml`, { cwd: bookshop }) + + expect(getFileHash(join(__dirname,'files/expectedConfigurableTemplatesChart/templates/cap-operator-cros-modified.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/templates/cap-operator-cros.yaml'))) + expect(getFileHash(join(__dirname,'files/expectedConfigurableTemplatesChart/values-modified.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/values.yaml'))) + expect(getFileHash(join(__dirname,'files/expectedConfigurableTemplatesChart/runtime-values.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/runtime-values.yaml'))) + }) + + it('Convert existing chart to configurable template chart first then transform runtime-values.yaml', async () => { + execSync(`cds add cap-operator`, { cwd: bookshop }) + + // Copy filled values.yaml + await cds.utils.copy(join(__dirname, 'files', 'values-of-simple-chart-filled.yaml'), join(bookshop, 'chart/values.yaml')) + await cds.utils.copy(join(__dirname, 'files', 'runtime-values-of-simple-chart.yaml'), join(bookshop, 'chart/runtime-values.yaml')) + execSync(`npx cap-op-plugin convert-to-configurable-template-chart`, { cwd: bookshop }) + + const log = execSync(`npx cap-op-plugin convert-to-configurable-template-chart --with-runtime-yaml chart/runtime-values.yaml`, { cwd: bookshop }).toString() + expect(log).to.include('Exisiting chart is already a configurable template chart. No need for conversion.') + expect(log).to.include('Transforming runtime values file') + }) + + it('Convert existing chart to configurable template chart with runtime-values.yaml then trigger again', async () => { + execSync(`cds add cap-operator`, { cwd: bookshop }) + + // Copy filled values.yaml + await cds.utils.copy(join(__dirname, 'files', 'values-of-simple-chart-filled.yaml'), join(bookshop, 'chart/values.yaml')) + await cds.utils.copy(join(__dirname, 'files', 'runtime-values-of-simple-chart.yaml'), join(bookshop, 'chart/runtime-values.yaml')) + execSync(`npx cap-op-plugin convert-to-configurable-template-chart --with-runtime-yaml chart/runtime-values.yaml`, { cwd: bookshop }) + + const log = execSync(`npx cap-op-plugin convert-to-configurable-template-chart --with-runtime-yaml chart/runtime-values.yaml`, { cwd: bookshop }).toString() + expect(log).to.include('Exisiting chart is already a configurable template chart. No need for conversion.') + expect(log).to.include('already in the configurable template chart format.') }) }) diff --git a/test/files/expectedChart/runtime-values.yaml b/test/files/expectedChart/runtime-values.yaml index 722a8fa..6cfd868 100644 --- a/test/files/expectedChart/runtime-values.yaml +++ b/test/files/expectedChart/runtime-values.yaml @@ -1,5 +1,5 @@ serviceInstances: - saas-registry: + saasRegistry: parameters: xsappname: bkshop appName: bkshop @@ -29,7 +29,7 @@ btp: imagePullSecrets: - regcred workloads: - app-router: + appRouter: deploymentDefinition: env: - name: TENANT_HOST_PATTERN @@ -39,7 +39,7 @@ workloads: env: - name: CDS_CONFIG value: '{"requires":{"cds.xt.DeploymentService":{"hdi":{"create":{"database_id":"sdasd-4c4d-4d4d-4d4d-123456789012"}}}}}' - tenant-job: + tenantJob: jobDefinition: env: - name: CDS_CONFIG diff --git a/test/files/expectedChart/templates/_helpers.tpl b/test/files/expectedChart/templates/_helpers.tpl deleted file mode 100644 index c0acba4..0000000 --- a/test/files/expectedChart/templates/_helpers.tpl +++ /dev/null @@ -1,8 +0,0 @@ -{{- define "capApplicationVersionName" -}} -{{ printf "cav-%s-%d" (include "appName" $) (.Release.Revision) }} -{{- end -}} - -{{- define "appName" -}} -{{- $xsuaa := index .Values.serviceInstances "xsuaa" -}} -{{ printf "%s" $xsuaa.parameters.xsappname }} -{{- end -}} diff --git a/test/files/expectedChart/templates/cap-operator-cros.yaml b/test/files/expectedChart/templates/cap-operator-cros.yaml deleted file mode 100644 index f3ace41..0000000 --- a/test/files/expectedChart/templates/cap-operator-cros.yaml +++ /dev/null @@ -1,91 +0,0 @@ ---- -apiVersion: sme.sap.com/v1alpha1 -kind: CAPApplication -metadata: - name: cap-{{ include "appName" $ }} -spec: - domains: - primary: {{.Values.app.domains.primary}} - {{- if .Values.app.domains.secondary }} - secondary: - {{- range .Values.app.domains.secondary }} - - {{.}} - {{- end }} - {{- end }} - istioIngressGatewayLabels: - {{- range $k, $v := .Values.app.istioIngressGatewayLabels }} - - name: {{ $k }} - value: {{ $v | default "invalidValue"}} - {{- end }} - btpAppName: {{ include "appName" $ }} - globalAccountId: "{{.Values.btp.globalAccountId}}" - provider: - subDomain: "{{.Values.btp.provider.subdomain}}" - tenantId: "{{.Values.btp.provider.tenantId}}" - btp: - services: - {{- $serviceInstances := .Values.serviceInstances }} - {{- range $k, $v := .Values.serviceBindings }} - {{- $serviceInstance := dict }} - {{- range $sik, $siv := $serviceInstances }} - {{- if eq $siv.name $v.serviceInstanceName }} - {{- $serviceInstance = $siv }} - {{- end }} - {{- end }} - {{- if hasKey $serviceInstance "serviceOfferingName" }} - - class: {{ get $serviceInstance "serviceOfferingName" | default "invalidValue" }} - {{- if $v.externalName }} - name: {{ $v.externalName | default "invalidValue" }} - {{- else }} - name: {{ $v.name | default "invalidValue" }} - {{- end }} - secret: {{ $v.secretName | default "invalidValue" }} - {{- end }} - {{- end }} ---- -apiVersion: sme.sap.com/v1alpha1 -kind: CAPApplicationVersion -metadata: - annotations: - helm.sh/hook: post-install,post-upgrade - helm.sh/resource-policy: keep - name: {{ include "capApplicationVersionName" $ }} -spec: - capApplicationInstance: "cap-{{ include "appName" $ }}" - version: "{{ .Release.Revision }}" - registrySecrets: - {{- range .Values.imagePullSecrets }} - - {{.}} - {{- end }} - workloads: - {{- range $k, $v := .Values.workloads }} - - name: {{ $v.name }} - - {{- if $v.labels }} - labels: {{- toYaml $v.labels | trim | nindent 6 }} - {{- end }} - - {{- if $v.annotations }} - annotations: {{- toYaml $v.annotations | trim | nindent 6 }} - {{- end }} - - {{- if $v.consumedBTPServices }} - consumedBTPServices: {{- toYaml $v.consumedBTPServices | trim | nindent 4 }} - {{- end }} - - {{- if $v.deploymentDefinition }} - deploymentDefinition: {{- toYaml $v.deploymentDefinition | trim | nindent 6 }} - {{- end }} - - {{- if $v.jobDefinition }} - jobDefinition: {{- toYaml $v.jobDefinition | trim | nindent 6 }} - {{- end }} - {{- end }} - - {{- if .Values.tenantOperations }} - tenantOperations: {{- toYaml .Values.tenantOperations | trim | nindent 4 }} - {{- end }} - - {{- if .Values.contentJobs }} - contentJobs: {{- toYaml .Values.contentJobs | trim | nindent 4 }} - {{- end }} diff --git a/test/files/expectedChart/values.schema.json b/test/files/expectedChart/values.schema.json deleted file mode 100644 index 49253c7..0000000 --- a/test/files/expectedChart/values.schema.json +++ /dev/null @@ -1,2786 +0,0 @@ -{ - "$defs": { - "AWSElasticBlockStoreVolumeSource": { - "additionalProperties": false, - "properties": { - "fsType": { - "type": "string" - }, - "partition": { - "type": "integer" - }, - "readOnly": { - "type": "boolean" - }, - "volumeID": { - "type": "string" - } - }, - "required": [ - "volumeID" - ], - "type": "object" - }, - "Affinity": { - "additionalProperties": false, - "properties": { - "nodeAffinity": { - "$ref": "#/$defs/NodeAffinity" - }, - "podAffinity": { - "$ref": "#/$defs/PodAffinity" - }, - "podAntiAffinity": { - "$ref": "#/$defs/PodAntiAffinity" - } - }, - "type": "object" - }, - "AppArmorProfile": { - "additionalProperties": false, - "properties": { - "localhostProfile": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "type" - ], - "type": "object" - }, - "AzureDiskVolumeSource": { - "additionalProperties": false, - "properties": { - "cachingMode": { - "type": "string" - }, - "diskName": { - "type": "string" - }, - "diskURI": { - "type": "string" - }, - "fsType": { - "type": "string" - }, - "kind": { - "type": "string" - }, - "readOnly": { - "type": "boolean" - } - }, - "required": [ - "diskName", - "diskURI" - ], - "type": "object" - }, - "AzureFileVolumeSource": { - "additionalProperties": false, - "properties": { - "readOnly": { - "type": "boolean" - }, - "secretName": { - "type": "string" - }, - "shareName": { - "type": "string" - } - }, - "required": [ - "secretName", - "shareName" - ], - "type": "object" - }, - "CSIVolumeSource": { - "additionalProperties": false, - "properties": { - "driver": { - "type": "string" - }, - "fsType": { - "type": "string" - }, - "nodePublishSecretRef": { - "$ref": "#/$defs/LocalObjectReference" - }, - "readOnly": { - "type": "boolean" - }, - "volumeAttributes": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - } - }, - "required": [ - "driver" - ], - "type": "object" - }, - "Capabilities": { - "additionalProperties": false, - "properties": { - "add": { - "items": { - "type": "string" - }, - "type": "array" - }, - "drop": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "type": "object" - }, - "CephFSVolumeSource": { - "additionalProperties": false, - "properties": { - "monitors": { - "items": { - "type": "string" - }, - "type": "array" - }, - "path": { - "type": "string" - }, - "readOnly": { - "type": "boolean" - }, - "secretFile": { - "type": "string" - }, - "secretRef": { - "$ref": "#/$defs/LocalObjectReference" - }, - "user": { - "type": "string" - } - }, - "required": [ - "monitors" - ], - "type": "object" - }, - "CinderVolumeSource": { - "additionalProperties": false, - "properties": { - "fsType": { - "type": "string" - }, - "readOnly": { - "type": "boolean" - }, - "secretRef": { - "$ref": "#/$defs/LocalObjectReference" - }, - "volumeID": { - "type": "string" - } - }, - "required": [ - "volumeID" - ], - "type": "object" - }, - "ClusterTrustBundleProjection": { - "additionalProperties": false, - "properties": { - "labelSelector": { - "$ref": "#/$defs/LabelSelector" - }, - "name": { - "type": "string" - }, - "optional": { - "type": "boolean" - }, - "path": { - "type": "string" - }, - "signerName": { - "type": "string" - } - }, - "required": [ - "path" - ], - "type": "object" - }, - "ConfigMapEnvSource": { - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - }, - "optional": { - "type": "boolean" - } - }, - "type": "object" - }, - "ConfigMapKeySelector": { - "additionalProperties": false, - "properties": { - "key": { - "type": "string" - }, - "name": { - "type": "string" - }, - "optional": { - "type": "boolean" - } - }, - "required": [ - "key" - ], - "type": "object" - }, - "ConfigMapProjection": { - "additionalProperties": false, - "properties": { - "items": { - "items": { - "$ref": "#/$defs/KeyToPath" - }, - "type": "array" - }, - "name": { - "type": "string" - }, - "optional": { - "type": "boolean" - } - }, - "type": "object" - }, - "ConfigMapVolumeSource": { - "additionalProperties": false, - "properties": { - "defaultMode": { - "type": "integer" - }, - "items": { - "items": { - "$ref": "#/$defs/KeyToPath" - }, - "type": "array" - }, - "name": { - "type": "string" - }, - "optional": { - "type": "boolean" - } - }, - "type": "object" - }, - "Container": { - "additionalProperties": false, - "properties": { - "args": { - "items": { - "type": "string" - }, - "type": "array" - }, - "command": { - "items": { - "type": "string" - }, - "type": "array" - }, - "env": { - "items": { - "$ref": "#/$defs/EnvVar" - }, - "type": "array" - }, - "envFrom": { - "items": { - "$ref": "#/$defs/EnvFromSource" - }, - "type": "array" - }, - "image": { - "type": "string" - }, - "imagePullPolicy": { - "type": "string" - }, - "lifecycle": { - "$ref": "#/$defs/Lifecycle" - }, - "livenessProbe": { - "$ref": "#/$defs/Probe" - }, - "name": { - "type": "string" - }, - "ports": { - "items": { - "$ref": "#/$defs/ContainerPort" - }, - "type": "array" - }, - "readinessProbe": { - "$ref": "#/$defs/Probe" - }, - "resizePolicy": { - "items": { - "$ref": "#/$defs/ContainerResizePolicy" - }, - "type": "array" - }, - "resources": { - "$ref": "#/$defs/ResourceRequirements" - }, - "restartPolicy": { - "type": "string" - }, - "securityContext": { - "$ref": "#/$defs/SecurityContext" - }, - "startupProbe": { - "$ref": "#/$defs/Probe" - }, - "stdin": { - "type": "boolean" - }, - "stdinOnce": { - "type": "boolean" - }, - "terminationMessagePath": { - "type": "string" - }, - "terminationMessagePolicy": { - "type": "string" - }, - "tty": { - "type": "boolean" - }, - "volumeDevices": { - "items": { - "$ref": "#/$defs/VolumeDevice" - }, - "type": "array" - }, - "volumeMounts": { - "items": { - "$ref": "#/$defs/VolumeMount" - }, - "type": "array" - }, - "workingDir": { - "type": "string" - } - }, - "required": [ - "name" - ], - "type": "object" - }, - "ContainerPort": { - "additionalProperties": false, - "properties": { - "containerPort": { - "type": "integer" - }, - "hostIP": { - "type": "string" - }, - "hostPort": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "protocol": { - "type": "string" - } - }, - "required": [ - "containerPort" - ], - "type": "object" - }, - "ContainerResizePolicy": { - "additionalProperties": false, - "properties": { - "resourceName": { - "type": "string" - }, - "restartPolicy": { - "type": "string" - } - }, - "required": [ - "resourceName", - "restartPolicy" - ], - "type": "object" - }, - "CredentialsRotationPolicy": { - "additionalProperties": false, - "properties": { - "enabled": { - "type": "boolean" - }, - "rotatedBindingTTL": { - "type": "string" - }, - "rotationFrequency": { - "type": "string" - } - }, - "required": [ - "enabled" - ], - "type": "object" - }, - "DeploymentDetails": { - "additionalProperties": false, - "properties": { - "affinity": { - "$ref": "#/$defs/Affinity" - }, - "command": { - "items": { - "type": "string" - }, - "type": "array" - }, - "env": { - "items": { - "$ref": "#/$defs/EnvVar" - }, - "type": "array" - }, - "image": { - "type": "string" - }, - "imagePullPolicy": { - "type": "string" - }, - "initContainers": { - "items": { - "$ref": "#/$defs/Container" - }, - "type": "array" - }, - "livenessProbe": { - "$ref": "#/$defs/Probe" - }, - "nodeName": { - "type": "string" - }, - "nodeSelector": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - "podSecurityContext": { - "$ref": "#/$defs/PodSecurityContext" - }, - "ports": { - "items": { - "$ref": "#/$defs/Ports" - }, - "type": "array" - }, - "priorityClassName": { - "type": "string" - }, - "readinessProbe": { - "$ref": "#/$defs/Probe" - }, - "replicas": { - "type": "integer" - }, - "resources": { - "$ref": "#/$defs/ResourceRequirements" - }, - "securityContext": { - "$ref": "#/$defs/SecurityContext" - }, - "serviceAccountName": { - "type": "string" - }, - "tolerations": { - "items": { - "$ref": "#/$defs/Toleration" - }, - "type": "array" - }, - "topologySpreadConstraints": { - "items": { - "$ref": "#/$defs/TopologySpreadConstraint" - }, - "type": "array" - }, - "type": { - "type": "string" - }, - "volumeMounts": { - "items": { - "$ref": "#/$defs/VolumeMount" - }, - "type": "array" - }, - "volumes": { - "items": { - "$ref": "#/$defs/Volume" - }, - "type": "array" - } - }, - "required": [ - "image", - "type" - ], - "type": "object" - }, - "Domains": { - "additionalProperties": false, - "properties": { - "primary": { - "type": "string" - }, - "secondary": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "primary", - "secondary" - ], - "type": "object" - }, - "DownwardAPIProjection": { - "additionalProperties": false, - "properties": { - "items": { - "items": { - "$ref": "#/$defs/DownwardAPIVolumeFile" - }, - "type": "array" - } - }, - "type": "object" - }, - "DownwardAPIVolumeFile": { - "additionalProperties": false, - "properties": { - "fieldRef": { - "$ref": "#/$defs/ObjectFieldSelector" - }, - "mode": { - "type": "integer" - }, - "path": { - "type": "string" - }, - "resourceFieldRef": { - "$ref": "#/$defs/ResourceFieldSelector" - } - }, - "required": [ - "path" - ], - "type": "object" - }, - "DownwardAPIVolumeSource": { - "additionalProperties": false, - "properties": { - "defaultMode": { - "type": "integer" - }, - "items": { - "items": { - "$ref": "#/$defs/DownwardAPIVolumeFile" - }, - "type": "array" - } - }, - "type": "object" - }, - "EmptyDirVolumeSource": { - "additionalProperties": false, - "properties": { - "medium": { - "type": "string" - }, - "sizeLimit": { - "$ref": "#/$defs/Quantity" - } - }, - "type": "object" - }, - "EnvFromSource": { - "additionalProperties": false, - "properties": { - "configMapRef": { - "$ref": "#/$defs/ConfigMapEnvSource" - }, - "prefix": { - "type": "string" - }, - "secretRef": { - "$ref": "#/$defs/SecretEnvSource" - } - }, - "type": "object" - }, - "EnvVar": { - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - }, - "valueFrom": { - "$ref": "#/$defs/EnvVarSource" - } - }, - "required": [ - "name" - ], - "type": "object" - }, - "EnvVarSource": { - "additionalProperties": false, - "properties": { - "configMapKeyRef": { - "$ref": "#/$defs/ConfigMapKeySelector" - }, - "fieldRef": { - "$ref": "#/$defs/ObjectFieldSelector" - }, - "resourceFieldRef": { - "$ref": "#/$defs/ResourceFieldSelector" - }, - "secretKeyRef": { - "$ref": "#/$defs/SecretKeySelector" - } - }, - "type": "object" - }, - "EphemeralVolumeSource": { - "additionalProperties": false, - "properties": { - "volumeClaimTemplate": { - "$ref": "#/$defs/PersistentVolumeClaimTemplate" - } - }, - "type": "object" - }, - "ExecAction": { - "additionalProperties": false, - "properties": { - "command": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "type": "object" - }, - "ExtraValue": { - "items": { - "type": "string" - }, - "type": "array" - }, - "FCVolumeSource": { - "additionalProperties": false, - "properties": { - "fsType": { - "type": "string" - }, - "lun": { - "type": "integer" - }, - "readOnly": { - "type": "boolean" - }, - "targetWWNs": { - "items": { - "type": "string" - }, - "type": "array" - }, - "wwids": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "type": "object" - }, - "FieldsV1": { - "additionalProperties": false, - "properties": {}, - "type": "object" - }, - "FlexVolumeSource": { - "additionalProperties": false, - "properties": { - "driver": { - "type": "string" - }, - "fsType": { - "type": "string" - }, - "options": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - "readOnly": { - "type": "boolean" - }, - "secretRef": { - "$ref": "#/$defs/LocalObjectReference" - } - }, - "required": [ - "driver" - ], - "type": "object" - }, - "FlockerVolumeSource": { - "additionalProperties": false, - "properties": { - "datasetName": { - "type": "string" - }, - "datasetUUID": { - "type": "string" - } - }, - "type": "object" - }, - "GCEPersistentDiskVolumeSource": { - "additionalProperties": false, - "properties": { - "fsType": { - "type": "string" - }, - "partition": { - "type": "integer" - }, - "pdName": { - "type": "string" - }, - "readOnly": { - "type": "boolean" - } - }, - "required": [ - "pdName" - ], - "type": "object" - }, - "GRPCAction": { - "additionalProperties": false, - "properties": { - "port": { - "type": "integer" - }, - "service": { - "type": "string" - } - }, - "required": [ - "port", - "service" - ], - "type": "object" - }, - "GitRepoVolumeSource": { - "additionalProperties": false, - "properties": { - "directory": { - "type": "string" - }, - "repository": { - "type": "string" - }, - "revision": { - "type": "string" - } - }, - "required": [ - "repository" - ], - "type": "object" - }, - "GlusterfsVolumeSource": { - "additionalProperties": false, - "properties": { - "endpoints": { - "type": "string" - }, - "path": { - "type": "string" - }, - "readOnly": { - "type": "boolean" - } - }, - "required": [ - "endpoints", - "path" - ], - "type": "object" - }, - "HTTPGetAction": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "httpHeaders": { - "items": { - "$ref": "#/$defs/HTTPHeader" - }, - "type": "array" - }, - "path": { - "type": "string" - }, - "port": { - "$ref": "#/$defs/IntOrString" - }, - "scheme": { - "type": "string" - } - }, - "required": [ - "port" - ], - "type": "object" - }, - "HTTPHeader": { - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - }, - "required": [ - "name", - "value" - ], - "type": "object" - }, - "HostPathVolumeSource": { - "additionalProperties": false, - "properties": { - "path": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "path" - ], - "type": "object" - }, - "ISCSIVolumeSource": { - "additionalProperties": false, - "properties": { - "chapAuthDiscovery": { - "type": "boolean" - }, - "chapAuthSession": { - "type": "boolean" - }, - "fsType": { - "type": "string" - }, - "initiatorName": { - "type": "string" - }, - "iqn": { - "type": "string" - }, - "iscsiInterface": { - "type": "string" - }, - "lun": { - "type": "integer" - }, - "portals": { - "items": { - "type": "string" - }, - "type": "array" - }, - "readOnly": { - "type": "boolean" - }, - "secretRef": { - "$ref": "#/$defs/LocalObjectReference" - }, - "targetPortal": { - "type": "string" - } - }, - "required": [ - "targetPortal", - "iqn", - "lun" - ], - "type": "object" - }, - "IntOrString": { - "additionalProperties": false, - "properties": { - "IntVal": { - "type": "integer" - }, - "StrVal": { - "type": "string" - }, - "Type": { - "type": "integer" - } - }, - "required": [ - "Type", - "IntVal", - "StrVal" - ], - "type": "object" - }, - "JobDetails": { - "additionalProperties": false, - "properties": { - "affinity": { - "$ref": "#/$defs/Affinity" - }, - "backoffLimit": { - "type": "integer" - }, - "command": { - "items": { - "type": "string" - }, - "type": "array" - }, - "env": { - "items": { - "$ref": "#/$defs/EnvVar" - }, - "type": "array" - }, - "image": { - "type": "string" - }, - "imagePullPolicy": { - "type": "string" - }, - "initContainers": { - "items": { - "$ref": "#/$defs/Container" - }, - "type": "array" - }, - "nodeName": { - "type": "string" - }, - "nodeSelector": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - "podSecurityContext": { - "$ref": "#/$defs/PodSecurityContext" - }, - "priorityClassName": { - "type": "string" - }, - "resources": { - "$ref": "#/$defs/ResourceRequirements" - }, - "securityContext": { - "$ref": "#/$defs/SecurityContext" - }, - "serviceAccountName": { - "type": "string" - }, - "tolerations": { - "items": { - "$ref": "#/$defs/Toleration" - }, - "type": "array" - }, - "topologySpreadConstraints": { - "items": { - "$ref": "#/$defs/TopologySpreadConstraint" - }, - "type": "array" - }, - "ttlSecondsAfterFinished": { - "type": "integer" - }, - "type": { - "type": "string" - }, - "volumeMounts": { - "items": { - "$ref": "#/$defs/VolumeMount" - }, - "type": "array" - }, - "volumes": { - "items": { - "$ref": "#/$defs/Volume" - }, - "type": "array" - } - }, - "required": [ - "image", - "type" - ], - "type": "object" - }, - "KeyToPath": { - "additionalProperties": false, - "properties": { - "key": { - "type": "string" - }, - "mode": { - "type": "integer" - }, - "path": { - "type": "string" - } - }, - "required": [ - "key", - "path" - ], - "type": "object" - }, - "LabelSelector": { - "additionalProperties": false, - "properties": { - "matchExpressions": { - "items": { - "$ref": "#/$defs/LabelSelectorRequirement" - }, - "type": "array" - }, - "matchLabels": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - } - }, - "type": "object" - }, - "LabelSelectorRequirement": { - "additionalProperties": false, - "properties": { - "key": { - "type": "string" - }, - "operator": { - "type": "string" - }, - "values": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "key", - "operator" - ], - "type": "object" - }, - "Lifecycle": { - "additionalProperties": false, - "properties": { - "postStart": { - "$ref": "#/$defs/LifecycleHandler" - }, - "preStop": { - "$ref": "#/$defs/LifecycleHandler" - } - }, - "type": "object" - }, - "LifecycleHandler": { - "additionalProperties": false, - "properties": { - "exec": { - "$ref": "#/$defs/ExecAction" - }, - "httpGet": { - "$ref": "#/$defs/HTTPGetAction" - }, - "sleep": { - "$ref": "#/$defs/SleepAction" - }, - "tcpSocket": { - "$ref": "#/$defs/TCPSocketAction" - } - }, - "type": "object" - }, - "LocalObjectReference": { - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - } - }, - "type": "object" - }, - "ManagedFieldsEntry": { - "additionalProperties": false, - "properties": { - "apiVersion": { - "type": "string" - }, - "fieldsType": { - "type": "string" - }, - "fieldsV1": { - "$ref": "#/$defs/FieldsV1" - }, - "manager": { - "type": "string" - }, - "operation": { - "type": "string" - }, - "subresource": { - "type": "string" - }, - "time": { - "$ref": "#/$defs/Time" - } - }, - "type": "object" - }, - "NFSVolumeSource": { - "additionalProperties": false, - "properties": { - "path": { - "type": "string" - }, - "readOnly": { - "type": "boolean" - }, - "server": { - "type": "string" - } - }, - "required": [ - "server", - "path" - ], - "type": "object" - }, - "NodeAffinity": { - "additionalProperties": false, - "properties": { - "preferredDuringSchedulingIgnoredDuringExecution": { - "items": { - "$ref": "#/$defs/PreferredSchedulingTerm" - }, - "type": "array" - }, - "requiredDuringSchedulingIgnoredDuringExecution": { - "$ref": "#/$defs/NodeSelector" - } - }, - "type": "object" - }, - "NodeSelector": { - "additionalProperties": false, - "properties": { - "nodeSelectorTerms": { - "items": { - "$ref": "#/$defs/NodeSelectorTerm" - }, - "type": "array" - } - }, - "required": [ - "nodeSelectorTerms" - ], - "type": "object" - }, - "NodeSelectorRequirement": { - "additionalProperties": false, - "properties": { - "key": { - "type": "string" - }, - "operator": { - "type": "string" - }, - "values": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "key", - "operator" - ], - "type": "object" - }, - "NodeSelectorTerm": { - "additionalProperties": false, - "properties": { - "matchExpressions": { - "items": { - "$ref": "#/$defs/NodeSelectorRequirement" - }, - "type": "array" - }, - "matchFields": { - "items": { - "$ref": "#/$defs/NodeSelectorRequirement" - }, - "type": "array" - } - }, - "type": "object" - }, - "ObjectFieldSelector": { - "additionalProperties": false, - "properties": { - "apiVersion": { - "type": "string" - }, - "fieldPath": { - "type": "string" - } - }, - "required": [ - "fieldPath" - ], - "type": "object" - }, - "ObjectMeta": { - "additionalProperties": false, - "properties": { - "annotations": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - "creationTimestamp": { - "$ref": "#/$defs/Time" - }, - "deletionGracePeriodSeconds": { - "type": "integer" - }, - "deletionTimestamp": { - "$ref": "#/$defs/Time" - }, - "finalizers": { - "items": { - "type": "string" - }, - "type": "array" - }, - "generateName": { - "type": "string" - }, - "generation": { - "type": "integer" - }, - "labels": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - "managedFields": { - "items": { - "$ref": "#/$defs/ManagedFieldsEntry" - }, - "type": "array" - }, - "name": { - "type": "string" - }, - "namespace": { - "type": "string" - }, - "ownerReferences": { - "items": { - "$ref": "#/$defs/OwnerReference" - }, - "type": "array" - }, - "resourceVersion": { - "type": "string" - }, - "selfLink": { - "type": "string" - }, - "uid": { - "type": "string" - } - }, - "type": "object" - }, - "OwnerReference": { - "additionalProperties": false, - "properties": { - "apiVersion": { - "type": "string" - }, - "blockOwnerDeletion": { - "type": "boolean" - }, - "controller": { - "type": "boolean" - }, - "kind": { - "type": "string" - }, - "name": { - "type": "string" - }, - "uid": { - "type": "string" - } - }, - "required": [ - "apiVersion", - "kind", - "name", - "uid" - ], - "type": "object" - }, - "ParametersFromSource": { - "additionalProperties": false, - "properties": { - "secretKeyRef": { - "$ref": "#/$defs/SecretKeyReference" - } - }, - "type": "object" - }, - "PersistentVolumeClaimSpec": { - "additionalProperties": false, - "properties": { - "accessModes": { - "items": { - "type": "string" - }, - "type": "array" - }, - "dataSource": { - "$ref": "#/$defs/TypedLocalObjectReference" - }, - "dataSourceRef": { - "$ref": "#/$defs/TypedObjectReference" - }, - "resources": { - "$ref": "#/$defs/VolumeResourceRequirements" - }, - "selector": { - "$ref": "#/$defs/LabelSelector" - }, - "storageClassName": { - "type": "string" - }, - "volumeAttributesClassName": { - "type": "string" - }, - "volumeMode": { - "type": "string" - }, - "volumeName": { - "type": "string" - } - }, - "type": "object" - }, - "PersistentVolumeClaimTemplate": { - "additionalProperties": false, - "properties": { - "metadata": { - "$ref": "#/$defs/ObjectMeta" - }, - "spec": { - "$ref": "#/$defs/PersistentVolumeClaimSpec" - } - }, - "required": [ - "spec" - ], - "type": "object" - }, - "PersistentVolumeClaimVolumeSource": { - "additionalProperties": false, - "properties": { - "claimName": { - "type": "string" - }, - "readOnly": { - "type": "boolean" - } - }, - "required": [ - "claimName" - ], - "type": "object" - }, - "PhotonPersistentDiskVolumeSource": { - "additionalProperties": false, - "properties": { - "fsType": { - "type": "string" - }, - "pdID": { - "type": "string" - } - }, - "required": [ - "pdID" - ], - "type": "object" - }, - "PodAffinity": { - "additionalProperties": false, - "properties": { - "preferredDuringSchedulingIgnoredDuringExecution": { - "items": { - "$ref": "#/$defs/WeightedPodAffinityTerm" - }, - "type": "array" - }, - "requiredDuringSchedulingIgnoredDuringExecution": { - "items": { - "$ref": "#/$defs/PodAffinityTerm" - }, - "type": "array" - } - }, - "type": "object" - }, - "PodAffinityTerm": { - "additionalProperties": false, - "properties": { - "labelSelector": { - "$ref": "#/$defs/LabelSelector" - }, - "matchLabelKeys": { - "items": { - "type": "string" - }, - "type": "array" - }, - "mismatchLabelKeys": { - "items": { - "type": "string" - }, - "type": "array" - }, - "namespaceSelector": { - "$ref": "#/$defs/LabelSelector" - }, - "namespaces": { - "items": { - "type": "string" - }, - "type": "array" - }, - "topologyKey": { - "type": "string" - } - }, - "required": [ - "topologyKey" - ], - "type": "object" - }, - "PodAntiAffinity": { - "additionalProperties": false, - "properties": { - "preferredDuringSchedulingIgnoredDuringExecution": { - "items": { - "$ref": "#/$defs/WeightedPodAffinityTerm" - }, - "type": "array" - }, - "requiredDuringSchedulingIgnoredDuringExecution": { - "items": { - "$ref": "#/$defs/PodAffinityTerm" - }, - "type": "array" - } - }, - "type": "object" - }, - "PodSecurityContext": { - "additionalProperties": false, - "properties": { - "appArmorProfile": { - "$ref": "#/$defs/AppArmorProfile" - }, - "fsGroup": { - "type": "integer" - }, - "fsGroupChangePolicy": { - "type": "string" - }, - "runAsGroup": { - "type": "integer" - }, - "runAsNonRoot": { - "type": "boolean" - }, - "runAsUser": { - "type": "integer" - }, - "seLinuxOptions": { - "$ref": "#/$defs/SELinuxOptions" - }, - "seccompProfile": { - "$ref": "#/$defs/SeccompProfile" - }, - "supplementalGroups": { - "items": { - "type": "integer" - }, - "type": "array" - }, - "sysctls": { - "items": { - "$ref": "#/$defs/Sysctl" - }, - "type": "array" - }, - "windowsOptions": { - "$ref": "#/$defs/WindowsSecurityContextOptions" - } - }, - "type": "object" - }, - "Ports": { - "additionalProperties": false, - "properties": { - "appProtocol": { - "type": "string" - }, - "name": { - "type": "string" - }, - "networkPolicy": { - "type": "string" - }, - "port": { - "type": "integer" - }, - "routerDestinationName": { - "type": "string" - } - }, - "required": [ - "name", - "port" - ], - "type": "object" - }, - "PortworxVolumeSource": { - "additionalProperties": false, - "properties": { - "fsType": { - "type": "string" - }, - "readOnly": { - "type": "boolean" - }, - "volumeID": { - "type": "string" - } - }, - "required": [ - "volumeID" - ], - "type": "object" - }, - "PreferredSchedulingTerm": { - "additionalProperties": false, - "properties": { - "preference": { - "$ref": "#/$defs/NodeSelectorTerm" - }, - "weight": { - "type": "integer" - } - }, - "required": [ - "weight", - "preference" - ], - "type": "object" - }, - "Probe": { - "additionalProperties": false, - "properties": { - "exec": { - "$ref": "#/$defs/ExecAction" - }, - "failureThreshold": { - "type": "integer" - }, - "grpc": { - "$ref": "#/$defs/GRPCAction" - }, - "httpGet": { - "$ref": "#/$defs/HTTPGetAction" - }, - "initialDelaySeconds": { - "type": "integer" - }, - "periodSeconds": { - "type": "integer" - }, - "successThreshold": { - "type": "integer" - }, - "tcpSocket": { - "$ref": "#/$defs/TCPSocketAction" - }, - "terminationGracePeriodSeconds": { - "type": "integer" - }, - "timeoutSeconds": { - "type": "integer" - } - }, - "type": "object" - }, - "ProjectedVolumeSource": { - "additionalProperties": false, - "properties": { - "defaultMode": { - "type": "integer" - }, - "sources": { - "items": { - "$ref": "#/$defs/VolumeProjection" - }, - "type": "array" - } - }, - "required": [ - "sources" - ], - "type": "object" - }, - "Quantity": { - "additionalProperties": false, - "type": "string" - }, - "QuobyteVolumeSource": { - "additionalProperties": false, - "properties": { - "group": { - "type": "string" - }, - "readOnly": { - "type": "boolean" - }, - "registry": { - "type": "string" - }, - "tenant": { - "type": "string" - }, - "user": { - "type": "string" - }, - "volume": { - "type": "string" - } - }, - "required": [ - "registry", - "volume" - ], - "type": "object" - }, - "RBDVolumeSource": { - "additionalProperties": false, - "properties": { - "fsType": { - "type": "string" - }, - "image": { - "type": "string" - }, - "keyring": { - "type": "string" - }, - "monitors": { - "items": { - "type": "string" - }, - "type": "array" - }, - "pool": { - "type": "string" - }, - "readOnly": { - "type": "boolean" - }, - "secretRef": { - "$ref": "#/$defs/LocalObjectReference" - }, - "user": { - "type": "string" - } - }, - "required": [ - "monitors", - "image" - ], - "type": "object" - }, - "RawExtension": { - "additionalProperties": true, - "properties": {}, - "type": "object" - }, - "ResourceClaim": { - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - } - }, - "required": [ - "name" - ], - "type": "object" - }, - "ResourceFieldSelector": { - "additionalProperties": false, - "properties": { - "containerName": { - "type": "string" - }, - "divisor": { - "$ref": "#/$defs/Quantity" - }, - "resource": { - "type": "string" - } - }, - "required": [ - "resource" - ], - "type": "object" - }, - "ResourceList": { - "additionalProperties": { - "$ref": "#/$defs/Quantity" - }, - "type": "object" - }, - "ResourceRequirements": { - "additionalProperties": false, - "properties": { - "claims": { - "items": { - "$ref": "#/$defs/ResourceClaim" - }, - "type": "array" - }, - "limits": { - "$ref": "#/$defs/ResourceList" - }, - "requests": { - "$ref": "#/$defs/ResourceList" - } - }, - "type": "object" - }, - "SELinuxOptions": { - "additionalProperties": false, - "properties": { - "level": { - "type": "string" - }, - "role": { - "type": "string" - }, - "type": { - "type": "string" - }, - "user": { - "type": "string" - } - }, - "type": "object" - }, - "ScaleIOVolumeSource": { - "additionalProperties": false, - "properties": { - "fsType": { - "type": "string" - }, - "gateway": { - "type": "string" - }, - "protectionDomain": { - "type": "string" - }, - "readOnly": { - "type": "boolean" - }, - "secretRef": { - "$ref": "#/$defs/LocalObjectReference" - }, - "sslEnabled": { - "type": "boolean" - }, - "storageMode": { - "type": "string" - }, - "storagePool": { - "type": "string" - }, - "system": { - "type": "string" - }, - "volumeName": { - "type": "string" - } - }, - "required": [ - "gateway", - "system", - "secretRef" - ], - "type": "object" - }, - "SeccompProfile": { - "additionalProperties": false, - "properties": { - "localhostProfile": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "type" - ], - "type": "object" - }, - "SecretEnvSource": { - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - }, - "optional": { - "type": "boolean" - } - }, - "type": "object" - }, - "SecretKeyReference": { - "additionalProperties": false, - "properties": { - "key": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": [ - "name", - "key" - ], - "type": "object" - }, - "SecretKeySelector": { - "additionalProperties": false, - "properties": { - "key": { - "type": "string" - }, - "name": { - "type": "string" - }, - "optional": { - "type": "boolean" - } - }, - "required": [ - "key" - ], - "type": "object" - }, - "SecretProjection": { - "additionalProperties": false, - "properties": { - "items": { - "items": { - "$ref": "#/$defs/KeyToPath" - }, - "type": "array" - }, - "name": { - "type": "string" - }, - "optional": { - "type": "boolean" - } - }, - "type": "object" - }, - "SecretVolumeSource": { - "additionalProperties": false, - "properties": { - "defaultMode": { - "type": "integer" - }, - "items": { - "items": { - "$ref": "#/$defs/KeyToPath" - }, - "type": "array" - }, - "optional": { - "type": "boolean" - }, - "secretName": { - "type": "string" - } - }, - "type": "object" - }, - "SecurityContext": { - "additionalProperties": false, - "properties": { - "allowPrivilegeEscalation": { - "type": "boolean" - }, - "appArmorProfile": { - "$ref": "#/$defs/AppArmorProfile" - }, - "capabilities": { - "$ref": "#/$defs/Capabilities" - }, - "privileged": { - "type": "boolean" - }, - "procMount": { - "type": "string" - }, - "readOnlyRootFilesystem": { - "type": "boolean" - }, - "runAsGroup": { - "type": "integer" - }, - "runAsNonRoot": { - "type": "boolean" - }, - "runAsUser": { - "type": "integer" - }, - "seLinuxOptions": { - "$ref": "#/$defs/SELinuxOptions" - }, - "seccompProfile": { - "$ref": "#/$defs/SeccompProfile" - }, - "windowsOptions": { - "$ref": "#/$defs/WindowsSecurityContextOptions" - } - }, - "type": "object" - }, - "ServiceAccountTokenProjection": { - "additionalProperties": false, - "properties": { - "audience": { - "type": "string" - }, - "expirationSeconds": { - "type": "integer" - }, - "path": { - "type": "string" - } - }, - "required": [ - "path" - ], - "type": "object" - }, - "SleepAction": { - "additionalProperties": false, - "properties": { - "seconds": { - "type": "integer" - } - }, - "required": [ - "seconds" - ], - "type": "object" - }, - "StorageOSVolumeSource": { - "additionalProperties": false, - "properties": { - "fsType": { - "type": "string" - }, - "readOnly": { - "type": "boolean" - }, - "secretRef": { - "$ref": "#/$defs/LocalObjectReference" - }, - "volumeName": { - "type": "string" - }, - "volumeNamespace": { - "type": "string" - } - }, - "type": "object" - }, - "Sysctl": { - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - }, - "required": [ - "name", - "value" - ], - "type": "object" - }, - "TCPSocketAction": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "$ref": "#/$defs/IntOrString" - } - }, - "required": [ - "port" - ], - "type": "object" - }, - "TenantOperationWorkloadReference": { - "additionalProperties": false, - "properties": { - "continueOnFailure": { - "type": "boolean" - }, - "workloadName": { - "type": "string" - } - }, - "required": [ - "workloadName" - ], - "type": "object" - }, - "TenantOperations": { - "additionalProperties": false, - "properties": { - "deprovisioning": { - "items": { - "$ref": "#/$defs/TenantOperationWorkloadReference" - }, - "type": "array" - }, - "provisioning": { - "items": { - "$ref": "#/$defs/TenantOperationWorkloadReference" - }, - "type": "array" - }, - "upgrade": { - "items": { - "$ref": "#/$defs/TenantOperationWorkloadReference" - }, - "type": "array" - } - }, - "type": "object" - }, - "Time": { - "additionalProperties": false, - "properties": {}, - "type": "object" - }, - "Toleration": { - "additionalProperties": false, - "properties": { - "effect": { - "type": "string" - }, - "key": { - "type": "string" - }, - "operator": { - "type": "string" - }, - "tolerationSeconds": { - "type": "integer" - }, - "value": { - "type": "string" - } - }, - "type": "object" - }, - "TopologySpreadConstraint": { - "additionalProperties": false, - "properties": { - "labelSelector": { - "$ref": "#/$defs/LabelSelector" - }, - "matchLabelKeys": { - "items": { - "type": "string" - }, - "type": "array" - }, - "maxSkew": { - "type": "integer" - }, - "minDomains": { - "type": "integer" - }, - "nodeAffinityPolicy": { - "type": "string" - }, - "nodeTaintsPolicy": { - "type": "string" - }, - "topologyKey": { - "type": "string" - }, - "whenUnsatisfiable": { - "type": "string" - } - }, - "required": [ - "maxSkew", - "topologyKey", - "whenUnsatisfiable" - ], - "type": "object" - }, - "TypedLocalObjectReference": { - "additionalProperties": false, - "properties": { - "apiGroup": { - "type": "string" - }, - "kind": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": [ - "apiGroup", - "kind", - "name" - ], - "type": "object" - }, - "TypedObjectReference": { - "additionalProperties": false, - "properties": { - "apiGroup": { - "type": "string" - }, - "kind": { - "type": "string" - }, - "name": { - "type": "string" - }, - "namespace": { - "type": "string" - } - }, - "required": [ - "apiGroup", - "kind", - "name" - ], - "type": "object" - }, - "UserInfo": { - "additionalProperties": false, - "properties": { - "extra": { - "additionalProperties": { - "$ref": "#/$defs/ExtraValue" - }, - "type": "object" - }, - "groups": { - "items": { - "type": "string" - }, - "type": "array" - }, - "uid": { - "type": "string" - }, - "username": { - "type": "string" - } - }, - "type": "object" - }, - "Volume": { - "additionalProperties": false, - "properties": { - "awsElasticBlockStore": { - "$ref": "#/$defs/AWSElasticBlockStoreVolumeSource" - }, - "azureDisk": { - "$ref": "#/$defs/AzureDiskVolumeSource" - }, - "azureFile": { - "$ref": "#/$defs/AzureFileVolumeSource" - }, - "cephfs": { - "$ref": "#/$defs/CephFSVolumeSource" - }, - "cinder": { - "$ref": "#/$defs/CinderVolumeSource" - }, - "configMap": { - "$ref": "#/$defs/ConfigMapVolumeSource" - }, - "csi": { - "$ref": "#/$defs/CSIVolumeSource" - }, - "downwardAPI": { - "$ref": "#/$defs/DownwardAPIVolumeSource" - }, - "emptyDir": { - "$ref": "#/$defs/EmptyDirVolumeSource" - }, - "ephemeral": { - "$ref": "#/$defs/EphemeralVolumeSource" - }, - "fc": { - "$ref": "#/$defs/FCVolumeSource" - }, - "flexVolume": { - "$ref": "#/$defs/FlexVolumeSource" - }, - "flocker": { - "$ref": "#/$defs/FlockerVolumeSource" - }, - "gcePersistentDisk": { - "$ref": "#/$defs/GCEPersistentDiskVolumeSource" - }, - "gitRepo": { - "$ref": "#/$defs/GitRepoVolumeSource" - }, - "glusterfs": { - "$ref": "#/$defs/GlusterfsVolumeSource" - }, - "hostPath": { - "$ref": "#/$defs/HostPathVolumeSource" - }, - "iscsi": { - "$ref": "#/$defs/ISCSIVolumeSource" - }, - "name": { - "type": "string" - }, - "nfs": { - "$ref": "#/$defs/NFSVolumeSource" - }, - "persistentVolumeClaim": { - "$ref": "#/$defs/PersistentVolumeClaimVolumeSource" - }, - "photonPersistentDisk": { - "$ref": "#/$defs/PhotonPersistentDiskVolumeSource" - }, - "portworxVolume": { - "$ref": "#/$defs/PortworxVolumeSource" - }, - "projected": { - "$ref": "#/$defs/ProjectedVolumeSource" - }, - "quobyte": { - "$ref": "#/$defs/QuobyteVolumeSource" - }, - "rbd": { - "$ref": "#/$defs/RBDVolumeSource" - }, - "scaleIO": { - "$ref": "#/$defs/ScaleIOVolumeSource" - }, - "secret": { - "$ref": "#/$defs/SecretVolumeSource" - }, - "storageos": { - "$ref": "#/$defs/StorageOSVolumeSource" - }, - "vsphereVolume": { - "$ref": "#/$defs/VsphereVirtualDiskVolumeSource" - } - }, - "required": [ - "name" - ], - "type": "object" - }, - "VolumeDevice": { - "additionalProperties": false, - "properties": { - "devicePath": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": [ - "name", - "devicePath" - ], - "type": "object" - }, - "VolumeMount": { - "additionalProperties": false, - "properties": { - "mountPath": { - "type": "string" - }, - "mountPropagation": { - "type": "string" - }, - "name": { - "type": "string" - }, - "readOnly": { - "type": "boolean" - }, - "recursiveReadOnly": { - "type": "string" - }, - "subPath": { - "type": "string" - }, - "subPathExpr": { - "type": "string" - } - }, - "required": [ - "name", - "mountPath" - ], - "type": "object" - }, - "VolumeProjection": { - "additionalProperties": false, - "properties": { - "clusterTrustBundle": { - "$ref": "#/$defs/ClusterTrustBundleProjection" - }, - "configMap": { - "$ref": "#/$defs/ConfigMapProjection" - }, - "downwardAPI": { - "$ref": "#/$defs/DownwardAPIProjection" - }, - "secret": { - "$ref": "#/$defs/SecretProjection" - }, - "serviceAccountToken": { - "$ref": "#/$defs/ServiceAccountTokenProjection" - } - }, - "type": "object" - }, - "VolumeResourceRequirements": { - "additionalProperties": false, - "properties": { - "limits": { - "$ref": "#/$defs/ResourceList" - }, - "requests": { - "$ref": "#/$defs/ResourceList" - } - }, - "type": "object" - }, - "VsphereVirtualDiskVolumeSource": { - "additionalProperties": false, - "properties": { - "fsType": { - "type": "string" - }, - "storagePolicyID": { - "type": "string" - }, - "storagePolicyName": { - "type": "string" - }, - "volumePath": { - "type": "string" - } - }, - "required": [ - "volumePath" - ], - "type": "object" - }, - "WeightedPodAffinityTerm": { - "additionalProperties": false, - "properties": { - "podAffinityTerm": { - "$ref": "#/$defs/PodAffinityTerm" - }, - "weight": { - "type": "integer" - } - }, - "required": [ - "weight", - "podAffinityTerm" - ], - "type": "object" - }, - "WindowsSecurityContextOptions": { - "additionalProperties": false, - "properties": { - "gmsaCredentialSpec": { - "type": "string" - }, - "gmsaCredentialSpecName": { - "type": "string" - }, - "hostProcess": { - "type": "boolean" - }, - "runAsUserName": { - "type": "string" - } - }, - "type": "object" - }, - "WorkloadDetails": { - "additionalProperties": false, - "properties": { - "annotations": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - "consumedBTPServices": { - "items": { - "type": "string" - }, - "type": "array" - }, - "deploymentDefinition": { - "$ref": "#/$defs/DeploymentDetails" - }, - "jobDefinition": { - "$ref": "#/$defs/JobDetails" - }, - "labels": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - "name": { - "type": "string" - } - }, - "required": [ - "name", - "consumedBTPServices" - ], - "type": "object" - }, - "app": { - "additionalProperties": false, - "properties": { - "domains": { - "$ref": "#/$defs/Domains" - }, - "istioIngressGatewayLabels": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - } - }, - "required": [ - "domains", - "istioIngressGatewayLabels" - ], - "type": "object" - }, - "btp": { - "additionalProperties": false, - "properties": { - "globalAccountId": { - "type": "string" - }, - "provider": { - "$ref": "#/$defs/provider" - } - }, - "required": [ - "globalAccountId", - "provider" - ], - "type": "object" - }, - "chartValue": { - "additionalProperties": true, - "properties": { - "app": { - "$ref": "#/$defs/app" - }, - "btp": { - "$ref": "#/$defs/btp" - }, - "contentJobs": { - "items": { - "type": "string" - }, - "type": "array" - }, - "imagePullSecrets": { - "items": { - "type": "string" - }, - "type": "array" - }, - "serviceBindings": { - "additionalProperties": { - "$ref": "#/$defs/serviceBindingExt" - }, - "type": "object" - }, - "serviceInstances": { - "additionalProperties": { - "$ref": "#/$defs/serviceInstanceExt" - }, - "type": "object" - }, - "tenantOperations": { - "$ref": "#/$defs/TenantOperations" - }, - "workloads": { - "additionalProperties": { - "$ref": "#/$defs/WorkloadDetails" - }, - "type": "object" - } - }, - "required": [ - "app", - "btp", - "serviceInstances", - "serviceBindings", - "workloads" - ], - "type": "object" - }, - "provider": { - "additionalProperties": false, - "properties": { - "subdomain": { - "type": "string" - }, - "tenantId": { - "type": "string" - } - }, - "required": [ - "subdomain", - "tenantId" - ], - "type": "object" - }, - "serviceBindingExt": { - "additionalProperties": false, - "properties": { - "credentialsRotationPolicy": { - "$ref": "#/$defs/CredentialsRotationPolicy" - }, - "externalName": { - "type": "string" - }, - "name": { - "type": "string" - }, - "parameters": { - "$ref": "#/$defs/RawExtension" - }, - "parametersFrom": { - "items": { - "$ref": "#/$defs/ParametersFromSource" - }, - "type": "array" - }, - "secretKey": { - "type": "string" - }, - "secretName": { - "type": "string" - }, - "secretRootKey": { - "type": "string" - }, - "secretTemplate": { - "type": "string" - }, - "serviceInstanceName": { - "type": "string" - }, - "serviceInstanceNamespace": { - "type": "string" - }, - "userInfo": { - "$ref": "#/$defs/UserInfo" - } - }, - "required": [ - "name", - "serviceInstanceName", - "secretName" - ], - "type": "object" - }, - "serviceInstanceExt": { - "additionalProperties": false, - "properties": { - "btpAccessCredentialsSecret": { - "type": "string" - }, - "customTags": { - "items": { - "type": "string" - }, - "type": "array" - }, - "dataCenter": { - "type": "string" - }, - "externalName": { - "type": "string" - }, - "jsonParameters": { - "type": "string" - }, - "name": { - "type": "string" - }, - "parameters": { - "$ref": "#/$defs/RawExtension" - }, - "parametersFrom": { - "items": { - "$ref": "#/$defs/ParametersFromSource" - }, - "type": "array" - }, - "serviceOfferingName": { - "type": "string" - }, - "servicePlanID": { - "type": "string" - }, - "servicePlanName": { - "type": "string" - }, - "shared": { - "type": "boolean" - }, - "userInfo": { - "$ref": "#/$defs/UserInfo" - } - }, - "required": [ - "name", - "serviceOfferingName", - "servicePlanName" - ], - "type": "object" - } - }, - "$ref": "#/$defs/chartValue", - "$schema": "https://json-schema.org/draft/2020-12/schema" -} diff --git a/test/files/expectedChart/values.yaml b/test/files/expectedChart/values.yaml index 6c59c44..f35c605 100644 --- a/test/files/expectedChart/values.yaml +++ b/test/files/expectedChart/values.yaml @@ -26,7 +26,7 @@ serviceInstances: credential-types: - binding-secret redirect-uris: [] - saas-registry: + saasRegistry: name: bookshop-saas-registry serviceOfferingName: saas-registry servicePlanName: application @@ -41,7 +41,7 @@ serviceInstances: onSubscriptionAsync: true onUnSubscriptionAsync: true category: CAP - service-manager: + serviceManager: name: bookshop-service-manager serviceOfferingName: service-manager servicePlanName: container @@ -71,13 +71,13 @@ serviceBindings: secretName: bookshop-uaa-bind-secret secretKey: credentials parameters: {} - saas-registry: + saasRegistry: name: bookshop-saas-registry-bind serviceInstanceName: bookshop-saas-registry secretName: bookshop-saas-registry-bind-secret secretKey: credentials parameters: {} - service-manager: + serviceManager: name: bookshop-service-manager-bind serviceInstanceName: bookshop-service-manager secretName: bookshop-service-manager-bind-secret @@ -97,7 +97,7 @@ btp: tenantId: null imagePullSecrets: [] workloads: - app-router: + appRouter: name: app-router labels: sme.sap.com/app-type: bookshop @@ -123,7 +123,7 @@ workloads: deploymentDefinition: type: CAP image: null - content-deploy: + contentDeploy: name: content-deploy labels: sme.sap.com/app-type: bookshop @@ -134,7 +134,7 @@ workloads: jobDefinition: type: Content image: null - tenant-job: + tenantJob: name: tenant-job labels: sme.sap.com/app-type: bookshop diff --git a/test/files/expectedChart/valuesWithDestination.yaml b/test/files/expectedChart/valuesWithDestination.yaml index 6c59c44..f35c605 100644 --- a/test/files/expectedChart/valuesWithDestination.yaml +++ b/test/files/expectedChart/valuesWithDestination.yaml @@ -26,7 +26,7 @@ serviceInstances: credential-types: - binding-secret redirect-uris: [] - saas-registry: + saasRegistry: name: bookshop-saas-registry serviceOfferingName: saas-registry servicePlanName: application @@ -41,7 +41,7 @@ serviceInstances: onSubscriptionAsync: true onUnSubscriptionAsync: true category: CAP - service-manager: + serviceManager: name: bookshop-service-manager serviceOfferingName: service-manager servicePlanName: container @@ -71,13 +71,13 @@ serviceBindings: secretName: bookshop-uaa-bind-secret secretKey: credentials parameters: {} - saas-registry: + saasRegistry: name: bookshop-saas-registry-bind serviceInstanceName: bookshop-saas-registry secretName: bookshop-saas-registry-bind-secret secretKey: credentials parameters: {} - service-manager: + serviceManager: name: bookshop-service-manager-bind serviceInstanceName: bookshop-service-manager secretName: bookshop-service-manager-bind-secret @@ -97,7 +97,7 @@ btp: tenantId: null imagePullSecrets: [] workloads: - app-router: + appRouter: name: app-router labels: sme.sap.com/app-type: bookshop @@ -123,7 +123,7 @@ workloads: deploymentDefinition: type: CAP image: null - content-deploy: + contentDeploy: name: content-deploy labels: sme.sap.com/app-type: bookshop @@ -134,7 +134,7 @@ workloads: jobDefinition: type: Content image: null - tenant-job: + tenantJob: name: tenant-job labels: sme.sap.com/app-type: bookshop diff --git a/test/files/expectedChart/valuesWithMTA.yaml b/test/files/expectedChart/valuesWithMTA.yaml index c3a76da..7d4b0ca 100644 --- a/test/files/expectedChart/valuesWithMTA.yaml +++ b/test/files/expectedChart/valuesWithMTA.yaml @@ -1,28 +1,28 @@ serviceInstances: - service-manager-container: + serviceManagerContainer: name: author-readings-service-manager serviceOfferingName: service-manager servicePlanName: container parameters: {} - destination-lite: + destinationLite: name: author-readings-destination-service serviceOfferingName: destination servicePlanName: lite parameters: HTML5Runtime_enabled: true - xsuaa-broker: + xsuaaBroker: name: author-readings-uaa serviceOfferingName: xsuaa servicePlanName: broker parameters: tenant-mode: shared xsappname: partner-refapp-ph2 - auditlog-oauth2: + auditlogOauth2: name: author-readings-auditlog serviceOfferingName: auditlog servicePlanName: oauth2 parameters: {} - saas-registry-application: + saasRegistryApplication: name: author-readings-registry serviceOfferingName: saas-registry servicePlanName: application @@ -39,73 +39,73 @@ serviceInstances: onSubscriptionAsync: true onUnSubscriptionAsync: true onUpdateDependenciesAsync: true - html5-apps-repo-app-runtime: + html5AppsRepoAppRuntime: name: author-readings-html5-runtime serviceOfferingName: html5-apps-repo servicePlanName: app-runtime parameters: {} - html5-apps-repo-app-host: + html5AppsRepoAppHost: name: author-readings-html5-repo-host serviceOfferingName: html5-apps-repo servicePlanName: app-host parameters: sizeLimit: 10 - application-logs-standard: + applicationLogsStandard: name: author-readings-logging serviceOfferingName: application-logs servicePlanName: standard parameters: {} serviceBindings: - author-readings-uaa-bind: + authorReadingsUaaBind: name: author-readings-uaa-bind parameters: {} secretKey: credentials secretName: author-readings-uaa-bind-secret serviceInstanceName: author-readings-uaa - author-readings-destination-service-bind: + authorReadingsDestinationServiceBind: name: author-readings-destination-service-bind parameters: {} secretKey: credentials secretName: author-readings-destination-service-bind-secret serviceInstanceName: author-readings-destination-service - author-readings-auditlog-bind: + authorReadingsAuditlogBind: name: author-readings-auditlog-bind parameters: {} secretKey: credentials secretName: author-readings-auditlog-bind-secret serviceInstanceName: author-readings-auditlog - author-readings-registry-bind: + authorReadingsRegistryBind: name: author-readings-registry-bind parameters: {} secretKey: credentials secretName: author-readings-registry-bind-secret serviceInstanceName: author-readings-registry - author-readings-html5-runtime-bind: + authorReadingsHtml5RuntimeBind: name: author-readings-html5-runtime-bind parameters: {} secretKey: credentials secretName: author-readings-html5-runtime-bind-secret serviceInstanceName: author-readings-html5-runtime - author-readings-service-manager-bind: + authorReadingsServiceManagerBind: name: author-readings-service-manager-bind parameters: {} secretKey: credentials secretName: author-readings-service-manager-bind-secret serviceInstanceName: author-readings-service-manager - author-readings-service-manager-author-readings-mtx-srv-bind: + authorReadingsServiceManagerAuthorReadingsMtxSrvBind: name: author-readings-service-manager-author-readings-mtx-srv-bind parameters: SUBSCRIPTION_URL: ${protocol}://\${tenant_subdomain}.${org}-${space}.${domain}/authorreadingmanager secretKey: credentials secretName: author-readings-service-manager-author-readings-mtx-srv-bind-secret serviceInstanceName: author-readings-service-manager - author-readings-logging-bind: + authorReadingsLoggingBind: name: author-readings-logging-bind parameters: {} secretKey: credentials secretName: author-readings-logging-bind-secret serviceInstanceName: author-readings-logging - author-readings-html5-repo-host-bind: + authorReadingsHtml5RepoHostBind: name: author-readings-html5-repo-host-bind parameters: {} secretKey: credentials @@ -125,7 +125,7 @@ btp: tenantId: null imagePullSecrets: [] workloads: - author-readings-approuter: + authorReadingsApprouter: name: author-readings-approuter consumedBTPServices: - author-readings-uaa-bind @@ -138,13 +138,13 @@ workloads: image: null env: - name: TENANT_HOST_PATTERN - value: ^(.*).${org}-${space}.${domain} + value: ^(.*).{{ template "domainPatterns" . }} - name: httpHeaders value: "[{ \"Content-Security-Policy\": \"frame-ancestors 'self' https://*.hana.ondemand.com\" }]" - name: CORS value: '[{"uriPattern":".*","allowedOrigin":[{"host":"*.${org}-${space}.${domain}","protocol":"https"}]}]' - author-readings-srv: + authorReadingsSrv: name: author-readings-srv consumedBTPServices: - author-readings-service-manager-bind @@ -162,7 +162,7 @@ workloads: value: "true" - name: SUBSCRIPTION_URL value: ${protocol}://\${tenant_subdomain}.${app-url}-srv.${domain} - author-readings-mtx-srv: + authorReadingsMtxSrv: name: author-readings-mtx-srv consumedBTPServices: - author-readings-auditlog-bind @@ -173,8 +173,7 @@ workloads: deploymentDefinition: type: null image: null - env: [] - author-readings-app-deployer: + authorReadingsAppDeployer: name: author-readings-app-deployer consumedBTPServices: - author-readings-html5-repo-host-bind @@ -182,8 +181,7 @@ workloads: jobDefinition: type: Content image: null - env: [] - author-readings-mtx: + authorReadingsMtx: name: author-readings-mtx consumedBTPServices: - author-readings-registry-bind @@ -193,8 +191,7 @@ workloads: jobDefinition: type: TenantOperation image: null - env: [] - author-readings: + authorReadings: name: author-readings consumedBTPServices: - author-readings-uaa-bind diff --git a/test/files/expectedConfigurableTemplatesChart/Chart.yaml b/test/files/expectedConfigurableTemplatesChart/Chart.yaml new file mode 100644 index 0000000..29e8753 --- /dev/null +++ b/test/files/expectedConfigurableTemplatesChart/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: bookshop +description: Helm chart to deploy bookshop using CAP Operator +type: application +version: 0.0.1 +appVersion: 1.0.0 +annotations: + app.kubernetes.io/managed-by: cap-operator-plugin + app.kubernetes.io/part-of: cap-operator-configurable-templates diff --git a/test/files/expectedConfigurableTemplatesChart/runtime-values.yaml b/test/files/expectedConfigurableTemplatesChart/runtime-values.yaml new file mode 100644 index 0000000..35cd951 --- /dev/null +++ b/test/files/expectedConfigurableTemplatesChart/runtime-values.yaml @@ -0,0 +1,31 @@ +serviceInstances: + saasRegistry: + parameters: + xsappname: bkshop + appName: bkshop + displayName: bkshop + description: A simple CAP project. + appUrls: + getDependencies: https://bem-aad-sadad-123456789012.bkshop.c-abc.kyma.ondemand.com/callback/v1.0/dependencies + onSubscription: https://cap-op.c-abc.kyma.ondemand.com/provision/tenants/{tenantId} + xsuaa: + parameters: + xsappname: bkshop + oauth2-configuration: + redirect-uris: + - https://*bkshop.c-abc.kyma.ondemand.com/** +app: + domains: + primary: bkshop.c-abc.kyma.ondemand.com + secondary: [] + istioIngressGatewayLabels: + istio: ingressgateway + app: istio-ingressgateway +btp: + globalAccountId: dc94db56-asda-adssa-dada-123456789012 + provider: + subdomain: bem-aad-sadad-123456789012 + tenantId: dasdsd-1234-1234-1234-123456789012 +imagePullSecrets: + - regcred +hanaInstanceId: sdasd-4c4d-4d4d-4d4d-123456789012 diff --git a/test/files/expectedConfigurableTemplatesChart/templates/_helpers.tpl b/test/files/expectedConfigurableTemplatesChart/templates/_helpers.tpl new file mode 100644 index 0000000..e9f637b --- /dev/null +++ b/test/files/expectedConfigurableTemplatesChart/templates/_helpers.tpl @@ -0,0 +1,45 @@ +{{- define "capApplicationVersionName" -}} +{{ printf "%s-%d" (include "appName" $) (.Release.Revision) }} +{{- end -}} + +{{- define "appName" -}} +{{- range $sik, $siv := .Values.serviceInstances}} + {{- if and (eq (get $siv "serviceOfferingName") "xsuaa") (eq (get $siv "servicePlanName") "broker") -}} + {{ printf "%s" $siv.parameters.xsappname }} + {{- break -}} + {{- end -}} +{{- end -}} +{{- end -}} + +{{- define "hasService" -}} +{{- $found := "false" -}} +{{- $offeringName := .offeringName -}} +{{- $planName := .planName -}} +{{- $si := .si -}} +{{- range $sik, $siv := $si}} + {{- if and (eq (get $siv "serviceOfferingName") $offeringName) (eq (get $siv "servicePlanName") $planName) -}} + {{- $found = "true" -}} + {{- end -}} +{{- end -}} +{{- $found -}} +{{- end -}} + +{{- define "domainPatterns" -}} + {{- if .Values.app.domains.secondary -}} + {{- $doms := list .Values.app.domains.primary -}} + {{- range .Values.app.domains.secondary -}} + {{- $doms = append $doms . -}} + {{- end -}} + {{- if gt (len $doms) 1 -}} + {{- join "|" $doms | printf "(%s)" -}} + {{- else -}} + {{- first $doms -}} + {{- end -}} + {{- else -}} + {{- printf "%s" .Values.app.domains.primary -}} + {{- end -}} +{{- end -}} + +{{- define "originalAppName" -}} +{{ print "bookshop" }} +{{- end -}} \ No newline at end of file diff --git a/test/files/expectedConfigurableTemplatesChart/templates/cap-operator-cros-modified.yaml b/test/files/expectedConfigurableTemplatesChart/templates/cap-operator-cros-modified.yaml new file mode 100644 index 0000000..f1a08db --- /dev/null +++ b/test/files/expectedConfigurableTemplatesChart/templates/cap-operator-cros-modified.yaml @@ -0,0 +1,120 @@ +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: {{ include "appName" $ }} +spec: + domains: + primary: {{.Values.app.domains.primary}} + {{- if .Values.app.domains.secondary }} + secondary: + {{- range .Values.app.domains.secondary }} + - {{.}} + {{- end }} + {{- end }} + istioIngressGatewayLabels: + {{- range $k, $v := .Values.app.istioIngressGatewayLabels }} + - name: {{ $k }} + value: {{ $v | default "invalidValue"}} + {{- end }} + btpAppName: {{ include "appName" $ }} + globalAccountId: {{.Values.btp.globalAccountId}} + provider: + subDomain: {{.Values.btp.provider.subdomain}} + tenantId: {{.Values.btp.provider.tenantId}} + btp: + services: + {{- $serviceInstances := .Values.serviceInstances }} + {{- range $k, $v := .Values.serviceBindings }} + {{- $serviceInstance := dict }} + {{- range $sik, $siv := $serviceInstances }} + {{- if eq $siv.name $v.serviceInstanceName }} + {{- $serviceInstance = $siv }} + {{- end }} + {{- end }} + {{- if hasKey $serviceInstance "serviceOfferingName" }} + - class: {{ get $serviceInstance "serviceOfferingName" | default "invalidValue" }} + {{- if $v.externalName }} + name: {{ $v.externalName | default "invalidValue" }} + {{- else }} + name: {{ $v.name | default "invalidValue" }} + {{- end }} + secret: {{ $v.secretName | default "invalidValue" }} + {{- end }} + {{- end }} +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/resource-policy: keep + name: {{ include "capApplicationVersionName" $ }} +spec: + capApplicationInstance: {{ include "appName" $ }} + version: {{ .Release.Revision }} + registrySecrets: + {{- range .Values.imagePullSecrets }} + - {{.}} + {{- end }} + workloads: + - name: app-router + labels: + sme.sap.com/app-type: bookshop + consumedBTPServices: + - bookshop-uaa-bind + - bookshop-destination-bind + - bookshop-html5-repo-runtime-bind + - bookshop-saas-registry-bind + deploymentDefinition: + type: Router + image: "{{.Values.workloads.appRouter.image}}" + env: + - name: TENANT_HOST_PATTERN + value: ^(.*).{{ template "domainPatterns" . }} + ports: + - name: router-port + port: 5000 + - name: server + labels: + sme.sap.com/app-type: bookshop + consumedBTPServices: + - bookshop-uaa-bind + - bookshop-saas-registry-bind + - bookshop-service-manager-bind + deploymentDefinition: + type: CAP + image: "{{.Values.workloads.server.image}}" + env: + - name: CDS_CONFIG + value: '{"requires":{"cds.xt.DeploymentService":{"hdi":{"create":{"database_id":"{{.Values.hanaInstanceId}}"}}}}}' + - name: content-deploy + labels: + sme.sap.com/app-type: bookshop + consumedBTPServices: + - bookshop-uaa-bind + - bookshop-saas-registry-bind + - bookshop-html5-repo-host-bind + jobDefinition: + type: Content + image: "{{.Values.workloads.contentDeploy.image}}" + - name: tenant-job + labels: + sme.sap.com/app-type: bookshop + consumedBTPServices: + - bookshop-uaa-bind + - bookshop-saas-registry-bind + - bookshop-service-manager-bind + jobDefinition: + type: TenantOperation + image: "{{.Values.workloads.tenantJob.image}}" + env: + - name: CDS_CONFIG + value: '{"requires":{"cds.xt.DeploymentService":{"hdi":{"create":{"database_id":"{{.Values.hanaInstanceId}}"}}}}}' + tenantOperations: + provisioning: + - workloadName: tenant-job + upgrade: + - workloadName: tenant-job + contentJobs: + - content-deploy diff --git a/test/files/expectedConfigurableTemplatesChart/templates/cap-operator-cros-mta.yaml b/test/files/expectedConfigurableTemplatesChart/templates/cap-operator-cros-mta.yaml new file mode 100644 index 0000000..a492577 --- /dev/null +++ b/test/files/expectedConfigurableTemplatesChart/templates/cap-operator-cros-mta.yaml @@ -0,0 +1,131 @@ +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: {{ include "appName" $ }} +spec: + domains: + primary: {{.Values.app.domains.primary}} + {{- if .Values.app.domains.secondary }} + secondary: + {{- range .Values.app.domains.secondary }} + - {{.}} + {{- end }} + {{- end }} + istioIngressGatewayLabels: + {{- range $k, $v := .Values.app.istioIngressGatewayLabels }} + - name: {{ $k }} + value: {{ $v | default "invalidValue"}} + {{- end }} + btpAppName: {{ include "appName" $ }} + globalAccountId: {{.Values.btp.globalAccountId}} + provider: + subDomain: {{.Values.btp.provider.subdomain}} + tenantId: {{.Values.btp.provider.tenantId}} + btp: + services: + {{- $serviceInstances := .Values.serviceInstances }} + {{- range $k, $v := .Values.serviceBindings }} + {{- $serviceInstance := dict }} + {{- range $sik, $siv := $serviceInstances }} + {{- if eq $siv.name $v.serviceInstanceName }} + {{- $serviceInstance = $siv }} + {{- end }} + {{- end }} + {{- if hasKey $serviceInstance "serviceOfferingName" }} + - class: {{ get $serviceInstance "serviceOfferingName" | default "invalidValue" }} + {{- if $v.externalName }} + name: {{ $v.externalName | default "invalidValue" }} + {{- else }} + name: {{ $v.name | default "invalidValue" }} + {{- end }} + secret: {{ $v.secretName | default "invalidValue" }} + {{- end }} + {{- end }} +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/resource-policy: keep + name: {{ include "capApplicationVersionName" $ }} +spec: + capApplicationInstance: {{ include "appName" $ }} + version: {{ .Release.Revision }} + registrySecrets: + {{- range .Values.imagePullSecrets }} + - {{.}} + {{- end }} + workloads: + - name: author-readings-approuter + consumedBTPServices: + - author-readings-uaa-bind + - author-readings-destination-service-bind + - author-readings-auditlog-bind + - author-readings-registry-bind + - author-readings-html5-runtime-bind + deploymentDefinition: + type: Router + image: "{{.Values.workloads.authorReadingsApprouter.image}}" + env: + - name: TENANT_HOST_PATTERN + value: ^(.*).{{ template "domainPatterns" . }} + - name: httpHeaders + value: "[{ \"Content-Security-Policy\": \"frame-ancestors 'self' + https://*.hana.ondemand.com\" }]" + - name: CORS + value: '[{"uriPattern":".*","allowedOrigin":[{"host":"*.${org}-${space}.${domain}","protocol":"https"}]}]' + - name: author-readings-srv + consumedBTPServices: + - author-readings-service-manager-bind + - author-readings-uaa-bind + - author-readings-destination-service-bind + - author-readings-auditlog-bind + - author-readings-registry-bind + deploymentDefinition: + type: null + image: "{{.Values.workloads.authorReadingsSrv.image}}" + env: + - name: OTLP_TRACE_URL + value: http://telemetry-otlp-traces.kyma-system:4318 + - name: IS_MTXS_ENABLED + value: "true" + - name: SUBSCRIPTION_URL + value: ${protocol}://\${tenant_subdomain}.${app-url}-srv.${domain} + - name: author-readings-mtx-srv + consumedBTPServices: + - author-readings-auditlog-bind + - author-readings-uaa-bind + - author-readings-destination-service-bind + - author-readings-service-manager-author-readings-mtx-srv-bind + - author-readings-logging-bind + deploymentDefinition: + type: null + image: "{{.Values.workloads.authorReadingsMtxSrv.image}}" + - name: author-readings-app-deployer + consumedBTPServices: + - author-readings-html5-repo-host-bind + - author-readings-uaa-bind + jobDefinition: + type: Content + image: "{{.Values.workloads.authorReadingsAppDeployer.image}}" + - name: author-readings-mtx + consumedBTPServices: + - author-readings-registry-bind + - author-readings-logging-bind + - author-readings-service-manager-bind + - author-readings-uaa-bind + jobDefinition: + type: TenantOperation + image: "{{.Values.workloads.authorReadingsMtx.image}}" + - name: author-readings + consumedBTPServices: + - author-readings-uaa-bind + - author-readings-logging-bind + deploymentDefinition: + type: null + image: "{{.Values.workloads.authorReadings.image}}" + env: + - name: TENANT_HOST_PATTERN + value: ^(.*)-${default-uri} diff --git a/test/files/expectedConfigurableTemplatesChart/values-modified.yaml b/test/files/expectedConfigurableTemplatesChart/values-modified.yaml new file mode 100644 index 0000000..9c9b93a --- /dev/null +++ b/test/files/expectedConfigurableTemplatesChart/values-modified.yaml @@ -0,0 +1,108 @@ +serviceInstances: + destination: + name: bookshop-destination + serviceOfferingName: destination + servicePlanName: lite + parameters: + HTML5Runtime_enabled: true + html5RepoRuntime: + name: bookshop-html5-repo-runtime + serviceOfferingName: html5-apps-repo + servicePlanName: app-runtime + parameters: {} + html5RepoHost: + name: bookshop-html5-repo-host + serviceOfferingName: html5-apps-repo + servicePlanName: app-host + parameters: {} + xsuaa: + name: bookshop-uaa + serviceOfferingName: xsuaa + servicePlanName: broker + parameters: + xsappname: bookshop + tenant-mode: shared + oauth2-configuration: + credential-types: + - binding-secret + redirect-uris: [] + saasRegistry: + name: bookshop-saas-registry + serviceOfferingName: saas-registry + servicePlanName: application + parameters: + plan: general + xsappname: bookshop + appName: bookshop + displayName: (bookshop) + description: (A simple CAP project.) + appUrls: + callbackTimeoutMillis: 300000 + onSubscriptionAsync: true + onUnSubscriptionAsync: true + category: CAP + serviceManager: + name: bookshop-service-manager + serviceOfferingName: service-manager + servicePlanName: container + parameters: {} +serviceBindings: + destination: + name: bookshop-destination-bind + serviceInstanceName: bookshop-destination + secretName: bookshop-destination-bind-secret + secretKey: credentials + parameters: {} + html5RepoRuntime: + name: bookshop-html5-repo-runtime-bind + serviceInstanceName: bookshop-html5-repo-runtime + secretName: bookshop-html5-repo-runtime-bind-secret + secretKey: credentials + parameters: {} + html5RepoHost: + name: bookshop-html5-repo-host-bind + serviceInstanceName: bookshop-html5-repo-host + secretName: bookshop-html5-repo-host-bind-secret + secretKey: credentials + parameters: {} + xsuaa: + name: bookshop-uaa-bind + serviceInstanceName: bookshop-uaa + secretName: bookshop-uaa-bind-secret + secretKey: credentials + parameters: {} + saasRegistry: + name: bookshop-saas-registry-bind + serviceInstanceName: bookshop-saas-registry + secretName: bookshop-saas-registry-bind-secret + secretKey: credentials + parameters: {} + serviceManager: + name: bookshop-service-manager-bind + serviceInstanceName: bookshop-service-manager + secretName: bookshop-service-manager-bind-secret + secretKey: credentials + parameters: {} +app: + domains: + primary: null + secondary: [] + istioIngressGatewayLabels: + istio: ingressgateway + app: istio-ingressgateway +btp: + globalAccountId: null + provider: + subdomain: null + tenantId: null +imagePullSecrets: [] +workloads: + appRouter: + image: gcr.io/test/app-router + server: + image: gcr.io/test/server + contentDeploy: + image: gcr.io/test/content-deploy + tenantJob: + image: gcr.io/test/tenant-job +hanaInstanceId: abcd diff --git a/test/files/expectedConfigurableTemplatesChart/values.yaml b/test/files/expectedConfigurableTemplatesChart/values.yaml new file mode 100644 index 0000000..dc434a2 --- /dev/null +++ b/test/files/expectedConfigurableTemplatesChart/values.yaml @@ -0,0 +1,108 @@ +serviceInstances: + destination: + name: bookshop-destination + serviceOfferingName: destination + servicePlanName: lite + parameters: + HTML5Runtime_enabled: true + html5RepoRuntime: + name: bookshop-html5-repo-runtime + serviceOfferingName: html5-apps-repo + servicePlanName: app-runtime + parameters: {} + html5RepoHost: + name: bookshop-html5-repo-host + serviceOfferingName: html5-apps-repo + servicePlanName: app-host + parameters: {} + xsuaa: + name: bookshop-uaa + serviceOfferingName: xsuaa + servicePlanName: broker + parameters: + xsappname: bookshop + tenant-mode: shared + oauth2-configuration: + credential-types: + - binding-secret + redirect-uris: [] + saasRegistry: + name: bookshop-saas-registry + serviceOfferingName: saas-registry + servicePlanName: application + parameters: + plan: general + xsappname: bookshop + appName: bookshop + displayName: (bookshop) + description: (A simple CAP project.) + appUrls: + callbackTimeoutMillis: 300000 + onSubscriptionAsync: true + onUnSubscriptionAsync: true + category: CAP + serviceManager: + name: bookshop-service-manager + serviceOfferingName: service-manager + servicePlanName: container + parameters: {} +serviceBindings: + destination: + name: bookshop-destination-bind + serviceInstanceName: bookshop-destination + secretName: bookshop-destination-bind-secret + secretKey: credentials + parameters: {} + html5RepoRuntime: + name: bookshop-html5-repo-runtime-bind + serviceInstanceName: bookshop-html5-repo-runtime + secretName: bookshop-html5-repo-runtime-bind-secret + secretKey: credentials + parameters: {} + html5RepoHost: + name: bookshop-html5-repo-host-bind + serviceInstanceName: bookshop-html5-repo-host + secretName: bookshop-html5-repo-host-bind-secret + secretKey: credentials + parameters: {} + xsuaa: + name: bookshop-uaa-bind + serviceInstanceName: bookshop-uaa + secretName: bookshop-uaa-bind-secret + secretKey: credentials + parameters: {} + saasRegistry: + name: bookshop-saas-registry-bind + serviceInstanceName: bookshop-saas-registry + secretName: bookshop-saas-registry-bind-secret + secretKey: credentials + parameters: {} + serviceManager: + name: bookshop-service-manager-bind + serviceInstanceName: bookshop-service-manager + secretName: bookshop-service-manager-bind-secret + secretKey: credentials + parameters: {} +app: + domains: + primary: null + secondary: [] + istioIngressGatewayLabels: + istio: ingressgateway + app: istio-ingressgateway +btp: + globalAccountId: null + provider: + subdomain: null + tenantId: null +imagePullSecrets: [] +hanaInstanceId: null +workloads: + server: + image: null + appRouter: + image: null + tenantJob: + image: null + contentDeploy: + image: null diff --git a/test/files/expectedConfigurableTemplatesChart/valuesWithMTA.yaml b/test/files/expectedConfigurableTemplatesChart/valuesWithMTA.yaml new file mode 100644 index 0000000..a9d82b5 --- /dev/null +++ b/test/files/expectedConfigurableTemplatesChart/valuesWithMTA.yaml @@ -0,0 +1,140 @@ +serviceInstances: + serviceManagerContainer: + name: author-readings-service-manager + serviceOfferingName: service-manager + servicePlanName: container + parameters: {} + destinationLite: + name: author-readings-destination-service + serviceOfferingName: destination + servicePlanName: lite + parameters: + HTML5Runtime_enabled: true + xsuaaBroker: + name: author-readings-uaa + serviceOfferingName: xsuaa + servicePlanName: broker + parameters: + tenant-mode: shared + xsappname: partner-refapp-ph2 + auditlogOauth2: + name: author-readings-auditlog + serviceOfferingName: auditlog + servicePlanName: oauth2 + parameters: {} + saasRegistryApplication: + name: author-readings-registry + serviceOfferingName: saas-registry + servicePlanName: application + parameters: + xsappname: partner-refapp-ph2 + appName: partner-refapp-ph2 + displayName: SME Partner Reference Sample Application + description: SME Partner Reference Sample Application + category: Category + appUrls: + getDependencies: ~{approuter-binding/app-url}/callback/v1.0/dependencies + onSubscription: ~{mtx-binding/app-url}/-/cds/saas-provisioning/tenant/{tenantId} + callbackTimeoutMillis: 300000 + onSubscriptionAsync: true + onUnSubscriptionAsync: true + onUpdateDependenciesAsync: true + html5AppsRepoAppRuntime: + name: author-readings-html5-runtime + serviceOfferingName: html5-apps-repo + servicePlanName: app-runtime + parameters: {} + html5AppsRepoAppHost: + name: author-readings-html5-repo-host + serviceOfferingName: html5-apps-repo + servicePlanName: app-host + parameters: + sizeLimit: 10 + applicationLogsStandard: + name: author-readings-logging + serviceOfferingName: application-logs + servicePlanName: standard + parameters: {} +serviceBindings: + authorReadingsUaaBind: + name: author-readings-uaa-bind + parameters: {} + secretKey: credentials + secretName: author-readings-uaa-bind-secret + serviceInstanceName: author-readings-uaa + authorReadingsDestinationServiceBind: + name: author-readings-destination-service-bind + parameters: {} + secretKey: credentials + secretName: author-readings-destination-service-bind-secret + serviceInstanceName: author-readings-destination-service + authorReadingsAuditlogBind: + name: author-readings-auditlog-bind + parameters: {} + secretKey: credentials + secretName: author-readings-auditlog-bind-secret + serviceInstanceName: author-readings-auditlog + authorReadingsRegistryBind: + name: author-readings-registry-bind + parameters: {} + secretKey: credentials + secretName: author-readings-registry-bind-secret + serviceInstanceName: author-readings-registry + authorReadingsHtml5RuntimeBind: + name: author-readings-html5-runtime-bind + parameters: {} + secretKey: credentials + secretName: author-readings-html5-runtime-bind-secret + serviceInstanceName: author-readings-html5-runtime + authorReadingsServiceManagerBind: + name: author-readings-service-manager-bind + parameters: {} + secretKey: credentials + secretName: author-readings-service-manager-bind-secret + serviceInstanceName: author-readings-service-manager + authorReadingsServiceManagerAuthorReadingsMtxSrvBind: + name: author-readings-service-manager-author-readings-mtx-srv-bind + parameters: + SUBSCRIPTION_URL: ${protocol}://\${tenant_subdomain}.${org}-${space}.${domain}/authorreadingmanager + secretKey: credentials + secretName: author-readings-service-manager-author-readings-mtx-srv-bind-secret + serviceInstanceName: author-readings-service-manager + authorReadingsLoggingBind: + name: author-readings-logging-bind + parameters: {} + secretKey: credentials + secretName: author-readings-logging-bind-secret + serviceInstanceName: author-readings-logging + authorReadingsHtml5RepoHostBind: + name: author-readings-html5-repo-host-bind + parameters: {} + secretKey: credentials + secretName: author-readings-html5-repo-host-bind-secret + serviceInstanceName: author-readings-html5-repo-host +app: + domains: + primary: null + secondary: [] + istioIngressGatewayLabels: + istio: ingressgateway + app: istio-ingressgateway +btp: + globalAccountId: null + provider: + subdomain: null + tenantId: null +imagePullSecrets: [] +hanaInstanceId: null +workloads: + authorReadingsApprouter: + image: null + authorReadingsSrv: + image: null + authorReadingsMtxSrv: + image: null + authorReadingsAppDeployer: + image: null + authorReadingsMtx: + image: null + authorReadings: + image: null diff --git a/test/files/runtime-values-of-simple-chart.yaml b/test/files/runtime-values-of-simple-chart.yaml new file mode 100644 index 0000000..6cfd868 --- /dev/null +++ b/test/files/runtime-values-of-simple-chart.yaml @@ -0,0 +1,46 @@ +serviceInstances: + saasRegistry: + parameters: + xsappname: bkshop + appName: bkshop + displayName: bkshop + description: A simple CAP project. + appUrls: + getDependencies: https://bem-aad-sadad-123456789012.bkshop.c-abc.kyma.ondemand.com/callback/v1.0/dependencies + onSubscription: https://cap-op.c-abc.kyma.ondemand.com/provision/tenants/{tenantId} + xsuaa: + parameters: + xsappname: bkshop + oauth2-configuration: + redirect-uris: + - https://*bkshop.c-abc.kyma.ondemand.com/** +app: + domains: + primary: bkshop.c-abc.kyma.ondemand.com + secondary: [] + istioIngressGatewayLabels: + istio: ingressgateway + app: istio-ingressgateway +btp: + globalAccountId: dc94db56-asda-adssa-dada-123456789012 + provider: + subdomain: bem-aad-sadad-123456789012 + tenantId: dasdsd-1234-1234-1234-123456789012 +imagePullSecrets: + - regcred +workloads: + appRouter: + deploymentDefinition: + env: + - name: TENANT_HOST_PATTERN + value: ^(.*).bkshop.c-abc.kyma.ondemand.com + server: + deploymentDefinition: + env: + - name: CDS_CONFIG + value: '{"requires":{"cds.xt.DeploymentService":{"hdi":{"create":{"database_id":"sdasd-4c4d-4d4d-4d4d-123456789012"}}}}}' + tenantJob: + jobDefinition: + env: + - name: CDS_CONFIG + value: '{"requires":{"cds.xt.DeploymentService":{"hdi":{"create":{"database_id":"sdasd-4c4d-4d4d-4d4d-123456789012"}}}}}' diff --git a/test/files/values-of-simple-chart-filled.yaml b/test/files/values-of-simple-chart-filled.yaml new file mode 100644 index 0000000..a18f1da --- /dev/null +++ b/test/files/values-of-simple-chart-filled.yaml @@ -0,0 +1,160 @@ +serviceInstances: + destination: + name: bookshop-destination + serviceOfferingName: destination + servicePlanName: lite + parameters: + HTML5Runtime_enabled: true + html5RepoRuntime: + name: bookshop-html5-repo-runtime + serviceOfferingName: html5-apps-repo + servicePlanName: app-runtime + parameters: {} + html5RepoHost: + name: bookshop-html5-repo-host + serviceOfferingName: html5-apps-repo + servicePlanName: app-host + parameters: {} + xsuaa: + name: bookshop-uaa + serviceOfferingName: xsuaa + servicePlanName: broker + parameters: + xsappname: bookshop + tenant-mode: shared + oauth2-configuration: + credential-types: + - binding-secret + redirect-uris: [] + saasRegistry: + name: bookshop-saas-registry + serviceOfferingName: saas-registry + servicePlanName: application + parameters: + plan: general + xsappname: bookshop + appName: bookshop + displayName: (bookshop) + description: (A simple CAP project.) + appUrls: + callbackTimeoutMillis: 300000 + onSubscriptionAsync: true + onUnSubscriptionAsync: true + category: CAP + serviceManager: + name: bookshop-service-manager + serviceOfferingName: service-manager + servicePlanName: container + parameters: {} +serviceBindings: + destination: + name: bookshop-destination-bind + serviceInstanceName: bookshop-destination + secretName: bookshop-destination-bind-secret + secretKey: credentials + parameters: {} + html5RepoRuntime: + name: bookshop-html5-repo-runtime-bind + serviceInstanceName: bookshop-html5-repo-runtime + secretName: bookshop-html5-repo-runtime-bind-secret + secretKey: credentials + parameters: {} + html5RepoHost: + name: bookshop-html5-repo-host-bind + serviceInstanceName: bookshop-html5-repo-host + secretName: bookshop-html5-repo-host-bind-secret + secretKey: credentials + parameters: {} + xsuaa: + name: bookshop-uaa-bind + serviceInstanceName: bookshop-uaa + secretName: bookshop-uaa-bind-secret + secretKey: credentials + parameters: {} + saasRegistry: + name: bookshop-saas-registry-bind + serviceInstanceName: bookshop-saas-registry + secretName: bookshop-saas-registry-bind-secret + secretKey: credentials + parameters: {} + serviceManager: + name: bookshop-service-manager-bind + serviceInstanceName: bookshop-service-manager + secretName: bookshop-service-manager-bind-secret + secretKey: credentials + parameters: {} +app: + domains: + primary: null + secondary: [] + istioIngressGatewayLabels: + istio: ingressgateway + app: istio-ingressgateway +btp: + globalAccountId: null + provider: + subdomain: null + tenantId: null +imagePullSecrets: [] +workloads: + appRouter: + name: app-router + labels: + sme.sap.com/app-type: bookshop + consumedBTPServices: + - bookshop-uaa-bind + - bookshop-destination-bind + - bookshop-html5-repo-runtime-bind + - bookshop-saas-registry-bind + deploymentDefinition: + type: Router + image: gcr.io/test/app-router + ports: + - name: router-port + port: 5000 + server: + name: server + labels: + sme.sap.com/app-type: bookshop + consumedBTPServices: + - bookshop-uaa-bind + - bookshop-saas-registry-bind + - bookshop-service-manager-bind + deploymentDefinition: + type: CAP + image: gcr.io/test/server + env: + - name: CDS_CONFIG + value: '{"requires":{"cds.xt.DeploymentService":{"hdi":{"create":{"database_id":"abcd"}}}}}' + contentDeploy: + name: content-deploy + labels: + sme.sap.com/app-type: bookshop + consumedBTPServices: + - bookshop-uaa-bind + - bookshop-saas-registry-bind + - bookshop-html5-repo-host-bind + jobDefinition: + type: Content + image: gcr.io/test/content-deploy + tenantJob: + name: tenant-job + labels: + sme.sap.com/app-type: bookshop + consumedBTPServices: + - bookshop-uaa-bind + - bookshop-saas-registry-bind + - bookshop-service-manager-bind + jobDefinition: + type: TenantOperation + image: gcr.io/test/tenant-job + env: + - name: CDS_CONFIG + value: '{"requires":{"cds.xt.DeploymentService":{"hdi":{"create":{"database_id":"abcd"}}}}}' +tenantOperations: + provisioning: + - workloadName: tenant-job + upgrade: + - workloadName: tenant-job +contentJobs: +- content-deploy