diff --git a/shell/components/ResourceDetail/index.vue b/shell/components/ResourceDetail/index.vue index 55aff48dd43..92ece3fae36 100644 --- a/shell/components/ResourceDetail/index.vue +++ b/shell/components/ResourceDetail/index.vue @@ -28,7 +28,6 @@ function modeFor(route) { } async function getYaml(store, model) { - const inStore = store.getters['currentStore'](model.type); let yaml; const opt = { headers: { accept: 'application/yaml' } }; @@ -36,9 +35,7 @@ async function getYaml(store, model) { yaml = (await model.followLink('view', opt)).data; } - const cleanedYaml = await store.dispatch(`${ inStore }/cleanForDownload`, yaml); - - return cleanedYaml; + return model.cleanForDownload(yaml); } export default { diff --git a/shell/models/__tests__/secret.test.ts b/shell/models/__tests__/secret.test.ts new file mode 100644 index 00000000000..7e27786d3a3 --- /dev/null +++ b/shell/models/__tests__/secret.test.ts @@ -0,0 +1,37 @@ +import Secret from '@shell/models/secret'; + +describe('class Secret', () => { + it('should contains the type attribute if cleanForDownload', async() => { + const secret = new Secret({}); + const yaml = `apiVersion: v1 +kind: Secret +metadata: + name: my-secret +type: Opaque +`; + const cleanYaml = await secret.cleanForDownload(yaml); + + expect(cleanYaml).toBe(yaml); + }); + + it('should remove id, links and actions keys if cleanForDownload', async() => { + const secret = new Secret({}); + const expectedYamlStr = `apiVersion: v1 +kind: Secret +metadata: + name: my-secret + namespace: default +type: Opaque +`; + const part = `id: test_id +links: + view: https://example.com +actions: + remove: https://example.com`; + const yaml = `${ expectedYamlStr } +${ part }`; + const cleanYaml = await secret.cleanForDownload(yaml); + + expect(cleanYaml).toBe(expectedYamlStr); + }); +}); diff --git a/shell/models/secret.js b/shell/models/secret.js index 8892e03c2e8..be380dfff78 100644 --- a/shell/models/secret.js +++ b/shell/models/secret.js @@ -9,6 +9,7 @@ import SteveModel from '@shell/plugins/steve/steve-class'; import { colorForState, stateDisplay, STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class'; import { diffFrom } from '@shell/utils/time'; import day from 'dayjs'; +import { steveCleanForDownload } from '@shell/plugins/steve/resource-utils'; export const TYPES = { OPAQUE: 'Opaque', @@ -456,4 +457,12 @@ export default class Secret extends SteveModel { return val; } + + async cleanForDownload(yaml) { + // secret resource contains the type attribute + // ref: https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/secret-v1/ + // ref: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types + + return steveCleanForDownload(yaml, { rootKeys: ['id', 'links', 'actions'] }); + } } diff --git a/shell/plugins/dashboard-store/resource-class.js b/shell/plugins/dashboard-store/resource-class.js index 176cfc6ee60..06a0cd466b7 100644 --- a/shell/plugins/dashboard-store/resource-class.js +++ b/shell/plugins/dashboard-store/resource-class.js @@ -1381,7 +1381,7 @@ export default class Resource { async download() { const value = await this.followLink('view', { headers: { accept: 'application/yaml' } }); - const data = await this.$dispatch('cleanForDownload', value.data); + const data = await this.cleanForDownload(value.data); downloadFile(`${ this.nameDisplay }.yaml`, data, 'application/yaml'); } @@ -1404,7 +1404,7 @@ export default class Resource { await eachLimit(items, 10, (item, idx) => { return item.followLink('view', { headers: { accept: 'application/yaml' } } ).then(async(data) => { const yaml = data.data || data; - const cleanedYaml = await this.$dispatch('cleanForDownload', yaml); + const cleanedYaml = await this.cleanForDownload(yaml); files[`resources/${ names[idx] }`] = cleanedYaml; }); @@ -1481,6 +1481,10 @@ export default class Resource { this.$dispatch(`cleanForDiff`, this.toJSON()); } + async cleanForDownload(yaml) { + return this.$dispatch(`cleanForDownload`, yaml); + } + yamlForSave(yaml) { try { const obj = jsyaml.load(yaml); diff --git a/shell/plugins/steve/__tests__/resource-utils.test.ts b/shell/plugins/steve/__tests__/resource-utils.test.ts new file mode 100644 index 00000000000..ad9ca592dfc --- /dev/null +++ b/shell/plugins/steve/__tests__/resource-utils.test.ts @@ -0,0 +1,159 @@ +import { steveCleanForDownload } from '@shell/plugins/steve/resource-utils'; + +describe('steve: ressource-utils', () => { + it('should do nothing if the yaml is not passed', () => { + const r = steveCleanForDownload(); + + expect(r).toBeUndefined(); + }); + it('should remove all default rootKeys', () => { + const expectedYamlStr = `apiVersion: v1 +kind: ConfigMap +metadata: + name: my-configmap +`; + const yamlStr = ` +id: test_id +links: + view: https://example.com2 +type: test_type +actions: + remove: https://example.com +${ expectedYamlStr } +`; + const cleanedYamlStr = steveCleanForDownload(yamlStr); + + expect(cleanedYamlStr).toBe(expectedYamlStr); + }); + it('should remove all the specified root keys', () => { + const part = `apiVersion: v1 +kind: Secret +metadata: + name: my-secret`; + + const rootKeyToYamlStringMap = { + id: 'id: test_id', + links: `links: + view: https://example.com`, + actions: `actions: + remove: https://example.com`, + type: 'type: Opaque' + }; + + const entries = Object.entries(rootKeyToYamlStringMap); + const yamlStr = `${ part } +${ entries.map(([_, str]) => str).join('\n') }`; + + entries.forEach(([key, str]) => { + const expectedYamlStr = `${ part } +${ entries.filter(([k]) => k !== key).map(([_, str]) => str).join('\n') }\n`; + const cleanedYamlStr = steveCleanForDownload(yamlStr, { rootKeys: [key] }); + + expect(cleanedYamlStr).toBe(expectedYamlStr); + }); + }); + it('should remove all default metadata keys', () => { + const expectedYamlStr = `apiVersion: v1 +kind: ConfigMap +metadata: + name: my-configmap +`; + const yamlStr = `apiVersion: v1 +kind: ConfigMap +metadata: + name: my-configmap + fields: + - kube-root-ca.crt + - 1 + - 7d23h + relationships: + - rel: 'owner' + state: 'active' +`; + const cleanedYamlStr = steveCleanForDownload(yamlStr); + + expect(cleanedYamlStr).toBe(expectedYamlStr); + }); + + it('should remove all the specified metadata keys', () => { + const part = `apiVersion: v1 +kind: ConfigMap +metadata: + name: my-configmap`; + + const metadataKeyToYamlStringMap = { + fields: +` fields: + - kube-root-ca.crt + - 1 + - 7d23h`, + relationships: +` relationships: + - rel: owner`, + state: ` state: active` + }; + + const entries = Object.entries(metadataKeyToYamlStringMap); + const yamlStr = `${ part } +${ entries.map(([_, str]) => str).join('\n') }`; + + entries.forEach(([key, str]) => { + const expectedYamlStr = `${ part } +${ entries.filter(([k]) => k !== key).map(([_, str]) => str).join('\n') }\n`; + const cleanedYamlStr = steveCleanForDownload(yamlStr, { metadataKeys: [key] }); + + expect(cleanedYamlStr).toBe(expectedYamlStr); + }); + }); + it('should remove all defalut condition keys', () => { + const expectedYamlStr = `apiVersion: v1 +kind: ConfigMap +metadata: + name: my-configmap +status: + conditions: + - {} + - {} + - message: message +`; + const yamlStr = `apiVersion: v1 +kind: ConfigMap +metadata: + name: my-configmap +status: + conditions: + - error: 'error' + - transitioning: false + - message: message +`; + const cleanedYamlStr = steveCleanForDownload(yamlStr); + + expect(cleanedYamlStr).toBe(expectedYamlStr); + }); + it('should remove all the specified condition keys', () => { + const part = `apiVersion: v1 +kind: ConfigMap +metadata: + name: my-configmap +status: + conditions: + - message: message`; + + const conditionKeyToYamlStringMap = { + error: ' - error: error', + transitioning: ' - transitioning: false' + }; + + const entries = Object.entries(conditionKeyToYamlStringMap); + const yamlStr = `${ part } +${ entries.map(([_, str]) => str).join('\n') }`; + + entries.forEach(([key, str]) => { + const expectedYamlStr = `${ part } +${ entries.map(([k, str]) => k === key ? ' - {}' : str).join('\n') }\n`; + const cleanedYamlStr = steveCleanForDownload(yamlStr, { conditionKeys: [key] }); + + expect(cleanedYamlStr).toBe(expectedYamlStr); + }); + }); +}); diff --git a/shell/plugins/steve/actions.js b/shell/plugins/steve/actions.js index 06c9cf37cf8..1ba53de2c2f 100644 --- a/shell/plugins/steve/actions.js +++ b/shell/plugins/steve/actions.js @@ -1,14 +1,14 @@ import https from 'https'; import { addParam, parse as parseUrl, stringify as unParseUrl } from '@shell/utils/url'; import { handleSpoofedRequest, loadSchemas } from '@shell/plugins/dashboard-store/actions'; -import { set } from '@shell/utils/object'; +import { dropKeys, set } from '@shell/utils/object'; import { deferred } from '@shell/utils/promise'; import { streamJson, streamingSupported } from '@shell/utils/stream'; import isObject from 'lodash/isObject'; import { classify } from '@shell/plugins/dashboard-store/classify'; import { NAMESPACE } from '@shell/config/types'; -import jsyaml from 'js-yaml'; import { handleKubeApiHeaderWarnings } from '@shell/plugins/steve/header-warnings'; +import { steveCleanForDownload } from '@shell/plugins/steve/resource-utils'; export default { @@ -330,31 +330,7 @@ export default { // remove fields added by steve before showing/downloading yamls cleanForDownload(ctx, yaml) { - if (!yaml) { - return; - } - const rootKeys = [ - 'id', - 'links', - 'type', - 'actions' - ]; - const metadataKeys = [ - 'fields', - 'relationships', - 'state', - ]; - const conditionKeys = [ - 'error', - 'transitioning', - ]; - const obj = jsyaml.load(yaml); - - dropKeys(obj, rootKeys); - dropKeys(obj?.metadata, metadataKeys); - (obj?.status?.conditions || []).forEach((condition) => dropKeys(condition, conditionKeys)); - - return jsyaml.dump(obj); + return steveCleanForDownload(yaml); } }; @@ -398,16 +374,6 @@ function dropUnderscores(obj) { } } -function dropKeys(obj, keys) { - if ( !obj ) { - return; - } - - for ( const k of keys ) { - delete obj[k]; - } -} - function dropCattleKeys(obj) { if ( !obj ) { return; diff --git a/shell/plugins/steve/resource-utils.ts b/shell/plugins/steve/resource-utils.ts new file mode 100644 index 00000000000..c4ece5ff9e7 --- /dev/null +++ b/shell/plugins/steve/resource-utils.ts @@ -0,0 +1,38 @@ +import { dropKeys } from '@shell/utils/object'; +import jsyaml from 'js-yaml'; + +export function steveCleanForDownload(yaml: string, keys?: { + rootKeys?: string[], + metadataKeys?: string[], + conditionKeys?: string[] + }): string | undefined { + if (!yaml) { + return; + } + + const { + rootKeys = [ + 'id', + 'links', + 'type', + 'actions' + ], + metadataKeys = [ + 'fields', + 'relationships', + 'state', + ], + conditionKeys = [ + 'error', + 'transitioning', + ] + } = keys || {}; + + const obj: any = jsyaml.load(yaml); + + dropKeys(obj, rootKeys); + dropKeys(obj?.metadata, metadataKeys); + (obj?.status?.conditions || []).forEach((condition: any) => dropKeys(condition, conditionKeys)); + + return jsyaml.dump(obj); +} diff --git a/shell/utils/object.js b/shell/utils/object.js index 5922cd6e5ee..5aba43f2d90 100644 --- a/shell/utils/object.js +++ b/shell/utils/object.js @@ -425,3 +425,13 @@ export function pickBy(obj = {}, predicate = (value, key) => false) { export const toDictionary = (array, callback) => Object.assign( {}, ...array.map((item) => ({ [item]: callback(item) })) ); + +export function dropKeys(obj, keys) { + if ( !obj ) { + return; + } + + for ( const k of keys ) { + delete obj[k]; + } +}