diff --git a/charts/opskubedbcom-druidopsrequest-editor/ui/create-ui.yaml b/charts/opskubedbcom-druidopsrequest-editor/ui/create-ui.yaml index f4a0e448a7..87ee766452 100644 --- a/charts/opskubedbcom-druidopsrequest-editor/ui/create-ui.yaml +++ b/charts/opskubedbcom-druidopsrequest-editor/ui/create-ui.yaml @@ -487,109 +487,176 @@ step: if: name: ifRequestTypeEqualsTo|Reconfigure type: function + loader: + name: fetchConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace elements: - type: block-layout label: Configuration elements: - - type: radio - label: Reconfigure Type - subtitle: Choose the action you'd like to perform. Select a new configuration secret, apply a custom configuration, or remove an existing setup. + - type: label-element + label: '' + subtitle: Select a new configuration secret, apply a custom configuration, or remove an existing setup to update your database settings + - type: tab-layout + label: New Config Secret + schema: temp/properties/reconfigurationType options: - - text: Select New Config Secret + - text: NEW CONFIG SECRET value: selectNewConfigSecret - - text: Apply Config + - text: APPLY CONFIG value: applyConfig - - text: Remove + - text: REMOVE value: remove - watcher: - func: onReconfigurationTypeChange - paths: - - temp/properties/reconfigurationType - validation: - type: required - schema: temp/properties/reconfigurationType - - type: block-layout - label: Config Secret - showLabels: true - if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret - type: function elements: - - type: label-element - label: Select a configuration secret from the available list to update your database settings - customClass: mb-10 - - type: select - addNewButton: - label: Create Secret - target: _blank - url: - function: createSecretUrl - label: Config Secret - loader: - name: getConfigSecrets - watchPaths: - - schema/properties/metadata/properties/namespace - init: - type: func - value: setValueFromDbDetails|/spec/configSecret/name - refresh: true - validation: - type: required - schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name - - type: label-element - label: Selected Config Secret - loader: getSelectedConfigSecret - watcher: - func: getSelectedConfigSecret - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - - type: editor - label: Value - readonly: true - hasCopy: false - watcher: - func: getSelectedConfigSecretValue - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - schema: temp/properties/configArray/items/properties/value - - type: array-object-form - label: Apply Config - buttonClass: is-light is-outlined - if: - name: ifReconfigurationTypeEqualsTo|applyConfig - type: function - validation: - type: required - watcher: - func: onApplyconfigChange - paths: - - temp/properties/applyConfig - schema: temp/properties/applyConfig - elements: - - type: label-element - label: New Apply Config - subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings. Enter the parameter you want to configure (e.g., druid.emitter). - customClass: mb-10 - - type: input - label: key - validation: - type: required - schema: key - - type: editor - hasCopy: false - label: value - validation: - type: required - schema: value - - type: switch - fullwidth: true - schema: schema/properties/spec/properties/configuration/properties/removeCustomConfig - label: Remove CustomConfig - if: - name: returnFalse - type: function + - type: block-layout + label: Config Secret + if: + name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret + type: function + elements: + - type: label-element + label: Config Secret + subtitle: Select a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name + refresh: true + label: Config Secret + loader: + name: getConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace + - temp/properties/createSecret/properties/status + init: + type: func + value: setValueFromDbDetails|/spec/configSecret/name + watcher: + func: onCreateSecretChange + paths: + - temp/properties/createSecret/properties/status + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|create + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + - type: block-layout + label: Create a New Config Secret + showLabels: true + if: + type: function + name: isCreateSecret + hasButton: + text: Save + hasCancel: cancelCreateSecret + action: createNewConfigSecret + elements: + - type: input + label: Secret Name + schema: temp/properties/createSecret/properties/name + validation: + type: required + - type: array-object-form + label: String Data + schema: temp/properties/createSecret/properties/data + buttonClass: is-light is-outlined + validation: + type: required + elements: + - type: select + label: Key + schema: key + loader: + name: onSelectedSecretChange + watchPaths: + - temp/properties/createSecret/properties/data + validation: + type: required + - type: textarea + label: Value + schema: value + - type: multi-file-editor + editorHeight: 500px + readonly: true + if: + type: function + name: isNotCreateSecret + loader: + name: onNewConfigSecretChange + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + schema: temp/properties/newConfigSecret + - type: block-layout + label: ApplyConfig + if: + name: ifReconfigurationTypeEqualsTo|applyConfig + type: function + elements: + - type: label-element + label: New Apply Config + subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings.
Enter the parameter you want to configure (e.g., max_connections). + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfiguration + refresh: true + label: Configuration + loader: getConfigSecretsforAppyConfig + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|apply + watchPaths: + - temp/properties/selectedConfiguration + - type: multi-file-editor + editorHeight: 500px + schema: temp/properties/applyConfig + loader: + name: setApplyConfig + watchPaths: + - temp/properties/selectedConfiguration + watcher: + func: onApplyconfigChange + paths: + - temp/properties/applyConfig + - type: block-layout + label: Remove + if: + name: ifReconfigurationTypeEqualsTo|remove + type: function + elements: + - type: label-element + label: Remove + subtitle: Selected a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfigurationRemove + refresh: true + label: Configuration + loader: + name: getConfigSecretsforAppyConfig + watchPaths: + - schema/properties/metadata/properties/namespace + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|remove + watchPaths: + - temp/properties/selectedConfigurationRemove + - type: multi-file-editor + editorHeight: 500px + readonly: true + init: + type: func + value: onRemoveConfigChange + watcher: + func: onRemoveConfigChange + paths: + - temp/properties/selectedConfigurationRemove + schema: temp/properties/removeConfig # Restart - type: block-layout label: TLS diff --git a/charts/opskubedbcom-druidopsrequest-editor/ui/functions.js b/charts/opskubedbcom-druidopsrequest-editor/ui/functions.js index 5cf6f25562..d1ef674046 100644 --- a/charts/opskubedbcom-druidopsrequest-editor/ui/functions.js +++ b/charts/opskubedbcom-druidopsrequest-editor/ui/functions.js @@ -1,4 +1,4 @@ -const { axios, useOperator, store } = window.vueHelpers || {} +const { axios, useOperator, store, useToast } = window.vueHelpers || {} // ============================================================ // MACHINE PROFILES - Predefined Resource Configurations @@ -323,9 +323,11 @@ const druidNodeTypes = [ let machinesFromPreset = [] let secretArray = [] +const configSecretKeys = ['.properties'] export const useFunc = (model) => { const route = store.state?.route + const toast = useToast() const { getValue, storeGet, discriminator, setDiscriminatorValue, commit } = useOperator( model, @@ -1058,33 +1060,415 @@ export const useFunc = (model) => { // CONFIGURATION FUNCTIONS // ============================================================ - /** - * Fetch all secrets from the current namespace for configuration - * Populates the global secretArray for later use in config secret value display - * @returns {Array} List of secrets with text/value properties - */ - async function getConfigSecrets() { + // Fetch and store database Infos + // for secret configurations in reconfigure + let configSecrets = [] + let secretConfigData = [] + + async function fetchConfigSecrets() { const owner = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') const namespace = getValue(model, '/metadata/namespace') // watchDependency('model#/metadata/namespace') - const resp = await axios.get( - `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets`, + const name = getValue(model, '/spec/databaseRef/name') + const dbGroup = getValue(model, '/route/params/group') + const dbKind = getValue(store.state, '/resource/definition/result/kind') + const dbResource = getValue(model, '/route/params/resource') + const dbVersion = getValue(model, '/route/params/version') + + try { + const resp = await axios.post( + `/clusters/${owner}/${cluster}/proxy/ui.kubedb.com/v1alpha1/databaseinfos`, + { + apiVersion: 'ui.kubedb.com/v1alpha1', + kind: 'DatabaseInfo', + request: { + source: { + ref: { + name: name, + namespace: namespace, + }, + resource: { + group: dbGroup, + kind: dbKind, + name: dbResource, + version: dbVersion, + }, + }, + keys: ['.properties'], + }, + }, + ) + configSecrets = resp?.data?.response?.availableSecrets || [] + secretConfigData = resp?.data?.response?.configurations || [] + } catch (e) { + console.log(e) + } + } + + async function getConfigSecrets(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'success') { + await fetchConfigSecrets() + } + const mappedSecrets = configSecrets.map((item) => { + return { text: item, value: item } + }) + mappedSecrets.push({ text: '+ Create a new Secret', value: 'Create' }) + return mappedSecrets + } + + async function getConfigSecretsforAppyConfig() { + const secrets = secretConfigData.map((item) => { + return { text: item.componentName, value: item.componentName } + }) + return secrets + } + + function getSelectedConfigurationData(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfiguration` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return [] + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, ) - const secrets = (resp && resp.data && resp.data.items) || [] - secretArray = secrets + if (!configuration) { + return [] + } - const filteredSecrets = secrets + const result = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedContent = atob(configuration.data[fileName]) + result.push({ + name: fileName, + content: decodedContent, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + result.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) - filteredSecrets.map((item) => { - const name = (item.metadata && item.metadata.name) || '' - item.text = name - item.value = name - return true + // Set the value to the model + commit('wizard/model$update', { + path: `/temp/${type}applyConfig`, + value: result, + force: true, + }) + + return result + } + + function getSelectedConfigurationName(configType, type) { + type = type ? type + '/' : '' + let path = '' + if (configType === 'create') path = `/spec/configuration/${type}/configSecret/name` + else if (configType === 'apply') path = `/${type}selectedConfiguration` + else if (configType === 'remove') path = `/${type}selectedConfigurationRemove` + + const selectedConfiguration = + configType === 'create' ? getValue(model, path) : getValue(discriminator, path) + + if (selectedConfiguration) + return { subtitle: ` You have selected ${selectedConfiguration} secret` } + else return { subtitle: 'No secret selected' } + } + + function getSelectedConfigurationValueForRemove(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfigurationRemove` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return '' + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, + ) + + if (!configuration) { + return '' + } + + let data = {} + // Decode base64 and parse YAML for each key in the secret data + Object.keys(configuration.data).forEach((item) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[item]) + // Parse YAML string to object + const parsedYaml = yaml.load(decodedString) + // Store the parsed object with the filename as key + data[item] = parsedYaml + } catch (e) { + console.error(`Error parsing ${item}:`, e) + data[item] = atob(configuration.data[item]) // Fallback to decoded string + } + }) + + // Convert data object back to YAML string + return yaml.dump(data) + } + + async function createNewConfigSecret(type) { + type = type ? type + '/' : '' + const { user, cluster } = route.params + const url = `/clusters/${user}/${cluster}/resources` + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + const secretName = getValue(discriminator, `${type}createSecret/name`) + const secretData = getValue(discriminator, `${type}createSecret/data`) + const secretDataObj = Object.fromEntries(secretData.map((item) => [item.key, item.value])) + + try { + const res = await axios.post(url, { + apiVersion: 'v1', + stringData: secretDataObj, + kind: 'Secret', + metadata: { + name: secretName, + namespace: namespace, + }, + type: 'Opaque', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'success', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/lastCreatedSecret`, + value: secretName, + }) + toast.success('Secret created successfully') + } catch (error) { + const errMsg = decodeError(error, 'Failed to create secret') + toast.error(errMsg, { timeout: 5000 }) + cancelCreateSecret() + } + } + + function decodeError(msg, defaultMsg) { + if (typeof msg === 'string') { + return msg || defaultMsg + } + return ( + (msg.response && msg.response.data && msg.response.data.message) || + (msg.response && msg.response.data) || + (msg.status && msg.status.status) || + defaultMsg + ) + } + + function isCreateSecret(type) { + type = type ? type + '/' : '' + const selectedSecret = getValue(model, `spec/configuration/${type}configSecret/name`) + const res = selectedSecret === 'Create' + + if (res === true) { + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'pending', + }) + } + return res + } + + function isNotCreateSecret(type) { + return !isCreateSecret(type) + } + + function onCreateSecretChange(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'cancelled') return '' + else if (secretStatus === 'success') { + const name = getValue(discriminator, `${type}createSecret/lastCreatedSecret`) + + const configFound = configSecrets.find((item) => item === name) + return configFound ? { text: name, value: name } : '' + } + } + + function cancelCreateSecret(type) { + type = type ? type + '/' : '' + commit('wizard/temp$delete', `${type}createSecret/name`) + commit('wizard/temp$delete', `${type}createSecret/data`) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'cancelled', + }) + } + + async function onApplyconfigChange(type) { + type = type ? type + '/' : '' + const configValue = getValue(discriminator, `${type}applyConfig`) + + if (!configValue) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + const tempConfigObj = {} + configValue.forEach((item) => { + if (item.name) { + tempConfigObj[item.name] = item.content + } + }) + if (Object.keys(tempConfigObj).length === 0) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}applyConfig`, + value: tempConfigObj, + }) + } + + function setApplyConfig(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfiguration` + const selectedConfig = getValue(discriminator, configPath) + if (!selectedConfig) { + return [{ name: '', content: '' }] + } + const applyconfigData = secretConfigData.find((item) => { + if (item.componentName === selectedConfig) { + return item + } + }) + const { applyConfig } = applyconfigData + const configObj = [] + + if (applyConfig) { + Object.keys(applyConfig).forEach((fileName) => { + configObj.push({ + name: fileName, + content: applyConfig[fileName], + }) + }) + } else { + configObj.push({ name: '', content: '' }) + } + return configObj + } + + function onRemoveConfigChange(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfigurationRemove` + const selectedConfig = getValue(discriminator, configPath) + + if (!selectedConfig) { + commit('wizard/model$delete', `/spec/configuration/${type}removeCustomConfig`) + return [{ name: '', content: '' }] + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}removeCustomConfig`, + value: true, + }) + + const configuration = secretConfigData.find((item) => item.componentName === selectedConfig) + + if (!configuration.data) { + return [{ name: '', content: '' }] + } + + const configObj = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) + return configObj + } + + async function onNewConfigSecretChange(type) { + type = type ? type + '/' : '' + const path = `/spec/configuration/${type}configSecret/name` + const selectedSecret = getValue(model, path) + + if (!selectedSecret) { + commit('wizard/model$delete', `/spec/configuration/${type}configSecret`) + return [{ name: '', content: '' }] + } + if (selectedSecret === 'Create') return [{ name: '', content: '' }] + + const owner = storeGet('/route/params/user') + const cluster = storeGet('/route/params/cluster') + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + + try { + // Fetch the secret data from API + const secretResp = await axios.get( + `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets/${selectedSecret}`, + ) + + const secretData = secretResp.data?.data || {} + const configObj = [] + + // Decode base64 and format as array of objects with name and content + Object.keys(secretData).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(secretData[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: secretData[fileName], // Fallback to original if decode fails + }) + } + }) + + return configObj + } catch (e) { + console.error('Error fetching secret:', e) + return [{ name: '', content: '' }] + } + } + + function onSelectedSecretChange(index) { + const secretData = getValue(discriminator, 'createSecret/data') || [] + const selfSecrets = secretData.map((item) => item.key) + + const remainingSecrets = configSecretKeys.filter((item) => !selfSecrets.includes(item)) + + const selfKey = getValue(discriminator, `createSecret/data/${index}/key`) + if (selfKey) { + remainingSecrets.push(selfKey) + } + const resSecret = remainingSecrets.map((item) => { + return { text: item, value: item } }) - return filteredSecrets + return resSecret } /** @@ -1218,26 +1602,6 @@ export const useFunc = (model) => { return data || 'No Data Found' } - function onApplyconfigChange(type) { - const configPath = `/${type}/applyConfig` - const applyconfig = getValue(discriminator, configPath) - - const configObj = {} - - if (applyconfig) { - applyconfig.forEach((item) => { - const { key, value } = item - configObj[key] = value - }) - } - - commit('wizard/model$update', { - path: `/spec/configuration/${type}/applyConfig`, - value: configObj, - force: true, - }) - } - // ============================================================ // RECONFIGURATION FUNCTIONS // ============================================================ @@ -1744,6 +2108,21 @@ export const useFunc = (model) => { getSelectedConfigSecretData, setSelectedConfigSecret, onApplyconfigChange, + fetchConfigSecrets, + getConfigSecretsforAppyConfig, + getSelectedConfigurationData, + getSelectedConfigurationName, + getSelectedConfigurationValueForRemove, + createNewConfigSecret, + decodeError, + isCreateSecret, + isNotCreateSecret, + onCreateSecretChange, + cancelCreateSecret, + setApplyConfig, + onRemoveConfigChange, + onNewConfigSecretChange, + onSelectedSecretChange, // Reconfiguration functions ifReconfigurationTypeEqualsTo, diff --git a/charts/opskubedbcom-elasticsearchopsrequest-editor/ui/create-ui.yaml b/charts/opskubedbcom-elasticsearchopsrequest-editor/ui/create-ui.yaml index 77cbe987a8..b3b94b6ed6 100644 --- a/charts/opskubedbcom-elasticsearchopsrequest-editor/ui/create-ui.yaml +++ b/charts/opskubedbcom-elasticsearchopsrequest-editor/ui/create-ui.yaml @@ -1137,113 +1137,176 @@ step: if: name: ifRequestTypeEqualsTo|Reconfigure type: function + loader: + name: fetchConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace elements: - type: block-layout label: Configuration elements: - - type: radio - label: Reconfigure Type - subtitle: Choose the action you'd like to perform. Select a new configuration secret, apply a custom configuration, or remove an existing setup. + - type: label-element + label: '' + subtitle: Select a new configuration secret, apply a custom configuration, or remove an existing setup to update your database settings + - type: tab-layout + label: New Config Secret + schema: temp/properties/reconfigurationType options: - - text: Select New Config Secret + - text: NEW CONFIG SECRET value: selectNewConfigSecret - - text: Apply Config + - text: APPLY CONFIG value: applyConfig - - text: Remove + - text: REMOVE value: remove - watcher: - func: onReconfigurationTypeChange - paths: - - temp/properties/reconfigurationType - validation: - type: required - schema: temp/properties/reconfigurationType - - type: block-layout - label: Configuration config secret - showLabels: true - if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret - type: function elements: - - type: label-element - label: Select a configuration secret from the available list to update your database settings - customClass: mb-10 - - type: select - addNewButton: - label: Create a new Secret - target: _blank - url: - function: createSecretUrl - label: Config Secret - loader: - name: getConfigSecrets - watchPaths: - - schema/properties/metadata/properties/namespace - init: - type: func - value: setValueFromDbDetails|/spec/configSecret/name - refresh: true - validation: - type: required - schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name - - type: label-element - label: Selected Config Secret - loader: getSelectedConfigSecret - watcher: - func: getSelectedConfigSecret - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - - type: editor - label: Value - readonly: true - hasCopy: false - if: - name: isConfigSelected - type: function - watcher: - func: getSelectedConfigSecretValue - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - schema: temp/properties/configArray/items/properties/value - - type: array-object-form - label: ApplyConfig - buttonClass: is-light is-outlined - if: - name: ifReconfigurationTypeEqualsTo|applyConfig - type: function - validation: - type: required - watcher: - func: onApplyconfigChange - paths: - - temp/properties/applyConfig - schema: temp/properties/applyConfig - elements: - - type: label-element - label: New Apply Config - subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings. Enter the parameter you want to configure (e.g., max_connections). - customClass: mb-10 - - type: input - label: key - validation: - type: required - schema: key - - type: editor - hasCopy: false - label: value - validation: - type: required - schema: value - - type: switch - schema: schema/properties/spec/properties/configuration/properties/removeCustomConfig - label: Remove CustomConfig - fullwidth: true - if: - name: returnFalse - type: function - + - type: block-layout + label: Config Secret + if: + name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret + type: function + elements: + - type: label-element + label: Config Secret + subtitle: Select a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name + refresh: true + label: Config Secret + loader: + name: getConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace + - temp/properties/createSecret/properties/status + init: + type: func + value: setValueFromDbDetails|/spec/configSecret/name + watcher: + func: onCreateSecretChange + paths: + - temp/properties/createSecret/properties/status + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|create + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + - type: block-layout + label: Create a New Config Secret + showLabels: true + if: + type: function + name: isCreateSecret + hasButton: + text: Save + hasCancel: cancelCreateSecret + action: createNewConfigSecret + elements: + - type: input + label: Secret Name + schema: temp/properties/createSecret/properties/name + validation: + type: required + - type: array-object-form + label: String Data + schema: temp/properties/createSecret/properties/data + buttonClass: is-light is-outlined + validation: + type: required + elements: + - type: select + label: Key + schema: key + loader: + name: onSelectedSecretChange + watchPaths: + - temp/properties/createSecret/properties/data + validation: + type: required + - type: textarea + label: Value + schema: value + - type: multi-file-editor + editorHeight: 500px + readonly: true + if: + type: function + name: isNotCreateSecret + loader: + name: onNewConfigSecretChange + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + schema: temp/properties/newConfigSecret + - type: block-layout + label: ApplyConfig + if: + name: ifReconfigurationTypeEqualsTo|applyConfig + type: function + elements: + - type: label-element + label: New Apply Config + subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings.
Enter the parameter you want to configure (e.g., max_connections). + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfiguration + refresh: true + label: Configuration + loader: getConfigSecretsforAppyConfig + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|apply + watchPaths: + - temp/properties/selectedConfiguration + - type: multi-file-editor + editorHeight: 500px + schema: temp/properties/applyConfig + loader: + name: setApplyConfig + watchPaths: + - temp/properties/selectedConfiguration + watcher: + func: onApplyconfigChange + paths: + - temp/properties/applyConfig + - type: block-layout + label: Remove + if: + name: ifReconfigurationTypeEqualsTo|remove + type: function + elements: + - type: label-element + label: Remove + subtitle: Selected a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfigurationRemove + refresh: true + label: Configuration + loader: + name: getConfigSecretsforAppyConfig + watchPaths: + - schema/properties/metadata/properties/namespace + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|remove + watchPaths: + - temp/properties/selectedConfigurationRemove + - type: multi-file-editor + editorHeight: 500px + readonly: true + init: + type: func + value: onRemoveConfigChange + watcher: + func: onRemoveConfigChange + paths: + - temp/properties/selectedConfigurationRemove + schema: temp/properties/removeConfig # Reconfigure TLS - type: block-layout label: TLS diff --git a/charts/opskubedbcom-elasticsearchopsrequest-editor/ui/functions.js b/charts/opskubedbcom-elasticsearchopsrequest-editor/ui/functions.js index 7dc32974af..422b157eeb 100644 --- a/charts/opskubedbcom-elasticsearchopsrequest-editor/ui/functions.js +++ b/charts/opskubedbcom-elasticsearchopsrequest-editor/ui/functions.js @@ -1,4 +1,4 @@ -const { axios, useOperator, store } = window.vueHelpers || {} +const { axios, useOperator, store, useToast } = window.vueHelpers || {} // ===================================================== // Machine Profiles Configuration @@ -314,9 +314,11 @@ const machineList = [ // ===================================================== let machinesFromPreset = [] let secretArray = [] +const configSecretKeys = ['elasticsearch.yml', 'data-elasticsearch.yml', 'ingest-elasticsearch.yml'] export const useFunc = (model) => { const route = store.state?.route + const toast = useToast() const { getValue, storeGet, discriminator, setDiscriminatorValue, commit } = useOperator( model, @@ -1089,28 +1091,415 @@ export const useFunc = (model) => { // Configuration/Reconfiguration Functions // ===================================================== - async function getConfigSecrets() { + // Fetch and store database Infos + // for secret configurations in reconfigure + let configSecrets = [] + let secretConfigData = [] + + async function fetchConfigSecrets() { const owner = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') const namespace = getValue(model, '/metadata/namespace') // watchDependency('model#/metadata/namespace') - const resp = await axios.get( - `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets`, + const name = getValue(model, '/spec/databaseRef/name') + const dbGroup = getValue(model, '/route/params/group') + const dbKind = getValue(store.state, '/resource/definition/result/kind') + const dbResource = getValue(model, '/route/params/resource') + const dbVersion = getValue(model, '/route/params/version') + + try { + const resp = await axios.post( + `/clusters/${owner}/${cluster}/proxy/ui.kubedb.com/v1alpha1/databaseinfos`, + { + apiVersion: 'ui.kubedb.com/v1alpha1', + kind: 'DatabaseInfo', + request: { + source: { + ref: { + name: name, + namespace: namespace, + }, + resource: { + group: dbGroup, + kind: dbKind, + name: dbResource, + version: dbVersion, + }, + }, + keys: ['elasticsearch.yml', 'data-elasticsearch.yml', 'ingest-elasticsearch.yml'], + }, + }, + ) + configSecrets = resp?.data?.response?.availableSecrets || [] + secretConfigData = resp?.data?.response?.configurations || [] + } catch (e) { + console.log(e) + } + } + + async function getConfigSecrets(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'success') { + await fetchConfigSecrets() + } + const mappedSecrets = configSecrets.map((item) => { + return { text: item, value: item } + }) + mappedSecrets.push({ text: '+ Create a new Secret', value: 'Create' }) + return mappedSecrets + } + + async function getConfigSecretsforAppyConfig() { + const secrets = secretConfigData.map((item) => { + return { text: item.componentName, value: item.componentName } + }) + return secrets + } + + function getSelectedConfigurationData(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfiguration` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return [] + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, ) - const secrets = (resp && resp.data && resp.data.items) || [] - secretArray = secrets + if (!configuration) { + return [] + } + + const result = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedContent = atob(configuration.data[fileName]) + result.push({ + name: fileName, + content: decodedContent, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + result.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) - const filteredSecrets = secrets + // Set the value to the model + commit('wizard/model$update', { + path: `/temp/${type}applyConfig`, + value: result, + force: true, + }) - filteredSecrets.map((item) => { - const name = (item.metadata && item.metadata.name) || '' - item.text = name - item.value = name - return true + return result + } + + function getSelectedConfigurationName(configType, type) { + type = type ? type + '/' : '' + let path = '' + if (configType === 'create') path = `/spec/configuration/${type}/configSecret/name` + else if (configType === 'apply') path = `/${type}selectedConfiguration` + else if (configType === 'remove') path = `/${type}selectedConfigurationRemove` + + const selectedConfiguration = + configType === 'create' ? getValue(model, path) : getValue(discriminator, path) + + if (selectedConfiguration) + return { subtitle: ` You have selected ${selectedConfiguration} secret` } + else return { subtitle: 'No secret selected' } + } + + function getSelectedConfigurationValueForRemove(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfigurationRemove` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return '' + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, + ) + + if (!configuration) { + return '' + } + + let data = {} + // Decode base64 and parse YAML for each key in the secret data + Object.keys(configuration.data).forEach((item) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[item]) + // Parse YAML string to object + const parsedYaml = yaml.load(decodedString) + // Store the parsed object with the filename as key + data[item] = parsedYaml + } catch (e) { + console.error(`Error parsing ${item}:`, e) + data[item] = atob(configuration.data[item]) // Fallback to decoded string + } + }) + + // Convert data object back to YAML string + return yaml.dump(data) + } + + async function createNewConfigSecret(type) { + type = type ? type + '/' : '' + const { user, cluster } = route.params + const url = `/clusters/${user}/${cluster}/resources` + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + const secretName = getValue(discriminator, `${type}createSecret/name`) + const secretData = getValue(discriminator, `${type}createSecret/data`) + const secretDataObj = Object.fromEntries(secretData.map((item) => [item.key, item.value])) + + try { + const res = await axios.post(url, { + apiVersion: 'v1', + stringData: secretDataObj, + kind: 'Secret', + metadata: { + name: secretName, + namespace: namespace, + }, + type: 'Opaque', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'success', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/lastCreatedSecret`, + value: secretName, + }) + toast.success('Secret created successfully') + } catch (error) { + const errMsg = decodeError(error, 'Failed to create secret') + toast.error(errMsg, { timeout: 5000 }) + cancelCreateSecret() + } + } + + function decodeError(msg, defaultMsg) { + if (typeof msg === 'string') { + return msg || defaultMsg + } + return ( + (msg.response && msg.response.data && msg.response.data.message) || + (msg.response && msg.response.data) || + (msg.status && msg.status.status) || + defaultMsg + ) + } + + function isCreateSecret(type) { + type = type ? type + '/' : '' + const selectedSecret = getValue(model, `spec/configuration/${type}configSecret/name`) + const res = selectedSecret === 'Create' + + if (res === true) { + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'pending', + }) + } + return res + } + + function isNotCreateSecret(type) { + return !isCreateSecret(type) + } + + function onCreateSecretChange(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'cancelled') return '' + else if (secretStatus === 'success') { + const name = getValue(discriminator, `${type}createSecret/lastCreatedSecret`) + + const configFound = configSecrets.find((item) => item === name) + return configFound ? { text: name, value: name } : '' + } + } + + function cancelCreateSecret(type) { + type = type ? type + '/' : '' + commit('wizard/temp$delete', `${type}createSecret/name`) + commit('wizard/temp$delete', `${type}createSecret/data`) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'cancelled', + }) + } + + async function onApplyconfigChange(type) { + type = type ? type + '/' : '' + const configValue = getValue(discriminator, `${type}applyConfig`) + + if (!configValue) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + const tempConfigObj = {} + configValue.forEach((item) => { + if (item.name) { + tempConfigObj[item.name] = item.content + } + }) + if (Object.keys(tempConfigObj).length === 0) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}applyConfig`, + value: tempConfigObj, + }) + } + + function setApplyConfig(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfiguration` + const selectedConfig = getValue(discriminator, configPath) + if (!selectedConfig) { + return [{ name: '', content: '' }] + } + const applyconfigData = secretConfigData.find((item) => { + if (item.componentName === selectedConfig) { + return item + } }) - return filteredSecrets + const { applyConfig } = applyconfigData + const configObj = [] + + if (applyConfig) { + Object.keys(applyConfig).forEach((fileName) => { + configObj.push({ + name: fileName, + content: applyConfig[fileName], + }) + }) + } else { + configObj.push({ name: '', content: '' }) + } + return configObj + } + + function onRemoveConfigChange(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfigurationRemove` + const selectedConfig = getValue(discriminator, configPath) + + if (!selectedConfig) { + commit('wizard/model$delete', `/spec/configuration/${type}removeCustomConfig`) + return [{ name: '', content: '' }] + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}removeCustomConfig`, + value: true, + }) + + const configuration = secretConfigData.find((item) => item.componentName === selectedConfig) + + if (!configuration.data) { + return [{ name: '', content: '' }] + } + + const configObj = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) + return configObj + } + + async function onNewConfigSecretChange(type) { + type = type ? type + '/' : '' + const path = `/spec/configuration/${type}configSecret/name` + const selectedSecret = getValue(model, path) + + if (!selectedSecret) { + commit('wizard/model$delete', `/spec/configuration/${type}configSecret`) + return [{ name: '', content: '' }] + } + if (selectedSecret === 'Create') return [{ name: '', content: '' }] + + const owner = storeGet('/route/params/user') + const cluster = storeGet('/route/params/cluster') + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + + try { + // Fetch the secret data from API + const secretResp = await axios.get( + `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets/${selectedSecret}`, + ) + + const secretData = secretResp.data?.data || {} + const configObj = [] + + // Decode base64 and format as array of objects with name and content + Object.keys(secretData).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(secretData[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: secretData[fileName], // Fallback to original if decode fails + }) + } + }) + + return configObj + } catch (e) { + console.error('Error fetching secret:', e) + return [{ name: '', content: '' }] + } + } + + function onSelectedSecretChange(index) { + const secretData = getValue(discriminator, 'createSecret/data') || [] + const selfSecrets = secretData.map((item) => item.key) + + const remainingSecrets = configSecretKeys.filter((item) => !selfSecrets.includes(item)) + + const selfKey = getValue(discriminator, `createSecret/data/${index}/key`) + if (selfKey) { + remainingSecrets.push(selfKey) + } + const resSecret = remainingSecrets.map((item) => { + return { text: item, value: item } + }) + return resSecret } function getSelectedConfigSecret(type) { @@ -1218,26 +1607,6 @@ export const useFunc = (model) => { } } - function onApplyconfigChange(type) { - const configPath = `/${type}/applyConfig` - const applyconfig = getValue(discriminator, configPath) - - const configObj = {} - - if (applyconfig) { - applyconfig.forEach((item) => { - const { key, value } = item - configObj[key] = value - }) - } - - commit('wizard/model$update', { - path: `/spec/configuration/${type}/applyConfig`, - value: configObj, - force: true, - }) - } - // ===================================================== // TLS Functions // ===================================================== @@ -1660,6 +2029,21 @@ export const useFunc = (model) => { ifReconfigurationTypeEqualsTo, onReconfigurationTypeChange, onApplyconfigChange, + fetchConfigSecrets, + getConfigSecretsforAppyConfig, + getSelectedConfigurationData, + getSelectedConfigurationName, + getSelectedConfigurationValueForRemove, + createNewConfigSecret, + decodeError, + isCreateSecret, + isNotCreateSecret, + onCreateSecretChange, + cancelCreateSecret, + setApplyConfig, + onRemoveConfigChange, + onNewConfigSecretChange, + onSelectedSecretChange, // TLS functions getDbTls, diff --git a/charts/opskubedbcom-kafkaopsrequest-editor/ui/create-ui.yaml b/charts/opskubedbcom-kafkaopsrequest-editor/ui/create-ui.yaml index 176e78fcda..6754078f21 100644 --- a/charts/opskubedbcom-kafkaopsrequest-editor/ui/create-ui.yaml +++ b/charts/opskubedbcom-kafkaopsrequest-editor/ui/create-ui.yaml @@ -442,108 +442,176 @@ step: if: name: ifRequestTypeEqualsTo|Reconfigure type: function + loader: + name: fetchConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace elements: - type: block-layout label: Configuration elements: - - type: radio - label: Reconfigure Type - subtitle: Choose the action you'd like to perform. Select a new configuration secret, apply a custom configuration, or remove an existing setup. + - type: label-element + label: '' + subtitle: Select a new configuration secret, apply a custom configuration, or remove an existing setup to update your database settings + - type: tab-layout + label: New Config Secret + schema: temp/properties/reconfigurationType options: - - text: Select New Config Secret + - text: NEW CONFIG SECRET value: selectNewConfigSecret - - text: Apply Config + - text: APPLY CONFIG value: applyConfig - - text: Remove + - text: REMOVE value: remove - watcher: - func: onReconfigurationTypeChange - paths: - - temp/properties/reconfigurationType - validation: - type: required - schema: temp/properties/reconfigurationType - - type: block-layout - label: Configuration config secret - showLabels: true - if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret - type: function - elements: - - type: label-element - label: Select a configuration secret from the available list to update your database settings - customClass: mb-10 - - type: select - addNewButton: - label: Create a new Secret - target: _blank - url: - function: createSecretUrl - label: Config Secret - loader: - name: getConfigSecrets - watchPaths: - - schema/properties/metadata/properties/namespace - init: - type: func - value: setValueFromDbDetails|/spec/configSecret/name - refresh: true - validation: - type: required - schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name - - type: label-element - label: Selected Config Secret - loader: getSelectedConfigSecret|node - watcher: - func: getSelectedConfigSecret|node - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - - type: editor - label: '' - readonly: true - hasCopy: false - loader: getSelectedConfigSecretValue|node - watcher: - func: getSelectedConfigSecretValue|node - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - schema: temp/properties/selectedConfigSecretData - - type: array-object-form - label: Apply Config - if: - name: ifReconfigurationTypeEqualsTo|applyConfig - type: function - validation: - type: required - watcher: - func: onApplyconfigChange - paths: - - temp/properties/applyConfig - schema: temp/properties/applyConfig - buttonClass: is-light is-outlined elements: - - type: label-element - label: New Apply Config - subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings. Enter the parameter you want to configure (e.g., max_connections). - customClass: mb-10 - - type: input - label: key - validation: - type: required - schema: key - - type: editor - hasCopy: false - label: value - validation: - type: required - schema: value - - type: switch - schema: schema/properties/spec/properties/configuration/properties/removeCustomConfig - label: Remove CustomConfig - fullwidth: true - if: - name: returnFalse - type: function + - type: block-layout + label: Config Secret + if: + name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret + type: function + elements: + - type: label-element + label: Config Secret + subtitle: Select a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name + refresh: true + label: Config Secret + loader: + name: getConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace + - temp/properties/createSecret/properties/status + init: + type: func + value: setValueFromDbDetails|/spec/configSecret/name + watcher: + func: onCreateSecretChange + paths: + - temp/properties/createSecret/properties/status + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|create + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + - type: block-layout + label: Create a New Config Secret + showLabels: true + if: + type: function + name: isCreateSecret + hasButton: + text: Save + hasCancel: cancelCreateSecret + action: createNewConfigSecret + elements: + - type: input + label: Secret Name + schema: temp/properties/createSecret/properties/name + validation: + type: required + - type: array-object-form + label: String Data + schema: temp/properties/createSecret/properties/data + buttonClass: is-light is-outlined + validation: + type: required + elements: + - type: select + label: Key + schema: key + loader: + name: onSelectedSecretChange + watchPaths: + - temp/properties/createSecret/properties/data + validation: + type: required + - type: textarea + label: Value + schema: value + - type: multi-file-editor + editorHeight: 500px + readonly: true + if: + type: function + name: isNotCreateSecret + loader: + name: onNewConfigSecretChange + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + schema: temp/properties/newConfigSecret + - type: block-layout + label: ApplyConfig + if: + name: ifReconfigurationTypeEqualsTo|applyConfig + type: function + elements: + - type: label-element + label: New Apply Config + subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings.
Enter the parameter you want to configure (e.g., max_connections). + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfiguration + refresh: true + label: Configuration + loader: getConfigSecretsforAppyConfig + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|apply + watchPaths: + - temp/properties/selectedConfiguration + - type: multi-file-editor + editorHeight: 500px + schema: temp/properties/applyConfig + loader: + name: setApplyConfig + watchPaths: + - temp/properties/selectedConfiguration + watcher: + func: onApplyconfigChange + paths: + - temp/properties/applyConfig + - type: block-layout + label: Remove + if: + name: ifReconfigurationTypeEqualsTo|remove + type: function + elements: + - type: label-element + label: Remove + subtitle: Selected a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfigurationRemove + refresh: true + label: Configuration + loader: + name: getConfigSecretsforAppyConfig + watchPaths: + - schema/properties/metadata/properties/namespace + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|remove + watchPaths: + - temp/properties/selectedConfigurationRemove + - type: multi-file-editor + editorHeight: 500px + readonly: true + init: + type: func + value: onRemoveConfigChange + watcher: + func: onRemoveConfigChange + paths: + - temp/properties/selectedConfigurationRemove + schema: temp/properties/removeConfig # Reconfigure TLS - type: block-layout label: TLS diff --git a/charts/opskubedbcom-kafkaopsrequest-editor/ui/functions.js b/charts/opskubedbcom-kafkaopsrequest-editor/ui/functions.js index 5221da8421..c9880fb815 100644 --- a/charts/opskubedbcom-kafkaopsrequest-editor/ui/functions.js +++ b/charts/opskubedbcom-kafkaopsrequest-editor/ui/functions.js @@ -1,4 +1,4 @@ -const { axios, useOperator, store } = window.vueHelpers || {} +const { axios, useOperator, store, useToast } = window.vueHelpers || {} const machines = { 'db.t.micro': { resources: { @@ -307,9 +307,11 @@ const machineList = [ let machinesFromPreset = [] let secretArray = [] +const configSecretKeys = ['server.properties', 'broker.properties', 'controller.properties'] export const useFunc = (model) => { const route = store.state?.route + const toast = useToast() const { getValue, storeGet, discriminator, setDiscriminatorValue, commit } = useOperator( model, @@ -878,30 +880,415 @@ export const useFunc = (model) => { return machine === 'custom' } - // for config secret - async function getConfigSecrets() { + // Fetch and store database Infos + // for secret configurations in reconfigure + let configSecrets = [] + let secretConfigData = [] + + async function fetchConfigSecrets() { const owner = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') const namespace = getValue(model, '/metadata/namespace') // watchDependency('model#/metadata/namespace') - const resp = await axios.get( - `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets`, + const name = getValue(model, '/spec/databaseRef/name') + const dbGroup = getValue(model, '/route/params/group') + const dbKind = getValue(store.state, '/resource/definition/result/kind') + const dbResource = getValue(model, '/route/params/resource') + const dbVersion = getValue(model, '/route/params/version') + + try { + const resp = await axios.post( + `/clusters/${owner}/${cluster}/proxy/ui.kubedb.com/v1alpha1/databaseinfos`, + { + apiVersion: 'ui.kubedb.com/v1alpha1', + kind: 'DatabaseInfo', + request: { + source: { + ref: { + name: name, + namespace: namespace, + }, + resource: { + group: dbGroup, + kind: dbKind, + name: dbResource, + version: dbVersion, + }, + }, + keys: ['server.properties', 'broker.properties', 'controller.properties'], + }, + }, + ) + configSecrets = resp?.data?.response?.availableSecrets || [] + secretConfigData = resp?.data?.response?.configurations || [] + } catch (e) { + console.log(e) + } + } + + async function getConfigSecrets(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'success') { + await fetchConfigSecrets() + } + const mappedSecrets = configSecrets.map((item) => { + return { text: item, value: item } + }) + mappedSecrets.push({ text: '+ Create a new Secret', value: 'Create' }) + return mappedSecrets + } + + async function getConfigSecretsforAppyConfig() { + const secrets = secretConfigData.map((item) => { + return { text: item.componentName, value: item.componentName } + }) + return secrets + } + + function getSelectedConfigurationData(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfiguration` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return [] + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, ) - const secrets = (resp && resp.data && resp.data.items) || [] + if (!configuration) { + return [] + } - const filteredSecrets = secrets + const result = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedContent = atob(configuration.data[fileName]) + result.push({ + name: fileName, + content: decodedContent, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + result.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) - secretArray = secrets + // Set the value to the model + commit('wizard/model$update', { + path: `/temp/${type}applyConfig`, + value: result, + force: true, + }) - filteredSecrets.map((item) => { - const name = (item.metadata && item.metadata.name) || '' - item.text = name - item.value = name - return true + return result + } + + function getSelectedConfigurationName(configType, type) { + type = type ? type + '/' : '' + let path = '' + if (configType === 'create') path = `/spec/configuration/${type}/configSecret/name` + else if (configType === 'apply') path = `/${type}selectedConfiguration` + else if (configType === 'remove') path = `/${type}selectedConfigurationRemove` + + const selectedConfiguration = + configType === 'create' ? getValue(model, path) : getValue(discriminator, path) + + if (selectedConfiguration) + return { subtitle: ` You have selected ${selectedConfiguration} secret` } + else return { subtitle: 'No secret selected' } + } + + function getSelectedConfigurationValueForRemove(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfigurationRemove` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return '' + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, + ) + + if (!configuration) { + return '' + } + + let data = {} + // Decode base64 and parse YAML for each key in the secret data + Object.keys(configuration.data).forEach((item) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[item]) + // Parse YAML string to object + const parsedYaml = yaml.load(decodedString) + // Store the parsed object with the filename as key + data[item] = parsedYaml + } catch (e) { + console.error(`Error parsing ${item}:`, e) + data[item] = atob(configuration.data[item]) // Fallback to decoded string + } }) - return filteredSecrets + + // Convert data object back to YAML string + return yaml.dump(data) + } + + async function createNewConfigSecret(type) { + type = type ? type + '/' : '' + const { user, cluster } = route.params + const url = `/clusters/${user}/${cluster}/resources` + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + const secretName = getValue(discriminator, `${type}createSecret/name`) + const secretData = getValue(discriminator, `${type}createSecret/data`) + const secretDataObj = Object.fromEntries(secretData.map((item) => [item.key, item.value])) + + try { + const res = await axios.post(url, { + apiVersion: 'v1', + stringData: secretDataObj, + kind: 'Secret', + metadata: { + name: secretName, + namespace: namespace, + }, + type: 'Opaque', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'success', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/lastCreatedSecret`, + value: secretName, + }) + toast.success('Secret created successfully') + } catch (error) { + const errMsg = decodeError(error, 'Failed to create secret') + toast.error(errMsg, { timeout: 5000 }) + cancelCreateSecret() + } + } + + function decodeError(msg, defaultMsg) { + if (typeof msg === 'string') { + return msg || defaultMsg + } + return ( + (msg.response && msg.response.data && msg.response.data.message) || + (msg.response && msg.response.data) || + (msg.status && msg.status.status) || + defaultMsg + ) + } + + function isCreateSecret(type) { + type = type ? type + '/' : '' + const selectedSecret = getValue(model, `spec/configuration/${type}configSecret/name`) + const res = selectedSecret === 'Create' + + if (res === true) { + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'pending', + }) + } + return res + } + + function isNotCreateSecret(type) { + return !isCreateSecret(type) + } + + function onCreateSecretChange(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'cancelled') return '' + else if (secretStatus === 'success') { + const name = getValue(discriminator, `${type}createSecret/lastCreatedSecret`) + + const configFound = configSecrets.find((item) => item === name) + return configFound ? { text: name, value: name } : '' + } + } + + function cancelCreateSecret(type) { + type = type ? type + '/' : '' + commit('wizard/temp$delete', `${type}createSecret/name`) + commit('wizard/temp$delete', `${type}createSecret/data`) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'cancelled', + }) + } + + async function onApplyconfigChange(type) { + type = type ? type + '/' : '' + const configValue = getValue(discriminator, `${type}applyConfig`) + + if (!configValue) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + const tempConfigObj = {} + configValue.forEach((item) => { + if (item.name) { + tempConfigObj[item.name] = item.content + } + }) + if (Object.keys(tempConfigObj).length === 0) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}applyConfig`, + value: tempConfigObj, + }) + } + + function setApplyConfig(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfiguration` + const selectedConfig = getValue(discriminator, configPath) + if (!selectedConfig) { + return [{ name: '', content: '' }] + } + const applyconfigData = secretConfigData.find((item) => { + if (item.componentName === selectedConfig) { + return item + } + }) + const { applyConfig } = applyconfigData + const configObj = [] + + if (applyConfig) { + Object.keys(applyConfig).forEach((fileName) => { + configObj.push({ + name: fileName, + content: applyConfig[fileName], + }) + }) + } else { + configObj.push({ name: '', content: '' }) + } + return configObj + } + + function onRemoveConfigChange(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfigurationRemove` + const selectedConfig = getValue(discriminator, configPath) + + if (!selectedConfig) { + commit('wizard/model$delete', `/spec/configuration/${type}removeCustomConfig`) + return [{ name: '', content: '' }] + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}removeCustomConfig`, + value: true, + }) + + const configuration = secretConfigData.find((item) => item.componentName === selectedConfig) + + if (!configuration.data) { + return [{ name: '', content: '' }] + } + + const configObj = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) + return configObj + } + + async function onNewConfigSecretChange(type) { + type = type ? type + '/' : '' + const path = `/spec/configuration/${type}configSecret/name` + const selectedSecret = getValue(model, path) + + if (!selectedSecret) { + commit('wizard/model$delete', `/spec/configuration/${type}configSecret`) + return [{ name: '', content: '' }] + } + if (selectedSecret === 'Create') return [{ name: '', content: '' }] + + const owner = storeGet('/route/params/user') + const cluster = storeGet('/route/params/cluster') + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + + try { + // Fetch the secret data from API + const secretResp = await axios.get( + `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets/${selectedSecret}`, + ) + + const secretData = secretResp.data?.data || {} + const configObj = [] + + // Decode base64 and format as array of objects with name and content + Object.keys(secretData).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(secretData[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: secretData[fileName], // Fallback to original if decode fails + }) + } + }) + + return configObj + } catch (e) { + console.error('Error fetching secret:', e) + return [{ name: '', content: '' }] + } + } + + function onSelectedSecretChange(index) { + const secretData = getValue(discriminator, 'createSecret/data') || [] + const selfSecrets = secretData.map((item) => item.key) + + const remainingSecrets = configSecretKeys.filter((item) => !selfSecrets.includes(item)) + + const selfKey = getValue(discriminator, `createSecret/data/${index}/key`) + if (selfKey) { + remainingSecrets.push(selfKey) + } + const resSecret = remainingSecrets.map((item) => { + return { text: item, value: item } + }) + return resSecret } function getSelectedConfigSecret(type) { @@ -1098,26 +1485,6 @@ export const useFunc = (model) => { return reconfigurationType === value } - function onApplyconfigChange() { - const configPath = `/applyConfig` - const applyconfig = getValue(discriminator, configPath) - - const configObj = {} - - if (applyconfig) { - applyconfig.forEach((item) => { - const { key, value } = item - configObj[key] = value - }) - } - - commit('wizard/model$update', { - path: `/spec/configuration/applyConfig`, - value: configObj, - force: true, - }) - } - function onReconfigurationTypeChange() { setDiscriminatorValue(`/applyConfig`, []) const reconfigurationType = getValue(discriminator, '/reconfigurationType') @@ -1448,5 +1815,20 @@ export const useFunc = (model) => { getSelectedConfigSecret, getSelectedConfigSecretValue, isVerticalScaleTopologyRequired, + fetchConfigSecrets, + getConfigSecretsforAppyConfig, + getSelectedConfigurationData, + getSelectedConfigurationName, + getSelectedConfigurationValueForRemove, + createNewConfigSecret, + decodeError, + isCreateSecret, + isNotCreateSecret, + onCreateSecretChange, + cancelCreateSecret, + setApplyConfig, + onRemoveConfigChange, + onNewConfigSecretChange, + onSelectedSecretChange, } } diff --git a/charts/opskubedbcom-mariadbopsrequest-editor/ui/create-ui.yaml b/charts/opskubedbcom-mariadbopsrequest-editor/ui/create-ui.yaml index b020540266..8b7188a519 100644 --- a/charts/opskubedbcom-mariadbopsrequest-editor/ui/create-ui.yaml +++ b/charts/opskubedbcom-mariadbopsrequest-editor/ui/create-ui.yaml @@ -263,106 +263,176 @@ step: if: name: ifRequestTypeEqualsTo|Reconfigure type: function + loader: + name: fetchConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace elements: - - type: radio - label: Reconfigure Type - subtitle: Choose the action you'd like to perform. Select a new configuration secret, apply a custom configuration , or remove an existing setup. - validation: - type: required - options: - - text: New Config Secret - value: selectNewConfigSecret - - text: Apply Config - value: applyConfig - - text: Remove - value: remove - schema: temp/properties/reconfigurationType - watcher: - func: onReconfigurationTypeChange - paths: - - temp/properties/reconfigurationType - type: block-layout - label: Config Secret - showLabels: true - if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret - type: function - elements: - - type: label-element - label: Select a configuration secret from the available list to update your database settings - customClass: mb-10 - - type: select - addNewButton: - label: Create a new Secret - target: _blank - url: - function: createSecretUrl - schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - refresh: true - label: Config Secret - loader: - name: getConfigSecrets - watchPaths: - - schema/properties/metadata/properties/namespace - init: - type: func - value: setValueFromDbDetails|/spec/configSecret/name - - type: label-element - label: Selected Config Secret - loader: getSelectedConfigSecret|mariadb - watcher: - func: getSelectedConfigSecret|mariadb - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - - type: editor - label: Value - readonly: true - hasCopy: false - watcher: - func: getSelectedConfigSecretValue|mariadb - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - schema: temp/properties/configArray/items/properties/value - - type: array-object-form - label: ApplyConfig - buttonClass: is-light is-outlined - schema: temp/properties/applyConfig - validation: - type: required - if: - name: ifReconfigurationTypeEqualsTo|applyConfig - type: function - watcher: - func: onApplyconfigChange - paths: - - temp/properties/applyConfig + label: Configuration elements: - type: label-element - label: New Apply Config - subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings. Enter the parameter you want to configure (e.g., max_connections). - customClass: mb-10 - - type: input - label: key - validation: - type: required - schema: key - - type: editor - label: value - hasCopy: false - validation: - type: required - schema: value - - type: switch - schema: schema/properties/spec/properties/configuration/properties/removeCustomConfig - label: Remove CustomConfig - fullwidth: true - if: - name: returnFalse - type: function + label: '' + subtitle: Select a new configuration secret, apply a custom configuration, or remove an existing setup to update your database settings + - type: tab-layout + label: New Config Secret + schema: temp/properties/reconfigurationType + options: + - text: NEW CONFIG SECRET + value: selectNewConfigSecret + - text: APPLY CONFIG + value: applyConfig + - text: REMOVE + value: remove + elements: + - type: block-layout + label: Config Secret + if: + name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret + type: function + elements: + - type: label-element + label: Config Secret + subtitle: Select a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name + refresh: true + label: Config Secret + loader: + name: getConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace + - temp/properties/createSecret/properties/status + init: + type: func + value: setValueFromDbDetails|/spec/configSecret/name + watcher: + func: onCreateSecretChange + paths: + - temp/properties/createSecret/properties/status + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|create + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + - type: block-layout + label: Create a New Config Secret + showLabels: true + if: + type: function + name: isCreateSecret + hasButton: + text: Save + hasCancel: cancelCreateSecret + action: createNewConfigSecret + elements: + - type: input + label: Secret Name + schema: temp/properties/createSecret/properties/name + validation: + type: required + - type: array-object-form + label: String Data + schema: temp/properties/createSecret/properties/data + buttonClass: is-light is-outlined + validation: + type: required + elements: + - type: select + label: Key + schema: key + loader: + name: onSelectedSecretChange + watchPaths: + - temp/properties/createSecret/properties/data + validation: + type: required + - type: textarea + label: Value + schema: value + - type: multi-file-editor + editorHeight: 500px + readonly: true + if: + type: function + name: isNotCreateSecret + loader: + name: onNewConfigSecretChange + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + schema: temp/properties/newConfigSecret + - type: block-layout + label: ApplyConfig + if: + name: ifReconfigurationTypeEqualsTo|applyConfig + type: function + elements: + - type: label-element + label: New Apply Config + subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings.
Enter the parameter you want to configure (e.g., max_connections). + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfiguration + refresh: true + label: Configuration + loader: getConfigSecretsforAppyConfig + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|apply + watchPaths: + - temp/properties/selectedConfiguration + - type: multi-file-editor + editorHeight: 500px + schema: temp/properties/applyConfig + loader: + name: setApplyConfig + watchPaths: + - temp/properties/selectedConfiguration + watcher: + func: onApplyconfigChange + paths: + - temp/properties/applyConfig + - type: block-layout + label: Remove + if: + name: ifReconfigurationTypeEqualsTo|remove + type: function + elements: + - type: label-element + label: Remove + subtitle: Selected a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfigurationRemove + refresh: true + label: Configuration + loader: + name: getConfigSecretsforAppyConfig + watchPaths: + - schema/properties/metadata/properties/namespace + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|remove + watchPaths: + - temp/properties/selectedConfigurationRemove + - type: multi-file-editor + editorHeight: 500px + readonly: true + init: + type: func + value: onRemoveConfigChange + watcher: + func: onRemoveConfigChange + paths: + - temp/properties/selectedConfigurationRemove + schema: temp/properties/removeConfig # Reconfigure TLS - type: block-layout label: TLS diff --git a/charts/opskubedbcom-mariadbopsrequest-editor/ui/functions.js b/charts/opskubedbcom-mariadbopsrequest-editor/ui/functions.js index 5fe6145c1c..2a4b975299 100644 --- a/charts/opskubedbcom-mariadbopsrequest-editor/ui/functions.js +++ b/charts/opskubedbcom-mariadbopsrequest-editor/ui/functions.js @@ -1,4 +1,4 @@ -const { axios, useOperator, store } = window.vueHelpers || {} +const { axios, useOperator, store, useToast } = window.vueHelpers || {} const machines = { 'db.t.micro': { resources: { @@ -306,9 +306,11 @@ const machineList = [ ] let machinesFromPreset = [] +const configSecretKeys = ['kubedb-user.cnf'] export const useFunc = (model) => { const route = store.state?.route + const toast = useToast() const { getValue, storeGet, discriminator, setDiscriminatorValue, commit } = useOperator( model, @@ -830,32 +832,419 @@ export const useFunc = (model) => { return machine === 'custom' } - // for config secret - let secretArray = [] - async function getConfigSecrets() { + // Fetch and store database Infos + // for secret configurations in reconfigure + let configSecrets = [] + let secretConfigData = [] + + async function fetchConfigSecrets() { const owner = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') const namespace = getValue(model, '/metadata/namespace') // watchDependency('model#/metadata/namespace') - const resp = await axios.get( - `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets`, + const name = getValue(model, '/spec/databaseRef/name') + const dbGroup = getValue(model, '/route/params/group') + const dbKind = getValue(store.state, '/resource/definition/result/kind') + const dbResource = getValue(model, '/route/params/resource') + const dbVersion = getValue(model, '/route/params/version') + + try { + const resp = await axios.post( + `/clusters/${owner}/${cluster}/proxy/ui.kubedb.com/v1alpha1/databaseinfos`, + { + apiVersion: 'ui.kubedb.com/v1alpha1', + kind: 'DatabaseInfo', + request: { + source: { + ref: { + name: name, + namespace: namespace, + }, + resource: { + group: dbGroup, + kind: dbKind, + name: dbResource, + version: dbVersion, + }, + }, + keys: ['kubedb-user.cnf'], + }, + }, + ) + configSecrets = resp?.data?.response?.availableSecrets || [] + secretConfigData = resp?.data?.response?.configurations || [] + } catch (e) { + console.log(e) + } + } + + async function getConfigSecrets(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'success') { + await fetchConfigSecrets() + } + const mappedSecrets = configSecrets.map((item) => { + return { text: item, value: item } + }) + mappedSecrets.push({ text: '+ Create a new Secret', value: 'Create' }) + return mappedSecrets + } + + async function getConfigSecretsforAppyConfig() { + const secrets = secretConfigData.map((item) => { + return { text: item.componentName, value: item.componentName } + }) + return secrets + } + + function getSelectedConfigurationData(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfiguration` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return [] + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, ) - const secrets = (resp && resp.data && resp.data.items) || [] - secretArray = secrets + if (!configuration) { + return [] + } - const filteredSecrets = secrets + const result = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedContent = atob(configuration.data[fileName]) + result.push({ + name: fileName, + content: decodedContent, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + result.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) - filteredSecrets.map((item) => { - const name = (item.metadata && item.metadata.name) || '' - item.text = name - item.value = name - return true + // Set the value to the model + commit('wizard/model$update', { + path: `/temp/${type}applyConfig`, + value: result, + force: true, }) - return filteredSecrets + + return result + } + + function getSelectedConfigurationName(configType, type) { + type = type ? type + '/' : '' + let path = '' + if (configType === 'create') path = `/spec/configuration/${type}/configSecret/name` + else if (configType === 'apply') path = `/${type}selectedConfiguration` + else if (configType === 'remove') path = `/${type}selectedConfigurationRemove` + + const selectedConfiguration = + configType === 'create' ? getValue(model, path) : getValue(discriminator, path) + + if (selectedConfiguration) + return { subtitle: ` You have selected ${selectedConfiguration} secret` } + else return { subtitle: 'No secret selected' } + } + + function getSelectedConfigurationValueForRemove(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfigurationRemove` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return '' + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, + ) + + if (!configuration) { + return '' + } + + let data = {} + // Decode base64 and parse YAML for each key in the secret data + Object.keys(configuration.data).forEach((item) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[item]) + // Parse YAML string to object + const parsedYaml = yaml.load(decodedString) + // Store the parsed object with the filename as key + data[item] = parsedYaml + } catch (e) { + console.error(`Error parsing ${item}:`, e) + data[item] = atob(configuration.data[item]) // Fallback to decoded string + } + }) + + // Convert data object back to YAML string + return yaml.dump(data) + } + + async function createNewConfigSecret(type) { + type = type ? type + '/' : '' + const { user, cluster } = route.params + const url = `/clusters/${user}/${cluster}/resources` + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + const secretName = getValue(discriminator, `${type}createSecret/name`) + const secretData = getValue(discriminator, `${type}createSecret/data`) + const secretDataObj = Object.fromEntries(secretData.map((item) => [item.key, item.value])) + + try { + const res = await axios.post(url, { + apiVersion: 'v1', + stringData: secretDataObj, + kind: 'Secret', + metadata: { + name: secretName, + namespace: namespace, + }, + type: 'Opaque', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'success', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/lastCreatedSecret`, + value: secretName, + }) + toast.success('Secret created successfully') + } catch (error) { + const errMsg = decodeError(error, 'Failed to create secret') + toast.error(errMsg, { timeout: 5000 }) + cancelCreateSecret() + } + } + + function decodeError(msg, defaultMsg) { + if (typeof msg === 'string') { + return msg || defaultMsg + } + return ( + (msg.response && msg.response.data && msg.response.data.message) || + (msg.response && msg.response.data) || + (msg.status && msg.status.status) || + defaultMsg + ) + } + + function isCreateSecret(type) { + type = type ? type + '/' : '' + const selectedSecret = getValue(model, `spec/configuration/${type}configSecret/name`) + const res = selectedSecret === 'Create' + + if (res === true) { + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'pending', + }) + } + return res + } + + function isNotCreateSecret(type) { + return !isCreateSecret(type) } + function onCreateSecretChange(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'cancelled') return '' + else if (secretStatus === 'success') { + const name = getValue(discriminator, `${type}createSecret/lastCreatedSecret`) + + const configFound = configSecrets.find((item) => item === name) + return configFound ? { text: name, value: name } : '' + } + } + + function cancelCreateSecret(type) { + type = type ? type + '/' : '' + commit('wizard/temp$delete', `${type}createSecret/name`) + commit('wizard/temp$delete', `${type}createSecret/data`) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'cancelled', + }) + } + + async function onApplyconfigChange(type) { + type = type ? type + '/' : '' + const configValue = getValue(discriminator, `${type}applyConfig`) + + if (!configValue) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + const tempConfigObj = {} + configValue.forEach((item) => { + if (item.name) { + tempConfigObj[item.name] = item.content + } + }) + if (Object.keys(tempConfigObj).length === 0) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}applyConfig`, + value: tempConfigObj, + }) + } + + function setApplyConfig(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfiguration` + const selectedConfig = getValue(discriminator, configPath) + if (!selectedConfig) { + return [{ name: '', content: '' }] + } + const applyconfigData = secretConfigData.find((item) => { + if (item.componentName === selectedConfig) { + return item + } + }) + const { applyConfig } = applyconfigData + const configObj = [] + + if (applyConfig) { + Object.keys(applyConfig).forEach((fileName) => { + configObj.push({ + name: fileName, + content: applyConfig[fileName], + }) + }) + } else { + configObj.push({ name: '', content: '' }) + } + return configObj + } + + function onRemoveConfigChange(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfigurationRemove` + const selectedConfig = getValue(discriminator, configPath) + + if (!selectedConfig) { + commit('wizard/model$delete', `/spec/configuration/${type}removeCustomConfig`) + return [{ name: '', content: '' }] + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}removeCustomConfig`, + value: true, + }) + + const configuration = secretConfigData.find((item) => item.componentName === selectedConfig) + + if (!configuration.data) { + return [{ name: '', content: '' }] + } + + const configObj = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) + return configObj + } + + async function onNewConfigSecretChange(type) { + type = type ? type + '/' : '' + const path = `/spec/configuration/${type}configSecret/name` + const selectedSecret = getValue(model, path) + + if (!selectedSecret) { + commit('wizard/model$delete', `/spec/configuration/${type}configSecret`) + return [{ name: '', content: '' }] + } + if (selectedSecret === 'Create') return [{ name: '', content: '' }] + + const owner = storeGet('/route/params/user') + const cluster = storeGet('/route/params/cluster') + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + + try { + // Fetch the secret data from API + const secretResp = await axios.get( + `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets/${selectedSecret}`, + ) + + const secretData = secretResp.data?.data || {} + const configObj = [] + + // Decode base64 and format as array of objects with name and content + Object.keys(secretData).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(secretData[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: secretData[fileName], // Fallback to original if decode fails + }) + } + }) + + return configObj + } catch (e) { + console.error('Error fetching secret:', e) + return [{ name: '', content: '' }] + } + } + + function onSelectedSecretChange(index) { + const secretData = getValue(discriminator, 'createSecret/data') || [] + const selfSecrets = secretData.map((item) => item.key) + + const remainingSecrets = configSecretKeys.filter((item) => !selfSecrets.includes(item)) + + const selfKey = getValue(discriminator, `createSecret/data/${index}/key`) + if (selfKey) { + remainingSecrets.push(selfKey) + } + const resSecret = remainingSecrets.map((item) => { + return { text: item, value: item } + }) + return resSecret + } + + let secretArray = [] + function createSecretUrl() { const user = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') @@ -977,25 +1366,6 @@ export const useFunc = (model) => { return reconfigurationType === value } - function onApplyconfigChange() { - const applyconfig = getValue(discriminator, '/applyConfig') - - const configObj = {} - - if (applyconfig) { - applyconfig.forEach((item) => { - const { key, value } = item - configObj[key] = value - }) - } - - commit('wizard/model$update', { - path: `/spec/configuration/applyConfig`, - value: configObj, - force: true, - }) - } - function onReconfigurationTypeChange() { setDiscriminatorValue(`/applyConfig`, []) const reconfigurationType = getValue(discriminator, '/reconfigurationType') @@ -1407,5 +1777,20 @@ export const useFunc = (model) => { onMachineChange, isMachineCustom, checkVolume, + fetchConfigSecrets, + getConfigSecretsforAppyConfig, + getSelectedConfigurationData, + getSelectedConfigurationName, + getSelectedConfigurationValueForRemove, + createNewConfigSecret, + decodeError, + isCreateSecret, + isNotCreateSecret, + onCreateSecretChange, + cancelCreateSecret, + setApplyConfig, + onRemoveConfigChange, + onNewConfigSecretChange, + onSelectedSecretChange, } } diff --git a/charts/opskubedbcom-memcachedopsrequest-editor/ui/create-ui.yaml b/charts/opskubedbcom-memcachedopsrequest-editor/ui/create-ui.yaml index cce12ccad9..d0d82a02de 100644 --- a/charts/opskubedbcom-memcachedopsrequest-editor/ui/create-ui.yaml +++ b/charts/opskubedbcom-memcachedopsrequest-editor/ui/create-ui.yaml @@ -194,85 +194,176 @@ step: if: name: ifRequestTypeEqualsTo|Reconfigure type: function + loader: + name: fetchConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace elements: - type: block-layout label: Configuration elements: - - type: radio - label: Reconfigure Type + - type: label-element + label: '' + subtitle: Select a new configuration secret, apply a custom configuration, or remove an existing setup to update your database settings + - type: tab-layout + label: New Config Secret + schema: temp/properties/reconfigurationType options: - - text: Select New Config Secret + - text: NEW CONFIG SECRET value: selectNewConfigSecret - - text: Apply Config + - text: APPLY CONFIG value: applyConfig - - text: Remove + - text: REMOVE value: remove - watcher: - func: onReconfigurationTypeChange - paths: - - temp/properties/reconfigurationType - validation: - type: required - schema: temp/properties/reconfigurationType - - type: block-layout - label: Configuration config secret - showLabels: false - if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret - type: function elements: - - type: select - addNewButton: - label: Create a new Secret - target: _blank - url: - function: createSecretUrl + - type: block-layout label: Config Secret if: name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret type: function - loader: - name: getConfigSecrets - watchPaths: - - schema/properties/metadata/properties/namespace - init: - type: func - value: setValueFromDbDetails|/spec/configSecret/name - refresh: true - validation: - type: required - schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name - - type: array-object-form - label: Apply Config - if: - name: ifReconfigurationTypeEqualsTo|applyConfig - type: function - validation: - type: required - watcher: - func: onApplyconfigChange - paths: - - temp/properties/applyConfig - schema: temp/properties/applyConfig - elements: - - type: input - label: key - validation: - type: required - schema: key - - type: editor - hasCopy: false - label: value - validation: - type: required - schema: value - - type: switch - fullwidth: true - schema: schema/properties/spec/properties/configuration/properties/removeCustomConfig - label: Remove CustomConfig - if: - name: returnFalse - type: function + elements: + - type: label-element + label: Config Secret + subtitle: Select a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name + refresh: true + label: Config Secret + loader: + name: getConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace + - temp/properties/createSecret/properties/status + init: + type: func + value: setValueFromDbDetails|/spec/configSecret/name + watcher: + func: onCreateSecretChange + paths: + - temp/properties/createSecret/properties/status + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|create + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + - type: block-layout + label: Create a New Config Secret + showLabels: true + if: + type: function + name: isCreateSecret + hasButton: + text: Save + hasCancel: cancelCreateSecret + action: createNewConfigSecret + elements: + - type: input + label: Secret Name + schema: temp/properties/createSecret/properties/name + validation: + type: required + - type: array-object-form + label: String Data + schema: temp/properties/createSecret/properties/data + buttonClass: is-light is-outlined + validation: + type: required + elements: + - type: select + label: Key + schema: key + loader: + name: onSelectedSecretChange + watchPaths: + - temp/properties/createSecret/properties/data + validation: + type: required + - type: textarea + label: Value + schema: value + - type: multi-file-editor + editorHeight: 500px + readonly: true + if: + type: function + name: isNotCreateSecret + loader: + name: onNewConfigSecretChange + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + schema: temp/properties/newConfigSecret + - type: block-layout + label: ApplyConfig + if: + name: ifReconfigurationTypeEqualsTo|applyConfig + type: function + elements: + - type: label-element + label: New Apply Config + subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings.
Enter the parameter you want to configure (e.g., max_connections). + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfiguration + refresh: true + label: Configuration + loader: getConfigSecretsforAppyConfig + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|apply + watchPaths: + - temp/properties/selectedConfiguration + - type: multi-file-editor + editorHeight: 500px + schema: temp/properties/applyConfig + loader: + name: setApplyConfig + watchPaths: + - temp/properties/selectedConfiguration + watcher: + func: onApplyconfigChange + paths: + - temp/properties/applyConfig + - type: block-layout + label: Remove + if: + name: ifReconfigurationTypeEqualsTo|remove + type: function + elements: + - type: label-element + label: Remove + subtitle: Selected a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfigurationRemove + refresh: true + label: Configuration + loader: + name: getConfigSecretsforAppyConfig + watchPaths: + - schema/properties/metadata/properties/namespace + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|remove + watchPaths: + - temp/properties/selectedConfigurationRemove + - type: multi-file-editor + editorHeight: 500px + readonly: true + init: + type: func + value: onRemoveConfigChange + watcher: + func: onRemoveConfigChange + paths: + - temp/properties/selectedConfigurationRemove + schema: temp/properties/removeConfig # common - type: time-picker label: Timeout diff --git a/charts/opskubedbcom-memcachedopsrequest-editor/ui/functions.js b/charts/opskubedbcom-memcachedopsrequest-editor/ui/functions.js index 5742f601f0..227897e072 100644 --- a/charts/opskubedbcom-memcachedopsrequest-editor/ui/functions.js +++ b/charts/opskubedbcom-memcachedopsrequest-editor/ui/functions.js @@ -1,4 +1,4 @@ -const { axios, useOperator, store } = window.vueHelpers || {} +const { axios, useOperator, store, useToast } = window.vueHelpers || {} const machines = { 'db.t.micro': { resources: { @@ -306,9 +306,11 @@ const machineList = [ ] let machinesFromPreset = [] +const configSecretKeys = ['kubedb-user.cnf'] export const useFunc = (model) => { const route = store.state?.route + const toast = useToast() const { getValue, storeGet, discriminator, setDiscriminatorValue, commit } = useOperator( model, @@ -845,28 +847,415 @@ export const useFunc = (model) => { return machine === 'custom' } - // for config secret - async function getConfigSecrets() { + // Fetch and store database Infos + // for secret configurations in reconfigure + let configSecrets = [] + let secretConfigData = [] + + async function fetchConfigSecrets() { const owner = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') const namespace = getValue(model, '/metadata/namespace') // watchDependency('model#/metadata/namespace') - const resp = await axios.get( - `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets`, + const name = getValue(model, '/spec/databaseRef/name') + const dbGroup = getValue(model, '/route/params/group') + const dbKind = getValue(store.state, '/resource/definition/result/kind') + const dbResource = getValue(model, '/route/params/resource') + const dbVersion = getValue(model, '/route/params/version') + + try { + const resp = await axios.post( + `/clusters/${owner}/${cluster}/proxy/ui.kubedb.com/v1alpha1/databaseinfos`, + { + apiVersion: 'ui.kubedb.com/v1alpha1', + kind: 'DatabaseInfo', + request: { + source: { + ref: { + name: name, + namespace: namespace, + }, + resource: { + group: dbGroup, + kind: dbKind, + name: dbResource, + version: dbVersion, + }, + }, + keys: ['kubedb-user.cnf'], + }, + }, + ) + configSecrets = resp?.data?.response?.availableSecrets || [] + secretConfigData = resp?.data?.response?.configurations || [] + } catch (e) { + console.log(e) + } + } + + async function getConfigSecrets(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'success') { + await fetchConfigSecrets() + } + const mappedSecrets = configSecrets.map((item) => { + return { text: item, value: item } + }) + mappedSecrets.push({ text: '+ Create a new Secret', value: 'Create' }) + return mappedSecrets + } + + async function getConfigSecretsforAppyConfig() { + const secrets = secretConfigData.map((item) => { + return { text: item.componentName, value: item.componentName } + }) + return secrets + } + + function getSelectedConfigurationData(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfiguration` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return [] + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, ) - const secrets = (resp && resp.data && resp.data.items) || [] + if (!configuration) { + return [] + } - const filteredSecrets = secrets + const result = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedContent = atob(configuration.data[fileName]) + result.push({ + name: fileName, + content: decodedContent, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + result.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) - filteredSecrets.map((item) => { - const name = (item.metadata && item.metadata.name) || '' - item.text = name - item.value = name - return true + // Set the value to the model + commit('wizard/model$update', { + path: `/temp/${type}applyConfig`, + value: result, + force: true, + }) + + return result + } + + function getSelectedConfigurationName(configType, type) { + type = type ? type + '/' : '' + let path = '' + if (configType === 'create') path = `/spec/configuration/${type}/configSecret/name` + else if (configType === 'apply') path = `/${type}selectedConfiguration` + else if (configType === 'remove') path = `/${type}selectedConfigurationRemove` + + const selectedConfiguration = + configType === 'create' ? getValue(model, path) : getValue(discriminator, path) + + if (selectedConfiguration) + return { subtitle: ` You have selected ${selectedConfiguration} secret` } + else return { subtitle: 'No secret selected' } + } + + function getSelectedConfigurationValueForRemove(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfigurationRemove` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return '' + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, + ) + + if (!configuration) { + return '' + } + + let data = {} + // Decode base64 and parse YAML for each key in the secret data + Object.keys(configuration.data).forEach((item) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[item]) + // Parse YAML string to object + const parsedYaml = yaml.load(decodedString) + // Store the parsed object with the filename as key + data[item] = parsedYaml + } catch (e) { + console.error(`Error parsing ${item}:`, e) + data[item] = atob(configuration.data[item]) // Fallback to decoded string + } + }) + + // Convert data object back to YAML string + return yaml.dump(data) + } + + async function createNewConfigSecret(type) { + type = type ? type + '/' : '' + const { user, cluster } = route.params + const url = `/clusters/${user}/${cluster}/resources` + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + const secretName = getValue(discriminator, `${type}createSecret/name`) + const secretData = getValue(discriminator, `${type}createSecret/data`) + const secretDataObj = Object.fromEntries(secretData.map((item) => [item.key, item.value])) + + try { + const res = await axios.post(url, { + apiVersion: 'v1', + stringData: secretDataObj, + kind: 'Secret', + metadata: { + name: secretName, + namespace: namespace, + }, + type: 'Opaque', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'success', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/lastCreatedSecret`, + value: secretName, + }) + toast.success('Secret created successfully') + } catch (error) { + const errMsg = decodeError(error, 'Failed to create secret') + toast.error(errMsg, { timeout: 5000 }) + cancelCreateSecret() + } + } + + function decodeError(msg, defaultMsg) { + if (typeof msg === 'string') { + return msg || defaultMsg + } + return ( + (msg.response && msg.response.data && msg.response.data.message) || + (msg.response && msg.response.data) || + (msg.status && msg.status.status) || + defaultMsg + ) + } + + function isCreateSecret(type) { + type = type ? type + '/' : '' + const selectedSecret = getValue(model, `spec/configuration/${type}configSecret/name`) + const res = selectedSecret === 'Create' + + if (res === true) { + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'pending', + }) + } + return res + } + + function isNotCreateSecret(type) { + return !isCreateSecret(type) + } + + function onCreateSecretChange(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'cancelled') return '' + else if (secretStatus === 'success') { + const name = getValue(discriminator, `${type}createSecret/lastCreatedSecret`) + + const configFound = configSecrets.find((item) => item === name) + return configFound ? { text: name, value: name } : '' + } + } + + function cancelCreateSecret(type) { + type = type ? type + '/' : '' + commit('wizard/temp$delete', `${type}createSecret/name`) + commit('wizard/temp$delete', `${type}createSecret/data`) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'cancelled', }) - return filteredSecrets + } + + async function onApplyconfigChange(type) { + type = type ? type + '/' : '' + const configValue = getValue(discriminator, `${type}applyConfig`) + + if (!configValue) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + const tempConfigObj = {} + configValue.forEach((item) => { + if (item.name) { + tempConfigObj[item.name] = item.content + } + }) + if (Object.keys(tempConfigObj).length === 0) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}applyConfig`, + value: tempConfigObj, + }) + } + + function setApplyConfig(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfiguration` + const selectedConfig = getValue(discriminator, configPath) + if (!selectedConfig) { + return [{ name: '', content: '' }] + } + const applyconfigData = secretConfigData.find((item) => { + if (item.componentName === selectedConfig) { + return item + } + }) + const { applyConfig } = applyconfigData + const configObj = [] + + if (applyConfig) { + Object.keys(applyConfig).forEach((fileName) => { + configObj.push({ + name: fileName, + content: applyConfig[fileName], + }) + }) + } else { + configObj.push({ name: '', content: '' }) + } + return configObj + } + + function onRemoveConfigChange(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfigurationRemove` + const selectedConfig = getValue(discriminator, configPath) + + if (!selectedConfig) { + commit('wizard/model$delete', `/spec/configuration/${type}removeCustomConfig`) + return [{ name: '', content: '' }] + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}removeCustomConfig`, + value: true, + }) + + const configuration = secretConfigData.find((item) => item.componentName === selectedConfig) + + if (!configuration.data) { + return [{ name: '', content: '' }] + } + + const configObj = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) + return configObj + } + + async function onNewConfigSecretChange(type) { + type = type ? type + '/' : '' + const path = `/spec/configuration/${type}configSecret/name` + const selectedSecret = getValue(model, path) + + if (!selectedSecret) { + commit('wizard/model$delete', `/spec/configuration/${type}configSecret`) + return [{ name: '', content: '' }] + } + if (selectedSecret === 'Create') return [{ name: '', content: '' }] + + const owner = storeGet('/route/params/user') + const cluster = storeGet('/route/params/cluster') + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + + try { + // Fetch the secret data from API + const secretResp = await axios.get( + `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets/${selectedSecret}`, + ) + + const secretData = secretResp.data?.data || {} + const configObj = [] + + // Decode base64 and format as array of objects with name and content + Object.keys(secretData).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(secretData[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: secretData[fileName], // Fallback to original if decode fails + }) + } + }) + + return configObj + } catch (e) { + console.error('Error fetching secret:', e) + return [{ name: '', content: '' }] + } + } + + function onSelectedSecretChange(index) { + const secretData = getValue(discriminator, 'createSecret/data') || [] + const selfSecrets = secretData.map((item) => item.key) + + const remainingSecrets = configSecretKeys.filter((item) => !selfSecrets.includes(item)) + + const selfKey = getValue(discriminator, `createSecret/data/${index}/key`) + if (selfKey) { + remainingSecrets.push(selfKey) + } + const resSecret = remainingSecrets.map((item) => { + return { text: item, value: item } + }) + return resSecret } function createSecretUrl() { @@ -990,25 +1379,6 @@ export const useFunc = (model) => { return reconfigurationType === value } - function onApplyconfigChange() { - const applyconfig = getValue(discriminator, '/applyConfig') - - const configObj = {} - - if (applyconfig) { - applyconfig.forEach((item) => { - const { key, value } = item - configObj[key] = value - }) - } - - commit('wizard/model$update', { - path: `/spec/configuration/applyConfig`, - value: configObj, - force: true, - }) - } - function onReconfigurationTypeChange() { setDiscriminatorValue('/applyConfig', []) const reconfigurationType = getValue(discriminator, '/reconfigurationType') @@ -1177,5 +1547,20 @@ export const useFunc = (model) => { setMachine, onMachineChange, isMachineCustom, + fetchConfigSecrets, + getConfigSecretsforAppyConfig, + getSelectedConfigurationData, + getSelectedConfigurationName, + getSelectedConfigurationValueForRemove, + createNewConfigSecret, + decodeError, + isCreateSecret, + isNotCreateSecret, + onCreateSecretChange, + cancelCreateSecret, + setApplyConfig, + onRemoveConfigChange, + onNewConfigSecretChange, + onSelectedSecretChange, } } diff --git a/charts/opskubedbcom-mongodbopsrequest-editor/ui/create-ui.yaml b/charts/opskubedbcom-mongodbopsrequest-editor/ui/create-ui.yaml index 6b62097a6f..217d9d5599 100644 --- a/charts/opskubedbcom-mongodbopsrequest-editor/ui/create-ui.yaml +++ b/charts/opskubedbcom-mongodbopsrequest-editor/ui/create-ui.yaml @@ -651,15 +651,20 @@ step: if: name: ifRequestTypeEqualsTo|Reconfigure type: function + loader: + name: fetchConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace elements: + - type: label-element + label: '' + subtitle: Select a new configuration secret, apply a custom configuration, or remove an existing setup to update your database settings - type: block-layout label: Standalone if: name: ifDbTypeEqualsTo|standalone|configuration type: function elements: - - type: label-element - label: '' - type: tab-layout label: New Config Secret schema: temp/properties/reconfigurationType @@ -687,7 +692,7 @@ step: refresh: true label: Config Secret loader: - name: getConfigSecrets + name: getConfigSecrets|standalone watchPaths: - schema/properties/metadata/properties/namespace - temp/properties/standalone/properties/createSecret/properties/status @@ -701,7 +706,7 @@ step: - type: label-element label: '' loader: - name: getSelectedConfigSecret|standalone + name: getSelectedConfigurationName|create|standalone watchPaths: - schema/properties/spec/properties/configuration/properties/standalone/properties/configSecret/properties/name - type: block-layout @@ -724,6 +729,8 @@ step: label: String Data schema: temp/properties/standalone/properties/createSecret/properties/data buttonClass: is-light is-outlined + validation: + type: required elements: - type: select label: Key @@ -767,7 +774,7 @@ step: - type: label-element label: '' loader: - name: getSelectedApplyConfigName|standalone + name: getSelectedConfigurationName|apply|standalone watchPaths: - temp/properties/standalone/selectedConfiguration - type: multi-file-editor @@ -803,7 +810,7 @@ step: - type: label-element label: '' loader: - name: getSelectedConfigurationName|standalone + name: getSelectedConfigurationName|remove|standalone watchPaths: - temp/properties/standalone/selectedConfigurationRemove - type: multi-file-editor @@ -823,8 +830,6 @@ step: name: ifDbTypeEqualsTo|replicaSet|configuration type: function elements: - - type: label-element - label: '' - type: tab-layout label: New Config Secret schema: temp/properties/reconfigurationType @@ -852,7 +857,7 @@ step: refresh: true label: Config Secret loader: - name: getConfigSecrets + name: getConfigSecrets|replicaSet watchPaths: - schema/properties/metadata/properties/namespace - temp/properties/replicaSet/properties/createSecret/properties/status @@ -866,7 +871,7 @@ step: - type: label-element label: '' loader: - name: getSelectedConfigSecret|replicaSet + name: getSelectedConfigurationName|create|replicaSet watchPaths: - schema/properties/spec/properties/configuration/properties/replicaSet/properties/configSecret/properties/name - type: block-layout @@ -889,6 +894,8 @@ step: label: String Data schema: temp/properties/replicaSet/properties/createSecret/properties/data buttonClass: is-light is-outlined + validation: + type: required elements: - type: select label: Key @@ -932,7 +939,7 @@ step: - type: label-element label: '' loader: - name: getSelectedApplyConfigName|replicaSet + name: getSelectedConfigurationName|apply|replicaSet watchPaths: - temp/properties/replicaSet/selectedConfiguration - type: multi-file-editor @@ -968,7 +975,7 @@ step: - type: label-element label: '' loader: - name: getSelectedConfigurationName|replicaSet + name: getSelectedConfigurationName|remove|replicaSet watchPaths: - temp/properties/replicaSet/selectedConfigurationRemove - type: multi-file-editor @@ -989,8 +996,6 @@ step: showLabels: true customClass: mt-10 elements: - - type: label-element - label: '' - type: tab-layout label: New Config Secret schema: temp/properties/reconfigurationType-configServer @@ -1018,7 +1023,7 @@ step: refresh: true label: Config Secret loader: - name: getConfigSecrets + name: getConfigSecrets|configServer watchPaths: - schema/properties/metadata/properties/namespace - temp/properties/configServer/properties/createSecret/properties/status @@ -1032,7 +1037,7 @@ step: - type: label-element label: '' loader: - name: getSelectedConfigSecret|configServer + name: getSelectedConfigurationName|create|configServer watchPaths: - schema/properties/spec/properties/configuration/properties/configServer/properties/configSecret/properties/name - type: block-layout @@ -1055,6 +1060,8 @@ step: label: String Data schema: temp/properties/configServer/properties/createSecret/properties/data buttonClass: is-light is-outlined + validation: + type: required elements: - type: select label: Key @@ -1098,7 +1105,7 @@ step: - type: label-element label: '' loader: - name: getSelectedApplyConfigName|configServer + name: getSelectedConfigurationName|apply|configServer watchPaths: - temp/properties/configServer/selectedConfiguration - type: multi-file-editor @@ -1134,7 +1141,7 @@ step: - type: label-element label: '' loader: - name: getSelectedConfigurationName|configServer + name: getSelectedConfigurationName|remove|configServer watchPaths: - temp/properties/configServer/selectedConfigurationRemove - type: multi-file-editor @@ -1152,8 +1159,6 @@ step: label: Mongos showLabels: true elements: - - type: label-element - label: '' - type: tab-layout label: New Config Secret schema: temp/properties/reconfigurationType-mongos @@ -1181,7 +1186,7 @@ step: refresh: true label: Config Secret loader: - name: getConfigSecrets + name: getConfigSecrets|mongos watchPaths: - schema/properties/metadata/properties/namespace - temp/properties/mongos/properties/createSecret/properties/status @@ -1195,7 +1200,7 @@ step: - type: label-element label: '' loader: - name: getSelectedConfigSecret|mongos + name: getSelectedConfigurationName|create|mongos watchPaths: - schema/properties/spec/properties/configuration/properties/mongos/properties/configSecret/properties/name - type: block-layout @@ -1218,6 +1223,8 @@ step: label: String Data schema: temp/properties/mongos/properties/createSecret/properties/data buttonClass: is-light is-outlined + validation: + type: required elements: - type: select label: Key @@ -1261,7 +1268,7 @@ step: - type: label-element label: '' loader: - name: getSelectedApplyConfigName|mongos + name: getSelectedConfigurationName|apply|mongos watchPaths: - temp/properties/mongos/selectedConfiguration - type: multi-file-editor @@ -1297,7 +1304,7 @@ step: - type: label-element label: '' loader: - name: getSelectedConfigurationName|mongos + name: getSelectedConfigurationName|remove|mongos watchPaths: - temp/properties/mongos/selectedConfigurationRemove - type: multi-file-editor @@ -1315,8 +1322,6 @@ step: label: Shard showLabels: true elements: - - type: label-element - label: '' - type: tab-layout label: New Config Secret schema: temp/properties/reconfigurationType-shard @@ -1344,7 +1349,7 @@ step: refresh: true label: Config Secret loader: - name: getConfigSecrets + name: getConfigSecrets|shard watchPaths: - schema/properties/metadata/properties/namespace - temp/properties/shard/properties/createSecret/properties/status @@ -1358,7 +1363,7 @@ step: - type: label-element label: '' loader: - name: getSelectedConfigSecret|shard + name: getSelectedConfigurationName|create|shard watchPaths: - schema/properties/spec/properties/configuration/properties/shard/properties/configSecret/properties/name - type: block-layout @@ -1381,6 +1386,8 @@ step: label: String Data schema: temp/properties/shard/properties/createSecret/properties/data buttonClass: is-light is-outlined + validation: + type: required elements: - type: select label: Key @@ -1424,7 +1431,7 @@ step: - type: label-element label: '' loader: - name: getSelectedApplyConfigName|shard + name: getSelectedConfigurationName|apply|shard watchPaths: - temp/properties/shard/selectedConfiguration - type: multi-file-editor @@ -1460,7 +1467,7 @@ step: - type: label-element label: '' loader: - name: getSelectedConfigurationName|shard + name: getSelectedConfigurationName|remove|shard watchPaths: - temp/properties/shard/selectedConfigurationRemove - type: multi-file-editor diff --git a/charts/opskubedbcom-mongodbopsrequest-editor/ui/functions.js b/charts/opskubedbcom-mongodbopsrequest-editor/ui/functions.js index 6698bc4abf..9a507605c2 100644 --- a/charts/opskubedbcom-mongodbopsrequest-editor/ui/functions.js +++ b/charts/opskubedbcom-mongodbopsrequest-editor/ui/functions.js @@ -315,7 +315,7 @@ const machineList = [ 'db.r.24xlarge', ] -const configSecretKeys = ['mongod.conf'] +const configSecretKeys = ['mongod.conf', 'replicaset.json', 'configuration.js'] let machinesFromPreset = [] @@ -900,9 +900,12 @@ export const useFunc = (model) => { return machine === 'custom' } - // for config secret - let newConfigSecrets = [] - async function getConfigSecrets() { + // Fetch and store database Infos + // for secret configurations in reconfigure + let configSecrets = [] + let secretConfigData = [] + + async function fetchConfigSecrets() { const owner = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') const namespace = getValue(model, '/metadata/namespace') @@ -913,7 +916,6 @@ export const useFunc = (model) => { const dbKind = getValue(store.state, '/resource/definition/result/kind') const dbResource = getValue(model, '/route/params/resource') const dbVersion = getValue(model, '/route/params/version') - let secrets = [] try { const resp = await axios.post( @@ -934,79 +936,47 @@ export const useFunc = (model) => { version: dbVersion, }, }, - keys: ['mongod.conf'], + keys: ['mongod.conf', 'replicaset.json', 'configuration.js'], }, }, ) - secrets = resp?.data?.response?.availableSecrets || [] - newConfigSecrets = secrets + configSecrets = resp?.data?.response?.availableSecrets || [] + secretConfigData = resp?.data?.response?.configurations || [] } catch (e) { console.log(e) } - const mappedSecrets = secrets.map((item) => { + } + + async function getConfigSecrets(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'success') { + await fetchConfigSecrets() + } + const mappedSecrets = configSecrets.map((item) => { return { text: item, value: item } }) mappedSecrets.push({ text: '+ Create a new Secret', value: 'Create' }) return mappedSecrets } - let ConfigurationsData = [] - async function getConfigSecretsforAppyConfig() { - const owner = storeGet('/route/params/user') - const cluster = storeGet('/route/params/cluster') - const namespace = getValue(model, '/metadata/namespace') - // watchDependency('model#/metadata/namespace') - - const name = getValue(model, '/spec/databaseRef/name') - const dbGroup = getValue(model, '/route/params/group') - const dbKind = getValue(store.state, '/resource/definition/result/kind') - const dbResource = getValue(model, '/route/params/resource') - const dbVersion = getValue(model, '/route/params/version') - - try { - const resp = await axios.post( - `/clusters/${owner}/${cluster}/proxy/ui.kubedb.com/v1alpha1/databaseinfos`, - { - apiVersion: 'ui.kubedb.com/v1alpha1', - kind: 'DatabaseInfo', - request: { - source: { - ref: { - name: name, - namespace: namespace, - }, - resource: { - group: dbGroup, - kind: dbKind, - name: dbResource, - version: dbVersion, - }, - }, - keys: ['mongod.conf'], - }, - }, - ) - ConfigurationsData = resp?.data?.response?.configurations || [] - const secrets = ConfigurationsData.map((item) => { - return { text: item.componentName, value: item.componentName } - }) - return secrets - } catch (e) { - console.log(e) - } - return [] + const secrets = secretConfigData.map((item) => { + return { text: item.componentName, value: item.componentName } + }) + return secrets } function getSelectedConfigurationData(type) { - const path = `/${type}/selectedConfiguration` + type = type ? type + '/' : '' + const path = `/${type}selectedConfiguration` const selectedConfiguration = getValue(discriminator, path) if (!selectedConfiguration) { return [] } - const configuration = ConfigurationsData.find( + const configuration = secretConfigData.find( (item) => item.componentName === selectedConfiguration, ) @@ -1035,7 +1005,7 @@ export const useFunc = (model) => { // Set the value to the model commit('wizard/model$update', { - path: `/temp/${type}/applyConfig`, + path: `/temp/${type}applyConfig`, value: result, force: true, }) @@ -1043,56 +1013,31 @@ export const useFunc = (model) => { return result } - function getSelectedConfigurationName(type) { - const path = `/${type}/selectedConfigurationRemove` - const selectedConfiguration = getValue(discriminator, path) - - if (!selectedConfiguration) { - return { subtitle: 'No secret selected' } - } + function getSelectedConfigurationName(configType, type) { + type = type ? type + '/' : '' + let path = '' + if (configType === 'create') path = `/spec/configuration/${type}/configSecret/name` + else if (configType === 'apply') path = `/${type}selectedConfiguration` + else if (configType === 'remove') path = `/${type}selectedConfigurationRemove` - const configuration = ConfigurationsData.find( - (item) => item.componentName === selectedConfiguration, - ) + const selectedConfiguration = + configType === 'create' ? getValue(model, path) : getValue(discriminator, path) - if (!configuration) { - return { subtitle: 'No secret selected' } - } - - if (configuration.componentName) - return { subtitle: ` You have selected ${configuration.componentName} secret` } + if (selectedConfiguration) + return { subtitle: ` You have selected ${selectedConfiguration} secret` } else return { subtitle: 'No secret selected' } } - function getSelectedApplyConfigName(type) { - const path = `/${type}/selectedConfiguration` - const selectedConfiguration = getValue(discriminator, path) - - if (!selectedConfiguration) { - return { subtitle: 'No configuration selected' } - } - - const configuration = ConfigurationsData.find( - (item) => item.componentName === selectedConfiguration, - ) - - if (!configuration) { - return { subtitle: 'No configuration selected' } - } - if (configuration.componentName) - return { subtitle: ` You have selected ${configuration.componentName} configuration` } - else return { subtitle: 'No configuration selected' } - } - function getSelectedConfigurationValueForRemove(type) { - const path = `/${type}/selectedConfigurationRemove` + type = type ? type + '/' : '' + const path = `/${type}selectedConfigurationRemove` const selectedConfiguration = getValue(discriminator, path) if (!selectedConfiguration) { return '' } - const configuration = ConfigurationsData.find( + const configuration = secretConfigData.find( (item) => item.componentName === selectedConfiguration, ) @@ -1134,11 +1079,12 @@ export const useFunc = (model) => { } async function createNewConfigSecret(type) { + type = type ? type + '/' : '' const { user, cluster } = route.params const url = `/clusters/${user}/${cluster}/resources` const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') - const secretName = getValue(discriminator, `${type}/createSecret/name`) - const secretData = getValue(discriminator, `${type}/createSecret/data`) + const secretName = getValue(discriminator, `${type}createSecret/name`) + const secretData = getValue(discriminator, `${type}createSecret/data`) const secretDataObj = Object.fromEntries(secretData.map((item) => [item.key, item.value])) try { @@ -1153,11 +1099,11 @@ export const useFunc = (model) => { type: 'Opaque', }) commit('wizard/temp$update', { - path: `${type}/createSecret/status`, + path: `${type}createSecret/status`, value: 'success', }) commit('wizard/temp$update', { - path: `${type}/createSecret/lastCreatedSecret`, + path: `${type}createSecret/lastCreatedSecret`, value: secretName, }) toast.success('Secret created successfully') @@ -1181,12 +1127,13 @@ export const useFunc = (model) => { } function isCreateSecret(type) { - const selectedSecret = getValue(model, `spec/configuration/${type}/configSecret/name`) + type = type ? type + '/' : '' + const selectedSecret = getValue(model, `spec/configuration/${type}configSecret/name`) const res = selectedSecret === 'Create' if (res === true) { commit('wizard/temp$update', { - path: `${type}/createSecret/status`, + path: `${type}createSecret/status`, value: 'pending', }) } @@ -1198,140 +1145,33 @@ export const useFunc = (model) => { } function onCreateSecretChange(type) { - const secretStatus = getValue(discriminator, `${type}/createSecret/status`) + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) if (secretStatus === 'cancelled') return '' else if (secretStatus === 'success') { - const name = getValue(discriminator, `${type}/createSecret/lastCreatedSecret`) + const name = getValue(discriminator, `${type}createSecret/lastCreatedSecret`) - const configFound = newConfigSecrets.find((item) => item === name) + const configFound = configSecrets.find((item) => item === name) return configFound ? { text: name, value: name } : '' } } function cancelCreateSecret(type) { - commit('wizard/temp$delete', `${type}/createSecret/name`) - commit('wizard/temp$delete', `${type}/createSecret/data`) + type = type ? type + '/' : '' + commit('wizard/temp$delete', `${type}createSecret/name`) + commit('wizard/temp$delete', `${type}createSecret/data`) commit('wizard/temp$update', { - path: `${type}/createSecret/status`, + path: `${type}createSecret/status`, value: 'cancelled', }) } - function isEqualToValueFromType(value) { - // watchDependency('discriminator#/valueFromType') - const valueFrom = getValue(discriminator, '/valueFromType') - return valueFrom === value - } - - async function getNamespacedResourceList({ namespace, group, version, resource }) { - const owner = storeGet('/route/params/user') - const cluster = storeGet('/route/params/cluster') - - const url = `/clusters/${owner}/${cluster}/proxy/${group}/${version}/namespaces/${namespace}/${resource}` - - let ans = [] - try { - const resp = await axios.get(url, { - params: { - filter: { items: { metadata: { name: null }, type: null } }, - }, - }) - - const items = (resp && resp.data && resp.data.items) || [] - ans = items - } catch (e) { - console.log(e) - } - - return ans - } - async function getResourceList({ group, version, resource }) { - const owner = storeGet('/route/params/user') - const cluster = storeGet('/route/params/cluster') - - const url = `/clusters/${owner}/${cluster}/proxy/${group}/${version}/${resource}` - - let ans = [] - try { - const resp = await axios.get(url, { - params: { - filter: { items: { metadata: { name: null }, type: null } }, - }, - }) - - const items = (resp && resp.data && resp.data.items) || [] - ans = items - } catch (e) { - console.log(e) - } - - return ans - } - async function resourceNames(group, version, resource) { - const namespace = getValue(model, '/metadata/namespace') - // watchDependency('model#/metadata/namespace') - - let resources = await getNamespacedResourceList({ - namespace, - group, - version, - resource, - }) - - if (resource === 'secrets') { - resources = resources.filter((item) => { - const validType = ['kubernetes.io/service-account-token', 'Opaque'] - return validType.includes(item.type) - }) - } - - return resources.map((resource) => { - const name = (resource.metadata && resource.metadata.name) || '' - return { - text: name, - value: name, - } - }) - } - async function unNamespacedResourceNames(group, version, resource) { - let resources = await getResourceList({ - group, - version, - resource, - }) - - if (resource === 'secrets') { - resources = resources.filter((item) => { - const validType = ['kubernetes.io/service-account-token', 'Opaque'] - return validType.includes(item.type) - }) - } - - return resources.map((resource) => { - const name = (resource.metadata && resource.metadata.name) || '' - return { - text: name, - value: name, - } - }) - } - - // reconfiguration type - function ifReconfigurationTypeEqualsTo(value, property, isShard) { - let path = '/reconfigurationType' - if (isShard) path += `-${property}` - const reconfigurationType = getValue(discriminator, path) - - const watchPath = `discriminator#${path}` - // watchDependency(watchPath) - return reconfigurationType === value - } - async function onApplyconfigChange(type) { - const configValue = getValue(discriminator, `/${type}/applyConfig`) + type = type ? type + '/' : '' + const configValue = getValue(discriminator, `${type}applyConfig`) if (!configValue) { - commit('wizard/model$delete', `/spec/configuration/${type}/applyConfig`) + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) return } const tempConfigObj = {} @@ -1341,70 +1181,58 @@ export const useFunc = (model) => { } }) if (Object.keys(tempConfigObj).length === 0) { - commit('wizard/model$delete', `/spec/configuration/${type}/applyConfig`) + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) return } commit('wizard/model$update', { - path: `/spec/configuration/${type}/applyConfig`, + path: `/spec/configuration/${type}applyConfig`, value: tempConfigObj, }) } function setApplyConfig(type) { - const configPath = `/${type}/selectedConfiguration` + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfiguration` const selectedConfig = getValue(discriminator, configPath) if (!selectedConfig) { return [{ name: '', content: '' }] } - const applyconfig = ConfigurationsData.find((item) => { + const applyconfigData = secretConfigData.find((item) => { if (item.componentName === selectedConfig) { return item } }) - - const { secretName, data } = applyconfig + const { applyConfig } = applyconfigData const configObj = [] - const tempConfigObj = {} - if (data) { - // Decode base64 and format as array of objects with name and content - Object.keys(data).forEach((fileName) => { - try { - // Decode base64 string - const decodedString = atob(data[fileName]) - configObj.push({ - name: fileName, - content: decodedString, - }) - tempConfigObj[fileName] = decodedString - } catch (e) { - console.error(`Error decoding ${fileName}:`, e) - configObj.push({ - name: fileName, - content: data[fileName], // Fallback to original if decode fails - }) - } + if (applyConfig) { + Object.keys(applyConfig).forEach((fileName) => { + configObj.push({ + name: fileName, + content: applyConfig[fileName], + }) }) } else { - configObj.push({ name: selectedConfig, content: '' }) + configObj.push({ name: '', content: '' }) } return configObj } function onRemoveConfigChange(type) { - const configPath = `/${type}/selectedConfigurationRemove` + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfigurationRemove` const selectedConfig = getValue(discriminator, configPath) if (!selectedConfig) { - commit('wizard/model$delete', `/spec/configuration/${type}/removeCustomConfig`) + commit('wizard/model$delete', `/spec/configuration/${type}removeCustomConfig`) return [{ name: '', content: '' }] } commit('wizard/model$update', { - path: `/spec/configuration/${type}/removeCustomConfig`, + path: `/spec/configuration/${type}removeCustomConfig`, value: true, }) - const configuration = ConfigurationsData.find((item) => item.componentName === selectedConfig) + const configuration = secretConfigData.find((item) => item.componentName === selectedConfig) if (!configuration.data) { return [{ name: '', content: '' }] @@ -1432,13 +1260,15 @@ export const useFunc = (model) => { } async function onNewConfigSecretChange(type) { - const path = `/spec/configuration/${type}/configSecret/name` + type = type ? type + '/' : '' + const path = `/spec/configuration/${type}configSecret/name` const selectedSecret = getValue(model, path) if (!selectedSecret) { - commit('wizard/model$delete', `/spec/configuration/${type}/configSecret`) + commit('wizard/model$delete', `/spec/configuration/${type}configSecret`) return [{ name: '', content: '' }] } + if (selectedSecret === 'Create') return [{ name: '', content: '' }] const owner = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') @@ -1479,12 +1309,13 @@ export const useFunc = (model) => { } function onSelectedSecretChange(type, index) { - const secretData = getValue(discriminator, `${type}/createSecret/data`) || [] + type = type ? type + '/' : '' + const secretData = getValue(discriminator, `${type}createSecret/data`) || [] const selfSecrets = secretData.map((item) => item.key) const remainingSecrets = configSecretKeys.filter((item) => !selfSecrets.includes(item)) - const selfKey = getValue(discriminator, `${type}/createSecret/data/${index}/key`) + const selfKey = getValue(discriminator, `${type}createSecret/data/${index}/key`) if (selfKey) { remainingSecrets.push(selfKey) } @@ -1494,6 +1325,16 @@ export const useFunc = (model) => { return resSecret } + function ifReconfigurationTypeEqualsTo(value, property, isShard) { + let path = '/reconfigurationType' + if (isShard) path += `-${property}` + const reconfigurationType = getValue(discriminator, path) + + const watchPath = `discriminator#${path}` + // watchDependency(watchPath) + return reconfigurationType === value + } + function onReconfigurationTypeChange(property, isShard) { setDiscriminatorValue(`/${property}/applyConfig`, []) let path = '/reconfigurationType' @@ -1514,6 +1355,105 @@ export const useFunc = (model) => { } } + function isEqualToValueFromType(value) { + // watchDependency('discriminator#/valueFromType') + const valueFrom = getValue(discriminator, '/valueFromType') + return valueFrom === value + } + + async function getNamespacedResourceList({ namespace, group, version, resource }) { + const owner = storeGet('/route/params/user') + const cluster = storeGet('/route/params/cluster') + + const url = `/clusters/${owner}/${cluster}/proxy/${group}/${version}/namespaces/${namespace}/${resource}` + + let ans = [] + try { + const resp = await axios.get(url, { + params: { + filter: { items: { metadata: { name: null }, type: null } }, + }, + }) + + const items = (resp && resp.data && resp.data.items) || [] + ans = items + } catch (e) { + console.log(e) + } + + return ans + } + async function getResourceList({ group, version, resource }) { + const owner = storeGet('/route/params/user') + const cluster = storeGet('/route/params/cluster') + + const url = `/clusters/${owner}/${cluster}/proxy/${group}/${version}/${resource}` + + let ans = [] + try { + const resp = await axios.get(url, { + params: { + filter: { items: { metadata: { name: null }, type: null } }, + }, + }) + + const items = (resp && resp.data && resp.data.items) || [] + ans = items + } catch (e) { + console.log(e) + } + + return ans + } + async function resourceNames(group, version, resource) { + const namespace = getValue(model, '/metadata/namespace') + // watchDependency('model#/metadata/namespace') + + let resources = await getNamespacedResourceList({ + namespace, + group, + version, + resource, + }) + + if (resource === 'secrets') { + resources = resources.filter((item) => { + const validType = ['kubernetes.io/service-account-token', 'Opaque'] + return validType.includes(item.type) + }) + } + + return resources.map((resource) => { + const name = (resource.metadata && resource.metadata.name) || '' + return { + text: name, + value: name, + } + }) + } + async function unNamespacedResourceNames(group, version, resource) { + let resources = await getResourceList({ + group, + version, + resource, + }) + + if (resource === 'secrets') { + resources = resources.filter((item) => { + const validType = ['kubernetes.io/service-account-token', 'Opaque'] + return validType.includes(item.type) + }) + } + + return resources.map((resource) => { + const name = (resource.metadata && resource.metadata.name) || '' + return { + text: name, + value: name, + } + }) + } + // for tls function hasTlsField() { const tls = getDbTls() @@ -1764,14 +1704,6 @@ export const useFunc = (model) => { return !!(model && model.alias) } - function getSelectedConfigSecret(type) { - const path = `/spec/configuration/${type}/configSecret/name` - const selectedSecret = getValue(model, path) - // watchDependency(`model#${path}`) - if (selectedSecret) return { subtitle: `You have selected ${selectedSecret} secret` } - return { subtitle: 'No secret selected' } - } - function objectToYaml(obj, indent = 0) { if (obj === null || obj === undefined) return 'null' if (typeof obj !== 'object') return JSON.stringify(obj) @@ -1901,7 +1833,7 @@ export const useFunc = (model) => { showAndInitOpsRequestType, ifDbTypeEqualsTo, getConfigSecrets, - getSelectedConfigSecret, + fetchConfigSecrets, createSecretUrl, isEqualToValueFromType, getNamespacedResourceList, @@ -1941,7 +1873,6 @@ export const useFunc = (model) => { getSelectedConfigurationValueForRemove, onRemoveConfigChange, onNewConfigSecretChange, - getSelectedApplyConfigName, createNewConfigSecret, isCreateSecret, isNotCreateSecret, diff --git a/charts/opskubedbcom-mssqlserveropsrequest-editor/ui/create-ui.yaml b/charts/opskubedbcom-mssqlserveropsrequest-editor/ui/create-ui.yaml index 0b7e55b998..e8665373b9 100644 --- a/charts/opskubedbcom-mssqlserveropsrequest-editor/ui/create-ui.yaml +++ b/charts/opskubedbcom-mssqlserveropsrequest-editor/ui/create-ui.yaml @@ -295,102 +295,177 @@ step: if: name: ifRequestTypeEqualsTo|Reconfigure type: function + loader: + name: fetchConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace elements: - type: block-layout label: Configuration elements: - - type: radio - label: Reconfigure Type - subtitle: Choose the action you'd like to perform. Select a new configuration secret, apply a custom configuration, or remove an existing setup. - validation: - type: required + - type: label-element + label: '' + subtitle: Select a new configuration secret, apply a custom configuration, or remove an existing setup to update your database settings + - type: tab-layout + label: New Config Secret + schema: temp/properties/reconfigurationType options: - - text: New Config Secret + - text: NEW CONFIG SECRET value: selectNewConfigSecret - - text: Apply Config + - text: APPLY CONFIG value: applyConfig - - text: Remove + - text: REMOVE value: remove - schema: temp/properties/reconfigurationType - watcher: - func: onReconfigurationTypeChange - paths: - - temp/properties/reconfigurationType - - type: block-layout - label: Config Secret - showLabels: true - if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret - type: function elements: - - type: label-element - label: Select a configuration secret from the available list to update your database settings - customClass: mb-10 - - type: select - addNewButton: - label: Create Secret - target: _blank - url: - function: createSecretUrl - schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - refresh: true - label: Config Secret - loader: - name: getConfigSecrets - watchPaths: - - schema/properties/metadata/properties/namespace - init: - type: func - value: setValueFromDbDetails|/spec/configSecret/name - - type: label-element - label: Selected Config Secret - loader: getSelectedConfigSecret - watcher: - func: getSelectedConfigSecret - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - - type: editor - label: Value - readonly: true - hasCopy: false - watcher: - func: getSelectedConfigSecretValue - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - schema: temp/properties/configArray/items/properties/value - - type: array-object-form - label: ApplyConfig - buttonClass: is-light is-outlined - schema: temp/properties/applyConfig - validation: - type: required - if: - name: ifReconfigurationTypeEqualsTo|applyConfig - type: function - watcher: - func: onApplyconfigChange - paths: - - temp/properties/applyConfig - elements: - - type: label-element - label: New Apply Config - subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings. Enter the parameter you want to configure (e.g., max_connections). - customClass: mb-10 - - type: input - label: key - validation: - type: required - schema: key - - type: editor - label: value - hasCopy: false - validation: - type: required - schema: value + - type: block-layout + label: Config Secret + if: + name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret + type: function + elements: + - type: label-element + label: Config Secret + subtitle: Select a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name + refresh: true + label: Config Secret + loader: + name: getConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace + - temp/properties/createSecret/properties/status + init: + type: func + value: setValueFromDbDetails|/spec/configSecret/name + watcher: + func: onCreateSecretChange + paths: + - temp/properties/createSecret/properties/status + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|create + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + - type: block-layout + label: Create a New Config Secret + showLabels: true + if: + type: function + name: isCreateSecret + hasButton: + text: Save + hasCancel: cancelCreateSecret + action: createNewConfigSecret + elements: + - type: input + label: Secret Name + schema: temp/properties/createSecret/properties/name + validation: + type: required + - type: array-object-form + label: String Data + schema: temp/properties/createSecret/properties/data + buttonClass: is-light is-outlined + validation: + type: required + elements: + - type: select + label: Key + schema: key + loader: + name: onSelectedSecretChange + watchPaths: + - temp/properties/createSecret/properties/data + validation: + type: required + - type: textarea + label: Value + schema: value + - type: multi-file-editor + editorHeight: 500px + readonly: true + if: + type: function + name: isNotCreateSecret + loader: + name: onNewConfigSecretChange + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + schema: temp/properties/newConfigSecret + - type: block-layout + label: ApplyConfig + if: + name: ifReconfigurationTypeEqualsTo|applyConfig + type: function + elements: + - type: label-element + label: New Apply Config + subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings.
Enter the parameter you want to configure (e.g., max_connections). + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfiguration + refresh: true + label: Configuration + loader: getConfigSecretsforAppyConfig + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|apply + watchPaths: + - temp/properties/selectedConfiguration + - type: multi-file-editor + editorHeight: 500px + schema: temp/properties/applyConfig + loader: + name: setApplyConfig + watchPaths: + - temp/properties/selectedConfiguration + watcher: + func: onApplyconfigChange + paths: + - temp/properties/applyConfig + - type: block-layout + label: Remove + if: + name: ifReconfigurationTypeEqualsTo|remove + type: function + elements: + - type: label-element + label: Remove + subtitle: Selected a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfigurationRemove + refresh: true + label: Configuration + loader: + name: getConfigSecretsforAppyConfig + watchPaths: + - schema/properties/metadata/properties/namespace + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|remove + watchPaths: + - temp/properties/selectedConfigurationRemove + - type: multi-file-editor + editorHeight: 500px + readonly: true + init: + type: func + value: onRemoveConfigChange + watcher: + func: onRemoveConfigChange + paths: + - temp/properties/selectedConfigurationRemove + schema: temp/properties/removeConfig + # common - type: block-layout label: OpsRequest Options diff --git a/charts/opskubedbcom-mssqlserveropsrequest-editor/ui/functions.js b/charts/opskubedbcom-mssqlserveropsrequest-editor/ui/functions.js index c856354745..96102b60f6 100644 --- a/charts/opskubedbcom-mssqlserveropsrequest-editor/ui/functions.js +++ b/charts/opskubedbcom-mssqlserveropsrequest-editor/ui/functions.js @@ -1,4 +1,4 @@ -const { axios, useOperator, store } = window.vueHelpers || {} +const { axios, useOperator, store, useToast } = window.vueHelpers || {} const machines = { 'db.t.micro': { resources: { @@ -306,9 +306,11 @@ const machineList = [ ] let machinesFromPreset = [] +const configSecretKeys = ['mssql.conf'] export const useFunc = (model) => { const route = store.state?.route + const toast = useToast() const { getValue, storeGet, discriminator, setDiscriminatorValue, commit } = useOperator( model, @@ -828,32 +830,419 @@ export const useFunc = (model) => { return machine === 'custom' } - // for config secret - let secretArray = [] - async function getConfigSecrets() { + // Fetch and store database Infos + // for secret configurations in reconfigure + let configSecrets = [] + let secretConfigData = [] + + async function fetchConfigSecrets() { const owner = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') const namespace = getValue(model, '/metadata/namespace') // watchDependency('model#/metadata/namespace') - const resp = await axios.get( - `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets`, + const name = getValue(model, '/spec/databaseRef/name') + const dbGroup = getValue(model, '/route/params/group') + const dbKind = getValue(store.state, '/resource/definition/result/kind') + const dbResource = getValue(model, '/route/params/resource') + const dbVersion = getValue(model, '/route/params/version') + + try { + const resp = await axios.post( + `/clusters/${owner}/${cluster}/proxy/ui.kubedb.com/v1alpha1/databaseinfos`, + { + apiVersion: 'ui.kubedb.com/v1alpha1', + kind: 'DatabaseInfo', + request: { + source: { + ref: { + name: name, + namespace: namespace, + }, + resource: { + group: dbGroup, + kind: dbKind, + name: dbResource, + version: dbVersion, + }, + }, + keys: ['mssql.conf'], + }, + }, + ) + configSecrets = resp?.data?.response?.availableSecrets || [] + secretConfigData = resp?.data?.response?.configurations || [] + } catch (e) { + console.log(e) + } + } + + async function getConfigSecrets(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'success') { + await fetchConfigSecrets() + } + const mappedSecrets = configSecrets.map((item) => { + return { text: item, value: item } + }) + mappedSecrets.push({ text: '+ Create a new Secret', value: 'Create' }) + return mappedSecrets + } + + async function getConfigSecretsforAppyConfig() { + const secrets = secretConfigData.map((item) => { + return { text: item.componentName, value: item.componentName } + }) + return secrets + } + + function getSelectedConfigurationData(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfiguration` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return [] + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, ) - const secrets = (resp && resp.data && resp.data.items) || [] - secretArray = secrets + if (!configuration) { + return [] + } - const filteredSecrets = secrets + const result = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedContent = atob(configuration.data[fileName]) + result.push({ + name: fileName, + content: decodedContent, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + result.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) - filteredSecrets.map((item) => { - const name = (item.metadata && item.metadata.name) || '' - item.text = name - item.value = name - return true + // Set the value to the model + commit('wizard/model$update', { + path: `/temp/${type}applyConfig`, + value: result, + force: true, + }) + + return result + } + + function getSelectedConfigurationName(configType, type) { + type = type ? type + '/' : '' + let path = '' + if (configType === 'create') path = `/spec/configuration/${type}/configSecret/name` + else if (configType === 'apply') path = `/${type}selectedConfiguration` + else if (configType === 'remove') path = `/${type}selectedConfigurationRemove` + + const selectedConfiguration = + configType === 'create' ? getValue(model, path) : getValue(discriminator, path) + + if (selectedConfiguration) + return { subtitle: ` You have selected ${selectedConfiguration} secret` } + else return { subtitle: 'No secret selected' } + } + + function getSelectedConfigurationValueForRemove(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfigurationRemove` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return '' + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, + ) + + if (!configuration) { + return '' + } + + let data = {} + // Decode base64 and parse YAML for each key in the secret data + Object.keys(configuration.data).forEach((item) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[item]) + // Parse YAML string to object + const parsedYaml = yaml.load(decodedString) + // Store the parsed object with the filename as key + data[item] = parsedYaml + } catch (e) { + console.error(`Error parsing ${item}:`, e) + data[item] = atob(configuration.data[item]) // Fallback to decoded string + } + }) + + // Convert data object back to YAML string + return yaml.dump(data) + } + + async function createNewConfigSecret(type) { + type = type ? type + '/' : '' + const { user, cluster } = route.params + const url = `/clusters/${user}/${cluster}/resources` + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + const secretName = getValue(discriminator, `${type}createSecret/name`) + const secretData = getValue(discriminator, `${type}createSecret/data`) + const secretDataObj = Object.fromEntries(secretData.map((item) => [item.key, item.value])) + + try { + const res = await axios.post(url, { + apiVersion: 'v1', + stringData: secretDataObj, + kind: 'Secret', + metadata: { + name: secretName, + namespace: namespace, + }, + type: 'Opaque', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'success', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/lastCreatedSecret`, + value: secretName, + }) + toast.success('Secret created successfully') + } catch (error) { + const errMsg = decodeError(error, 'Failed to create secret') + toast.error(errMsg, { timeout: 5000 }) + cancelCreateSecret() + } + } + + function decodeError(msg, defaultMsg) { + if (typeof msg === 'string') { + return msg || defaultMsg + } + return ( + (msg.response && msg.response.data && msg.response.data.message) || + (msg.response && msg.response.data) || + (msg.status && msg.status.status) || + defaultMsg + ) + } + + function isCreateSecret(type) { + type = type ? type + '/' : '' + const selectedSecret = getValue(model, `spec/configuration/${type}configSecret/name`) + const res = selectedSecret === 'Create' + + if (res === true) { + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'pending', + }) + } + return res + } + + function isNotCreateSecret(type) { + return !isCreateSecret(type) + } + + function onCreateSecretChange(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'cancelled') return '' + else if (secretStatus === 'success') { + const name = getValue(discriminator, `${type}createSecret/lastCreatedSecret`) + + const configFound = configSecrets.find((item) => item === name) + return configFound ? { text: name, value: name } : '' + } + } + + function cancelCreateSecret(type) { + type = type ? type + '/' : '' + commit('wizard/temp$delete', `${type}createSecret/name`) + commit('wizard/temp$delete', `${type}createSecret/data`) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'cancelled', + }) + } + + async function onApplyconfigChange(type) { + type = type ? type + '/' : '' + const configValue = getValue(discriminator, `${type}applyConfig`) + + if (!configValue) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + const tempConfigObj = {} + configValue.forEach((item) => { + if (item.name) { + tempConfigObj[item.name] = item.content + } + }) + if (Object.keys(tempConfigObj).length === 0) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}applyConfig`, + value: tempConfigObj, + }) + } + + function setApplyConfig(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfiguration` + const selectedConfig = getValue(discriminator, configPath) + if (!selectedConfig) { + return [{ name: '', content: '' }] + } + const applyconfigData = secretConfigData.find((item) => { + if (item.componentName === selectedConfig) { + return item + } + }) + const { applyConfig } = applyconfigData + const configObj = [] + + if (applyConfig) { + Object.keys(applyConfig).forEach((fileName) => { + configObj.push({ + name: fileName, + content: applyConfig[fileName], + }) + }) + } else { + configObj.push({ name: '', content: '' }) + } + return configObj + } + + function onRemoveConfigChange(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfigurationRemove` + const selectedConfig = getValue(discriminator, configPath) + + if (!selectedConfig) { + commit('wizard/model$delete', `/spec/configuration/${type}removeCustomConfig`) + return [{ name: '', content: '' }] + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}removeCustomConfig`, + value: true, + }) + + const configuration = secretConfigData.find((item) => item.componentName === selectedConfig) + + if (!configuration.data) { + return [{ name: '', content: '' }] + } + + const configObj = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) + return configObj + } + + async function onNewConfigSecretChange(type) { + type = type ? type + '/' : '' + const path = `/spec/configuration/${type}configSecret/name` + const selectedSecret = getValue(model, path) + + if (!selectedSecret) { + commit('wizard/model$delete', `/spec/configuration/${type}configSecret`) + return [{ name: '', content: '' }] + } + if (selectedSecret === 'Create') return [{ name: '', content: '' }] + + const owner = storeGet('/route/params/user') + const cluster = storeGet('/route/params/cluster') + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + + try { + // Fetch the secret data from API + const secretResp = await axios.get( + `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets/${selectedSecret}`, + ) + + const secretData = secretResp.data?.data || {} + const configObj = [] + + // Decode base64 and format as array of objects with name and content + Object.keys(secretData).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(secretData[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: secretData[fileName], // Fallback to original if decode fails + }) + } + }) + + return configObj + } catch (e) { + console.error('Error fetching secret:', e) + return [{ name: '', content: '' }] + } + } + + function onSelectedSecretChange(index) { + const secretData = getValue(discriminator, 'createSecret/data') || [] + const selfSecrets = secretData.map((item) => item.key) + + const remainingSecrets = configSecretKeys.filter((item) => !selfSecrets.includes(item)) + + const selfKey = getValue(discriminator, `createSecret/data/${index}/key`) + if (selfKey) { + remainingSecrets.push(selfKey) + } + const resSecret = remainingSecrets.map((item) => { + return { text: item, value: item } }) - return filteredSecrets + return resSecret } + let secretArray = [] + function createSecretUrl() { const user = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') @@ -977,24 +1366,6 @@ export const useFunc = (model) => { return reconfigurationType === value } - function onApplyconfigChange() { - const applyconfig = getValue(discriminator, '/applyConfig') - - const configObj = {} - if (applyconfig) { - applyconfig.forEach((item) => { - const { key, value } = item - configObj[key] = value - }) - } - - commit('wizard/model$update', { - path: '/spec/configuration/applyConfig', - value: configObj, - force: true, - }) - } - function onReconfigurationTypeChange() { const reconfigurationType = getValue(discriminator, '/reconfigurationType') setDiscriminatorValue('/applyConfig', []) @@ -1389,5 +1760,20 @@ export const useFunc = (model) => { onMachineChange, isMachineCustom, checkVolume, + fetchConfigSecrets, + getConfigSecretsforAppyConfig, + getSelectedConfigurationData, + getSelectedConfigurationName, + getSelectedConfigurationValueForRemove, + createNewConfigSecret, + decodeError, + isCreateSecret, + isNotCreateSecret, + onCreateSecretChange, + cancelCreateSecret, + setApplyConfig, + onRemoveConfigChange, + onNewConfigSecretChange, + onSelectedSecretChange, } } diff --git a/charts/opskubedbcom-mysqlopsrequest-editor/ui/create-ui.yaml b/charts/opskubedbcom-mysqlopsrequest-editor/ui/create-ui.yaml index f7958aff64..f88e2eab6e 100644 --- a/charts/opskubedbcom-mysqlopsrequest-editor/ui/create-ui.yaml +++ b/charts/opskubedbcom-mysqlopsrequest-editor/ui/create-ui.yaml @@ -302,115 +302,176 @@ step: if: name: ifRequestTypeEqualsTo|Reconfigure type: function + loader: + name: fetchConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace elements: - type: block-layout label: Configuration elements: - - type: radio - label: Reconfigure Type - subtitle: Choose the action you'd like to perform. Select a new configuration secret, apply a custom configuration , or remove an existing setup. - validation: - type: required + - type: label-element + label: '' + subtitle: Select a new configuration secret, apply a custom configuration, or remove an existing setup to update your database settings + - type: tab-layout + label: New Config Secret + schema: temp/properties/reconfigurationType options: - - text: New Config Secret + - text: NEW CONFIG SECRET value: selectNewConfigSecret - - text: Apply Config + - text: APPLY CONFIG value: applyConfig - - text: Remove + - text: REMOVE value: remove - schema: temp/properties/reconfigurationType - watcher: - func: onReconfigurationTypeChange - paths: - - temp/properties/reconfigurationType - - type: block-layout - label: Config Secret - showLabels: true - if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret - type: function - elements: - - type: label-element - label: Select a configuration secret from the available list to update your database settings - customClass: mb-10 - - type: select - addNewButton: - label: Create a new Secret - target: _blank - url: - function: createSecretUrl - schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - refresh: true - label: Config Secret - loader: - name: getConfigSecrets - watchPaths: - - schema/properties/metadata/properties/namespace - init: - type: func - value: setValueFromDbDetails|/spec/configSecret/name - - type: label-element - label: Selected Config Secret - loader: getSelectedConfigSecret|mysql - if: - name: isConfigSelected - type: function - watcher: - func: getSelectedConfigSecret|mysql - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - - type: editor - label: Value - readonly: true - hasCopy: false - if: - name: isConfigSelected - type: function - loader: - name: getSelectedConfigSecretValue|mysql - watchPaths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - schema: temp/properties/configArray/items/properties/value - - type: array-object-form - label: ApplyConfig - buttonClass: is-light is-outlined - schema: temp/properties/applyConfig - validation: - type: required - if: - name: ifReconfigurationTypeEqualsTo|applyConfig - type: function - watcher: - func: onApplyconfigChange - paths: - - temp/properties/applyConfig elements: - - type: label-element - label: New Apply Config - subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings. Enter the parameter you want to configure (e.g., max_connections). - customClass: mb-10 - - type: input - label: key - validation: - type: required - schema: key - - type: editor - label: value - hasCopy: false - validation: - type: required - schema: value - - type: switch - schema: schema/properties/spec/properties/configuration/properties/removeCustomConfig - label: Remove CustomConfig - fullwidth: true - if: - name: returnFalse - type: function + - type: block-layout + label: Config Secret + if: + name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret + type: function + elements: + - type: label-element + label: Config Secret + subtitle: Select a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name + refresh: true + label: Config Secret + loader: + name: getConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace + - temp/properties/createSecret/properties/status + init: + type: func + value: setValueFromDbDetails|/spec/configSecret/name + watcher: + func: onCreateSecretChange + paths: + - temp/properties/createSecret/properties/status + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|create + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + - type: block-layout + label: Create a New Config Secret + showLabels: true + if: + type: function + name: isCreateSecret + hasButton: + text: Save + hasCancel: cancelCreateSecret + action: createNewConfigSecret + elements: + - type: input + label: Secret Name + schema: temp/properties/createSecret/properties/name + validation: + type: required + - type: array-object-form + label: String Data + schema: temp/properties/createSecret/properties/data + buttonClass: is-light is-outlined + validation: + type: required + elements: + - type: select + label: Key + schema: key + loader: + name: onSelectedSecretChange + watchPaths: + - temp/properties/createSecret/properties/data + validation: + type: required + - type: textarea + label: Value + schema: value + - type: multi-file-editor + editorHeight: 500px + readonly: true + if: + type: function + name: isNotCreateSecret + loader: + name: onNewConfigSecretChange + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + schema: temp/properties/newConfigSecret + - type: block-layout + label: ApplyConfig + if: + name: ifReconfigurationTypeEqualsTo|applyConfig + type: function + elements: + - type: label-element + label: New Apply Config + subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings.
Enter the parameter you want to configure (e.g., max_connections). + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfiguration + refresh: true + label: Configuration + loader: getConfigSecretsforAppyConfig + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|apply + watchPaths: + - temp/properties/selectedConfiguration + - type: multi-file-editor + editorHeight: 500px + schema: temp/properties/applyConfig + loader: + name: setApplyConfig + watchPaths: + - temp/properties/selectedConfiguration + watcher: + func: onApplyconfigChange + paths: + - temp/properties/applyConfig + - type: block-layout + label: Remove + if: + name: ifReconfigurationTypeEqualsTo|remove + type: function + elements: + - type: label-element + label: Remove + subtitle: Selected a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfigurationRemove + refresh: true + label: Configuration + loader: + name: getConfigSecretsforAppyConfig + watchPaths: + - schema/properties/metadata/properties/namespace + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|remove + watchPaths: + - temp/properties/selectedConfigurationRemove + - type: multi-file-editor + editorHeight: 500px + readonly: true + init: + type: func + value: onRemoveConfigChange + watcher: + func: onRemoveConfigChange + paths: + - temp/properties/selectedConfigurationRemove + schema: temp/properties/removeConfig # Reconfigure TLS - type: block-layout label: TLS diff --git a/charts/opskubedbcom-mysqlopsrequest-editor/ui/functions.js b/charts/opskubedbcom-mysqlopsrequest-editor/ui/functions.js index eee03f36e7..3d89d34b4b 100644 --- a/charts/opskubedbcom-mysqlopsrequest-editor/ui/functions.js +++ b/charts/opskubedbcom-mysqlopsrequest-editor/ui/functions.js @@ -1,4 +1,4 @@ -const { axios, useOperator, store } = window.vueHelpers || {} +const { axios, useOperator, store, useToast } = window.vueHelpers || {} const machines = { 'db.t.micro': { resources: { @@ -306,9 +306,11 @@ const machineList = [ ] let machinesFromPreset = [] +const configSecretKeys = ['kubedb-user.cnf'] export const useFunc = (model) => { const route = store.state?.route + const toast = useToast() const { getValue, storeGet, discriminator, setDiscriminatorValue, commit } = useOperator( model, @@ -838,32 +840,419 @@ export const useFunc = (model) => { return machine === 'custom' } - // for config secret - let secretArray = [] - async function getConfigSecrets() { + // Fetch and store database Infos + // for secret configurations in reconfigure + let configSecrets = [] + let secretConfigData = [] + + async function fetchConfigSecrets() { const owner = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') const namespace = getValue(model, '/metadata/namespace') // watchDependency('model#/metadata/namespace') - const resp = await axios.get( - `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets`, + const name = getValue(model, '/spec/databaseRef/name') + const dbGroup = getValue(model, '/route/params/group') + const dbKind = getValue(store.state, '/resource/definition/result/kind') + const dbResource = getValue(model, '/route/params/resource') + const dbVersion = getValue(model, '/route/params/version') + + try { + const resp = await axios.post( + `/clusters/${owner}/${cluster}/proxy/ui.kubedb.com/v1alpha1/databaseinfos`, + { + apiVersion: 'ui.kubedb.com/v1alpha1', + kind: 'DatabaseInfo', + request: { + source: { + ref: { + name: name, + namespace: namespace, + }, + resource: { + group: dbGroup, + kind: dbKind, + name: dbResource, + version: dbVersion, + }, + }, + keys: ['kubedb-user.cnf'], + }, + }, + ) + configSecrets = resp?.data?.response?.availableSecrets || [] + secretConfigData = resp?.data?.response?.configurations || [] + } catch (e) { + console.log(e) + } + } + + async function getConfigSecrets(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'success') { + await fetchConfigSecrets() + } + const mappedSecrets = configSecrets.map((item) => { + return { text: item, value: item } + }) + mappedSecrets.push({ text: '+ Create a new Secret', value: 'Create' }) + return mappedSecrets + } + + async function getConfigSecretsforAppyConfig() { + const secrets = secretConfigData.map((item) => { + return { text: item.componentName, value: item.componentName } + }) + return secrets + } + + function getSelectedConfigurationData(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfiguration` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return [] + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, ) - const secrets = (resp && resp.data && resp.data.items) || [] - secretArray = secrets + if (!configuration) { + return [] + } - const filteredSecrets = secrets + const result = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedContent = atob(configuration.data[fileName]) + result.push({ + name: fileName, + content: decodedContent, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + result.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) - filteredSecrets.map((item) => { - const name = (item.metadata && item.metadata.name) || '' - item.text = name - item.value = name - return true + // Set the value to the model + commit('wizard/model$update', { + path: `/temp/${type}applyConfig`, + value: result, + force: true, }) - return filteredSecrets + + return result + } + + function getSelectedConfigurationName(configType, type) { + type = type ? type + '/' : '' + let path = '' + if (configType === 'create') path = `/spec/configuration/${type}/configSecret/name` + else if (configType === 'apply') path = `/${type}selectedConfiguration` + else if (configType === 'remove') path = `/${type}selectedConfigurationRemove` + + const selectedConfiguration = + configType === 'create' ? getValue(model, path) : getValue(discriminator, path) + + if (selectedConfiguration) + return { subtitle: ` You have selected ${selectedConfiguration} secret` } + else return { subtitle: 'No secret selected' } + } + + function getSelectedConfigurationValueForRemove(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfigurationRemove` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return '' + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, + ) + + if (!configuration) { + return '' + } + + let data = {} + // Decode base64 and parse YAML for each key in the secret data + Object.keys(configuration.data).forEach((item) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[item]) + // Parse YAML string to object + const parsedYaml = yaml.load(decodedString) + // Store the parsed object with the filename as key + data[item] = parsedYaml + } catch (e) { + console.error(`Error parsing ${item}:`, e) + data[item] = atob(configuration.data[item]) // Fallback to decoded string + } + }) + + // Convert data object back to YAML string + return yaml.dump(data) + } + + async function createNewConfigSecret(type) { + type = type ? type + '/' : '' + const { user, cluster } = route.params + const url = `/clusters/${user}/${cluster}/resources` + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + const secretName = getValue(discriminator, `${type}createSecret/name`) + const secretData = getValue(discriminator, `${type}createSecret/data`) + const secretDataObj = Object.fromEntries(secretData.map((item) => [item.key, item.value])) + + try { + const res = await axios.post(url, { + apiVersion: 'v1', + stringData: secretDataObj, + kind: 'Secret', + metadata: { + name: secretName, + namespace: namespace, + }, + type: 'Opaque', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'success', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/lastCreatedSecret`, + value: secretName, + }) + toast.success('Secret created successfully') + } catch (error) { + const errMsg = decodeError(error, 'Failed to create secret') + toast.error(errMsg, { timeout: 5000 }) + cancelCreateSecret() + } + } + + function decodeError(msg, defaultMsg) { + if (typeof msg === 'string') { + return msg || defaultMsg + } + return ( + (msg.response && msg.response.data && msg.response.data.message) || + (msg.response && msg.response.data) || + (msg.status && msg.status.status) || + defaultMsg + ) + } + + function isCreateSecret(type) { + type = type ? type + '/' : '' + const selectedSecret = getValue(model, `spec/configuration/${type}configSecret/name`) + const res = selectedSecret === 'Create' + + if (res === true) { + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'pending', + }) + } + return res + } + + function isNotCreateSecret(type) { + return !isCreateSecret(type) } + function onCreateSecretChange(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'cancelled') return '' + else if (secretStatus === 'success') { + const name = getValue(discriminator, `${type}createSecret/lastCreatedSecret`) + + const configFound = configSecrets.find((item) => item === name) + return configFound ? { text: name, value: name } : '' + } + } + + function cancelCreateSecret(type) { + type = type ? type + '/' : '' + commit('wizard/temp$delete', `${type}createSecret/name`) + commit('wizard/temp$delete', `${type}createSecret/data`) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'cancelled', + }) + } + + async function onApplyconfigChange(type) { + type = type ? type + '/' : '' + const configValue = getValue(discriminator, `${type}applyConfig`) + + if (!configValue) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + const tempConfigObj = {} + configValue.forEach((item) => { + if (item.name) { + tempConfigObj[item.name] = item.content + } + }) + if (Object.keys(tempConfigObj).length === 0) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}applyConfig`, + value: tempConfigObj, + }) + } + + function setApplyConfig(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfiguration` + const selectedConfig = getValue(discriminator, configPath) + if (!selectedConfig) { + return [{ name: '', content: '' }] + } + const applyconfigData = secretConfigData.find((item) => { + if (item.componentName === selectedConfig) { + return item + } + }) + const { applyConfig } = applyconfigData + const configObj = [] + + if (applyConfig) { + Object.keys(applyConfig).forEach((fileName) => { + configObj.push({ + name: fileName, + content: applyConfig[fileName], + }) + }) + } else { + configObj.push({ name: '', content: '' }) + } + return configObj + } + + function onRemoveConfigChange(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfigurationRemove` + const selectedConfig = getValue(discriminator, configPath) + + if (!selectedConfig) { + commit('wizard/model$delete', `/spec/configuration/${type}removeCustomConfig`) + return [{ name: '', content: '' }] + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}removeCustomConfig`, + value: true, + }) + + const configuration = secretConfigData.find((item) => item.componentName === selectedConfig) + + if (!configuration.data) { + return [{ name: '', content: '' }] + } + + const configObj = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) + return configObj + } + + async function onNewConfigSecretChange(type) { + type = type ? type + '/' : '' + const path = `/spec/configuration/${type}configSecret/name` + const selectedSecret = getValue(model, path) + + if (!selectedSecret) { + commit('wizard/model$delete', `/spec/configuration/${type}configSecret`) + return [{ name: '', content: '' }] + } + if (selectedSecret === 'Create') return [{ name: '', content: '' }] + + const owner = storeGet('/route/params/user') + const cluster = storeGet('/route/params/cluster') + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + + try { + // Fetch the secret data from API + const secretResp = await axios.get( + `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets/${selectedSecret}`, + ) + + const secretData = secretResp.data?.data || {} + const configObj = [] + + // Decode base64 and format as array of objects with name and content + Object.keys(secretData).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(secretData[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: secretData[fileName], // Fallback to original if decode fails + }) + } + }) + + return configObj + } catch (e) { + console.error('Error fetching secret:', e) + return [{ name: '', content: '' }] + } + } + + function onSelectedSecretChange(index) { + const secretData = getValue(discriminator, 'createSecret/data') || [] + const selfSecrets = secretData.map((item) => item.key) + + const remainingSecrets = configSecretKeys.filter((item) => !selfSecrets.includes(item)) + + const selfKey = getValue(discriminator, `createSecret/data/${index}/key`) + if (selfKey) { + remainingSecrets.push(selfKey) + } + const resSecret = remainingSecrets.map((item) => { + return { text: item, value: item } + }) + return resSecret + } + + let secretArray = [] + function createSecretUrl() { const user = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') @@ -989,25 +1378,6 @@ export const useFunc = (model) => { return reconfigurationType === value } - function onApplyconfigChange() { - const applyconfig = getValue(discriminator, '/applyConfig') - - const configObj = {} - - if (applyconfig) { - applyconfig.forEach((item) => { - const { key, value } = item - configObj[key] = value - }) - } - - commit('wizard/model$update', { - path: '/spec/configuration/applyConfig', - value: configObj, - force: true, - }) - } - function onReconfigurationTypeChange() { const reconfigurationType = getValue(discriminator, '/reconfigurationType') setDiscriminatorValue('/applyConfig', []) @@ -1442,5 +1812,20 @@ export const useFunc = (model) => { checkVolume, setConfigFiles, isConfigSelected, + fetchConfigSecrets, + getConfigSecretsforAppyConfig, + getSelectedConfigurationData, + getSelectedConfigurationName, + getSelectedConfigurationValueForRemove, + createNewConfigSecret, + decodeError, + isCreateSecret, + isNotCreateSecret, + onCreateSecretChange, + cancelCreateSecret, + setApplyConfig, + onRemoveConfigChange, + onNewConfigSecretChange, + onSelectedSecretChange, } } diff --git a/charts/opskubedbcom-perconaxtradbopsrequest-editor/ui/create-ui.yaml b/charts/opskubedbcom-perconaxtradbopsrequest-editor/ui/create-ui.yaml index 35cd01b68e..ba9ee4a19d 100644 --- a/charts/opskubedbcom-perconaxtradbopsrequest-editor/ui/create-ui.yaml +++ b/charts/opskubedbcom-perconaxtradbopsrequest-editor/ui/create-ui.yaml @@ -280,106 +280,176 @@ step: if: name: ifRequestTypeEqualsTo|Reconfigure type: function + loader: + name: fetchConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace elements: - - type: radio - label: Reconfigure Type - subtitle: Choose the action you'd like to perform. Select a new configuration secret, apply a custom configuration , or remove an existing setup. - validation: - type: required - options: - - text: New Config Secret - value: selectNewConfigSecret - - text: Apply Config - value: applyConfig - - text: Remove testing - value: remove - schema: temp/properties/reconfigurationType - watcher: - func: onReconfigurationTypeChange|perconaxtradb - paths: - - temp/properties/reconfigurationType - type: block-layout - label: Config Secret - showLabels: true - if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret - type: function - elements: - - type: label-element - label: Select a configuration secret from the available list to update your database settings - customClass: mb-10 - - type: select - addNewButton: - label: Create Secret - target: _blank - url: - function: createSecretUrl - schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - refresh: true - label: Config Secret - loader: - name: getConfigSecrets - watchPaths: - - schema/properties/metadata/properties/namespace - init: - type: func - value: setValueFromDbDetails|/spec/configSecret/name - - type: label-element - label: Selected Config Secret - loader: getSelectedConfigSecret|perconaxtradb - watcher: - func: getSelectedConfigSecret|perconaxtradb - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - - type: editor - label: Value - readonly: true - hasCopy: false - watcher: - func: getSelectedConfigSecretValue|perconaxtradb - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - schema: temp/properties/configArray/items/properties/value - - type: array-object-form - label: ApplyConfig - buttonClass: is-light is-outlined - schema: temp/properties/perconaxtradb/applyConfig - validation: - type: required - if: - name: ifReconfigurationTypeEqualsTo|applyConfig - type: function - watcher: - func: onApplyconfigChange|perconaxtradb - paths: - - temp/properties/perconaxtradb/applyConfig + label: Configuration elements: - type: label-element - label: New Apply Config - subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings. Enter the parameter you want to configure (e.g., max_connections). - customClass: mb-10 - - type: input - label: key - validation: - type: required - schema: key - - type: editor - label: value - hasCopy: false - validation: - type: required - schema: temp/properties/configArray/items/properties/value - - type: switch - schema: schema/properties/spec/properties/configuration/properties/removeCustomConfig - label: Remove CustomConfig - fullwidth: true - if: - name: returnFalse - type: function + label: '' + subtitle: Select a new configuration secret, apply a custom configuration, or remove an existing setup to update your database settings + - type: tab-layout + label: New Config Secret + schema: temp/properties/reconfigurationType + options: + - text: NEW CONFIG SECRET + value: selectNewConfigSecret + - text: APPLY CONFIG + value: applyConfig + - text: REMOVE + value: remove + elements: + - type: block-layout + label: Config Secret + if: + name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret + type: function + elements: + - type: label-element + label: Config Secret + subtitle: Select a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name + refresh: true + label: Config Secret + loader: + name: getConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace + - temp/properties/createSecret/properties/status + init: + type: func + value: setValueFromDbDetails|/spec/configSecret/name + watcher: + func: onCreateSecretChange + paths: + - temp/properties/createSecret/properties/status + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|create + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + - type: block-layout + label: Create a New Config Secret + showLabels: true + if: + type: function + name: isCreateSecret + hasButton: + text: Save + hasCancel: cancelCreateSecret + action: createNewConfigSecret + elements: + - type: input + label: Secret Name + schema: temp/properties/createSecret/properties/name + validation: + type: required + - type: array-object-form + label: String Data + schema: temp/properties/createSecret/properties/data + buttonClass: is-light is-outlined + validation: + type: required + elements: + - type: select + label: Key + schema: key + loader: + name: onSelectedSecretChange + watchPaths: + - temp/properties/createSecret/properties/data + validation: + type: required + - type: textarea + label: Value + schema: value + - type: multi-file-editor + editorHeight: 500px + readonly: true + if: + type: function + name: isNotCreateSecret + loader: + name: onNewConfigSecretChange + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + schema: temp/properties/newConfigSecret + - type: block-layout + label: ApplyConfig + if: + name: ifReconfigurationTypeEqualsTo|applyConfig + type: function + elements: + - type: label-element + label: New Apply Config + subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings.
Enter the parameter you want to configure (e.g., max_connections). + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfiguration + refresh: true + label: Configuration + loader: getConfigSecretsforAppyConfig + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|apply + watchPaths: + - temp/properties/selectedConfiguration + - type: multi-file-editor + editorHeight: 500px + schema: temp/properties/applyConfig + loader: + name: setApplyConfig + watchPaths: + - temp/properties/selectedConfiguration + watcher: + func: onApplyconfigChange + paths: + - temp/properties/applyConfig + - type: block-layout + label: Remove + if: + name: ifReconfigurationTypeEqualsTo|remove + type: function + elements: + - type: label-element + label: Remove + subtitle: Selected a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfigurationRemove + refresh: true + label: Configuration + loader: + name: getConfigSecretsforAppyConfig + watchPaths: + - schema/properties/metadata/properties/namespace + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|remove + watchPaths: + - temp/properties/selectedConfigurationRemove + - type: multi-file-editor + editorHeight: 500px + readonly: true + init: + type: func + value: onRemoveConfigChange + watcher: + func: onRemoveConfigChange + paths: + - temp/properties/selectedConfigurationRemove + schema: temp/properties/removeConfig # Reconfigure TLS - type: block-layout label: TLS diff --git a/charts/opskubedbcom-perconaxtradbopsrequest-editor/ui/functions.js b/charts/opskubedbcom-perconaxtradbopsrequest-editor/ui/functions.js index 9f59f7cc6d..b575be8271 100644 --- a/charts/opskubedbcom-perconaxtradbopsrequest-editor/ui/functions.js +++ b/charts/opskubedbcom-perconaxtradbopsrequest-editor/ui/functions.js @@ -1,4 +1,4 @@ -const { axios, useOperator, store } = window.vueHelpers || {} +const { axios, useOperator, store, useToast } = window.vueHelpers || {} const machines = { 'db.t.micro': { resources: { @@ -305,11 +305,13 @@ const machineList = [ 'db.r.24xlarge', ] +const configSecretKeys = ['kubedb-user.cnf'] let machinesFromPreset = [] let secretArray = [] export const useFunc = (model) => { const route = store.state?.route + const toast = useToast() const { getValue, storeGet, discriminator, setDiscriminatorValue, commit } = useOperator( model, @@ -837,29 +839,415 @@ export const useFunc = (model) => { return machine === 'custom' } - // for config secret - async function getConfigSecrets() { + // Fetch and store database Infos + // for secret configurations in reconfigure + let configSecrets = [] + let secretConfigData = [] + + async function fetchConfigSecrets() { const owner = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') const namespace = getValue(model, '/metadata/namespace') // watchDependency('model#/metadata/namespace') - const resp = await axios.get( - `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets`, + const name = getValue(model, '/spec/databaseRef/name') + const dbGroup = getValue(model, '/route/params/group') + const dbKind = getValue(store.state, '/resource/definition/result/kind') + const dbResource = getValue(model, '/route/params/resource') + const dbVersion = getValue(model, '/route/params/version') + + try { + const resp = await axios.post( + `/clusters/${owner}/${cluster}/proxy/ui.kubedb.com/v1alpha1/databaseinfos`, + { + apiVersion: 'ui.kubedb.com/v1alpha1', + kind: 'DatabaseInfo', + request: { + source: { + ref: { + name: name, + namespace: namespace, + }, + resource: { + group: dbGroup, + kind: dbKind, + name: dbResource, + version: dbVersion, + }, + }, + keys: ['kubedb-user.cnf'], + }, + }, + ) + configSecrets = resp?.data?.response?.availableSecrets || [] + secretConfigData = resp?.data?.response?.configurations || [] + } catch (e) { + console.log(e) + } + } + + async function getConfigSecrets(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'success') { + await fetchConfigSecrets() + } + const mappedSecrets = configSecrets.map((item) => { + return { text: item, value: item } + }) + mappedSecrets.push({ text: '+ Create a new Secret', value: 'Create' }) + return mappedSecrets + } + + async function getConfigSecretsforAppyConfig() { + const secrets = secretConfigData.map((item) => { + return { text: item.componentName, value: item.componentName } + }) + return secrets + } + + function getSelectedConfigurationData(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfiguration` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return [] + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, ) - const secrets = (resp && resp.data && resp.data.items) || [] - secretArray = secrets + if (!configuration) { + return [] + } + + const result = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedContent = atob(configuration.data[fileName]) + result.push({ + name: fileName, + content: decodedContent, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + result.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) - const filteredSecrets = secrets + // Set the value to the model + commit('wizard/model$update', { + path: `/temp/${type}applyConfig`, + value: result, + force: true, + }) - filteredSecrets.map((item) => { - const name = (item.metadata && item.metadata.name) || '' - item.text = name - item.value = name - return true + return result + } + + function getSelectedConfigurationName(configType, type) { + type = type ? type + '/' : '' + let path = '' + if (configType === 'create') path = `/spec/configuration/${type}/configSecret/name` + else if (configType === 'apply') path = `/${type}selectedConfiguration` + else if (configType === 'remove') path = `/${type}selectedConfigurationRemove` + + const selectedConfiguration = + configType === 'create' ? getValue(model, path) : getValue(discriminator, path) + + if (selectedConfiguration) + return { subtitle: ` You have selected ${selectedConfiguration} secret` } + else return { subtitle: 'No secret selected' } + } + + function getSelectedConfigurationValueForRemove(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfigurationRemove` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return '' + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, + ) + + if (!configuration) { + return '' + } + + let data = {} + // Decode base64 and parse YAML for each key in the secret data + Object.keys(configuration.data).forEach((item) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[item]) + // Parse YAML string to object + const parsedYaml = yaml.load(decodedString) + // Store the parsed object with the filename as key + data[item] = parsedYaml + } catch (e) { + console.error(`Error parsing ${item}:`, e) + data[item] = atob(configuration.data[item]) // Fallback to decoded string + } + }) + + // Convert data object back to YAML string + return yaml.dump(data) + } + + async function createNewConfigSecret(type) { + type = type ? type + '/' : '' + const { user, cluster } = route.params + const url = `/clusters/${user}/${cluster}/resources` + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + const secretName = getValue(discriminator, `${type}createSecret/name`) + const secretData = getValue(discriminator, `${type}createSecret/data`) + const secretDataObj = Object.fromEntries(secretData.map((item) => [item.key, item.value])) + + try { + const res = await axios.post(url, { + apiVersion: 'v1', + stringData: secretDataObj, + kind: 'Secret', + metadata: { + name: secretName, + namespace: namespace, + }, + type: 'Opaque', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'success', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/lastCreatedSecret`, + value: secretName, + }) + toast.success('Secret created successfully') + } catch (error) { + const errMsg = decodeError(error, 'Failed to create secret') + toast.error(errMsg, { timeout: 5000 }) + cancelCreateSecret() + } + } + + function decodeError(msg, defaultMsg) { + if (typeof msg === 'string') { + return msg || defaultMsg + } + return ( + (msg.response && msg.response.data && msg.response.data.message) || + (msg.response && msg.response.data) || + (msg.status && msg.status.status) || + defaultMsg + ) + } + + function isCreateSecret(type) { + type = type ? type + '/' : '' + const selectedSecret = getValue(model, `spec/configuration/${type}configSecret/name`) + const res = selectedSecret === 'Create' + + if (res === true) { + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'pending', + }) + } + return res + } + + function isNotCreateSecret(type) { + return !isCreateSecret(type) + } + + function onCreateSecretChange(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'cancelled') return '' + else if (secretStatus === 'success') { + const name = getValue(discriminator, `${type}createSecret/lastCreatedSecret`) + + const configFound = configSecrets.find((item) => item === name) + return configFound ? { text: name, value: name } : '' + } + } + + function cancelCreateSecret(type) { + type = type ? type + '/' : '' + commit('wizard/temp$delete', `${type}createSecret/name`) + commit('wizard/temp$delete', `${type}createSecret/data`) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'cancelled', + }) + } + + async function onApplyconfigChange(type) { + type = type ? type + '/' : '' + const configValue = getValue(discriminator, `${type}applyConfig`) + + if (!configValue) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + const tempConfigObj = {} + configValue.forEach((item) => { + if (item.name) { + tempConfigObj[item.name] = item.content + } + }) + if (Object.keys(tempConfigObj).length === 0) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}applyConfig`, + value: tempConfigObj, + }) + } + + function setApplyConfig(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfiguration` + const selectedConfig = getValue(discriminator, configPath) + if (!selectedConfig) { + return [{ name: '', content: '' }] + } + const applyconfigData = secretConfigData.find((item) => { + if (item.componentName === selectedConfig) { + return item + } }) - return filteredSecrets + const { applyConfig } = applyconfigData + const configObj = [] + + if (applyConfig) { + Object.keys(applyConfig).forEach((fileName) => { + configObj.push({ + name: fileName, + content: applyConfig[fileName], + }) + }) + } else { + configObj.push({ name: '', content: '' }) + } + return configObj + } + + function onRemoveConfigChange(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfigurationRemove` + const selectedConfig = getValue(discriminator, configPath) + + if (!selectedConfig) { + commit('wizard/model$delete', `/spec/configuration/${type}removeCustomConfig`) + return [{ name: '', content: '' }] + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}removeCustomConfig`, + value: true, + }) + + const configuration = secretConfigData.find((item) => item.componentName === selectedConfig) + + if (!configuration.data) { + return [{ name: '', content: '' }] + } + + const configObj = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) + return configObj + } + + async function onNewConfigSecretChange(type) { + type = type ? type + '/' : '' + const path = `/spec/configuration/${type}configSecret/name` + const selectedSecret = getValue(model, path) + + if (!selectedSecret) { + commit('wizard/model$delete', `/spec/configuration/${type}configSecret`) + return [{ name: '', content: '' }] + } + if (selectedSecret === 'Create') return [{ name: '', content: '' }] + + const owner = storeGet('/route/params/user') + const cluster = storeGet('/route/params/cluster') + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + + try { + // Fetch the secret data from API + const secretResp = await axios.get( + `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets/${selectedSecret}`, + ) + + const secretData = secretResp.data?.data || {} + const configObj = [] + + // Decode base64 and format as array of objects with name and content + Object.keys(secretData).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(secretData[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: secretData[fileName], // Fallback to original if decode fails + }) + } + }) + + return configObj + } catch (e) { + console.error('Error fetching secret:', e) + return [{ name: '', content: '' }] + } + } + + function onSelectedSecretChange(index) { + const secretData = getValue(discriminator, 'createSecret/data') || [] + const selfSecrets = secretData.map((item) => item.key) + + const remainingSecrets = configSecretKeys.filter((item) => !selfSecrets.includes(item)) + + const selfKey = getValue(discriminator, `createSecret/data/${index}/key`) + if (selfKey) { + remainingSecrets.push(selfKey) + } + const resSecret = remainingSecrets.map((item) => { + return { text: item, value: item } + }) + return resSecret } function createSecretUrl() { @@ -983,26 +1371,6 @@ export const useFunc = (model) => { return reconfigurationType === value } - function onApplyconfigChange() { - const configPath = `/applyConfig` - const applyconfig = getValue(discriminator, configPath) - - const configObj = {} - - if (applyconfig) { - applyconfig.forEach((item) => { - const { key, value } = item - configObj[key] = value - }) - } - - commit('wizard/model$update', { - path: `/spec/configuration/applyConfig`, - value: configObj, - force: true, - }) - } - function onReconfigurationTypeChange() { setDiscriminatorValue(`/applyConfig`, []) const reconfigurationType = getValue(discriminator, '/reconfigurationType') @@ -1407,5 +1775,20 @@ export const useFunc = (model) => { onMachineChange, isMachineCustom, checkVolume, + fetchConfigSecrets, + getConfigSecretsforAppyConfig, + getSelectedConfigurationData, + getSelectedConfigurationName, + getSelectedConfigurationValueForRemove, + createNewConfigSecret, + decodeError, + isCreateSecret, + isNotCreateSecret, + onCreateSecretChange, + cancelCreateSecret, + setApplyConfig, + onRemoveConfigChange, + onNewConfigSecretChange, + onSelectedSecretChange, } } diff --git a/charts/opskubedbcom-pgbounceropsrequest-editor/ui/create-ui.yaml b/charts/opskubedbcom-pgbounceropsrequest-editor/ui/create-ui.yaml index b5817a0bc5..b162b03c98 100644 --- a/charts/opskubedbcom-pgbounceropsrequest-editor/ui/create-ui.yaml +++ b/charts/opskubedbcom-pgbounceropsrequest-editor/ui/create-ui.yaml @@ -219,109 +219,176 @@ step: if: name: ifRequestTypeEqualsTo|Reconfigure type: function + loader: + name: fetchConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace elements: - type: block-layout - label: PgBouncer Configuration + label: Configuration elements: - - type: radio - label: Reconfigure Type - subtitle: Choose the action you'd like to perform. Select a new configuration secret, apply a custom configuration , or remove an existing setup. + - type: label-element + label: '' + subtitle: Select a new configuration secret, apply a custom configuration, or remove an existing setup to update your database settings + - type: tab-layout + label: New Config Secret + schema: temp/properties/reconfigurationType options: - - text: Select New Config Secret + - text: NEW CONFIG SECRET value: selectNewConfigSecret - - text: Apply Config + - text: APPLY CONFIG value: applyConfig - - text: Remove + - text: REMOVE value: remove - watcher: - func: onReconfigurationTypeChange - paths: - - temp/properties/reconfigurationType - validation: - type: required - schema: temp/properties/reconfigurationType - - type: block-layout - label: PgBouncer config secret - showLabels: true - if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret - type: function elements: - - type: label-element - label: Select a configuration secret from the available list to update your database settings - customClass: mb-10 - - type: select - addNewButton: - label: Create a new Secret - target: _blank - url: - function: createSecretUrl + - type: block-layout label: Config Secret - loader: - name: getConfigSecrets - watchPaths: - - schema/properties/metadata/properties/namespace - init: - type: func - value: setValueFromDbDetails|/spec/configuration/pgbouncer/configSecret/name - refresh: true - validation: - type: required - schema: schema/properties/spec/properties/configuration/properties/pgbouncer/properties/configSecret/properties/name - - type: label-element - label: Selected Config Secret - loader: getSelectedConfigSecret - watcher: - func: getSelectedConfigSecret - paths: - - schema/properties/spec/properties/configuration/properties/pgbouncer/properties/configSecret/properties/name - - type: editor - label: Value - readonly: true - hasCopy: false - watcher: - func: getSelectedConfigSecretValue - paths: - - schema/properties/spec/properties/configuration/properties/pgbouncer/properties/configSecret/properties/name - validation: - type: required - schema: temp/properties/configArray/items/properties/value - - type: array-object-form - label: Apply Config - buttonClass: is-light is-outlined - if: - name: ifReconfigurationTypeEqualsTo|applyConfig - type: function - validation: - type: required - watcher: - func: onApplyconfigChange - paths: - - temp/properties/applyConfig - schema: temp/properties/applyConfig - elements: - - type: label-element - label: New Apply Config - subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings. Enter the parameter you want to configure (e.g., max_connections). - customClass: mb-10 - - type: input - label: key - validation: - type: required - schema: key - - type: editor - hasCopy: false - label: value - validation: - type: required - schema: value - - type: switch - schema: schema/properties/spec/properties/configuration/properties/pgbouncer/properties/removeCustomConfig - label: Remove CustomConfig - fullwidth: true - if: - name: returnFalse - type: function + if: + name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret + type: function + elements: + - type: label-element + label: Config Secret + subtitle: Select a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name + refresh: true + label: Config Secret + loader: + name: getConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace + - temp/properties/createSecret/properties/status + init: + type: func + value: setValueFromDbDetails|/spec/configSecret/name + watcher: + func: onCreateSecretChange + paths: + - temp/properties/createSecret/properties/status + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|create + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + - type: block-layout + label: Create a New Config Secret + showLabels: true + if: + type: function + name: isCreateSecret + hasButton: + text: Save + hasCancel: cancelCreateSecret + action: createNewConfigSecret + elements: + - type: input + label: Secret Name + schema: temp/properties/createSecret/properties/name + validation: + type: required + - type: array-object-form + label: String Data + schema: temp/properties/createSecret/properties/data + buttonClass: is-light is-outlined + validation: + type: required + elements: + - type: select + label: Key + schema: key + loader: + name: onSelectedSecretChange + watchPaths: + - temp/properties/createSecret/properties/data + validation: + type: required + - type: textarea + label: Value + schema: value + - type: multi-file-editor + editorHeight: 500px + readonly: true + if: + type: function + name: isNotCreateSecret + loader: + name: onNewConfigSecretChange + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + schema: temp/properties/newConfigSecret + - type: block-layout + label: ApplyConfig + if: + name: ifReconfigurationTypeEqualsTo|applyConfig + type: function + elements: + - type: label-element + label: New Apply Config + subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings.
Enter the parameter you want to configure (e.g., max_connections). + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfiguration + refresh: true + label: Configuration + loader: getConfigSecretsforAppyConfig + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|apply + watchPaths: + - temp/properties/selectedConfiguration + - type: multi-file-editor + editorHeight: 500px + schema: temp/properties/applyConfig + loader: + name: setApplyConfig + watchPaths: + - temp/properties/selectedConfiguration + watcher: + func: onApplyconfigChange + paths: + - temp/properties/applyConfig + - type: block-layout + label: Remove + if: + name: ifReconfigurationTypeEqualsTo|remove + type: function + elements: + - type: label-element + label: Remove + subtitle: Selected a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfigurationRemove + refresh: true + label: Configuration + loader: + name: getConfigSecretsforAppyConfig + watchPaths: + - schema/properties/metadata/properties/namespace + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|remove + watchPaths: + - temp/properties/selectedConfigurationRemove + - type: multi-file-editor + editorHeight: 500px + readonly: true + init: + type: func + value: onRemoveConfigChange + watcher: + func: onRemoveConfigChange + paths: + - temp/properties/selectedConfigurationRemove + schema: temp/properties/removeConfig # common - type: block-layout label: OpsRequest Options diff --git a/charts/opskubedbcom-pgbounceropsrequest-editor/ui/functions.js b/charts/opskubedbcom-pgbounceropsrequest-editor/ui/functions.js index 0c8a7360e9..0b5c680993 100644 --- a/charts/opskubedbcom-pgbounceropsrequest-editor/ui/functions.js +++ b/charts/opskubedbcom-pgbounceropsrequest-editor/ui/functions.js @@ -1,4 +1,4 @@ -const { axios, useOperator, store } = window.vueHelpers || {} +const { axios, useOperator, store, useToast } = window.vueHelpers || {} const machines = { 'db.t.micro': { resources: { @@ -307,9 +307,11 @@ const machineList = [ let machinesFromPreset = [] let secretArray = [] +const configSecretKeys = ['kubedb-user.ini'] export const useFunc = (model) => { const route = store.state?.route + const toast = useToast() const { getValue, storeGet, discriminator, setDiscriminatorValue, commit } = useOperator( model, @@ -829,29 +831,415 @@ export const useFunc = (model) => { return machine === 'custom' } - // for config secret - async function getConfigSecrets() { + // Fetch and store database Infos + // for secret configurations in reconfigure + let configSecrets = [] + let secretConfigData = [] + + async function fetchConfigSecrets() { const owner = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') const namespace = getValue(model, '/metadata/namespace') // watchDependency('model#/metadata/namespace') - const resp = await axios.get( - `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets`, + const name = getValue(model, '/spec/databaseRef/name') + const dbGroup = getValue(model, '/route/params/group') + const dbKind = getValue(store.state, '/resource/definition/result/kind') + const dbResource = getValue(model, '/route/params/resource') + const dbVersion = getValue(model, '/route/params/version') + + try { + const resp = await axios.post( + `/clusters/${owner}/${cluster}/proxy/ui.kubedb.com/v1alpha1/databaseinfos`, + { + apiVersion: 'ui.kubedb.com/v1alpha1', + kind: 'DatabaseInfo', + request: { + source: { + ref: { + name: name, + namespace: namespace, + }, + resource: { + group: dbGroup, + kind: dbKind, + name: dbResource, + version: dbVersion, + }, + }, + keys: ['kubedb-user.ini'], + }, + }, + ) + configSecrets = resp?.data?.response?.availableSecrets || [] + secretConfigData = resp?.data?.response?.configurations || [] + } catch (e) { + console.log(e) + } + } + + async function getConfigSecrets(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'success') { + await fetchConfigSecrets() + } + const mappedSecrets = configSecrets.map((item) => { + return { text: item, value: item } + }) + mappedSecrets.push({ text: '+ Create a new Secret', value: 'Create' }) + return mappedSecrets + } + + async function getConfigSecretsforAppyConfig() { + const secrets = secretConfigData.map((item) => { + return { text: item.componentName, value: item.componentName } + }) + return secrets + } + + function getSelectedConfigurationData(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfiguration` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return [] + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, ) - const secrets = (resp && resp.data && resp.data.items) || [] - secretArray = secrets + if (!configuration) { + return [] + } + + const result = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedContent = atob(configuration.data[fileName]) + result.push({ + name: fileName, + content: decodedContent, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + result.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) - const filteredSecrets = secrets + // Set the value to the model + commit('wizard/model$update', { + path: `/temp/${type}applyConfig`, + value: result, + force: true, + }) - filteredSecrets.map((item) => { - const name = (item.metadata && item.metadata.name) || '' - item.text = name - item.value = name - return true + return result + } + + function getSelectedConfigurationName(configType, type) { + type = type ? type + '/' : '' + let path = '' + if (configType === 'create') path = `/spec/configuration/${type}/configSecret/name` + else if (configType === 'apply') path = `/${type}selectedConfiguration` + else if (configType === 'remove') path = `/${type}selectedConfigurationRemove` + + const selectedConfiguration = + configType === 'create' ? getValue(model, path) : getValue(discriminator, path) + + if (selectedConfiguration) + return { subtitle: ` You have selected ${selectedConfiguration} secret` } + else return { subtitle: 'No secret selected' } + } + + function getSelectedConfigurationValueForRemove(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfigurationRemove` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return '' + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, + ) + + if (!configuration) { + return '' + } + + let data = {} + // Decode base64 and parse YAML for each key in the secret data + Object.keys(configuration.data).forEach((item) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[item]) + // Parse YAML string to object + const parsedYaml = yaml.load(decodedString) + // Store the parsed object with the filename as key + data[item] = parsedYaml + } catch (e) { + console.error(`Error parsing ${item}:`, e) + data[item] = atob(configuration.data[item]) // Fallback to decoded string + } + }) + + // Convert data object back to YAML string + return yaml.dump(data) + } + + async function createNewConfigSecret(type) { + type = type ? type + '/' : '' + const { user, cluster } = route.params + const url = `/clusters/${user}/${cluster}/resources` + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + const secretName = getValue(discriminator, `${type}createSecret/name`) + const secretData = getValue(discriminator, `${type}createSecret/data`) + const secretDataObj = Object.fromEntries(secretData.map((item) => [item.key, item.value])) + + try { + const res = await axios.post(url, { + apiVersion: 'v1', + stringData: secretDataObj, + kind: 'Secret', + metadata: { + name: secretName, + namespace: namespace, + }, + type: 'Opaque', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'success', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/lastCreatedSecret`, + value: secretName, + }) + toast.success('Secret created successfully') + } catch (error) { + const errMsg = decodeError(error, 'Failed to create secret') + toast.error(errMsg, { timeout: 5000 }) + cancelCreateSecret() + } + } + + function decodeError(msg, defaultMsg) { + if (typeof msg === 'string') { + return msg || defaultMsg + } + return ( + (msg.response && msg.response.data && msg.response.data.message) || + (msg.response && msg.response.data) || + (msg.status && msg.status.status) || + defaultMsg + ) + } + + function isCreateSecret(type) { + type = type ? type + '/' : '' + const selectedSecret = getValue(model, `spec/configuration/${type}configSecret/name`) + const res = selectedSecret === 'Create' + + if (res === true) { + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'pending', + }) + } + return res + } + + function isNotCreateSecret(type) { + return !isCreateSecret(type) + } + + function onCreateSecretChange(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'cancelled') return '' + else if (secretStatus === 'success') { + const name = getValue(discriminator, `${type}createSecret/lastCreatedSecret`) + + const configFound = configSecrets.find((item) => item === name) + return configFound ? { text: name, value: name } : '' + } + } + + function cancelCreateSecret(type) { + type = type ? type + '/' : '' + commit('wizard/temp$delete', `${type}createSecret/name`) + commit('wizard/temp$delete', `${type}createSecret/data`) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'cancelled', + }) + } + + async function onApplyconfigChange(type) { + type = type ? type + '/' : '' + const configValue = getValue(discriminator, `${type}applyConfig`) + + if (!configValue) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + const tempConfigObj = {} + configValue.forEach((item) => { + if (item.name) { + tempConfigObj[item.name] = item.content + } + }) + if (Object.keys(tempConfigObj).length === 0) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}applyConfig`, + value: tempConfigObj, + }) + } + + function setApplyConfig(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfiguration` + const selectedConfig = getValue(discriminator, configPath) + if (!selectedConfig) { + return [{ name: '', content: '' }] + } + const applyconfigData = secretConfigData.find((item) => { + if (item.componentName === selectedConfig) { + return item + } }) - return filteredSecrets + const { applyConfig } = applyconfigData + const configObj = [] + + if (applyConfig) { + Object.keys(applyConfig).forEach((fileName) => { + configObj.push({ + name: fileName, + content: applyConfig[fileName], + }) + }) + } else { + configObj.push({ name: '', content: '' }) + } + return configObj + } + + function onRemoveConfigChange(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfigurationRemove` + const selectedConfig = getValue(discriminator, configPath) + + if (!selectedConfig) { + commit('wizard/model$delete', `/spec/configuration/${type}removeCustomConfig`) + return [{ name: '', content: '' }] + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}removeCustomConfig`, + value: true, + }) + + const configuration = secretConfigData.find((item) => item.componentName === selectedConfig) + + if (!configuration.data) { + return [{ name: '', content: '' }] + } + + const configObj = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) + return configObj + } + + async function onNewConfigSecretChange(type) { + type = type ? type + '/' : '' + const path = `/spec/configuration/${type}configSecret/name` + const selectedSecret = getValue(model, path) + + if (!selectedSecret) { + commit('wizard/model$delete', `/spec/configuration/${type}configSecret`) + return [{ name: '', content: '' }] + } + if (selectedSecret === 'Create') return [{ name: '', content: '' }] + + const owner = storeGet('/route/params/user') + const cluster = storeGet('/route/params/cluster') + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + + try { + // Fetch the secret data from API + const secretResp = await axios.get( + `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets/${selectedSecret}`, + ) + + const secretData = secretResp.data?.data || {} + const configObj = [] + + // Decode base64 and format as array of objects with name and content + Object.keys(secretData).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(secretData[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: secretData[fileName], // Fallback to original if decode fails + }) + } + }) + + return configObj + } catch (e) { + console.error('Error fetching secret:', e) + return [{ name: '', content: '' }] + } + } + + function onSelectedSecretChange(index) { + const secretData = getValue(discriminator, 'createSecret/data') || [] + const selfSecrets = secretData.map((item) => item.key) + + const remainingSecrets = configSecretKeys.filter((item) => !selfSecrets.includes(item)) + + const selfKey = getValue(discriminator, `createSecret/data/${index}/key`) + if (selfKey) { + remainingSecrets.push(selfKey) + } + const resSecret = remainingSecrets.map((item) => { + return { text: item, value: item } + }) + return resSecret } function createSecretUrl() { @@ -975,26 +1363,6 @@ export const useFunc = (model) => { return reconfigurationType === value } - function onApplyconfigChange() { - const configPath = '/applyConfig' - const applyconfig = getValue(discriminator, configPath) - - const configObj = {} - - if (applyconfig) { - applyconfig.forEach((item) => { - const { key, value } = item - configObj[key] = value - }) - } - - commit('wizard/model$update', { - path: `/spec/configuration/pgbouncer/applyConfig`, - value: configObj, - force: true, - }) - } - function onReconfigurationTypeChange() { setDiscriminatorValue('/applyConfig', []) const reconfigurationType = getValue(discriminator, '/reconfigurationType') @@ -1350,5 +1718,20 @@ export const useFunc = (model) => { setMachine, onMachineChange, isMachineCustom, + fetchConfigSecrets, + getConfigSecretsforAppyConfig, + getSelectedConfigurationData, + getSelectedConfigurationName, + getSelectedConfigurationValueForRemove, + createNewConfigSecret, + decodeError, + isCreateSecret, + isNotCreateSecret, + onCreateSecretChange, + cancelCreateSecret, + setApplyConfig, + onRemoveConfigChange, + onNewConfigSecretChange, + onSelectedSecretChange, } } diff --git a/charts/opskubedbcom-pgpoolopsrequest-editor/ui/create-ui.yaml b/charts/opskubedbcom-pgpoolopsrequest-editor/ui/create-ui.yaml index 663aba85af..9caeba2ed6 100644 --- a/charts/opskubedbcom-pgpoolopsrequest-editor/ui/create-ui.yaml +++ b/charts/opskubedbcom-pgpoolopsrequest-editor/ui/create-ui.yaml @@ -221,112 +221,176 @@ step: if: name: ifRequestTypeEqualsTo|Reconfigure type: function + loader: + name: fetchConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace elements: - type: block-layout label: Configuration elements: - - type: radio - label: Reconfigure Type - subtitle: Choose the action you'd like to perform. Select a new configuration secret, apply a custom configuration , or remove an existing setup. - validation: - type: required + - type: label-element + label: '' + subtitle: Select a new configuration secret, apply a custom configuration, or remove an existing setup to update your database settings + - type: tab-layout + label: New Config Secret + schema: temp/properties/reconfigurationType options: - - text: New Config Secret + - text: NEW CONFIG SECRET value: selectNewConfigSecret - - text: Apply Config + - text: APPLY CONFIG value: applyConfig - - text: Remove + - text: REMOVE value: remove - schema: temp/properties/reconfigurationType - watcher: - func: onReconfigurationTypeChange - paths: - - temp/properties/reconfigurationType - - type: block-layout - label: Config Secret - showLabels: true - if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret - type: function elements: - - type: label-element - label: Select a configuration secret from the available list to update your database settings - customClass: mb-10 - - type: select - addNewButton: - label: Create a new Secret - target: _blank - url: - function: createSecretUrl - schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - refresh: true - label: Config Secret - loader: - name: getConfigSecrets - watchPaths: - - schema/properties/metadata/properties/namespace - init: - type: func - value: setValueFromDbDetails|/spec/configSecret/name - - type: label-element - label: Selected Config Secret - loader: getSelectedConfigSecret - watcher: - func: getSelectedConfigSecret - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - - type: editor - label: Value - readonly: true - hasCopy: false - if: - name: isConfigSelected - type: function - watcher: - func: getSelectedConfigSecretValue - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - schema: temp/properties/configArray/items/properties/value - - type: array-object-form - label: ApplyConfig - buttonClass: is-light is-outlined - schema: temp/properties/applyConfig - validation: - type: required - if: - name: ifReconfigurationTypeEqualsTo|applyConfig - type: function - watcher: - func: onApplyconfigChange - paths: - - temp/properties/applyConfig - elements: - - type: label-element - label: New Apply Config - subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings. Enter the parameter you want to configure (e.g., max_connections). - customClass: mb-10 - - type: input - label: key - validation: - type: required - schema: key - - type: editor - label: value - hasCopy: false - validation: - type: required - schema: value - - type: switch - schema: schema/properties/spec/properties/configuration/properties/removeCustomConfig - label: Remove CustomConfig - fullwidth: true - if: - name: returnFalse - type: function + - type: block-layout + label: Config Secret + if: + name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret + type: function + elements: + - type: label-element + label: Config Secret + subtitle: Select a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name + refresh: true + label: Config Secret + loader: + name: getConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace + - temp/properties/createSecret/properties/status + init: + type: func + value: setValueFromDbDetails|/spec/configSecret/name + watcher: + func: onCreateSecretChange + paths: + - temp/properties/createSecret/properties/status + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|create + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + - type: block-layout + label: Create a New Config Secret + showLabels: true + if: + type: function + name: isCreateSecret + hasButton: + text: Save + hasCancel: cancelCreateSecret + action: createNewConfigSecret + elements: + - type: input + label: Secret Name + schema: temp/properties/createSecret/properties/name + validation: + type: required + - type: array-object-form + label: String Data + schema: temp/properties/createSecret/properties/data + buttonClass: is-light is-outlined + validation: + type: required + elements: + - type: select + label: Key + schema: key + loader: + name: onSelectedSecretChange + watchPaths: + - temp/properties/createSecret/properties/data + validation: + type: required + - type: textarea + label: Value + schema: value + - type: multi-file-editor + editorHeight: 500px + readonly: true + if: + type: function + name: isNotCreateSecret + loader: + name: onNewConfigSecretChange + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + schema: temp/properties/newConfigSecret + - type: block-layout + label: ApplyConfig + if: + name: ifReconfigurationTypeEqualsTo|applyConfig + type: function + elements: + - type: label-element + label: New Apply Config + subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings.
Enter the parameter you want to configure (e.g., max_connections). + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfiguration + refresh: true + label: Configuration + loader: getConfigSecretsforAppyConfig + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|apply + watchPaths: + - temp/properties/selectedConfiguration + - type: multi-file-editor + editorHeight: 500px + schema: temp/properties/applyConfig + loader: + name: setApplyConfig + watchPaths: + - temp/properties/selectedConfiguration + watcher: + func: onApplyconfigChange + paths: + - temp/properties/applyConfig + - type: block-layout + label: Remove + if: + name: ifReconfigurationTypeEqualsTo|remove + type: function + elements: + - type: label-element + label: Remove + subtitle: Selected a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfigurationRemove + refresh: true + label: Configuration + loader: + name: getConfigSecretsforAppyConfig + watchPaths: + - schema/properties/metadata/properties/namespace + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|remove + watchPaths: + - temp/properties/selectedConfigurationRemove + - type: multi-file-editor + editorHeight: 500px + readonly: true + init: + type: func + value: onRemoveConfigChange + watcher: + func: onRemoveConfigChange + paths: + - temp/properties/selectedConfigurationRemove + schema: temp/properties/removeConfig # Reconfigure TLS - type: block-layout label: TLS diff --git a/charts/opskubedbcom-pgpoolopsrequest-editor/ui/functions.js b/charts/opskubedbcom-pgpoolopsrequest-editor/ui/functions.js index 378f894b80..94c6f3df62 100644 --- a/charts/opskubedbcom-pgpoolopsrequest-editor/ui/functions.js +++ b/charts/opskubedbcom-pgpoolopsrequest-editor/ui/functions.js @@ -1,4 +1,4 @@ -const { axios, useOperator, store } = window.vueHelpers || {} +const { axios, useOperator, store, useToast } = window.vueHelpers || {} const machines = { 'db.t.micro': { resources: { @@ -305,11 +305,13 @@ const machineList = [ 'db.r.24xlarge', ] +const configSecretKeys = ['*.conf'] let machinesFromPreset = [] let secretArray = [] export const useFunc = (model) => { const route = store.state?.route + const toast = useToast() const { getValue, storeGet, discriminator, setDiscriminatorValue, commit } = useOperator( model, @@ -844,29 +846,415 @@ export const useFunc = (model) => { return machine === 'custom' } - // for config secret - async function getConfigSecrets() { + // Fetch and store database Infos + // for secret configurations in reconfigure + let configSecrets = [] + let secretConfigData = [] + + async function fetchConfigSecrets() { const owner = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') const namespace = getValue(model, '/metadata/namespace') // watchDependency('model#/metadata/namespace') - const resp = await axios.get( - `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets`, + const name = getValue(model, '/spec/databaseRef/name') + const dbGroup = getValue(model, '/route/params/group') + const dbKind = getValue(store.state, '/resource/definition/result/kind') + const dbResource = getValue(model, '/route/params/resource') + const dbVersion = getValue(model, '/route/params/version') + + try { + const resp = await axios.post( + `/clusters/${owner}/${cluster}/proxy/ui.kubedb.com/v1alpha1/databaseinfos`, + { + apiVersion: 'ui.kubedb.com/v1alpha1', + kind: 'DatabaseInfo', + request: { + source: { + ref: { + name: name, + namespace: namespace, + }, + resource: { + group: dbGroup, + kind: dbKind, + name: dbResource, + version: dbVersion, + }, + }, + keys: ['*.conf'], + }, + }, + ) + configSecrets = resp?.data?.response?.availableSecrets || [] + secretConfigData = resp?.data?.response?.configurations || [] + } catch (e) { + console.log(e) + } + } + + async function getConfigSecrets(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'success') { + await fetchConfigSecrets() + } + const mappedSecrets = configSecrets.map((item) => { + return { text: item, value: item } + }) + mappedSecrets.push({ text: '+ Create a new Secret', value: 'Create' }) + return mappedSecrets + } + + async function getConfigSecretsforAppyConfig() { + const secrets = secretConfigData.map((item) => { + return { text: item.componentName, value: item.componentName } + }) + return secrets + } + + function getSelectedConfigurationData(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfiguration` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return [] + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, ) - const secrets = (resp && resp.data && resp.data.items) || [] - secretArray = secrets + if (!configuration) { + return [] + } - const filteredSecrets = secrets + const result = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedContent = atob(configuration.data[fileName]) + result.push({ + name: fileName, + content: decodedContent, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + result.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) - filteredSecrets.map((item) => { - const name = (item.metadata && item.metadata.name) || '' - item.text = name - item.value = name - return true + // Set the value to the model + commit('wizard/model$update', { + path: `/temp/${type}applyConfig`, + value: result, + force: true, }) - return filteredSecrets + + return result + } + + function getSelectedConfigurationName(configType, type) { + type = type ? type + '/' : '' + let path = '' + if (configType === 'create') path = `/spec/configuration/${type}/configSecret/name` + else if (configType === 'apply') path = `/${type}selectedConfiguration` + else if (configType === 'remove') path = `/${type}selectedConfigurationRemove` + + const selectedConfiguration = + configType === 'create' ? getValue(model, path) : getValue(discriminator, path) + + if (selectedConfiguration) + return { subtitle: ` You have selected ${selectedConfiguration} secret` } + else return { subtitle: 'No secret selected' } + } + + function getSelectedConfigurationValueForRemove(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfigurationRemove` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return '' + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, + ) + + if (!configuration) { + return '' + } + + let data = {} + // Decode base64 and parse YAML for each key in the secret data + Object.keys(configuration.data).forEach((item) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[item]) + // Parse YAML string to object + const parsedYaml = yaml.load(decodedString) + // Store the parsed object with the filename as key + data[item] = parsedYaml + } catch (e) { + console.error(`Error parsing ${item}:`, e) + data[item] = atob(configuration.data[item]) // Fallback to decoded string + } + }) + + // Convert data object back to YAML string + return yaml.dump(data) + } + + async function createNewConfigSecret(type) { + type = type ? type + '/' : '' + const { user, cluster } = route.params + const url = `/clusters/${user}/${cluster}/resources` + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + const secretName = getValue(discriminator, `${type}createSecret/name`) + const secretData = getValue(discriminator, `${type}createSecret/data`) + const secretDataObj = Object.fromEntries(secretData.map((item) => [item.key, item.value])) + + try { + const res = await axios.post(url, { + apiVersion: 'v1', + stringData: secretDataObj, + kind: 'Secret', + metadata: { + name: secretName, + namespace: namespace, + }, + type: 'Opaque', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'success', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/lastCreatedSecret`, + value: secretName, + }) + toast.success('Secret created successfully') + } catch (error) { + const errMsg = decodeError(error, 'Failed to create secret') + toast.error(errMsg, { timeout: 5000 }) + cancelCreateSecret() + } + } + + function decodeError(msg, defaultMsg) { + if (typeof msg === 'string') { + return msg || defaultMsg + } + return ( + (msg.response && msg.response.data && msg.response.data.message) || + (msg.response && msg.response.data) || + (msg.status && msg.status.status) || + defaultMsg + ) + } + + function isCreateSecret(type) { + type = type ? type + '/' : '' + const selectedSecret = getValue(model, `spec/configuration/${type}configSecret/name`) + const res = selectedSecret === 'Create' + + if (res === true) { + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'pending', + }) + } + return res + } + + function isNotCreateSecret(type) { + return !isCreateSecret(type) + } + + function onCreateSecretChange(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'cancelled') return '' + else if (secretStatus === 'success') { + const name = getValue(discriminator, `${type}createSecret/lastCreatedSecret`) + + const configFound = configSecrets.find((item) => item === name) + return configFound ? { text: name, value: name } : '' + } + } + + function cancelCreateSecret(type) { + type = type ? type + '/' : '' + commit('wizard/temp$delete', `${type}createSecret/name`) + commit('wizard/temp$delete', `${type}createSecret/data`) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'cancelled', + }) + } + + async function onApplyconfigChange(type) { + type = type ? type + '/' : '' + const configValue = getValue(discriminator, `${type}applyConfig`) + + if (!configValue) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + const tempConfigObj = {} + configValue.forEach((item) => { + if (item.name) { + tempConfigObj[item.name] = item.content + } + }) + if (Object.keys(tempConfigObj).length === 0) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}applyConfig`, + value: tempConfigObj, + }) + } + + function setApplyConfig(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfiguration` + const selectedConfig = getValue(discriminator, configPath) + if (!selectedConfig) { + return [{ name: '', content: '' }] + } + const applyconfigData = secretConfigData.find((item) => { + if (item.componentName === selectedConfig) { + return item + } + }) + const { applyConfig } = applyconfigData + const configObj = [] + + if (applyConfig) { + Object.keys(applyConfig).forEach((fileName) => { + configObj.push({ + name: fileName, + content: applyConfig[fileName], + }) + }) + } else { + configObj.push({ name: '', content: '' }) + } + return configObj + } + + function onRemoveConfigChange(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfigurationRemove` + const selectedConfig = getValue(discriminator, configPath) + + if (!selectedConfig) { + commit('wizard/model$delete', `/spec/configuration/${type}removeCustomConfig`) + return [{ name: '', content: '' }] + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}removeCustomConfig`, + value: true, + }) + + const configuration = secretConfigData.find((item) => item.componentName === selectedConfig) + + if (!configuration.data) { + return [{ name: '', content: '' }] + } + + const configObj = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) + return configObj + } + + async function onNewConfigSecretChange(type) { + type = type ? type + '/' : '' + const path = `/spec/configuration/${type}configSecret/name` + const selectedSecret = getValue(model, path) + + if (!selectedSecret) { + commit('wizard/model$delete', `/spec/configuration/${type}configSecret`) + return [{ name: '', content: '' }] + } + if (selectedSecret === 'Create') return [{ name: '', content: '' }] + + const owner = storeGet('/route/params/user') + const cluster = storeGet('/route/params/cluster') + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + + try { + // Fetch the secret data from API + const secretResp = await axios.get( + `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets/${selectedSecret}`, + ) + + const secretData = secretResp.data?.data || {} + const configObj = [] + + // Decode base64 and format as array of objects with name and content + Object.keys(secretData).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(secretData[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: secretData[fileName], // Fallback to original if decode fails + }) + } + }) + + return configObj + } catch (e) { + console.error('Error fetching secret:', e) + return [{ name: '', content: '' }] + } + } + + function onSelectedSecretChange(index) { + const secretData = getValue(discriminator, 'createSecret/data') || [] + const selfSecrets = secretData.map((item) => item.key) + + const remainingSecrets = configSecretKeys.filter((item) => !selfSecrets.includes(item)) + + const selfKey = getValue(discriminator, `createSecret/data/${index}/key`) + if (selfKey) { + remainingSecrets.push(selfKey) + } + const resSecret = remainingSecrets.map((item) => { + return { text: item, value: item } + }) + return resSecret } function isConfigSelected() { @@ -1050,24 +1438,6 @@ export const useFunc = (model) => { return reconfigurationType === value } - function onApplyconfigChange() { - const applyconfig = getValue(discriminator, '/applyConfig') - - const configObj = {} - if (applyconfig) { - applyconfig.forEach((item) => { - const { key, value } = item - configObj[key] = value - }) - } - - commit('wizard/model$update', { - path: '/spec/configuration/applyConfig', - value: configObj, - force: true, - }) - } - function onReconfigurationTypeChange() { const reconfigurationType = getValue(discriminator, '/reconfigurationType') setDiscriminatorValue('/applyConfig', []) @@ -1382,5 +1752,20 @@ export const useFunc = (model) => { onMachineChange, isMachineCustom, objectToYaml, + fetchConfigSecrets, + getConfigSecretsforAppyConfig, + getSelectedConfigurationData, + getSelectedConfigurationName, + getSelectedConfigurationValueForRemove, + createNewConfigSecret, + decodeError, + isCreateSecret, + isNotCreateSecret, + onCreateSecretChange, + cancelCreateSecret, + setApplyConfig, + onRemoveConfigChange, + onNewConfigSecretChange, + onSelectedSecretChange, } } diff --git a/charts/opskubedbcom-postgresopsrequest-editor/ui/create-ui.yaml b/charts/opskubedbcom-postgresopsrequest-editor/ui/create-ui.yaml index d97b7ee8ac..33776d0d0a 100644 --- a/charts/opskubedbcom-postgresopsrequest-editor/ui/create-ui.yaml +++ b/charts/opskubedbcom-postgresopsrequest-editor/ui/create-ui.yaml @@ -269,111 +269,177 @@ step: if: name: ifRequestTypeEqualsTo|Reconfigure type: function + loader: + name: fetchConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace elements: - type: block-layout label: Configuration - showLabels: false elements: - - type: radio - label: Reconfigure Type + - type: label-element + label: '' + subtitle: Select a new configuration secret, apply a custom configuration, or remove an existing setup to update your database settings + - type: tab-layout + label: New Config Secret + schema: temp/properties/reconfigurationType options: - - text: Select New Config Secret + - text: NEW CONFIG SECRET value: selectNewConfigSecret - - text: Apply Config + - text: APPLY CONFIG value: applyConfig - - text: Remove + - text: REMOVE value: remove - watcher: - func: onReconfigurationTypeChange - paths: - - temp/properties/reconfigurationType - validation: - type: required - schema: temp/properties/reconfigurationType - - type: block-layout - label: Config Secret - showLabels: true - if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret - type: function elements: - - type: label-element - label: Select a configuration secret from the available list to update your database settings - customClass: mb-10 - - type: select - addNewButton: - label: Create a new Secret - target: _blank - url: - function: createSecretUrl - schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - refresh: true - label: Config Secret - loader: - name: getConfigSecrets - watchPaths: - - schema/properties/metadata/properties/namespace - init: - type: func - value: setValueFromDbDetails|/spec/configSecret/name - - type: label-element - label: Selected Config Secret - loader: getSelectedConfigSecret - if: - name: isConfigSelected - type: function - watcher: - func: getSelectedConfigSecret - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - - type: editor - label: Value - readonly: true - hasCopy: false - if: - name: isConfigSelected - type: function - loader: - name: getSelectedConfigSecretValue - watchPaths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - schema: temp/properties/configArray/items/properties/value - - type: array-object-form - label: Apply Config - buttonClass: is-light is-outlined - if: - name: ifReconfigurationTypeEqualsTo|applyConfig - type: function - validation: - type: required - watcher: - func: onApplyconfigChange - paths: - - temp/properties/applyConfig - schema: temp/properties/applyConfig - elements: - - type: input - label: key - validation: - type: required - schema: key - - type: editor - hasCopy: false - label: value - validation: - type: required - schema: value - - type: switch - schema: schema/properties/spec/properties/configuration/properties/removeCustomConfig - label: Remove CustomConfig - fullwidth: true - if: - name: returnFalse - type: function + - type: block-layout + label: Config Secret + if: + name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret + type: function + elements: + - type: label-element + label: Config Secret + subtitle: Select a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name + refresh: true + label: Config Secret + loader: + name: getConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace + - temp/properties/createSecret/properties/status + init: + type: func + value: setValueFromDbDetails|/spec/configSecret/name + watcher: + func: onCreateSecretChange + paths: + - temp/properties/createSecret/properties/status + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|create + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + - type: block-layout + label: Create a New Config Secret + showLabels: true + if: + type: function + name: isCreateSecret + hasButton: + text: Save + hasCancel: cancelCreateSecret + action: createNewConfigSecret + elements: + - type: input + label: Secret Name + schema: temp/properties/createSecret/properties/name + validation: + type: required + - type: array-object-form + label: String Data + schema: temp/properties/createSecret/properties/data + buttonClass: is-light is-outlined + validation: + type: required + elements: + - type: select + label: Key + schema: key + loader: + name: onSelectedSecretChange + watchPaths: + - temp/properties/createSecret/properties/data + validation: + type: required + - type: textarea + label: Value + schema: value + - type: multi-file-editor + editorHeight: 500px + readonly: true + if: + type: function + name: isNotCreateSecret + loader: + name: onNewConfigSecretChange + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + schema: temp/properties/newConfigSecret + - type: block-layout + label: ApplyConfig + if: + name: ifReconfigurationTypeEqualsTo|applyConfig + type: function + elements: + - type: label-element + label: New Apply Config + subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings.
Enter the parameter you want to configure (e.g., max_connections). + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfiguration + refresh: true + label: Configuration + loader: getConfigSecretsforAppyConfig + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|apply + watchPaths: + - temp/properties/selectedConfiguration + - type: multi-file-editor + editorHeight: 500px + schema: temp/properties/applyConfig + loader: + name: setApplyConfig + watchPaths: + - temp/properties/selectedConfiguration + watcher: + func: onApplyconfigChange + paths: + - temp/properties/applyConfig + - type: block-layout + label: Remove + if: + name: ifReconfigurationTypeEqualsTo|remove + type: function + elements: + - type: label-element + label: Remove + subtitle: Selected a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfigurationRemove + refresh: true + label: Configuration + loader: + name: getConfigSecretsforAppyConfig + watchPaths: + - schema/properties/metadata/properties/namespace + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|remove + watchPaths: + - temp/properties/selectedConfigurationRemove + - type: multi-file-editor + editorHeight: 500px + readonly: true + init: + type: func + value: onRemoveConfigChange + watcher: + func: onRemoveConfigChange + paths: + - temp/properties/selectedConfigurationRemove + schema: temp/properties/removeConfig + # Reconfigure TLS - type: block-layout label: TLS diff --git a/charts/opskubedbcom-postgresopsrequest-editor/ui/functions.js b/charts/opskubedbcom-postgresopsrequest-editor/ui/functions.js index f8a1d8f938..709b82118b 100644 --- a/charts/opskubedbcom-postgresopsrequest-editor/ui/functions.js +++ b/charts/opskubedbcom-postgresopsrequest-editor/ui/functions.js @@ -1,4 +1,4 @@ -const { axios, useOperator, store } = window.vueHelpers || {} +const { axios, useOperator, store, useToast } = window.vueHelpers || {} const machines = { 'db.t.micro': { resources: { @@ -304,11 +304,12 @@ const machineList = [ 'db.r.16xlarge', 'db.r.24xlarge', ] - +const configSecretKeys = ['user.conf'] let machinesFromPreset = [] export const useFunc = (model) => { const route = store.state?.route + const toast = useToast() const { getValue, storeGet, discriminator, setDiscriminatorValue, commit } = useOperator( model, @@ -834,32 +835,419 @@ export const useFunc = (model) => { return machine === 'custom' } - // for config secret - let secretArray = [] - async function getConfigSecrets() { + // Fetch and store database Infos + // for secret configurations in reconfigure + let configSecrets = [] + let secretConfigData = [] + + async function fetchConfigSecrets() { const owner = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') const namespace = getValue(model, '/metadata/namespace') // watchDependency('model#/metadata/namespace') - const resp = await axios.get( - `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets`, + const name = getValue(model, '/spec/databaseRef/name') + const dbGroup = getValue(model, '/route/params/group') + const dbKind = getValue(store.state, '/resource/definition/result/kind') + const dbResource = getValue(model, '/route/params/resource') + const dbVersion = getValue(model, '/route/params/version') + + try { + const resp = await axios.post( + `/clusters/${owner}/${cluster}/proxy/ui.kubedb.com/v1alpha1/databaseinfos`, + { + apiVersion: 'ui.kubedb.com/v1alpha1', + kind: 'DatabaseInfo', + request: { + source: { + ref: { + name: name, + namespace: namespace, + }, + resource: { + group: dbGroup, + kind: dbKind, + name: dbResource, + version: dbVersion, + }, + }, + keys: ['user.conf'], + }, + }, + ) + configSecrets = resp?.data?.response?.availableSecrets || [] + secretConfigData = resp?.data?.response?.configurations || [] + } catch (e) { + console.log(e) + } + } + + async function getConfigSecrets(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'success') { + await fetchConfigSecrets() + } + const mappedSecrets = configSecrets.map((item) => { + return { text: item, value: item } + }) + mappedSecrets.push({ text: '+ Create a new Secret', value: 'Create' }) + return mappedSecrets + } + + async function getConfigSecretsforAppyConfig() { + const secrets = secretConfigData.map((item) => { + return { text: item.componentName, value: item.componentName } + }) + return secrets + } + + function getSelectedConfigurationData(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfiguration` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return [] + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, ) - const secrets = (resp && resp.data && resp.data.items) || [] - secretArray = secrets + if (!configuration) { + return [] + } - const filteredSecrets = secrets + const result = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedContent = atob(configuration.data[fileName]) + result.push({ + name: fileName, + content: decodedContent, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + result.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) - filteredSecrets.map((item) => { - const name = (item.metadata && item.metadata.name) || '' - item.text = name - item.value = name - return true + // Set the value to the model + commit('wizard/model$update', { + path: `/temp/${type}applyConfig`, + value: result, + force: true, + }) + + return result + } + + function getSelectedConfigurationName(configType, type) { + type = type ? type + '/' : '' + let path = '' + if (configType === 'create') path = `/spec/configuration/${type}/configSecret/name` + else if (configType === 'apply') path = `/${type}selectedConfiguration` + else if (configType === 'remove') path = `/${type}selectedConfigurationRemove` + + const selectedConfiguration = + configType === 'create' ? getValue(model, path) : getValue(discriminator, path) + + if (selectedConfiguration) + return { subtitle: ` You have selected ${selectedConfiguration} secret` } + else return { subtitle: 'No secret selected' } + } + + function getSelectedConfigurationValueForRemove(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfigurationRemove` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return '' + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, + ) + + if (!configuration) { + return '' + } + + let data = {} + // Decode base64 and parse YAML for each key in the secret data + Object.keys(configuration.data).forEach((item) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[item]) + // Parse YAML string to object + const parsedYaml = yaml.load(decodedString) + // Store the parsed object with the filename as key + data[item] = parsedYaml + } catch (e) { + console.error(`Error parsing ${item}:`, e) + data[item] = atob(configuration.data[item]) // Fallback to decoded string + } + }) + + // Convert data object back to YAML string + return yaml.dump(data) + } + + async function createNewConfigSecret(type) { + type = type ? type + '/' : '' + const { user, cluster } = route.params + const url = `/clusters/${user}/${cluster}/resources` + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + const secretName = getValue(discriminator, `${type}createSecret/name`) + const secretData = getValue(discriminator, `${type}createSecret/data`) + const secretDataObj = Object.fromEntries(secretData.map((item) => [item.key, item.value])) + + try { + const res = await axios.post(url, { + apiVersion: 'v1', + stringData: secretDataObj, + kind: 'Secret', + metadata: { + name: secretName, + namespace: namespace, + }, + type: 'Opaque', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'success', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/lastCreatedSecret`, + value: secretName, + }) + toast.success('Secret created successfully') + } catch (error) { + const errMsg = decodeError(error, 'Failed to create secret') + toast.error(errMsg, { timeout: 5000 }) + cancelCreateSecret() + } + } + + function decodeError(msg, defaultMsg) { + if (typeof msg === 'string') { + return msg || defaultMsg + } + return ( + (msg.response && msg.response.data && msg.response.data.message) || + (msg.response && msg.response.data) || + (msg.status && msg.status.status) || + defaultMsg + ) + } + + function isCreateSecret(type) { + type = type ? type + '/' : '' + const selectedSecret = getValue(model, `spec/configuration/${type}configSecret/name`) + const res = selectedSecret === 'Create' + + if (res === true) { + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'pending', + }) + } + return res + } + + function isNotCreateSecret(type) { + return !isCreateSecret(type) + } + + function onCreateSecretChange(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'cancelled') return '' + else if (secretStatus === 'success') { + const name = getValue(discriminator, `${type}createSecret/lastCreatedSecret`) + + const configFound = configSecrets.find((item) => item === name) + return configFound ? { text: name, value: name } : '' + } + } + + function cancelCreateSecret(type) { + type = type ? type + '/' : '' + commit('wizard/temp$delete', `${type}createSecret/name`) + commit('wizard/temp$delete', `${type}createSecret/data`) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'cancelled', + }) + } + + async function onApplyconfigChange(type) { + type = type ? type + '/' : '' + const configValue = getValue(discriminator, `${type}applyConfig`) + + if (!configValue) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + const tempConfigObj = {} + configValue.forEach((item) => { + if (item.name) { + tempConfigObj[item.name] = item.content + } + }) + if (Object.keys(tempConfigObj).length === 0) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}applyConfig`, + value: tempConfigObj, + }) + } + + function setApplyConfig(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfiguration` + const selectedConfig = getValue(discriminator, configPath) + if (!selectedConfig) { + return [{ name: '', content: '' }] + } + const applyconfigData = secretConfigData.find((item) => { + if (item.componentName === selectedConfig) { + return item + } + }) + const { applyConfig } = applyconfigData + const configObj = [] + + if (applyConfig) { + Object.keys(applyConfig).forEach((fileName) => { + configObj.push({ + name: fileName, + content: applyConfig[fileName], + }) + }) + } else { + configObj.push({ name: '', content: '' }) + } + return configObj + } + + function onRemoveConfigChange(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfigurationRemove` + const selectedConfig = getValue(discriminator, configPath) + + if (!selectedConfig) { + commit('wizard/model$delete', `/spec/configuration/${type}removeCustomConfig`) + return [{ name: '', content: '' }] + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}removeCustomConfig`, + value: true, }) - return filteredSecrets + + const configuration = secretConfigData.find((item) => item.componentName === selectedConfig) + + if (!configuration.data) { + return [{ name: '', content: '' }] + } + + const configObj = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) + return configObj } + async function onNewConfigSecretChange(type) { + type = type ? type + '/' : '' + const path = `/spec/configuration/${type}configSecret/name` + const selectedSecret = getValue(model, path) + + if (!selectedSecret) { + commit('wizard/model$delete', `/spec/configuration/${type}configSecret`) + return [{ name: '', content: '' }] + } + if (selectedSecret === 'Create') return [{ name: '', content: '' }] + + const owner = storeGet('/route/params/user') + const cluster = storeGet('/route/params/cluster') + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + + try { + // Fetch the secret data from API + const secretResp = await axios.get( + `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets/${selectedSecret}`, + ) + + const secretData = secretResp.data?.data || {} + const configObj = [] + + // Decode base64 and format as array of objects with name and content + Object.keys(secretData).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(secretData[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: secretData[fileName], // Fallback to original if decode fails + }) + } + }) + + return configObj + } catch (e) { + console.error('Error fetching secret:', e) + return [{ name: '', content: '' }] + } + } + + function onSelectedSecretChange(index) { + const secretData = getValue(discriminator, 'createSecret/data') || [] + const selfSecrets = secretData.map((item) => item.key) + + const remainingSecrets = configSecretKeys.filter((item) => !selfSecrets.includes(item)) + + const selfKey = getValue(discriminator, `createSecret/data/${index}/key`) + if (selfKey) { + remainingSecrets.push(selfKey) + } + const resSecret = remainingSecrets.map((item) => { + return { text: item, value: item } + }) + return resSecret + } + + let secretArray = [] + function createSecretUrl() { const user = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') @@ -985,25 +1373,6 @@ export const useFunc = (model) => { return reconfigurationType === value } - function onApplyconfigChange() { - const applyconfig = getValue(discriminator, '/applyConfig') - - const configObj = {} - - if (applyconfig) { - applyconfig.forEach((item) => { - const { key, value } = item - configObj[key] = value - }) - } - - commit('wizard/model$update', { - path: '/spec/configuration/applyConfig', - value: configObj, - force: true, - }) - } - function onReconfigurationTypeChange() { const reconfigurationType = getValue(discriminator, '/reconfigurationType') setDiscriminatorValue('/applyConfig', []) @@ -1316,13 +1685,6 @@ export const useFunc = (model) => { return !!(model && model.alias) } - function getSelectedConfigSecret() { - const path = `/spec/configuration/configSecret/name` - const selectedSecret = getValue(model, path) - // watchDependency(`model#${path}`) - return `You have selected ${selectedSecret} secret` || 'No secret selected' - } - function objectToYaml(obj, indent = 0) { if (obj === null || obj === undefined) return 'null' if (typeof obj !== 'object') return JSON.stringify(obj) @@ -1500,7 +1862,21 @@ export const useFunc = (model) => { onMachineChange, isMachineCustom, checkVolume, - getSelectedConfigSecret, getSelectedConfigSecretValue, + fetchConfigSecrets, + getConfigSecretsforAppyConfig, + getSelectedConfigurationData, + getSelectedConfigurationName, + getSelectedConfigurationValueForRemove, + createNewConfigSecret, + decodeError, + isCreateSecret, + isNotCreateSecret, + onCreateSecretChange, + cancelCreateSecret, + setApplyConfig, + onRemoveConfigChange, + onNewConfigSecretChange, + onSelectedSecretChange, } } diff --git a/charts/opskubedbcom-rabbitmqopsrequest-editor/ui/create-ui.yaml b/charts/opskubedbcom-rabbitmqopsrequest-editor/ui/create-ui.yaml index 656b3d080b..9a36e8c736 100644 --- a/charts/opskubedbcom-rabbitmqopsrequest-editor/ui/create-ui.yaml +++ b/charts/opskubedbcom-rabbitmqopsrequest-editor/ui/create-ui.yaml @@ -273,114 +273,176 @@ step: if: name: ifRequestTypeEqualsTo|Reconfigure type: function + loader: + name: fetchConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace elements: - type: block-layout label: Configuration - showLabels: false elements: - - type: radio - label: Reconfigure Type + - type: label-element + label: '' + subtitle: Select a new configuration secret, apply a custom configuration, or remove an existing setup to update your database settings + - type: tab-layout + label: New Config Secret + schema: temp/properties/reconfigurationType options: - - text: New Config Secret + - text: NEW CONFIG SECRET value: selectNewConfigSecret - - text: Apply Config + - text: APPLY CONFIG value: applyConfig - - text: Remove + - text: REMOVE value: remove - watcher: - func: onReconfigurationTypeChange - paths: - - temp/properties/reconfigurationType - validation: - type: required - schema: temp/properties/reconfigurationType - - type: block-layout - label: Config Secret - showLabels: true - if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret - type: function - elements: - - type: label-element - label: Select a configuration secret from the available list to update your database settings - customClass: mb-10 - - type: select - addNewButton: - label: Create a new Secret - target: _blank - url: - function: createSecretUrl - label: Config Secret - if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret - type: function - loader: - name: getConfigSecrets - watchPaths: - - schema/properties/metadata/properties/namespace - init: - type: func - value: setValueFromDbDetails|/spec/configSecret/name - refresh: true - validation: - type: required - schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name - - type: label-element - label: Selected Config Secret - loader: getSelectedConfigSecret - if: - name: isConfigSelected - type: function - watcher: - func: getSelectedConfigSecret - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - - type: editor - label: Value - readonly: true - hasCopy: false - if: - name: isConfigSelected - type: function - loader: - name: getSelectedConfigSecretValue - watchPaths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - schema: temp/properties/configArray/items/properties/value - - type: array-object-form - label: Apply Config - buttonClass: is-light is-outlined - if: - name: ifReconfigurationTypeEqualsTo|applyConfig - type: function - validation: - type: required - watcher: - func: onApplyconfigChange - paths: - - temp/properties/applyConfig - schema: temp/properties/applyConfig elements: - - type: input - label: key - validation: - type: required - schema: key - - type: editor - hasCopy: false - label: value - validation: - type: required - schema: value - - type: switch - schema: schema/properties/spec/properties/configuration/properties/removeCustomConfig - label: Remove CustomConfig - fullwidth: true - if: - name: returnFalse - type: function + - type: block-layout + label: Config Secret + if: + name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret + type: function + elements: + - type: label-element + label: Config Secret + subtitle: Select a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name + refresh: true + label: Config Secret + loader: + name: getConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace + - temp/properties/createSecret/properties/status + init: + type: func + value: setValueFromDbDetails|/spec/configSecret/name + watcher: + func: onCreateSecretChange + paths: + - temp/properties/createSecret/properties/status + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|create + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + - type: block-layout + label: Create a New Config Secret + showLabels: true + if: + type: function + name: isCreateSecret + hasButton: + text: Save + hasCancel: cancelCreateSecret + action: createNewConfigSecret + elements: + - type: input + label: Secret Name + schema: temp/properties/createSecret/properties/name + validation: + type: required + - type: array-object-form + label: String Data + schema: temp/properties/createSecret/properties/data + buttonClass: is-light is-outlined + validation: + type: required + elements: + - type: select + label: Key + schema: key + loader: + name: onSelectedSecretChange + watchPaths: + - temp/properties/createSecret/properties/data + validation: + type: required + - type: textarea + label: Value + schema: value + - type: multi-file-editor + editorHeight: 500px + readonly: true + if: + type: function + name: isNotCreateSecret + loader: + name: onNewConfigSecretChange + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + schema: temp/properties/newConfigSecret + - type: block-layout + label: ApplyConfig + if: + name: ifReconfigurationTypeEqualsTo|applyConfig + type: function + elements: + - type: label-element + label: New Apply Config + subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings.
Enter the parameter you want to configure (e.g., max_connections). + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfiguration + refresh: true + label: Configuration + loader: getConfigSecretsforAppyConfig + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|apply + watchPaths: + - temp/properties/selectedConfiguration + - type: multi-file-editor + editorHeight: 500px + schema: temp/properties/applyConfig + loader: + name: setApplyConfig + watchPaths: + - temp/properties/selectedConfiguration + watcher: + func: onApplyconfigChange + paths: + - temp/properties/applyConfig + - type: block-layout + label: Remove + if: + name: ifReconfigurationTypeEqualsTo|remove + type: function + elements: + - type: label-element + label: Remove + subtitle: Selected a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfigurationRemove + refresh: true + label: Configuration + loader: + name: getConfigSecretsforAppyConfig + watchPaths: + - schema/properties/metadata/properties/namespace + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|remove + watchPaths: + - temp/properties/selectedConfigurationRemove + - type: multi-file-editor + editorHeight: 500px + readonly: true + init: + type: func + value: onRemoveConfigChange + watcher: + func: onRemoveConfigChange + paths: + - temp/properties/selectedConfigurationRemove + schema: temp/properties/removeConfig # Reconfigure TLS - type: block-layout label: TLS diff --git a/charts/opskubedbcom-rabbitmqopsrequest-editor/ui/functions.js b/charts/opskubedbcom-rabbitmqopsrequest-editor/ui/functions.js index 890c16c645..badf868eea 100644 --- a/charts/opskubedbcom-rabbitmqopsrequest-editor/ui/functions.js +++ b/charts/opskubedbcom-rabbitmqopsrequest-editor/ui/functions.js @@ -1,4 +1,4 @@ -const { axios, useOperator, store } = window.vueHelpers || {} +const { axios, useOperator, store, useToast } = window.vueHelpers || {} const machines = { 'db.t.micro': { resources: { @@ -305,10 +305,12 @@ const machineList = [ 'db.r.24xlarge', ] +const configSecretKeys = ['rabbitmq.conf'] let machinesFromPreset = [] export const useFunc = (model) => { const route = store.state?.route + const toast = useToast() const { getValue, storeGet, discriminator, setDiscriminatorValue, commit } = useOperator( model, @@ -821,31 +823,419 @@ export const useFunc = (model) => { return machine === 'custom' } - let secretArray = [] - async function getConfigSecrets() { + // Fetch and store database Infos + // for secret configurations in reconfigure + let configSecrets = [] + let secretConfigData = [] + + async function fetchConfigSecrets() { const owner = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') const namespace = getValue(model, '/metadata/namespace') // watchDependency('model#/metadata/namespace') - const resp = await axios.get( - `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets`, + const name = getValue(model, '/spec/databaseRef/name') + const dbGroup = getValue(model, '/route/params/group') + const dbKind = getValue(store.state, '/resource/definition/result/kind') + const dbResource = getValue(model, '/route/params/resource') + const dbVersion = getValue(model, '/route/params/version') + + try { + const resp = await axios.post( + `/clusters/${owner}/${cluster}/proxy/ui.kubedb.com/v1alpha1/databaseinfos`, + { + apiVersion: 'ui.kubedb.com/v1alpha1', + kind: 'DatabaseInfo', + request: { + source: { + ref: { + name: name, + namespace: namespace, + }, + resource: { + group: dbGroup, + kind: dbKind, + name: dbResource, + version: dbVersion, + }, + }, + keys: ['rabbitmq.conf'], + }, + }, + ) + configSecrets = resp?.data?.response?.availableSecrets || [] + secretConfigData = resp?.data?.response?.configurations || [] + } catch (e) { + console.log(e) + } + } + + async function getConfigSecrets(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'success') { + await fetchConfigSecrets() + } + const mappedSecrets = configSecrets.map((item) => { + return { text: item, value: item } + }) + mappedSecrets.push({ text: '+ Create a new Secret', value: 'Create' }) + return mappedSecrets + } + + async function getConfigSecretsforAppyConfig() { + const secrets = secretConfigData.map((item) => { + return { text: item.componentName, value: item.componentName } + }) + return secrets + } + + function getSelectedConfigurationData(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfiguration` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return [] + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, ) - const secrets = (resp && resp.data && resp.data.items) || [] + if (!configuration) { + return [] + } - const filteredSecrets = secrets + const result = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedContent = atob(configuration.data[fileName]) + result.push({ + name: fileName, + content: decodedContent, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + result.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) - filteredSecrets.map((item) => { - const name = (item.metadata && item.metadata.name) || '' - item.text = name - item.value = name - return true + // Set the value to the model + commit('wizard/model$update', { + path: `/temp/${type}applyConfig`, + value: result, + force: true, + }) + + return result + } + + function getSelectedConfigurationName(configType, type) { + type = type ? type + '/' : '' + let path = '' + if (configType === 'create') path = `/spec/configuration/${type}/configSecret/name` + else if (configType === 'apply') path = `/${type}selectedConfiguration` + else if (configType === 'remove') path = `/${type}selectedConfigurationRemove` + + const selectedConfiguration = + configType === 'create' ? getValue(model, path) : getValue(discriminator, path) + + if (selectedConfiguration) + return { subtitle: ` You have selected ${selectedConfiguration} secret` } + else return { subtitle: 'No secret selected' } + } + + function getSelectedConfigurationValueForRemove(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfigurationRemove` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return '' + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, + ) + + if (!configuration) { + return '' + } + + let data = {} + // Decode base64 and parse YAML for each key in the secret data + Object.keys(configuration.data).forEach((item) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[item]) + // Parse YAML string to object + const parsedYaml = yaml.load(decodedString) + // Store the parsed object with the filename as key + data[item] = parsedYaml + } catch (e) { + console.error(`Error parsing ${item}:`, e) + data[item] = atob(configuration.data[item]) // Fallback to decoded string + } }) - secretArray = secrets - return filteredSecrets + + // Convert data object back to YAML string + return yaml.dump(data) } + async function createNewConfigSecret(type) { + type = type ? type + '/' : '' + const { user, cluster } = route.params + const url = `/clusters/${user}/${cluster}/resources` + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + const secretName = getValue(discriminator, `${type}createSecret/name`) + const secretData = getValue(discriminator, `${type}createSecret/data`) + const secretDataObj = Object.fromEntries(secretData.map((item) => [item.key, item.value])) + + try { + const res = await axios.post(url, { + apiVersion: 'v1', + stringData: secretDataObj, + kind: 'Secret', + metadata: { + name: secretName, + namespace: namespace, + }, + type: 'Opaque', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'success', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/lastCreatedSecret`, + value: secretName, + }) + toast.success('Secret created successfully') + } catch (error) { + const errMsg = decodeError(error, 'Failed to create secret') + toast.error(errMsg, { timeout: 5000 }) + cancelCreateSecret() + } + } + + function decodeError(msg, defaultMsg) { + if (typeof msg === 'string') { + return msg || defaultMsg + } + return ( + (msg.response && msg.response.data && msg.response.data.message) || + (msg.response && msg.response.data) || + (msg.status && msg.status.status) || + defaultMsg + ) + } + + function isCreateSecret(type) { + type = type ? type + '/' : '' + const selectedSecret = getValue(model, `spec/configuration/${type}configSecret/name`) + const res = selectedSecret === 'Create' + + if (res === true) { + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'pending', + }) + } + return res + } + + function isNotCreateSecret(type) { + return !isCreateSecret(type) + } + + function onCreateSecretChange(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'cancelled') return '' + else if (secretStatus === 'success') { + const name = getValue(discriminator, `${type}createSecret/lastCreatedSecret`) + + const configFound = configSecrets.find((item) => item === name) + return configFound ? { text: name, value: name } : '' + } + } + + function cancelCreateSecret(type) { + type = type ? type + '/' : '' + commit('wizard/temp$delete', `${type}createSecret/name`) + commit('wizard/temp$delete', `${type}createSecret/data`) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'cancelled', + }) + } + + async function onApplyconfigChange(type) { + type = type ? type + '/' : '' + const configValue = getValue(discriminator, `${type}applyConfig`) + + if (!configValue) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + const tempConfigObj = {} + configValue.forEach((item) => { + if (item.name) { + tempConfigObj[item.name] = item.content + } + }) + if (Object.keys(tempConfigObj).length === 0) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}applyConfig`, + value: tempConfigObj, + }) + } + + function setApplyConfig(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfiguration` + const selectedConfig = getValue(discriminator, configPath) + if (!selectedConfig) { + return [{ name: '', content: '' }] + } + const applyconfigData = secretConfigData.find((item) => { + if (item.componentName === selectedConfig) { + return item + } + }) + const { applyConfig } = applyconfigData + const configObj = [] + + if (applyConfig) { + Object.keys(applyConfig).forEach((fileName) => { + configObj.push({ + name: fileName, + content: applyConfig[fileName], + }) + }) + } else { + configObj.push({ name: '', content: '' }) + } + return configObj + } + + function onRemoveConfigChange(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfigurationRemove` + const selectedConfig = getValue(discriminator, configPath) + + if (!selectedConfig) { + commit('wizard/model$delete', `/spec/configuration/${type}removeCustomConfig`) + return [{ name: '', content: '' }] + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}removeCustomConfig`, + value: true, + }) + + const configuration = secretConfigData.find((item) => item.componentName === selectedConfig) + + if (!configuration.data) { + return [{ name: '', content: '' }] + } + + const configObj = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) + return configObj + } + + async function onNewConfigSecretChange(type) { + type = type ? type + '/' : '' + const path = `/spec/configuration/${type}configSecret/name` + const selectedSecret = getValue(model, path) + + if (!selectedSecret) { + commit('wizard/model$delete', `/spec/configuration/${type}configSecret`) + return [{ name: '', content: '' }] + } + if (selectedSecret === 'Create') return [{ name: '', content: '' }] + + const owner = storeGet('/route/params/user') + const cluster = storeGet('/route/params/cluster') + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + + try { + // Fetch the secret data from API + const secretResp = await axios.get( + `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets/${selectedSecret}`, + ) + + const secretData = secretResp.data?.data || {} + const configObj = [] + + // Decode base64 and format as array of objects with name and content + Object.keys(secretData).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(secretData[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: secretData[fileName], // Fallback to original if decode fails + }) + } + }) + + return configObj + } catch (e) { + console.error('Error fetching secret:', e) + return [{ name: '', content: '' }] + } + } + + function onSelectedSecretChange(index) { + const secretData = getValue(discriminator, 'createSecret/data') || [] + const selfSecrets = secretData.map((item) => item.key) + + const remainingSecrets = configSecretKeys.filter((item) => !selfSecrets.includes(item)) + + const selfKey = getValue(discriminator, `createSecret/data/${index}/key`) + if (selfKey) { + remainingSecrets.push(selfKey) + } + const resSecret = remainingSecrets.map((item) => { + return { text: item, value: item } + }) + return resSecret + } + + let secretArray = [] + function createSecretUrl() { const user = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') @@ -968,24 +1358,6 @@ export const useFunc = (model) => { return reconfigurationType === value } - function onApplyconfigChange() { - const applyconfig = getValue(discriminator, '/applyConfig') - - const configObj = {} - if (applyconfig) { - applyconfig.forEach((item) => { - const { key, value } = item - configObj[key] = value - }) - } - - commit('wizard/model$update', { - path: '/spec/configuration/applyConfig', - value: configObj, - force: true, - }) - } - function onReconfigurationTypeChange() { const reconfigurationType = getValue(discriminator, '/reconfigurationType') setDiscriminatorValue('/applyConfig', []) @@ -1409,5 +1781,20 @@ export const useFunc = (model) => { isConfigSelected, getSelectedConfigSecret, getSelectedConfigSecretValue, + fetchConfigSecrets, + getConfigSecretsforAppyConfig, + getSelectedConfigurationData, + getSelectedConfigurationName, + getSelectedConfigurationValueForRemove, + createNewConfigSecret, + decodeError, + isCreateSecret, + isNotCreateSecret, + onCreateSecretChange, + cancelCreateSecret, + setApplyConfig, + onRemoveConfigChange, + onNewConfigSecretChange, + onSelectedSecretChange, } } diff --git a/charts/opskubedbcom-redisopsrequest-editor/ui/create-ui.yaml b/charts/opskubedbcom-redisopsrequest-editor/ui/create-ui.yaml index 2d2c0d732f..9a0c7aadd6 100644 --- a/charts/opskubedbcom-redisopsrequest-editor/ui/create-ui.yaml +++ b/charts/opskubedbcom-redisopsrequest-editor/ui/create-ui.yaml @@ -276,115 +276,176 @@ step: if: name: ifRequestTypeEqualsTo|Reconfigure type: function + loader: + name: fetchConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace elements: - type: block-layout label: Configuration - showLabels: false elements: - - type: radio - label: Reconfigure Type + - type: label-element + label: '' + subtitle: Select a new configuration secret, apply a custom configuration, or remove an existing setup to update your database settings + - type: tab-layout + label: New Config Secret + schema: temp/properties/reconfigurationType options: - - text: New Config Secret + - text: NEW CONFIG SECRET value: selectNewConfigSecret - - text: Apply Config + - text: APPLY CONFIG value: applyConfig - - text: Remove + - text: REMOVE value: remove - watcher: - func: onReconfigurationTypeChange - paths: - - temp/properties/reconfigurationType - validation: - type: required - schema: temp/properties/reconfigurationType - - type: block-layout - label: Config Secret - showLabels: true - if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret - type: function - elements: - - type: label-element - label: Select a configuration secret from the available list to update your database settings - customClass: mb-10 - - type: select - addNewButton: - label: Create a new Secret - target: _blank - url: - function: createSecretUrl - schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - refresh: true - label: Config Secret - loader: - name: getConfigSecrets - watchPaths: - - schema/properties/metadata/properties/namespace - init: - type: func - value: setValueFromDbDetails|/spec/configSecret/name - - type: label-element - label: Selected Config Secret - loader: getSelectedConfigSecret - if: - name: isConfigSelected - type: function - watcher: - func: getSelectedConfigSecret - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - - type: editor - label: Value - readonly: true - hasCopy: false - if: - name: isConfigSelected - type: function - loader: - name: getSelectedConfigSecretValue - watchPaths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - schema: temp/properties/configArray/items/properties/value - - type: array-object-form - label: ApplyConfig - buttonClass: is-light is-outlined - schema: temp/properties/applyConfig - validation: - type: required - if: - name: ifReconfigurationTypeEqualsTo|applyConfig - type: function - watcher: - func: onApplyconfigChange - paths: - - temp/properties/applyConfig elements: - - type: label-element - label: New Apply Config - subtitle: Define custom configurations for your database using key-value pairs. Each entry allows you to specify configuration parameters. - customClass: mb-10 - - type: input - label: key - validation: - type: required - schema: key - - type: editor - label: value - hasCopy: false - validation: - type: required - schema: value - - type: switch - schema: schema/properties/spec/properties/configuration/properties/removeCustomConfig - label: Remove CustomConfig - fullwidth: true - if: - name: returnFalse - type: function + - type: block-layout + label: Config Secret + if: + name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret + type: function + elements: + - type: label-element + label: Config Secret + subtitle: Select a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name + refresh: true + label: Config Secret + loader: + name: getConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace + - temp/properties/createSecret/properties/status + init: + type: func + value: setValueFromDbDetails|/spec/configSecret/name + watcher: + func: onCreateSecretChange + paths: + - temp/properties/createSecret/properties/status + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|create + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + - type: block-layout + label: Create a New Config Secret + showLabels: true + if: + type: function + name: isCreateSecret + hasButton: + text: Save + hasCancel: cancelCreateSecret + action: createNewConfigSecret + elements: + - type: input + label: Secret Name + schema: temp/properties/createSecret/properties/name + validation: + type: required + - type: array-object-form + label: String Data + schema: temp/properties/createSecret/properties/data + buttonClass: is-light is-outlined + validation: + type: required + elements: + - type: select + label: Key + schema: key + loader: + name: onSelectedSecretChange + watchPaths: + - temp/properties/createSecret/properties/data + validation: + type: required + - type: textarea + label: Value + schema: value + - type: multi-file-editor + editorHeight: 500px + readonly: true + if: + type: function + name: isNotCreateSecret + loader: + name: onNewConfigSecretChange + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + schema: temp/properties/newConfigSecret + - type: block-layout + label: ApplyConfig + if: + name: ifReconfigurationTypeEqualsTo|applyConfig + type: function + elements: + - type: label-element + label: New Apply Config + subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings.
Enter the parameter you want to configure (e.g., max_connections). + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfiguration + refresh: true + label: Configuration + loader: getConfigSecretsforAppyConfig + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|apply + watchPaths: + - temp/properties/selectedConfiguration + - type: multi-file-editor + editorHeight: 500px + schema: temp/properties/applyConfig + loader: + name: setApplyConfig + watchPaths: + - temp/properties/selectedConfiguration + watcher: + func: onApplyconfigChange + paths: + - temp/properties/applyConfig + - type: block-layout + label: Remove + if: + name: ifReconfigurationTypeEqualsTo|remove + type: function + elements: + - type: label-element + label: Remove + subtitle: Selected a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfigurationRemove + refresh: true + label: Configuration + loader: + name: getConfigSecretsforAppyConfig + watchPaths: + - schema/properties/metadata/properties/namespace + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|remove + watchPaths: + - temp/properties/selectedConfigurationRemove + - type: multi-file-editor + editorHeight: 500px + readonly: true + init: + type: func + value: onRemoveConfigChange + watcher: + func: onRemoveConfigChange + paths: + - temp/properties/selectedConfigurationRemove + schema: temp/properties/removeConfig # Reconfigure TLS - type: block-layout label: TLS diff --git a/charts/opskubedbcom-redisopsrequest-editor/ui/functions.js b/charts/opskubedbcom-redisopsrequest-editor/ui/functions.js index 4b536dc52e..044238d55e 100644 --- a/charts/opskubedbcom-redisopsrequest-editor/ui/functions.js +++ b/charts/opskubedbcom-redisopsrequest-editor/ui/functions.js @@ -1,4 +1,4 @@ -const { axios, useOperator, store } = window.vueHelpers || {} +const { axios, useOperator, store, useToast } = window.vueHelpers || {} const machines = { 'db.t.micro': { resources: { @@ -306,9 +306,11 @@ const machineList = [ ] let machinesFromPreset = [] +const configSecretKeys = ['kubedb-user.conf'] export const useFunc = (model) => { const route = store.state?.route + const toast = useToast() const { getValue, storeGet, discriminator, setDiscriminatorValue, commit } = useOperator( model, @@ -826,28 +828,416 @@ export const useFunc = (model) => { // for config secret let secretArray = [] - async function getConfigSecrets() { + + // Fetch and store database Infos + // for secret configurations in reconfigure + let configSecrets = [] + let secretConfigData = [] + + async function fetchConfigSecrets() { const owner = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') const namespace = getValue(model, '/metadata/namespace') // watchDependency('model#/metadata/namespace') - const resp = await axios.get( - `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets`, + const name = getValue(model, '/spec/databaseRef/name') + const dbGroup = getValue(model, '/route/params/group') + const dbKind = getValue(store.state, '/resource/definition/result/kind') + const dbResource = getValue(model, '/route/params/resource') + const dbVersion = getValue(model, '/route/params/version') + + try { + const resp = await axios.post( + `/clusters/${owner}/${cluster}/proxy/ui.kubedb.com/v1alpha1/databaseinfos`, + { + apiVersion: 'ui.kubedb.com/v1alpha1', + kind: 'DatabaseInfo', + request: { + source: { + ref: { + name: name, + namespace: namespace, + }, + resource: { + group: dbGroup, + kind: dbKind, + name: dbResource, + version: dbVersion, + }, + }, + keys: ['kubedb-user.conf'], + }, + }, + ) + configSecrets = resp?.data?.response?.availableSecrets || [] + secretConfigData = resp?.data?.response?.configurations || [] + } catch (e) { + console.log(e) + } + } + + async function getConfigSecrets(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'success') { + await fetchConfigSecrets() + } + const mappedSecrets = configSecrets.map((item) => { + return { text: item, value: item } + }) + mappedSecrets.push({ text: '+ Create a new Secret', value: 'Create' }) + return mappedSecrets + } + + async function getConfigSecretsforAppyConfig() { + const secrets = secretConfigData.map((item) => { + return { text: item.componentName, value: item.componentName } + }) + return secrets + } + + function getSelectedConfigurationData(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfiguration` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return [] + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, ) - const secrets = (resp && resp.data && resp.data.items) || [] - secretArray = secrets + if (!configuration) { + return [] + } - const filteredSecrets = secrets + const result = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedContent = atob(configuration.data[fileName]) + result.push({ + name: fileName, + content: decodedContent, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + result.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) - filteredSecrets.map((item) => { - const name = (item.metadata && item.metadata.name) || '' - item.text = name - item.value = name - return true + // Set the value to the model + commit('wizard/model$update', { + path: `/temp/${type}applyConfig`, + value: result, + force: true, + }) + + return result + } + + function getSelectedConfigurationName(configType, type) { + type = type ? type + '/' : '' + let path = '' + if (configType === 'create') path = `/spec/configuration/${type}/configSecret/name` + else if (configType === 'apply') path = `/${type}selectedConfiguration` + else if (configType === 'remove') path = `/${type}selectedConfigurationRemove` + + const selectedConfiguration = + configType === 'create' ? getValue(model, path) : getValue(discriminator, path) + + if (selectedConfiguration) + return { subtitle: ` You have selected ${selectedConfiguration} secret` } + else return { subtitle: 'No secret selected' } + } + + function getSelectedConfigurationValueForRemove(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfigurationRemove` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return '' + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, + ) + + if (!configuration) { + return '' + } + + let data = {} + // Decode base64 and parse YAML for each key in the secret data + Object.keys(configuration.data).forEach((item) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[item]) + // Parse YAML string to object + const parsedYaml = yaml.load(decodedString) + // Store the parsed object with the filename as key + data[item] = parsedYaml + } catch (e) { + console.error(`Error parsing ${item}:`, e) + data[item] = atob(configuration.data[item]) // Fallback to decoded string + } + }) + + // Convert data object back to YAML string + return yaml.dump(data) + } + + async function createNewConfigSecret(type) { + type = type ? type + '/' : '' + const { user, cluster } = route.params + const url = `/clusters/${user}/${cluster}/resources` + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + const secretName = getValue(discriminator, `${type}createSecret/name`) + const secretData = getValue(discriminator, `${type}createSecret/data`) + const secretDataObj = Object.fromEntries(secretData.map((item) => [item.key, item.value])) + + try { + const res = await axios.post(url, { + apiVersion: 'v1', + stringData: secretDataObj, + kind: 'Secret', + metadata: { + name: secretName, + namespace: namespace, + }, + type: 'Opaque', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'success', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/lastCreatedSecret`, + value: secretName, + }) + toast.success('Secret created successfully') + } catch (error) { + const errMsg = decodeError(error, 'Failed to create secret') + toast.error(errMsg, { timeout: 5000 }) + cancelCreateSecret() + } + } + + function decodeError(msg, defaultMsg) { + if (typeof msg === 'string') { + return msg || defaultMsg + } + return ( + (msg.response && msg.response.data && msg.response.data.message) || + (msg.response && msg.response.data) || + (msg.status && msg.status.status) || + defaultMsg + ) + } + + function isCreateSecret(type) { + type = type ? type + '/' : '' + const selectedSecret = getValue(model, `spec/configuration/${type}configSecret/name`) + const res = selectedSecret === 'Create' + + if (res === true) { + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'pending', + }) + } + return res + } + + function isNotCreateSecret(type) { + return !isCreateSecret(type) + } + + function onCreateSecretChange(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'cancelled') return '' + else if (secretStatus === 'success') { + const name = getValue(discriminator, `${type}createSecret/lastCreatedSecret`) + + const configFound = configSecrets.find((item) => item === name) + return configFound ? { text: name, value: name } : '' + } + } + + function cancelCreateSecret(type) { + type = type ? type + '/' : '' + commit('wizard/temp$delete', `${type}createSecret/name`) + commit('wizard/temp$delete', `${type}createSecret/data`) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'cancelled', + }) + } + + async function onApplyconfigChange(type) { + type = type ? type + '/' : '' + const configValue = getValue(discriminator, `${type}applyConfig`) + + if (!configValue) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + const tempConfigObj = {} + configValue.forEach((item) => { + if (item.name) { + tempConfigObj[item.name] = item.content + } + }) + if (Object.keys(tempConfigObj).length === 0) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}applyConfig`, + value: tempConfigObj, + }) + } + + function setApplyConfig(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfiguration` + const selectedConfig = getValue(discriminator, configPath) + if (!selectedConfig) { + return [{ name: '', content: '' }] + } + const applyconfigData = secretConfigData.find((item) => { + if (item.componentName === selectedConfig) { + return item + } }) - return filteredSecrets + const { applyConfig } = applyconfigData + const configObj = [] + + if (applyConfig) { + Object.keys(applyConfig).forEach((fileName) => { + configObj.push({ + name: fileName, + content: applyConfig[fileName], + }) + }) + } else { + configObj.push({ name: '', content: '' }) + } + return configObj + } + + function onRemoveConfigChange(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfigurationRemove` + const selectedConfig = getValue(discriminator, configPath) + + if (!selectedConfig) { + commit('wizard/model$delete', `/spec/configuration/${type}removeCustomConfig`) + return [{ name: '', content: '' }] + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}removeCustomConfig`, + value: true, + }) + + const configuration = secretConfigData.find((item) => item.componentName === selectedConfig) + + if (!configuration.data) { + return [{ name: '', content: '' }] + } + + const configObj = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) + return configObj + } + + async function onNewConfigSecretChange(type) { + type = type ? type + '/' : '' + const path = `/spec/configuration/${type}configSecret/name` + const selectedSecret = getValue(model, path) + + if (!selectedSecret) { + commit('wizard/model$delete', `/spec/configuration/${type}configSecret`) + return [{ name: '', content: '' }] + } + if (selectedSecret === 'Create') return [{ name: '', content: '' }] + + const owner = storeGet('/route/params/user') + const cluster = storeGet('/route/params/cluster') + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + + try { + // Fetch the secret data from API + const secretResp = await axios.get( + `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets/${selectedSecret}`, + ) + + const secretData = secretResp.data?.data || {} + const configObj = [] + + // Decode base64 and format as array of objects with name and content + Object.keys(secretData).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(secretData[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: secretData[fileName], // Fallback to original if decode fails + }) + } + }) + + return configObj + } catch (e) { + console.error('Error fetching secret:', e) + return [{ name: '', content: '' }] + } + } + + function onSelectedSecretChange(index) { + const secretData = getValue(discriminator, 'createSecret/data') || [] + const selfSecrets = secretData.map((item) => item.key) + + const remainingSecrets = configSecretKeys.filter((item) => !selfSecrets.includes(item)) + + const selfKey = getValue(discriminator, `createSecret/data/${index}/key`) + if (selfKey) { + remainingSecrets.push(selfKey) + } + const resSecret = remainingSecrets.map((item) => { + return { text: item, value: item } + }) + return resSecret } function createSecretUrl() { @@ -1029,24 +1419,6 @@ export const useFunc = (model) => { return reconfigurationType === value } - function onApplyconfigChange() { - const applyconfig = getValue(discriminator, '/applyConfig') - - const configObj = {} - if (applyconfig) { - applyconfig.forEach((item) => { - const { key, value } = item - configObj[key] = value - }) - } - - commit('wizard/model$update', { - path: '/spec/configuration/applyConfig', - value: configObj, - force: true, - }) - } - function onReconfigurationTypeChange() { const reconfigurationType = getValue(discriminator, '/reconfigurationType') setDiscriminatorValue('/applyConfig', []) @@ -1405,5 +1777,20 @@ export const useFunc = (model) => { isMachineCustom, checkVolume, setReplicas, + fetchConfigSecrets, + getConfigSecretsforAppyConfig, + getSelectedConfigurationData, + getSelectedConfigurationName, + getSelectedConfigurationValueForRemove, + createNewConfigSecret, + decodeError, + isCreateSecret, + isNotCreateSecret, + onCreateSecretChange, + cancelCreateSecret, + setApplyConfig, + onRemoveConfigChange, + onNewConfigSecretChange, + onSelectedSecretChange, } } diff --git a/charts/opskubedbcom-singlestoreopsrequest-editor/ui/create-ui.yaml b/charts/opskubedbcom-singlestoreopsrequest-editor/ui/create-ui.yaml index bbf4336ac9..f7d2c87e47 100644 --- a/charts/opskubedbcom-singlestoreopsrequest-editor/ui/create-ui.yaml +++ b/charts/opskubedbcom-singlestoreopsrequest-editor/ui/create-ui.yaml @@ -466,109 +466,177 @@ step: if: name: ifRequestTypeEqualsTo|Reconfigure type: function + loader: + name: fetchConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace elements: - - type: radio - label: Reconfigure Type - subtitle: Choose the action you'd like to perform. Select a new configuration secret, apply a custom configuration, or remove an existing setup. - options: - - text: New Config Secret - value: selectNewConfigSecret - - text: Apply Config - value: applyConfig - - text: Remove - value: remove - watcher: - func: onReconfigurationTypeChange - paths: - - temp/properties/reconfigurationType - validation: - type: required - schema: temp/properties/reconfigurationType - type: block-layout - label: Config Secret - showLabels: true - if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret - type: function - elements: - - type: label-element - label: Select a configuration secret from the available list to update your database settings - customClass: mb-10 - - type: select - addNewButton: - label: Create a new Secret - target: _blank - url: - function: createSecretUrl - label: Config Secret - loader: - name: getConfigSecrets - watchPaths: - - schema/properties/metadata/properties/namespace - init: - type: func - value: setValueFromDbDetails|/spec/configSecret/name - refresh: true - validation: - type: required - schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name - - type: label-element - label: Selected Config Secret - loader: getSelectedConfigSecret - watcher: - func: getSelectedConfigSecret - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - - type: editor - label: Value - readonly: true - hasCopy: false - if: - name: isConfigSelected - type: function - watcher: - func: getSelectedConfigSecretValue - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - schema: temp/properties/configArray/items/properties/value - - type: array-object-form - label: ApplyConfig - buttonClass: is-light is-outlined - if: - name: ifReconfigurationTypeEqualsTo|applyConfig - type: function - validation: - type: required - watcher: - func: onApplyconfigChange - paths: - - temp/properties/applyConfig - schema: temp/properties/applyConfig + label: Configuration elements: - type: label-element - label: New Apply Config - subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings. Enter the parameter you want to configure (e.g., max_connections). - customClass: mb-10 - - type: input - label: key - validation: - type: required - schema: key - - type: editor - hasCopy: false - label: value - validation: - type: required - schema: value - - type: switch - schema: schema/properties/spec/properties/configuration/properties/removeCustomConfig - label: Remove CustomConfig - fullwidth: true - if: - name: returnFalse - type: function + label: '' + subtitle: Select a new configuration secret, apply a custom configuration, or remove an existing setup to update your database settings + - type: tab-layout + label: New Config Secret + schema: temp/properties/reconfigurationType + options: + - text: NEW CONFIG SECRET + value: selectNewConfigSecret + - text: APPLY CONFIG + value: applyConfig + - text: REMOVE + value: remove + elements: + - type: block-layout + label: Config Secret + if: + name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret + type: function + elements: + - type: label-element + label: Config Secret + subtitle: Select a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name + refresh: true + label: Config Secret + loader: + name: getConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace + - temp/properties/createSecret/properties/status + init: + type: func + value: setValueFromDbDetails|/spec/configSecret/name + watcher: + func: onCreateSecretChange + paths: + - temp/properties/createSecret/properties/status + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|create + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + - type: block-layout + label: Create a New Config Secret + showLabels: true + if: + type: function + name: isCreateSecret + hasButton: + text: Save + hasCancel: cancelCreateSecret + action: createNewConfigSecret + elements: + - type: input + label: Secret Name + schema: temp/properties/createSecret/properties/name + validation: + type: required + - type: array-object-form + label: String Data + schema: temp/properties/createSecret/properties/data + buttonClass: is-light is-outlined + validation: + type: required + elements: + - type: select + label: Key + schema: key + loader: + name: onSelectedSecretChange + watchPaths: + - temp/properties/createSecret/properties/data + validation: + type: required + - type: textarea + label: Value + schema: value + - type: multi-file-editor + editorHeight: 500px + readonly: true + if: + type: function + name: isNotCreateSecret + loader: + name: onNewConfigSecretChange + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + schema: temp/properties/newConfigSecret + - type: block-layout + label: ApplyConfig + if: + name: ifReconfigurationTypeEqualsTo|applyConfig + type: function + elements: + - type: label-element + label: New Apply Config + subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings.
Enter the parameter you want to configure (e.g., max_connections). + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfiguration + refresh: true + label: Configuration + loader: getConfigSecretsforAppyConfig + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|apply + watchPaths: + - temp/properties/selectedConfiguration + - type: multi-file-editor + editorHeight: 500px + schema: temp/properties/applyConfig + loader: + name: setApplyConfig + watchPaths: + - temp/properties/selectedConfiguration + watcher: + func: onApplyconfigChange + paths: + - temp/properties/applyConfig + - type: block-layout + label: Remove + if: + name: ifReconfigurationTypeEqualsTo|remove + type: function + elements: + - type: label-element + label: Remove + subtitle: Selected a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfigurationRemove + refresh: true + label: Configuration + loader: + name: getConfigSecretsforAppyConfig + watchPaths: + - schema/properties/metadata/properties/namespace + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|remove + watchPaths: + - temp/properties/selectedConfigurationRemove + - type: multi-file-editor + editorHeight: 500px + readonly: true + init: + type: func + value: onRemoveConfigChange + watcher: + func: onRemoveConfigChange + paths: + - temp/properties/selectedConfigurationRemove + schema: temp/properties/removeConfig + # Reconfigure TLS - type: block-layout label: TLS diff --git a/charts/opskubedbcom-singlestoreopsrequest-editor/ui/functions.js b/charts/opskubedbcom-singlestoreopsrequest-editor/ui/functions.js index ac233ded5a..05e0e26547 100644 --- a/charts/opskubedbcom-singlestoreopsrequest-editor/ui/functions.js +++ b/charts/opskubedbcom-singlestoreopsrequest-editor/ui/functions.js @@ -1,4 +1,4 @@ -const { axios, useOperator, store } = window.vueHelpers || {} +const { axios, useOperator, store, useToast } = window.vueHelpers || {} const machines = { 'db.t.micro': { resources: { @@ -306,9 +306,11 @@ const machineList = [ ] let machinesFromPreset = [] +const configSecretKeys = ['kubedb-user.cnf'] export const useFunc = (model) => { const route = store.state?.route + const toast = useToast() const { getValue, storeGet, discriminator, setDiscriminatorValue, commit } = useOperator( model, @@ -878,32 +880,419 @@ export const useFunc = (model) => { return resource[0]?.resources } - // for config secret - let secretArray = [] - async function getConfigSecrets() { + // Fetch and store database Infos + // for secret configurations in reconfigure + let configSecrets = [] + let secretConfigData = [] + + async function fetchConfigSecrets() { const owner = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') const namespace = getValue(model, '/metadata/namespace') // watchDependency('model#/metadata/namespace') - const resp = await axios.get( - `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets`, + const name = getValue(model, '/spec/databaseRef/name') + const dbGroup = getValue(model, '/route/params/group') + const dbKind = getValue(store.state, '/resource/definition/result/kind') + const dbResource = getValue(model, '/route/params/resource') + const dbVersion = getValue(model, '/route/params/version') + + try { + const resp = await axios.post( + `/clusters/${owner}/${cluster}/proxy/ui.kubedb.com/v1alpha1/databaseinfos`, + { + apiVersion: 'ui.kubedb.com/v1alpha1', + kind: 'DatabaseInfo', + request: { + source: { + ref: { + name: name, + namespace: namespace, + }, + resource: { + group: dbGroup, + kind: dbKind, + name: dbResource, + version: dbVersion, + }, + }, + keys: ['kubedb-user.cnf'], + }, + }, + ) + configSecrets = resp?.data?.response?.availableSecrets || [] + secretConfigData = resp?.data?.response?.configurations || [] + } catch (e) { + console.log(e) + } + } + + async function getConfigSecrets(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'success') { + await fetchConfigSecrets() + } + const mappedSecrets = configSecrets.map((item) => { + return { text: item, value: item } + }) + mappedSecrets.push({ text: '+ Create a new Secret', value: 'Create' }) + return mappedSecrets + } + + async function getConfigSecretsforAppyConfig() { + const secrets = secretConfigData.map((item) => { + return { text: item.componentName, value: item.componentName } + }) + return secrets + } + + function getSelectedConfigurationData(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfiguration` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return [] + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, ) - const secrets = (resp && resp.data && resp.data.items) || [] - secretArray = secrets + if (!configuration) { + return [] + } - const filteredSecrets = secrets + const result = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedContent = atob(configuration.data[fileName]) + result.push({ + name: fileName, + content: decodedContent, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + result.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) - filteredSecrets.map((item) => { - const name = (item.metadata && item.metadata.name) || '' - item.text = name - item.value = name - return true + // Set the value to the model + commit('wizard/model$update', { + path: `/temp/${type}applyConfig`, + value: result, + force: true, }) - return filteredSecrets + + return result + } + + function getSelectedConfigurationName(configType, type) { + type = type ? type + '/' : '' + let path = '' + if (configType === 'create') path = `/spec/configuration/${type}/configSecret/name` + else if (configType === 'apply') path = `/${type}selectedConfiguration` + else if (configType === 'remove') path = `/${type}selectedConfigurationRemove` + + const selectedConfiguration = + configType === 'create' ? getValue(model, path) : getValue(discriminator, path) + + if (selectedConfiguration) + return { subtitle: ` You have selected ${selectedConfiguration} secret` } + else return { subtitle: 'No secret selected' } + } + + function getSelectedConfigurationValueForRemove(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfigurationRemove` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return '' + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, + ) + + if (!configuration) { + return '' + } + + let data = {} + // Decode base64 and parse YAML for each key in the secret data + Object.keys(configuration.data).forEach((item) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[item]) + // Parse YAML string to object + const parsedYaml = yaml.load(decodedString) + // Store the parsed object with the filename as key + data[item] = parsedYaml + } catch (e) { + console.error(`Error parsing ${item}:`, e) + data[item] = atob(configuration.data[item]) // Fallback to decoded string + } + }) + + // Convert data object back to YAML string + return yaml.dump(data) + } + + async function createNewConfigSecret(type) { + type = type ? type + '/' : '' + const { user, cluster } = route.params + const url = `/clusters/${user}/${cluster}/resources` + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + const secretName = getValue(discriminator, `${type}createSecret/name`) + const secretData = getValue(discriminator, `${type}createSecret/data`) + const secretDataObj = Object.fromEntries(secretData.map((item) => [item.key, item.value])) + + try { + const res = await axios.post(url, { + apiVersion: 'v1', + stringData: secretDataObj, + kind: 'Secret', + metadata: { + name: secretName, + namespace: namespace, + }, + type: 'Opaque', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'success', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/lastCreatedSecret`, + value: secretName, + }) + toast.success('Secret created successfully') + } catch (error) { + const errMsg = decodeError(error, 'Failed to create secret') + toast.error(errMsg, { timeout: 5000 }) + cancelCreateSecret() + } + } + + function decodeError(msg, defaultMsg) { + if (typeof msg === 'string') { + return msg || defaultMsg + } + return ( + (msg.response && msg.response.data && msg.response.data.message) || + (msg.response && msg.response.data) || + (msg.status && msg.status.status) || + defaultMsg + ) + } + + function isCreateSecret(type) { + type = type ? type + '/' : '' + const selectedSecret = getValue(model, `spec/configuration/${type}configSecret/name`) + const res = selectedSecret === 'Create' + + if (res === true) { + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'pending', + }) + } + return res + } + + function isNotCreateSecret(type) { + return !isCreateSecret(type) } + function onCreateSecretChange(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'cancelled') return '' + else if (secretStatus === 'success') { + const name = getValue(discriminator, `${type}createSecret/lastCreatedSecret`) + + const configFound = configSecrets.find((item) => item === name) + return configFound ? { text: name, value: name } : '' + } + } + + function cancelCreateSecret(type) { + type = type ? type + '/' : '' + commit('wizard/temp$delete', `${type}createSecret/name`) + commit('wizard/temp$delete', `${type}createSecret/data`) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'cancelled', + }) + } + + async function onApplyconfigChange(type) { + type = type ? type + '/' : '' + const configValue = getValue(discriminator, `${type}applyConfig`) + + if (!configValue) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + const tempConfigObj = {} + configValue.forEach((item) => { + if (item.name) { + tempConfigObj[item.name] = item.content + } + }) + if (Object.keys(tempConfigObj).length === 0) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}applyConfig`, + value: tempConfigObj, + }) + } + + function setApplyConfig(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfiguration` + const selectedConfig = getValue(discriminator, configPath) + if (!selectedConfig) { + return [{ name: '', content: '' }] + } + const applyconfigData = secretConfigData.find((item) => { + if (item.componentName === selectedConfig) { + return item + } + }) + const { applyConfig } = applyconfigData + const configObj = [] + + if (applyConfig) { + Object.keys(applyConfig).forEach((fileName) => { + configObj.push({ + name: fileName, + content: applyConfig[fileName], + }) + }) + } else { + configObj.push({ name: '', content: '' }) + } + return configObj + } + + function onRemoveConfigChange(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfigurationRemove` + const selectedConfig = getValue(discriminator, configPath) + + if (!selectedConfig) { + commit('wizard/model$delete', `/spec/configuration/${type}removeCustomConfig`) + return [{ name: '', content: '' }] + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}removeCustomConfig`, + value: true, + }) + + const configuration = secretConfigData.find((item) => item.componentName === selectedConfig) + + if (!configuration.data) { + return [{ name: '', content: '' }] + } + + const configObj = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) + return configObj + } + + async function onNewConfigSecretChange(type) { + type = type ? type + '/' : '' + const path = `/spec/configuration/${type}configSecret/name` + const selectedSecret = getValue(model, path) + + if (!selectedSecret) { + commit('wizard/model$delete', `/spec/configuration/${type}configSecret`) + return [{ name: '', content: '' }] + } + if (selectedSecret === 'Create') return [{ name: '', content: '' }] + + const owner = storeGet('/route/params/user') + const cluster = storeGet('/route/params/cluster') + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + + try { + // Fetch the secret data from API + const secretResp = await axios.get( + `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets/${selectedSecret}`, + ) + + const secretData = secretResp.data?.data || {} + const configObj = [] + + // Decode base64 and format as array of objects with name and content + Object.keys(secretData).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(secretData[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: secretData[fileName], // Fallback to original if decode fails + }) + } + }) + + return configObj + } catch (e) { + console.error('Error fetching secret:', e) + return [{ name: '', content: '' }] + } + } + + function onSelectedSecretChange(index) { + const secretData = getValue(discriminator, 'createSecret/data') || [] + const selfSecrets = secretData.map((item) => item.key) + + const remainingSecrets = configSecretKeys.filter((item) => !selfSecrets.includes(item)) + + const selfKey = getValue(discriminator, `createSecret/data/${index}/key`) + if (selfKey) { + remainingSecrets.push(selfKey) + } + const resSecret = remainingSecrets.map((item) => { + return { text: item, value: item } + }) + return resSecret + } + + let secretArray = [] + function isConfigSelected() { const path = `/spec/configuration/configSecret/name` const selectedSecret = getValue(model, path) @@ -1085,25 +1474,6 @@ export const useFunc = (model) => { return reconfigurationType === value } - function onApplyconfigChange() { - const applyconfig = getValue(discriminator, '/applyConfig') - - const configObj = {} - - if (applyconfig) { - applyconfig.forEach((item) => { - const { key, value } = item - configObj[key] = value - }) - } - - commit('wizard/model$update', { - path: `/spec/configuration/applyConfig`, - value: configObj, - force: true, - }) - } - function onReconfigurationTypeChange() { setDiscriminatorValue('/applyConfig', []) const reconfigurationType = getValue(discriminator, '/reconfigurationType') @@ -1456,5 +1826,20 @@ export const useFunc = (model) => { checkVolume, setResource, isVerticalScaleTopologyRequired, + fetchConfigSecrets, + getConfigSecretsforAppyConfig, + getSelectedConfigurationData, + getSelectedConfigurationName, + getSelectedConfigurationValueForRemove, + createNewConfigSecret, + decodeError, + isCreateSecret, + isNotCreateSecret, + onCreateSecretChange, + cancelCreateSecret, + setApplyConfig, + onRemoveConfigChange, + onNewConfigSecretChange, + onSelectedSecretChange, } } diff --git a/charts/opskubedbcom-solropsrequest-editor/ui/create-ui.yaml b/charts/opskubedbcom-solropsrequest-editor/ui/create-ui.yaml index 18acb745de..1b48af9120 100644 --- a/charts/opskubedbcom-solropsrequest-editor/ui/create-ui.yaml +++ b/charts/opskubedbcom-solropsrequest-editor/ui/create-ui.yaml @@ -570,428 +570,670 @@ step: if: name: ifRequestTypeEqualsTo|Reconfigure type: function + loader: + name: fetchConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace elements: + - type: label-element + label: '' + subtitle: Select a new configuration secret, apply a custom configuration, or remove an existing setup to update your database settings - type: block-layout label: Combined if: name: ifDbTypeEqualsTo|Combined|configuration type: function elements: - - type: radio - label: Reconfigure Type - subtitle: Choose the action you'd like to perform. Select a new configuration secret, apply a custom configuration , or remove an existing setup. - validation: - type: required + - type: tab-layout + label: New Config Secret + schema: temp/properties/reconfigurationType options: - - text: New Config Secret + - text: NEW CONFIG SECRET value: selectNewConfigSecret - - text: Apply Config + - text: APPLY CONFIG value: applyConfig - - text: Remove + - text: REMOVE value: remove - schema: temp/properties/reconfigurationType - watcher: - func: onReconfigurationTypeChange|node - paths: - - temp/properties/reconfigurationType - - type: block-layout - label: Config Secret - showLabels: true - if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret - type: function - elements: - - type: label-element - label: Select a configuration secret from the available list to update your database settings - customClass: mb-10 - - type: select - addNewButton: - label: Create Secret - target: _blank - url: - function: createSecretUrl - schema: schema/properties/spec/properties/configuration/properties/node/properties/configSecret/properties/name - validation: - type: required - refresh: true - label: Config Secret - loader: - name: getConfigSecrets - watchPaths: - - schema/properties/metadata/properties/namespace - init: - type: func - value: setValueFromDbDetails|/spec/configSecret/name - - type: label-element - label: Selected Config Secret - loader: getSelectedConfigSecret|node - watcher: - func: getSelectedConfigSecret|node - paths: - - schema/properties/spec/properties/configuration/properties/node/properties/configSecret/properties/name - - type: editor - label: Value - readonly: true - hasCopy: false - watcher: - func: getSelectedConfigSecretValue|node - paths: - - schema/properties/spec/properties/configuration/properties/node/properties/configSecret/properties/name - validation: - type: required - schema: temp/properties/configArray/items/properties/value - - type: array-object-form - label: ApplyConfig - buttonClass: is-light is-outlined - schema: temp/properties/node/applyConfig - validation: - type: required - if: - name: ifReconfigurationTypeEqualsTo|applyConfig - type: function - watcher: - func: onApplyconfigChange|node - paths: - - temp/properties/node/applyConfig elements: - - type: label-element - label: New Apply Config - subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings. Enter the parameter you want to configure (e.g., max_connections). - customClass: mb-10 - - type: input - label: key - validation: - type: required - schema: key - - type: editor - label: value - hasCopy: false - validation: - type: required - schema: value - - type: switch - schema: schema/properties/spec/properties/configuration/properties/node/properties/removeCustomConfig - label: Remove CustomConfig - fullwidth: true - if: - name: returnFalse - type: function - - type: block-layout - elements: - - type: block-layout - label: Coordinator - showLabels: true - elements: - - type: radio - label: Reconfigure Type - subtitle: Choose the action you'd like to perform. Select a new configuration secret, apply a custom configuration , or remove an existing setup. - validation: - type: required - schema: temp/properties/reconfigurationType-coordinator - options: - - text: New Config Secret - value: selectNewConfigSecret - - text: Apply Config - value: applyConfig - - text: Remove - value: remove - watcher: - func: onReconfigurationTypeChange|coordinator|true - paths: - - temp/properties/reconfigurationType-coordinator - type: block-layout label: Config Secret - showLabels: true if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret|coordinator|true + name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret type: function elements: - type: label-element - label: Select a configuration secret from the available list to update your database settings + label: Config Secret + subtitle: Select a configuration secret from the available list to update your database settings customClass: mb-10 - type: select - addNewButton: - label: Create a new Secret - target: _blank - url: - function: createSecretUrl - schema: schema/properties/spec/properties/configuration/properties/coordinator/properties/configSecret/properties/name - validation: - type: required + customClass: mb-2 + schema: schema/properties/spec/properties/configuration/properties/node/properties/configSecret/properties/name refresh: true label: Config Secret loader: - name: getConfigSecrets + name: getConfigSecrets|node watchPaths: - schema/properties/metadata/properties/namespace + - temp/properties/node/properties/createSecret/properties/status init: type: func - value: setValueFromDbDetails|/spec/topology/coordinator/configSecret/name - - type: label-element - label: Selected Config Secret - loader: getSelectedConfigSecret|coordinator + value: setValueFromDbDetails|/spec/configSecret/name watcher: - func: getSelectedConfigSecret|coordinator + func: onCreateSecretChange|node paths: - - schema/properties/spec/properties/configuration/properties/coordinator/properties/configSecret/properties/name - - type: editor - label: Value + - temp/properties/node/properties/createSecret/properties/status + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|create|node + watchPaths: + - schema/properties/spec/properties/configuration/properties/node/properties/configSecret/properties/name + - type: block-layout + label: Create a New Config Secret + showLabels: true + if: + type: function + name: isCreateSecret|node + hasButton: + text: Save + hasCancel: cancelCreateSecret|node + action: createNewConfigSecret|node + elements: + - type: input + label: Secret Name + schema: temp/properties/node/properties/createSecret/properties/name + validation: + type: required + - type: array-object-form + label: String Data + schema: temp/properties/node/properties/createSecret/properties/data + buttonClass: is-light is-outlined + validation: + type: required + elements: + - type: select + label: Key + schema: key + loader: + name: onSelectedSecretChange|node + watchPaths: + - temp/properties/node/properties/createSecret/properties/data + validation: + type: required + - type: textarea + label: Value + schema: value + - type: multi-file-editor + editorHeight: 500px readonly: true - hasCopy: false - watcher: - func: getSelectedConfigSecretValue|coordinator - paths: - - schema/properties/spec/properties/configuration/properties/coordinator/properties/configSecret/properties/name - validation: - type: required - schema: temp/properties/configArray/items/properties/value - - type: array-object-form + if: + type: function + name: isNotCreateSecret|node + loader: + name: onNewConfigSecretChange|node + watchPaths: + - schema/properties/spec/properties/configuration/properties/node/properties/configSecret/properties/name + schema: temp/properties/node/newConfigSecret + - type: block-layout label: ApplyConfig - schema: temp/properties/coordinator/applyConfig - buttonClass: is-light is-outlined - validation: - type: required if: - name: ifReconfigurationTypeEqualsTo|applyConfig|coordinator|true + name: ifReconfigurationTypeEqualsTo|applyConfig type: function - watcher: - func: onApplyconfigChange|coordinator - paths: - - temp/properties/coordinator/applyConfig elements: - type: label-element label: New Apply Config - subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings. Enter the parameter you want to configure (e.g., max_connections). + subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings.
Enter the parameter you want to configure (e.g., max_connections). customClass: mb-10 - - type: input - label: key - validation: - type: required - schema: key - - type: editor - label: value - hasCopy: false - validation: - type: required - schema: value - - - type: switch - label: Remove CustomConfig - fullwidth: true - schema: schema/properties/spec/properties/configuration/properties/coordinator/properties/removeCustomConfig - if: - name: returnFalse - type: function - - type: block-layout - label: Data - showLabels: true - elements: - - type: radio - label: Reconfigure Type - subtitle: Choose the action you'd like to perform. Select a new configuration secret, apply a custom configuration , or remove an existing setup. - validation: - type: required - schema: temp/properties/reconfigurationType-data - options: - - text: New Config Secret - value: selectNewConfigSecret - - text: Apply Config - value: applyConfig - - text: Remove - value: remove - watcher: - func: onReconfigurationTypeChange|data|true - paths: - - temp/properties/reconfigurationType-data + - type: select + customClass: mb-2 + schema: temp/properties/node/selectedConfiguration + refresh: true + label: Configuration + loader: getConfigSecretsforAppyConfig + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|apply|node + watchPaths: + - temp/properties/node/selectedConfiguration + - type: multi-file-editor + editorHeight: 500px + schema: temp/properties/node/applyConfig + loader: + name: setApplyConfig|node + watchPaths: + - temp/properties/node/selectedConfiguration + watcher: + func: onApplyconfigChange|node + paths: + - temp/properties/node/applyConfig - type: block-layout - label: Config Secret - showLabels: true + label: Remove if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret|data|true + name: ifReconfigurationTypeEqualsTo|remove type: function elements: - - type: label-element - label: Select a configuration secret from the available list to update your database settings + - type: label-element + label: Remove + subtitle: Selected a configuration secret from the available list to update your database settings customClass: mb-10 - type: select - addNewButton: - label: Create Secret - target: _blank - url: - function: createSecretUrl - schema: schema/properties/spec/properties/configuration/properties/data/properties/configSecret/properties/name - validation: - type: required + customClass: mb-2 + schema: temp/properties/node/selectedConfigurationRemove refresh: true - label: Config Secret + label: Configuration loader: - name: getConfigSecrets + name: getConfigSecretsforAppyConfig watchPaths: - schema/properties/metadata/properties/namespace - init: - type: func - value: setValueFromDbDetails|/spec/topology/data/configSecret/name - type: label-element - label: Selected Config Secret - loader: getSelectedConfigSecret|data - watcher: - func: getSelectedConfigSecret|data - paths: - - schema/properties/spec/properties/configuration/properties/data/properties/configSecret/properties/name - - type: editor - label: Value + label: '' + loader: + name: getSelectedConfigurationName|remove|node + watchPaths: + - temp/properties/node/selectedConfigurationRemove + - type: multi-file-editor + editorHeight: 500px readonly: true - hasCopy: false + init: + type: func + value: onRemoveConfigChange|node watcher: - func: getSelectedConfigSecretValue|data + func: onRemoveConfigChange|node paths: - - schema/properties/spec/properties/configuration/properties/data/properties/configSecret/properties/name - validation: - type: required - schema: temp/properties/configArray/items/properties/value - - type: array-object-form - label: ApplyConfig - schema: temp/properties/data/applyConfig - buttonClass: is-light is-outlined - validation: - type: required - if: - name: ifReconfigurationTypeEqualsTo|applyConfig|data|true - type: function - watcher: - func: onApplyconfigChange|data - paths: - - temp/properties/data/applyConfig + - temp/properties/node/selectedConfigurationRemove + schema: temp/properties/node/removeConfig + - type: block-layout + elements: + - type: block-layout + label: Coordinator + showLabels: true + elements: + - type: tab-layout + label: New Config Secret + schema: temp/properties/reconfigurationType-coordinator + options: + - text: NEW CONFIG SECRET + value: selectNewConfigSecret + - text: APPLY CONFIG + value: applyConfig + - text: REMOVE + value: remove elements: - - type: label-element - label: New Apply Config - subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings. Enter the parameter you want to configure (e.g., max_connections). - customClass: mb-10 - - type: input - label: key - validation: - type: required - schema: key - - type: editor - label: value - hasCopy: false - validation: - type: required - schema: value - - type: switch - label: Remove CustomConfig - fullwidth: true - schema: schema/properties/spec/properties/configuration/properties/data/properties/removeCustomConfig - if: - name: returnFalse - type: function + - type: block-layout + label: Config Secret + if: + name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret|coordinator|true + type: function + elements: + - type: label-element + label: Config Secret + subtitle: Select a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: schema/properties/spec/properties/configuration/properties/coordinator/properties/configSecret/properties/name + refresh: true + label: Config Secret + loader: + name: getConfigSecrets|coordinator + watchPaths: + - schema/properties/metadata/properties/namespace + - temp/properties/coordinator/properties/createSecret/properties/status + init: + type: func + value: setValueFromDbDetails|/spec/topology/coordinator/configSecret/name + watcher: + func: onCreateSecretChange|coordinator + paths: + - temp/properties/coordinator/properties/createSecret/properties/status + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|create|coordinator + watchPaths: + - schema/properties/spec/properties/configuration/properties/coordinator/properties/configSecret/properties/name + - type: block-layout + label: Create a New Config Secret + showLabels: true + if: + type: function + name: isCreateSecret|coordinator + hasButton: + text: Save + hasCancel: cancelCreateSecret|coordinator + action: createNewConfigSecret|coordinator + elements: + - type: input + label: Secret Name + schema: temp/properties/coordinator/properties/createSecret/properties/name + validation: + type: required + - type: array-object-form + label: String Data + schema: temp/properties/coordinator/properties/createSecret/properties/data + buttonClass: is-light is-outlined + validation: + type: required + elements: + - type: select + label: Key + schema: key + loader: + name: onSelectedSecretChange|coordinator + watchPaths: + - temp/properties/coordinator/properties/createSecret/properties/data + validation: + type: required + - type: textarea + label: Value + schema: value + - type: multi-file-editor + editorHeight: 500px + readonly: true + if: + type: function + name: isNotCreateSecret|coordinator + loader: + name: onNewConfigSecretChange|coordinator + watchPaths: + - schema/properties/spec/properties/configuration/properties/coordinator/properties/configSecret/properties/name + schema: temp/properties/coordinator/newConfigSecret + - type: block-layout + label: ApplyConfig + if: + name: ifReconfigurationTypeEqualsTo|applyConfig|coordinator|true + type: function + elements: + - type: label-element + label: New Apply Config + subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings.
Enter the parameter you want to configure (e.g., max_connections). + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/coordinator/selectedConfiguration + refresh: true + label: Configuration + loader: getConfigSecretsforAppyConfig + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|apply|coordinator + watchPaths: + - temp/properties/coordinator/selectedConfiguration + - type: multi-file-editor + editorHeight: 500px + schema: temp/properties/coordinator/applyConfig + loader: + name: setApplyConfig|coordinator + watchPaths: + - temp/properties/coordinator/selectedConfiguration + watcher: + func: onApplyconfigChange|coordinator + paths: + - temp/properties/coordinator/applyConfig + - type: block-layout + label: Remove + if: + name: ifReconfigurationTypeEqualsTo|remove|coordinator|true + type: function + elements: + - type: label-element + label: Remove + subtitle: Selected a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/coordinator/selectedConfigurationRemove + refresh: true + label: Configuration + loader: + name: getConfigSecretsforAppyConfig + watchPaths: + - schema/properties/metadata/properties/namespace + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|remove|coordinator + watchPaths: + - temp/properties/coordinator/selectedConfigurationRemove + - type: multi-file-editor + editorHeight: 500px + readonly: true + init: + type: func + value: onRemoveConfigChange|coordinator + watcher: + func: onRemoveConfigChange|coordinator + paths: + - temp/properties/coordinator/selectedConfigurationRemove + schema: temp/properties/coordinator/removeConfig + - type: block-layout + label: Data + showLabels: true + elements: + - type: tab-layout + label: New Config Secret + schema: temp/properties/reconfigurationType-data + options: + - text: NEW CONFIG SECRET + value: selectNewConfigSecret + - text: APPLY CONFIG + value: applyConfig + - text: REMOVE + value: remove + elements: + - type: block-layout + label: Config Secret + if: + name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret|data|true + type: function + elements: + - type: label-element + label: Config Secret + subtitle: Select a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: schema/properties/spec/properties/configuration/properties/data/properties/configSecret/properties/name + refresh: true + label: Config Secret + loader: + name: getConfigSecrets|data + watchPaths: + - schema/properties/metadata/properties/namespace + - temp/properties/data/properties/createSecret/properties/status + init: + type: func + value: setValueFromDbDetails|/spec/topology/data/configSecret/name + watcher: + func: onCreateSecretChange|data + paths: + - temp/properties/data/properties/createSecret/properties/status + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|create|data + watchPaths: + - schema/properties/spec/properties/configuration/properties/data/properties/configSecret/properties/name + - type: block-layout + label: Create a New Config Secret + showLabels: true + if: + type: function + name: isCreateSecret|data + hasButton: + text: Save + hasCancel: cancelCreateSecret|data + action: createNewConfigSecret|data + elements: + - type: input + label: Secret Name + schema: temp/properties/data/properties/createSecret/properties/name + validation: + type: required + - type: array-object-form + label: String Data + schema: temp/properties/data/properties/createSecret/properties/data + buttonClass: is-light is-outlined + validation: + type: required + elements: + - type: select + label: Key + schema: key + loader: + name: onSelectedSecretChange|data + watchPaths: + - temp/properties/data/properties/createSecret/properties/data + validation: + type: required + - type: textarea + label: Value + schema: value + - type: multi-file-editor + editorHeight: 500px + readonly: true + if: + type: function + name: isNotCreateSecret|data + loader: + name: onNewConfigSecretChange|data + watchPaths: + - schema/properties/spec/properties/configuration/properties/data/properties/configSecret/properties/name + schema: temp/properties/data/newConfigSecret + - type: block-layout + label: ApplyConfig + if: + name: ifReconfigurationTypeEqualsTo|applyConfig|data|true + type: function + elements: + - type: label-element + label: New Apply Config + subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings.
Enter the parameter you want to configure (e.g., max_connections). + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/data/selectedConfiguration + refresh: true + label: Configuration + loader: getConfigSecretsforAppyConfig + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|apply|data + watchPaths: + - temp/properties/data/selectedConfiguration + - type: multi-file-editor + editorHeight: 500px + schema: temp/properties/data/applyConfig + loader: + name: setApplyConfig|data + watchPaths: + - temp/properties/data/selectedConfiguration + watcher: + func: onApplyconfigChange|data + paths: + - temp/properties/data/applyConfig + - type: block-layout + label: Remove + if: + name: ifReconfigurationTypeEqualsTo|remove|data|true + type: function + elements: + - type: label-element + label: Remove + subtitle: Selected a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/data/selectedConfigurationRemove + refresh: true + label: Configuration + loader: + name: getConfigSecretsforAppyConfig + watchPaths: + - schema/properties/metadata/properties/namespace + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|remove|data + watchPaths: + - temp/properties/data/selectedConfigurationRemove + - type: multi-file-editor + editorHeight: 500px + readonly: true + init: + type: func + value: onRemoveConfigChange|data + watcher: + func: onRemoveConfigChange|data + paths: + - temp/properties/data/selectedConfigurationRemove + schema: temp/properties/data/removeConfig - type: block-layout label: Overseer showLabels: true elements: - - type: radio - label: Reconfigure Type - subtitle: Choose the action you'd like to perform. Select a new configuration secret, apply a custom configuration , or remove an existing setup. - validation: - type: required + - type: tab-layout + label: New Config Secret schema: temp/properties/reconfigurationType-overseer options: - - text: New Config Secret - value: selectNewConfigSecret - - text: Apply Config - value: applyConfig - - text: Remove - value: remove - watcher: - func: onReconfigurationTypeChange|overseer|true - paths: - - temp/properties/reconfigurationType-overseer - - - type: block-layout - label: Config Secret - showLabels: true - if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret|overseer|true - type: function - elements: - - type: label-element - label: Select a configuration secret from the available list to update your database settings - customClass: mb-10 - - type: select - addNewButton: - label: Create Secret - target: _blank - url: - function: createSecretUrl - schema: schema/properties/spec/properties/configuration/properties/overseer/properties/configSecret/properties/name - validation: - type: required - refresh: true - label: Config Secret - loader: - name: getConfigSecrets - watchPaths: - - schema/properties/metadata/properties/namespace - init: - type: func - value: setValueFromDbDetails|/spec/topology/overseer/configSecret/name - - type: label-element - label: Selected Config Secret - loader: getSelectedConfigSecret|overseer - watcher: - func: getSelectedConfigSecret|overseer - paths: - - schema/properties/spec/properties/configuration/properties/overseer/properties/configSecret/properties/name - - type: editor - label: Value - readonly: true - hasCopy: false - if: - name: isConfigSelected - type: function - watcher: - func: getSelectedConfigSecretValue|overseer - paths: - - schema/properties/spec/properties/configuration/properties/overseer/properties/configSecret/properties/name - validation: - type: required - schema: temp/properties/configArray/items/properties/value - - type: array-object-form - label: ApplyConfig - schema: temp/properties/overseer/applyConfig - buttonClass: is-light is-outlined - validation: - type: required - if: - name: ifReconfigurationTypeEqualsTo|applyConfig|overseer|true - type: function - watcher: - func: onApplyconfigChange|overseer - paths: - - temp/properties/overseer/applyConfig + - text: NEW CONFIG SECRET + value: selectNewConfigSecret + - text: APPLY CONFIG + value: applyConfig + - text: REMOVE + value: remove elements: - - type: label-element - label: New Apply Config - subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings. Enter the parameter you want to configure (e.g., max_connections). - customClass: mb-10 - - type: input - label: key - validation: - type: required - schema: key - - type: editor - label: value - hasCopy: false - validation: - type: required - schema: value - - type: switch - label: Remove CustomConfig - fullwidth: true - schema: schema/properties/spec/properties/configuration/properties/overseer/properties/removeCustomConfig - if: - name: returnFalse - type: function + - type: block-layout + label: Config Secret + if: + name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret|overseer|true + type: function + elements: + - type: label-element + label: Config Secret + subtitle: Select a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: schema/properties/spec/properties/configuration/properties/overseer/properties/configSecret/properties/name + refresh: true + label: Config Secret + loader: + name: getConfigSecrets|overseer + watchPaths: + - schema/properties/metadata/properties/namespace + - temp/properties/overseer/properties/createSecret/properties/status + init: + type: func + value: setValueFromDbDetails|/spec/topology/overseer/configSecret/name + watcher: + func: onCreateSecretChange|overseer + paths: + - temp/properties/overseer/properties/createSecret/properties/status + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|create|overseer + watchPaths: + - schema/properties/spec/properties/configuration/properties/overseer/properties/configSecret/properties/name + - type: block-layout + label: Create a New Config Secret + showLabels: true + if: + type: function + name: isCreateSecret|overseer + hasButton: + text: Save + hasCancel: cancelCreateSecret|overseer + action: createNewConfigSecret|overseer + elements: + - type: input + label: Secret Name + schema: temp/properties/overseer/properties/createSecret/properties/name + validation: + type: required + - type: array-object-form + label: String Data + schema: temp/properties/overseer/properties/createSecret/properties/data + buttonClass: is-light is-outlined + validation: + type: required + elements: + - type: select + label: Key + schema: key + loader: + name: onSelectedSecretChange|overseer + watchPaths: + - temp/properties/overseer/properties/createSecret/properties/data + validation: + type: required + - type: textarea + label: Value + schema: value + - type: multi-file-editor + editorHeight: 500px + readonly: true + if: + type: function + name: isNotCreateSecret|overseer + loader: + name: onNewConfigSecretChange|overseer + watchPaths: + - schema/properties/spec/properties/configuration/properties/overseer/properties/configSecret/properties/name + schema: temp/properties/overseer/newConfigSecret + - type: block-layout + label: ApplyConfig + if: + name: ifReconfigurationTypeEqualsTo|applyConfig|overseer|true + type: function + elements: + - type: label-element + label: New Apply Config + subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings.
Enter the parameter you want to configure (e.g., max_connections). + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/overseer/selectedConfiguration + refresh: true + label: Configuration + loader: getConfigSecretsforAppyConfig + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|apply|overseer + watchPaths: + - temp/properties/overseer/selectedConfiguration + - type: multi-file-editor + editorHeight: 500px + schema: temp/properties/overseer/applyConfig + loader: + name: setApplyConfig|overseer + watchPaths: + - temp/properties/overseer/selectedConfiguration + watcher: + func: onApplyconfigChange|overseer + paths: + - temp/properties/overseer/applyConfig + - type: block-layout + label: Remove + if: + name: ifReconfigurationTypeEqualsTo|remove|overseer|true + type: function + elements: + - type: label-element + label: Remove + subtitle: Selected a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/overseer/selectedConfigurationRemove + refresh: true + label: Configuration + loader: + name: getConfigSecretsforAppyConfig + watchPaths: + - schema/properties/metadata/properties/namespace + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|remove|overseer + watchPaths: + - temp/properties/overseer/selectedConfigurationRemove + - type: multi-file-editor + editorHeight: 500px + readonly: true + init: + type: func + value: onRemoveConfigChange|overseer + watcher: + func: onRemoveConfigChange|overseer + paths: + - temp/properties/overseer/selectedConfigurationRemove + schema: temp/properties/overseer/removeConfig label: Topology Reconfigure form if: name: ifDbTypeEqualsTo|Topology|configuration diff --git a/charts/opskubedbcom-solropsrequest-editor/ui/functions.js b/charts/opskubedbcom-solropsrequest-editor/ui/functions.js index 0fb0a2a6ae..150b1928cb 100644 --- a/charts/opskubedbcom-solropsrequest-editor/ui/functions.js +++ b/charts/opskubedbcom-solropsrequest-editor/ui/functions.js @@ -1,4 +1,4 @@ -const { axios, useOperator, store } = window.vueHelpers || {} +const { axios, useOperator, store, useToast } = window.vueHelpers || {} const machines = { 'db.t.micro': { resources: { @@ -306,9 +306,11 @@ const machineList = [ ] let machinesFromPreset = [] +const configSecretKeys = ['solr.xml'] export const useFunc = (model) => { const route = store.state?.route + const toast = useToast() const { getValue, storeGet, discriminator, setDiscriminatorValue, commit } = useOperator( model, @@ -874,29 +876,416 @@ export const useFunc = (model) => { return machine === 'custom' } - // for config secret - async function getConfigSecrets() { + // Fetch and store database Infos + // for secret configurations in reconfigure + let configSecrets = [] + let secretConfigData = [] + + async function fetchConfigSecrets() { const owner = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') const namespace = getValue(model, '/metadata/namespace') // watchDependency('model#/metadata/namespace') - const resp = await axios.get( - `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets`, + const name = getValue(model, '/spec/databaseRef/name') + const dbGroup = getValue(model, '/route/params/group') + const dbKind = getValue(store.state, '/resource/definition/result/kind') + const dbResource = getValue(model, '/route/params/resource') + const dbVersion = getValue(model, '/route/params/version') + + try { + const resp = await axios.post( + `/clusters/${owner}/${cluster}/proxy/ui.kubedb.com/v1alpha1/databaseinfos`, + { + apiVersion: 'ui.kubedb.com/v1alpha1', + kind: 'DatabaseInfo', + request: { + source: { + ref: { + name: name, + namespace: namespace, + }, + resource: { + group: dbGroup, + kind: dbKind, + name: dbResource, + version: dbVersion, + }, + }, + keys: ['solr.xml'], + }, + }, + ) + configSecrets = resp?.data?.response?.availableSecrets || [] + secretConfigData = resp?.data?.response?.configurations || [] + } catch (e) { + console.log(e) + } + } + + async function getConfigSecrets(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'success') { + await fetchConfigSecrets() + } + const mappedSecrets = configSecrets.map((item) => { + return { text: item, value: item } + }) + mappedSecrets.push({ text: '+ Create a new Secret', value: 'Create' }) + return mappedSecrets + } + + async function getConfigSecretsforAppyConfig() { + const secrets = secretConfigData.map((item) => { + return { text: item.componentName, value: item.componentName } + }) + return secrets + } + + function getSelectedConfigurationData(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfiguration` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return [] + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, ) - const secrets = (resp && resp.data && resp.data.items) || [] - secretArray = secrets + if (!configuration) { + return [] + } - const filteredSecrets = secrets + const result = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedContent = atob(configuration.data[fileName]) + result.push({ + name: fileName, + content: decodedContent, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + result.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) - filteredSecrets.map((item) => { - const name = (item.metadata && item.metadata.name) || '' - item.text = name - item.value = name - return true + // Set the value to the model + commit('wizard/model$update', { + path: `/temp/${type}applyConfig`, + value: result, + force: true, + }) + + return result + } + + function getSelectedConfigurationName(configType, type) { + type = type ? type + '/' : '' + let path = '' + if (configType === 'create') path = `/spec/configuration/${type}/configSecret/name` + else if (configType === 'apply') path = `/${type}selectedConfiguration` + else if (configType === 'remove') path = `/${type}selectedConfigurationRemove` + + const selectedConfiguration = + configType === 'create' ? getValue(model, path) : getValue(discriminator, path) + + if (selectedConfiguration) + return { subtitle: ` You have selected ${selectedConfiguration} secret` } + else return { subtitle: 'No secret selected' } + } + + function getSelectedConfigurationValueForRemove(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfigurationRemove` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return '' + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, + ) + + if (!configuration) { + return '' + } + + let data = {} + // Decode base64 and parse YAML for each key in the secret data + Object.keys(configuration.data).forEach((item) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[item]) + // Parse YAML string to object + const parsedYaml = yaml.load(decodedString) + // Store the parsed object with the filename as key + data[item] = parsedYaml + } catch (e) { + console.error(`Error parsing ${item}:`, e) + data[item] = atob(configuration.data[item]) // Fallback to decoded string + } }) - return filteredSecrets + + // Convert data object back to YAML string + return yaml.dump(data) + } + + async function createNewConfigSecret(type) { + type = type ? type + '/' : '' + const { user, cluster } = route.params + const url = `/clusters/${user}/${cluster}/resources` + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + const secretName = getValue(discriminator, `${type}createSecret/name`) + const secretData = getValue(discriminator, `${type}createSecret/data`) + const secretDataObj = Object.fromEntries(secretData.map((item) => [item.key, item.value])) + + try { + const res = await axios.post(url, { + apiVersion: 'v1', + stringData: secretDataObj, + kind: 'Secret', + metadata: { + name: secretName, + namespace: namespace, + }, + type: 'Opaque', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'success', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/lastCreatedSecret`, + value: secretName, + }) + toast.success('Secret created successfully') + } catch (error) { + const errMsg = decodeError(error, 'Failed to create secret') + toast.error(errMsg, { timeout: 5000 }) + cancelCreateSecret() + } + } + + function decodeError(msg, defaultMsg) { + if (typeof msg === 'string') { + return msg || defaultMsg + } + return ( + (msg.response && msg.response.data && msg.response.data.message) || + (msg.response && msg.response.data) || + (msg.status && msg.status.status) || + defaultMsg + ) + } + + function isCreateSecret(type) { + type = type ? type + '/' : '' + const selectedSecret = getValue(model, `spec/configuration/${type}configSecret/name`) + const res = selectedSecret === 'Create' + + if (res === true) { + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'pending', + }) + } + return res + } + + function isNotCreateSecret(type) { + return !isCreateSecret(type) + } + + function onCreateSecretChange(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'cancelled') return '' + else if (secretStatus === 'success') { + const name = getValue(discriminator, `${type}createSecret/lastCreatedSecret`) + + const configFound = configSecrets.find((item) => item === name) + return configFound ? { text: name, value: name } : '' + } + } + + function cancelCreateSecret(type) { + type = type ? type + '/' : '' + commit('wizard/temp$delete', `${type}createSecret/name`) + commit('wizard/temp$delete', `${type}createSecret/data`) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'cancelled', + }) + } + + async function onApplyconfigChange(type) { + type = type ? type + '/' : '' + const configValue = getValue(discriminator, `${type}applyConfig`) + + if (!configValue) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + const tempConfigObj = {} + configValue.forEach((item) => { + if (item.name) { + tempConfigObj[item.name] = item.content + } + }) + if (Object.keys(tempConfigObj).length === 0) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}applyConfig`, + value: tempConfigObj, + }) + } + + function setApplyConfig(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfiguration` + const selectedConfig = getValue(discriminator, configPath) + if (!selectedConfig) { + return [{ name: '', content: '' }] + } + const applyconfigData = secretConfigData.find((item) => { + if (item.componentName === selectedConfig) { + return item + } + }) + const { applyConfig } = applyconfigData + const configObj = [] + + if (applyConfig) { + Object.keys(applyConfig).forEach((fileName) => { + configObj.push({ + name: fileName, + content: applyConfig[fileName], + }) + }) + } else { + configObj.push({ name: '', content: '' }) + } + return configObj + } + + function onRemoveConfigChange(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfigurationRemove` + const selectedConfig = getValue(discriminator, configPath) + + if (!selectedConfig) { + commit('wizard/model$delete', `/spec/configuration/${type}removeCustomConfig`) + return [{ name: '', content: '' }] + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}removeCustomConfig`, + value: true, + }) + + const configuration = secretConfigData.find((item) => item.componentName === selectedConfig) + + if (!configuration.data) { + return [{ name: '', content: '' }] + } + + const configObj = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) + return configObj + } + + async function onNewConfigSecretChange(type) { + type = type ? type + '/' : '' + const path = `/spec/configuration/${type}configSecret/name` + const selectedSecret = getValue(model, path) + + if (!selectedSecret) { + commit('wizard/model$delete', `/spec/configuration/${type}configSecret`) + return [{ name: '', content: '' }] + } + if (selectedSecret === 'Create') return [{ name: '', content: '' }] + + const owner = storeGet('/route/params/user') + const cluster = storeGet('/route/params/cluster') + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + + try { + // Fetch the secret data from API + const secretResp = await axios.get( + `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets/${selectedSecret}`, + ) + + const secretData = secretResp.data?.data || {} + const configObj = [] + + // Decode base64 and format as array of objects with name and content + Object.keys(secretData).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(secretData[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: secretData[fileName], // Fallback to original if decode fails + }) + } + }) + + return configObj + } catch (e) { + console.error('Error fetching secret:', e) + return [{ name: '', content: '' }] + } + } + + function onSelectedSecretChange(type, index) { + type = type ? type + '/' : '' + const secretData = getValue(discriminator, `${type}createSecret/data`) || [] + const selfSecrets = secretData.map((item) => item.key) + + const remainingSecrets = configSecretKeys.filter((item) => !selfSecrets.includes(item)) + + const selfKey = getValue(discriminator, `${type}createSecret/data/${index}/key`) + if (selfKey) { + remainingSecrets.push(selfKey) + } + const resSecret = remainingSecrets.map((item) => { + return { text: item, value: item } + }) + return resSecret } function createSecretUrl() { @@ -1022,26 +1411,6 @@ export const useFunc = (model) => { return reconfigurationType === value } - function onApplyconfigChange(type) { - const configPath = `/${type}/applyConfig` - const applyconfig = getValue(discriminator, configPath) - - const configObj = {} - - if (applyconfig) { - applyconfig.forEach((item) => { - const { key, value } = item - configObj[key] = value - }) - } - - commit('wizard/model$update', { - path: `/spec/configuration/${type}/applyConfig`, - value: configObj, - force: true, - }) - } - function onReconfigurationTypeChange(property, isShard) { setDiscriminatorValue(`/${property}/applyConfig`, []) let path = '/reconfigurationType' @@ -1450,5 +1819,20 @@ export const useFunc = (model) => { checkVolume, getSelectedConfigSecretValue, objectToYaml, + fetchConfigSecrets, + getConfigSecretsforAppyConfig, + getSelectedConfigurationData, + getSelectedConfigurationName, + getSelectedConfigurationValueForRemove, + createNewConfigSecret, + decodeError, + isCreateSecret, + isNotCreateSecret, + onCreateSecretChange, + cancelCreateSecret, + setApplyConfig, + onRemoveConfigChange, + onNewConfigSecretChange, + onSelectedSecretChange, } } diff --git a/charts/opskubedbcom-zookeeperopsrequest-editor/ui/create-ui.yaml b/charts/opskubedbcom-zookeeperopsrequest-editor/ui/create-ui.yaml index f2e4a49ea3..00e7239749 100644 --- a/charts/opskubedbcom-zookeeperopsrequest-editor/ui/create-ui.yaml +++ b/charts/opskubedbcom-zookeeperopsrequest-editor/ui/create-ui.yaml @@ -264,106 +264,177 @@ step: if: name: ifRequestTypeEqualsTo|Reconfigure type: function + loader: + name: fetchConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace elements: - - type: radio - label: Reconfigure Type - subtitle: Choose the action you'd like to perform. Select a new configuration secret, apply a custom configuration, or remove an existing setup. - validation: - type: required - options: - - text: New Config Secret - value: selectNewConfigSecret - - text: Apply Config - value: applyConfig - - text: Remove - value: remove - schema: temp/properties/reconfigurationType - watcher: - func: onReconfigurationTypeChange - paths: - - temp/properties/reconfigurationType - type: block-layout - label: Config Secret - showLabels: true - if: - name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret - type: function + label: Configuration elements: - - type: label-element - label: Select a configuration secret from the available list to update your database settings - customClass: mb-10 - - type: select - addNewButton: - label: Create a new Secret - target: _blank - url: - function: createSecretUrl - schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - refresh: true - label: Config Secret - loader: - name: getConfigSecrets - watchPaths: - - schema/properties/metadata/properties/namespace - init: - type: func - value: setValueFromDbDetails|/spec/configSecret/name - type: label-element - label: Selected Config Secret - loader: getSelectedConfigSecret - watcher: - func: getSelectedConfigSecret - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - - type: editor - label: Value - readonly: true - hasCopy: false - watcher: - func: getSelectedConfigSecretValue - paths: - - schema/properties/spec/properties/configuration/properties/configSecret/properties/name - validation: - type: required - schema: temp/properties/configArray/items/properties/value - - type: array-object-form - label: ApplyConfig - buttonClass: is-light is-outlined - schema: temp/properties/applyConfig - validation: - type: required - if: - name: ifReconfigurationTypeEqualsTo|applyConfig - type: function - watcher: - func: onApplyconfigChange - paths: - - temp/properties/applyConfig - elements: - - type: label-element - label: New Apply Config - subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings. Enter the parameter you want to configure (e.g., tickTime, initLimit). - customClass: mb-10 - - type: input - label: key - validation: - type: required - schema: key - - type: editor - label: value - hasCopy: false - validation: - type: required - schema: value - - type: switch - schema: schema/properties/spec/properties/configuration/properties/removeCustomConfig - label: Remove CustomConfig - fullwidth: true - if: - name: returnFalse - type: function + label: '' + subtitle: Select a new configuration secret, apply a custom configuration, or remove an existing setup to update your database settings + - type: tab-layout + label: New Config Secret + schema: temp/properties/reconfigurationType + options: + - text: NEW CONFIG SECRET + value: selectNewConfigSecret + - text: APPLY CONFIG + value: applyConfig + - text: REMOVE + value: remove + elements: + - type: block-layout + label: Config Secret + if: + name: ifReconfigurationTypeEqualsTo|selectNewConfigSecret + type: function + elements: + - type: label-element + label: Config Secret + subtitle: Select a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: schema/properties/spec/properties/configuration/properties/configSecret/properties/name + refresh: true + label: Config Secret + loader: + name: getConfigSecrets + watchPaths: + - schema/properties/metadata/properties/namespace + - temp/properties/createSecret/properties/status + init: + type: func + value: setValueFromDbDetails|/spec/configSecret/name + watcher: + func: onCreateSecretChange + paths: + - temp/properties/createSecret/properties/status + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|create + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + - type: block-layout + label: Create a New Config Secret + showLabels: true + if: + type: function + name: isCreateSecret + hasButton: + text: Save + hasCancel: cancelCreateSecret + action: createNewConfigSecret + elements: + - type: input + label: Secret Name + schema: temp/properties/createSecret/properties/name + validation: + type: required + - type: array-object-form + label: String Data + schema: temp/properties/createSecret/properties/data + buttonClass: is-light is-outlined + validation: + type: required + elements: + - type: select + label: Key + schema: key + loader: + name: onSelectedSecretChange + watchPaths: + - temp/properties/createSecret/properties/data + validation: + type: required + - type: textarea + label: Value + schema: value + - type: multi-file-editor + editorHeight: 500px + readonly: true + if: + type: function + name: isNotCreateSecret + loader: + name: onNewConfigSecretChange + watchPaths: + - schema/properties/spec/properties/configuration/properties/configSecret/properties/name + schema: temp/properties/newConfigSecret + - type: block-layout + label: ApplyConfig + if: + name: ifReconfigurationTypeEqualsTo|applyConfig + type: function + elements: + - type: label-element + label: New Apply Config + subtitle: Define custom configurations for your database using key-value pairs. These parameters will overwrite existing settings.
Enter the parameter you want to configure (e.g., max_connections). + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfiguration + refresh: true + label: Configuration + loader: getConfigSecretsforAppyConfig + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|apply + watchPaths: + - temp/properties/selectedConfiguration + - type: multi-file-editor + editorHeight: 500px + schema: temp/properties/applyConfig + loader: + name: setApplyConfig + watchPaths: + - temp/properties/selectedConfiguration + watcher: + func: onApplyconfigChange + paths: + - temp/properties/applyConfig + - type: block-layout + label: Remove + if: + name: ifReconfigurationTypeEqualsTo|remove + type: function + elements: + - type: label-element + label: Remove + subtitle: Selected a configuration secret from the available list to update your database settings + customClass: mb-10 + - type: select + customClass: mb-2 + schema: temp/properties/selectedConfigurationRemove + refresh: true + label: Configuration + loader: + name: getConfigSecretsforAppyConfig + watchPaths: + - schema/properties/metadata/properties/namespace + - type: label-element + label: '' + loader: + name: getSelectedConfigurationName|remove + watchPaths: + - temp/properties/selectedConfigurationRemove + - type: multi-file-editor + editorHeight: 500px + readonly: true + init: + type: func + value: onRemoveConfigChange + watcher: + func: onRemoveConfigChange + paths: + - temp/properties/selectedConfigurationRemove + schema: temp/properties/removeConfig + # common - type: block-layout label: OpsRequest Options diff --git a/charts/opskubedbcom-zookeeperopsrequest-editor/ui/functions.js b/charts/opskubedbcom-zookeeperopsrequest-editor/ui/functions.js index eb9a6a7d22..d89c6f517b 100644 --- a/charts/opskubedbcom-zookeeperopsrequest-editor/ui/functions.js +++ b/charts/opskubedbcom-zookeeperopsrequest-editor/ui/functions.js @@ -1,4 +1,4 @@ -const { axios, useOperator, store } = window.vueHelpers || {} +const { axios, useOperator, store, useToast } = window.vueHelpers || {} const machines = { 'db.t.micro': { @@ -307,9 +307,11 @@ const machineList = [ ] let machinesFromPreset = [] +const configSecretKeys = ['zoo.cfg'] export const useFunc = (model) => { const route = store.state?.route + const toast = useToast() const { getValue, storeGet, discriminator, setDiscriminatorValue, commit } = useOperator( model, @@ -678,34 +680,419 @@ export const useFunc = (model) => { return !ver } - // for config secret - let secretArray = [] + // Fetch and store database Infos + // for secret configurations in reconfigure + let configSecrets = [] + let secretConfigData = [] - async function getConfigSecrets() { + async function fetchConfigSecrets() { const owner = storeGet('/route/params/user') const cluster = storeGet('/route/params/cluster') const namespace = getValue(model, '/metadata/namespace') - // watchDependency'model#/metadata/namespace') + // watchDependency('model#/metadata/namespace') - const resp = await axios.get( - `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets`, + const name = getValue(model, '/spec/databaseRef/name') + const dbGroup = getValue(model, '/route/params/group') + const dbKind = getValue(store.state, '/resource/definition/result/kind') + const dbResource = getValue(model, '/route/params/resource') + const dbVersion = getValue(model, '/route/params/version') + + try { + const resp = await axios.post( + `/clusters/${owner}/${cluster}/proxy/ui.kubedb.com/v1alpha1/databaseinfos`, + { + apiVersion: 'ui.kubedb.com/v1alpha1', + kind: 'DatabaseInfo', + request: { + source: { + ref: { + name: name, + namespace: namespace, + }, + resource: { + group: dbGroup, + kind: dbKind, + name: dbResource, + version: dbVersion, + }, + }, + keys: ['zoo.cfg'], + }, + }, + ) + configSecrets = resp?.data?.response?.availableSecrets || [] + secretConfigData = resp?.data?.response?.configurations || [] + } catch (e) { + console.log(e) + } + } + + async function getConfigSecrets(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'success') { + await fetchConfigSecrets() + } + const mappedSecrets = configSecrets.map((item) => { + return { text: item, value: item } + }) + mappedSecrets.push({ text: '+ Create a new Secret', value: 'Create' }) + return mappedSecrets + } + + async function getConfigSecretsforAppyConfig() { + const secrets = secretConfigData.map((item) => { + return { text: item.componentName, value: item.componentName } + }) + return secrets + } + + function getSelectedConfigurationData(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfiguration` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return [] + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, ) - const secrets = (resp && resp.data && resp.data.items) || [] + if (!configuration) { + return [] + } - const filteredSecrets = secrets + const result = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedContent = atob(configuration.data[fileName]) + result.push({ + name: fileName, + content: decodedContent, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + result.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } + }) - filteredSecrets.map((item) => { - const name = (item.metadata && item.metadata.name) || '' - item.text = name - item.value = name - return true + // Set the value to the model + commit('wizard/model$update', { + path: `/temp/${type}applyConfig`, + value: result, + force: true, + }) + + return result + } + + function getSelectedConfigurationName(configType, type) { + type = type ? type + '/' : '' + let path = '' + if (configType === 'create') path = `/spec/configuration/${type}/configSecret/name` + else if (configType === 'apply') path = `/${type}selectedConfiguration` + else if (configType === 'remove') path = `/${type}selectedConfigurationRemove` + + const selectedConfiguration = + configType === 'create' ? getValue(model, path) : getValue(discriminator, path) + + if (selectedConfiguration) + return { subtitle: ` You have selected ${selectedConfiguration} secret` } + else return { subtitle: 'No secret selected' } + } + + function getSelectedConfigurationValueForRemove(type) { + type = type ? type + '/' : '' + const path = `/${type}selectedConfigurationRemove` + const selectedConfiguration = getValue(discriminator, path) + + if (!selectedConfiguration) { + return '' + } + + const configuration = secretConfigData.find( + (item) => item.componentName === selectedConfiguration, + ) + + if (!configuration) { + return '' + } + + let data = {} + // Decode base64 and parse YAML for each key in the secret data + Object.keys(configuration.data).forEach((item) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[item]) + // Parse YAML string to object + const parsedYaml = yaml.load(decodedString) + // Store the parsed object with the filename as key + data[item] = parsedYaml + } catch (e) { + console.error(`Error parsing ${item}:`, e) + data[item] = atob(configuration.data[item]) // Fallback to decoded string + } + }) + + // Convert data object back to YAML string + return yaml.dump(data) + } + + async function createNewConfigSecret(type) { + type = type ? type + '/' : '' + const { user, cluster } = route.params + const url = `/clusters/${user}/${cluster}/resources` + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + const secretName = getValue(discriminator, `${type}createSecret/name`) + const secretData = getValue(discriminator, `${type}createSecret/data`) + const secretDataObj = Object.fromEntries(secretData.map((item) => [item.key, item.value])) + + try { + const res = await axios.post(url, { + apiVersion: 'v1', + stringData: secretDataObj, + kind: 'Secret', + metadata: { + name: secretName, + namespace: namespace, + }, + type: 'Opaque', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'success', + }) + commit('wizard/temp$update', { + path: `${type}createSecret/lastCreatedSecret`, + value: secretName, + }) + toast.success('Secret created successfully') + } catch (error) { + const errMsg = decodeError(error, 'Failed to create secret') + toast.error(errMsg, { timeout: 5000 }) + cancelCreateSecret() + } + } + + function decodeError(msg, defaultMsg) { + if (typeof msg === 'string') { + return msg || defaultMsg + } + return ( + (msg.response && msg.response.data && msg.response.data.message) || + (msg.response && msg.response.data) || + (msg.status && msg.status.status) || + defaultMsg + ) + } + + function isCreateSecret(type) { + type = type ? type + '/' : '' + const selectedSecret = getValue(model, `spec/configuration/${type}configSecret/name`) + const res = selectedSecret === 'Create' + + if (res === true) { + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'pending', + }) + } + return res + } + + function isNotCreateSecret(type) { + return !isCreateSecret(type) + } + + function onCreateSecretChange(type) { + type = type ? type + '/' : '' + const secretStatus = getValue(discriminator, `${type}createSecret/status`) + if (secretStatus === 'cancelled') return '' + else if (secretStatus === 'success') { + const name = getValue(discriminator, `${type}createSecret/lastCreatedSecret`) + + const configFound = configSecrets.find((item) => item === name) + return configFound ? { text: name, value: name } : '' + } + } + + function cancelCreateSecret(type) { + type = type ? type + '/' : '' + commit('wizard/temp$delete', `${type}createSecret/name`) + commit('wizard/temp$delete', `${type}createSecret/data`) + commit('wizard/temp$update', { + path: `${type}createSecret/status`, + value: 'cancelled', + }) + } + + async function onApplyconfigChange(type) { + type = type ? type + '/' : '' + const configValue = getValue(discriminator, `${type}applyConfig`) + + if (!configValue) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + const tempConfigObj = {} + configValue.forEach((item) => { + if (item.name) { + tempConfigObj[item.name] = item.content + } + }) + if (Object.keys(tempConfigObj).length === 0) { + commit('wizard/model$delete', `/spec/configuration/${type}applyConfig`) + return + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}applyConfig`, + value: tempConfigObj, + }) + } + + function setApplyConfig(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfiguration` + const selectedConfig = getValue(discriminator, configPath) + if (!selectedConfig) { + return [{ name: '', content: '' }] + } + const applyconfigData = secretConfigData.find((item) => { + if (item.componentName === selectedConfig) { + return item + } + }) + const { applyConfig } = applyconfigData + const configObj = [] + + if (applyConfig) { + Object.keys(applyConfig).forEach((fileName) => { + configObj.push({ + name: fileName, + content: applyConfig[fileName], + }) + }) + } else { + configObj.push({ name: '', content: '' }) + } + return configObj + } + + function onRemoveConfigChange(type) { + type = type ? type + '/' : '' + const configPath = `/${type}selectedConfigurationRemove` + const selectedConfig = getValue(discriminator, configPath) + + if (!selectedConfig) { + commit('wizard/model$delete', `/spec/configuration/${type}removeCustomConfig`) + return [{ name: '', content: '' }] + } + commit('wizard/model$update', { + path: `/spec/configuration/${type}removeCustomConfig`, + value: true, + }) + + const configuration = secretConfigData.find((item) => item.componentName === selectedConfig) + + if (!configuration.data) { + return [{ name: '', content: '' }] + } + + const configObj = [] + // Decode base64 and format as array of objects with name and content + Object.keys(configuration.data).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(configuration.data[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: configuration.data[fileName], // Fallback to original if decode fails + }) + } }) + return configObj + } + + async function onNewConfigSecretChange(type) { + type = type ? type + '/' : '' + const path = `/spec/configuration/${type}configSecret/name` + const selectedSecret = getValue(model, path) + + if (!selectedSecret) { + commit('wizard/model$delete', `/spec/configuration/${type}configSecret`) + return [{ name: '', content: '' }] + } + if (selectedSecret === 'Create') return [{ name: '', content: '' }] + + const owner = storeGet('/route/params/user') + const cluster = storeGet('/route/params/cluster') + const namespace = storeGet('/route/query/namespace') || getValue(model, '/metadata/namespace') + + try { + // Fetch the secret data from API + const secretResp = await axios.get( + `/clusters/${owner}/${cluster}/proxy/core/v1/namespaces/${namespace}/secrets/${selectedSecret}`, + ) + + const secretData = secretResp.data?.data || {} + const configObj = [] + + // Decode base64 and format as array of objects with name and content + Object.keys(secretData).forEach((fileName) => { + try { + // Decode base64 string + const decodedString = atob(secretData[fileName]) + configObj.push({ + name: fileName, + content: decodedString, + }) + } catch (e) { + console.error(`Error decoding ${fileName}:`, e) + configObj.push({ + name: fileName, + content: secretData[fileName], // Fallback to original if decode fails + }) + } + }) - secretArray = filteredSecrets - return filteredSecrets + return configObj + } catch (e) { + console.error('Error fetching secret:', e) + return [{ name: '', content: '' }] + } + } + + function onSelectedSecretChange(index) { + const secretData = getValue(discriminator, 'createSecret/data') || [] + const selfSecrets = secretData.map((item) => item.key) + + const remainingSecrets = configSecretKeys.filter((item) => !selfSecrets.includes(item)) + + const selfKey = getValue(discriminator, `createSecret/data/${index}/key`) + if (selfKey) { + remainingSecrets.push(selfKey) + } + const resSecret = remainingSecrets.map((item) => { + return { text: item, value: item } + }) + return resSecret } + let secretArray = [] + function objectToYaml(obj, indent = 0) { if (obj === null || obj === undefined) return 'null' if (typeof obj !== 'object') return JSON.stringify(obj) @@ -887,24 +1274,6 @@ export const useFunc = (model) => { return reconfigurationType === value } - function onApplyconfigChange() { - const applyconfig = getValue(discriminator, '/applyConfig') - - const configObj = {} - if (applyconfig) { - applyconfig.forEach((item) => { - const { key, value } = item - configObj[key] = value - }) - } - - commit('wizard/model$update', { - path: '/spec/configuration/applyConfig', - value: configObj, - force: true, - }) - } - function onReconfigurationTypeChange() { const reconfigurationType = getValue(discriminator, '/reconfigurationType') setDiscriminatorValue('/applyConfig', []) @@ -1249,5 +1618,20 @@ export const useFunc = (model) => { onMachineChange, isMachineCustom, checkVolume, + fetchConfigSecrets, + getConfigSecretsforAppyConfig, + getSelectedConfigurationData, + getSelectedConfigurationName, + getSelectedConfigurationValueForRemove, + createNewConfigSecret, + decodeError, + isCreateSecret, + isNotCreateSecret, + onCreateSecretChange, + cancelCreateSecret, + setApplyConfig, + onRemoveConfigChange, + onNewConfigSecretChange, + onSelectedSecretChange, } } diff --git a/schemas/ui-schema.json b/schemas/ui-schema.json index c5f02feb89..3176b085f8 100644 --- a/schemas/ui-schema.json +++ b/schemas/ui-schema.json @@ -500,7 +500,7 @@ "type": "string" }, "loader": { - "type": "string" + "$ref": "#/definitions/LoaderType" }, "showLabels": { "type": "boolean" @@ -676,7 +676,7 @@ "type": "string" }, "loader": { - "type": "string" + "$ref": "#/definitions/LoaderType" }, "showLabels": { "type": "boolean" @@ -1479,7 +1479,7 @@ "type": "string" }, "loader": { - "type": "string" + "$ref": "#/definitions/LoaderType" }, "options": { "$ref": "#/definitions/Options"