@rancher/shell 3.0.12-rc.4 → 3.0.12-rc.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/styles/global/_button.scss +1 -1
- package/assets/translations/en-us.yaml +39 -10
- package/components/ActionDropdownShell.vue +5 -3
- package/components/ButtonGroup.vue +26 -1
- package/components/CruResource.vue +51 -2
- package/components/PromptRestore.vue +93 -32
- package/components/Questions/index.vue +1 -0
- package/components/ResourceTable.vue +1 -0
- package/components/SortableTable/index.vue +4 -3
- package/components/Wizard.vue +14 -1
- package/components/__tests__/ButtonGroup.test.ts +56 -0
- package/components/__tests__/PromptRestore.test.ts +169 -19
- package/components/fleet/GitRepoAdvancedTab.vue +1 -0
- package/components/fleet/GitRepoMetadataTab.vue +5 -0
- package/components/fleet/HelmOpAppCoConfigTab.vue +4 -0
- package/components/fleet/HelmOpMetadataTab.vue +5 -0
- package/components/form/FileSelector.vue +39 -1
- package/components/form/PrivateRegistry.constants.ts +7 -0
- package/components/form/PrivateRegistry.vue +253 -18
- package/components/form/SelectOrCreateAuthSecret.vue +140 -17
- package/components/form/__tests__/FileSelector.test.ts +23 -0
- package/components/form/__tests__/PrivateRegistry.test.ts +463 -73
- package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +122 -0
- package/components/formatter/EtcdSnapshotName.vue +73 -0
- package/components/nav/Header.vue +8 -1
- package/components/templates/default.vue +7 -0
- package/config/features.js +1 -0
- package/config/labels-annotations.js +2 -0
- package/config/product/manager.js +6 -0
- package/config/secret.ts +10 -0
- package/config/settings.ts +6 -2
- package/config/types.js +7 -0
- package/detail/provisioning.cattle.io.cluster.vue +79 -3
- package/dialog/RotateEncryptionKeyDialog.vue +33 -9
- package/dialog/__tests__/RotateEncryptionKeyDialog.test.ts +78 -0
- package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +92 -0
- package/edit/__tests__/fleet.cattle.io.helmop.test.ts +101 -0
- package/edit/__tests__/management.cattle.io.setting.test.ts +2 -1
- package/edit/compliance.cattle.io.clusterscanprofile.vue +39 -41
- package/edit/fleet.cattle.io.gitrepo.vue +70 -16
- package/edit/fleet.cattle.io.helmop.vue +51 -5
- package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
- package/edit/{management.cattle.io.setting.vue → management.cattle.io.setting/index.vue} +32 -9
- package/edit/management.cattle.io.setting/system-default-registry-pull-secrets.vue +81 -0
- package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +3 -12
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +18 -0
- package/edit/provisioning.cattle.io.cluster/rke2.vue +5 -1
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +0 -1
- package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +14 -55
- package/models/__tests__/provisioning.cattle.io.cluster.test.ts +156 -0
- package/models/__tests__/secret.test.ts +68 -1
- package/models/management.cattle.io.cluster.js +21 -3
- package/models/pod.js +13 -2
- package/models/provisioning.cattle.io.cluster.js +59 -9
- package/models/rke.cattle.io.etcdsnapshot.js +17 -9
- package/models/secret.js +19 -0
- package/models/workload.js +12 -7
- package/package.json +1 -1
- package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +485 -107
- package/pages/c/_cluster/apps/charts/install.vue +114 -28
- package/pkg/require-asset.lib.js +25 -0
- package/pkg/vue.config.js +7 -0
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +84 -0
- package/plugins/dashboard-store/getters.js +0 -1
- package/plugins/dashboard-store/resource-class.js +52 -12
- package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +30 -0
- package/rancher-components/Form/TextArea/__tests__/TextAreaAutoGrow.test.ts +95 -0
- package/rancher-components/RcButton/index.ts +1 -1
- package/rancher-components/RcDropdown/RcDropdownTrigger.vue +6 -1
- package/store/__tests__/features.test.ts +131 -0
- package/store/__tests__/growl.test.ts +374 -0
- package/store/__tests__/modal.test.ts +131 -0
- package/store/__tests__/slideInPanel.test.ts +88 -0
- package/store/__tests__/type-map.utils.test.ts +433 -0
- package/store/features.js +4 -0
- package/types/shell/index.d.ts +62 -0
- package/utils/__tests__/operation-cr.test.ts +34 -0
- package/utils/operation-cr.js +19 -0
- package/utils/require-asset.ts +7 -0
- package/utils/validators/__tests__/private-registry.test.ts +27 -15
- package/utils/validators/private-registry.ts +15 -4
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, onMounted, toRef } from 'vue';
|
|
3
|
+
import { useStore } from 'vuex';
|
|
4
|
+
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
|
5
|
+
import { SECRET } from '@shell/config/types';
|
|
6
|
+
import { SECRET_TYPES } from '@shell/config/secret';
|
|
7
|
+
|
|
8
|
+
const AUTH_TYPES = [
|
|
9
|
+
SECRET_TYPES.DOCKER_JSON,
|
|
10
|
+
SECRET_TYPES.BASIC,
|
|
11
|
+
SECRET_TYPES.SSH,
|
|
12
|
+
SECRET_TYPES.RKE_AUTH_CONFIG
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const props = defineProps<{
|
|
16
|
+
settingValue: Record<string, any>;
|
|
17
|
+
defaultValue?: Record<string, any>;
|
|
18
|
+
rules?: Array<(value: any) => string | undefined>;
|
|
19
|
+
}>();
|
|
20
|
+
|
|
21
|
+
const emit = defineEmits(['update:settingValue']);
|
|
22
|
+
|
|
23
|
+
const store = useStore();
|
|
24
|
+
const settingValue = toRef(props, 'settingValue');
|
|
25
|
+
const defaultValue = toRef(props, 'defaultValue');
|
|
26
|
+
const secrets = ref<any[]>([]);
|
|
27
|
+
|
|
28
|
+
const secretOptions = computed(() => secrets.value
|
|
29
|
+
.filter((s) => AUTH_TYPES.includes(s._type))
|
|
30
|
+
.map((s) => {
|
|
31
|
+
const { dataPreview, subTypeDisplay, metadata } = s;
|
|
32
|
+
|
|
33
|
+
const label = subTypeDisplay && dataPreview ? `${ metadata.name } (${ subTypeDisplay }: ${ dataPreview })` : `${ metadata.name } (${ subTypeDisplay })`;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
label,
|
|
37
|
+
value: s.metadata?.name
|
|
38
|
+
};
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const selectedSecrets = computed({
|
|
43
|
+
get: () => {
|
|
44
|
+
const sValue = settingValue.value || defaultValue.value || '';
|
|
45
|
+
|
|
46
|
+
return sValue.split(',').reduce((all: string[], secret: string) => {
|
|
47
|
+
if (!!secret.trim()) {
|
|
48
|
+
all.push(secret.trim());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return all;
|
|
52
|
+
}, []);
|
|
53
|
+
},
|
|
54
|
+
set: (neu: string[]) => {
|
|
55
|
+
const newSettingValue = neu?.length ? neu.join(',') : null;
|
|
56
|
+
|
|
57
|
+
emit('update:settingValue', newSettingValue);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
onMounted(async() => {
|
|
62
|
+
const res = await store.dispatch('management/findAll', {
|
|
63
|
+
type: SECRET,
|
|
64
|
+
opt: { namespaced: 'cattle-system' },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
secrets.value = Array.isArray(res) ? res : [];
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<template>
|
|
73
|
+
<LabeledSelect
|
|
74
|
+
id="pull-secrets"
|
|
75
|
+
v-model:value="selectedSecrets"
|
|
76
|
+
label="Image Pull Secrets"
|
|
77
|
+
:options="secretOptions"
|
|
78
|
+
:rules="rules"
|
|
79
|
+
:multiple="true"
|
|
80
|
+
/>
|
|
81
|
+
</template>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import Loading from '@shell/components/Loading';
|
|
3
3
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
4
|
-
import { NORMAN
|
|
4
|
+
import { NORMAN } from '@shell/config/types';
|
|
5
5
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
6
6
|
import CruResource from '@shell/components/CruResource';
|
|
7
7
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
@@ -50,11 +50,8 @@ export default {
|
|
|
50
50
|
const field = this.$store.getters['plugins/credentialFieldForDriver'](this.driverName);
|
|
51
51
|
|
|
52
52
|
this.newCredential = await this.$store.dispatch('rancher/create', {
|
|
53
|
-
type:
|
|
54
|
-
|
|
55
|
-
namespace: DEFAULT_WORKSPACE,
|
|
56
|
-
annotations: { [CAPI.CREDENTIAL_DRIVER]: this.driverName }
|
|
57
|
-
},
|
|
53
|
+
type: NORMAN.CLOUD_CREDENTIAL,
|
|
54
|
+
annotations: { [CAPI.CREDENTIAL_DRIVER]: this.driverName },
|
|
58
55
|
[`${ field }credentialConfig`]: {}
|
|
59
56
|
});
|
|
60
57
|
|
|
@@ -216,12 +213,6 @@ export default {
|
|
|
216
213
|
}
|
|
217
214
|
}
|
|
218
215
|
|
|
219
|
-
if ( this.newCredential.metadata.name ) {
|
|
220
|
-
delete this.newCredential.metadata.generateName;
|
|
221
|
-
} else {
|
|
222
|
-
this.newCredential.metadata.generateName = 'cloud-credential-';
|
|
223
|
-
}
|
|
224
|
-
|
|
225
216
|
try {
|
|
226
217
|
const res = await this.newCredential.save();
|
|
227
218
|
|
|
@@ -806,4 +806,22 @@ describe('component: rke2', () => {
|
|
|
806
806
|
expect(wrapper.vm.value.spec.rkeConfig.machineGlobalConfig[INGRESS_CONTROLLER]).toBe(INGRESS_NGINX);
|
|
807
807
|
});
|
|
808
808
|
});
|
|
809
|
+
|
|
810
|
+
describe('computed: canEditAsYaml', () => {
|
|
811
|
+
it('should return false when isUpstreamCAPIProvider is true', () => {
|
|
812
|
+
const vm = { isUpstreamCAPIProvider: true } as any;
|
|
813
|
+
|
|
814
|
+
const canEditAsYaml = rke2.computed!.canEditAsYaml.call(vm);
|
|
815
|
+
|
|
816
|
+
expect(canEditAsYaml).toBe(false);
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
it('should return true when isUpstreamCAPIProvider is false', () => {
|
|
820
|
+
const vm = { isUpstreamCAPIProvider: false } as any;
|
|
821
|
+
|
|
822
|
+
const canEditAsYaml = rke2.computed!.canEditAsYaml.call(vm);
|
|
823
|
+
|
|
824
|
+
expect(canEditAsYaml).toBe(true);
|
|
825
|
+
});
|
|
826
|
+
});
|
|
809
827
|
});
|
|
@@ -919,6 +919,10 @@ export default {
|
|
|
919
919
|
return this.needCredential && !this.credentialId;
|
|
920
920
|
},
|
|
921
921
|
|
|
922
|
+
canEditAsYaml() {
|
|
923
|
+
return !(this.isUpstreamCAPIProvider);
|
|
924
|
+
},
|
|
925
|
+
|
|
922
926
|
overallFormValidationPassed() {
|
|
923
927
|
return this.validationPassed &&
|
|
924
928
|
this.fvFormIsValid &&
|
|
@@ -1118,7 +1122,6 @@ export default {
|
|
|
1118
1122
|
|
|
1119
1123
|
this.rkeConfig.etcd.disableSnapshots = disableSnapshots;
|
|
1120
1124
|
}
|
|
1121
|
-
|
|
1122
1125
|
// Namespaces if required - this is mainly for custom provisioners via extensions that want
|
|
1123
1126
|
// to allow creating their resources in a different namespace
|
|
1124
1127
|
if (this.needsNamespace) {
|
|
@@ -2459,6 +2462,7 @@ export default {
|
|
|
2459
2462
|
:done-route="doneRoute"
|
|
2460
2463
|
:apply-hooks="applyHooks"
|
|
2461
2464
|
:generate-yaml="generateYaml"
|
|
2465
|
+
:can-yaml="canEditAsYaml"
|
|
2462
2466
|
class="rke2"
|
|
2463
2467
|
component-testid="rke2-custom-create"
|
|
2464
2468
|
@done="done"
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
3
2
|
import { Banner } from '@components/Banner';
|
|
4
|
-
import
|
|
5
|
-
import SelectOrCreateAuthSecret from '@shell/components/form/SelectOrCreateAuthSecret';
|
|
3
|
+
import PrivateRegistry from '@shell/components/form/PrivateRegistry.vue';
|
|
6
4
|
import AdvancedSection from '@shell/components/AdvancedSection.vue';
|
|
7
5
|
import RegistryConfigs from '@shell/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs';
|
|
8
6
|
import RegistryMirrors from '@shell/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryMirrors';
|
|
@@ -10,10 +8,8 @@ import RegistryMirrors from '@shell/edit/provisioning.cattle.io.cluster/tabs/reg
|
|
|
10
8
|
export default {
|
|
11
9
|
emits: ['custom-registry-changed', 'registry-host-changed', 'registry-secret-changed', 'input', 'update-configs-changed', 'registry-validation-changed'],
|
|
12
10
|
components: {
|
|
13
|
-
LabeledInput,
|
|
14
11
|
Banner,
|
|
15
|
-
|
|
16
|
-
SelectOrCreateAuthSecret,
|
|
12
|
+
PrivateRegistry,
|
|
17
13
|
AdvancedSection,
|
|
18
14
|
RegistryConfigs,
|
|
19
15
|
RegistryMirrors
|
|
@@ -65,55 +61,18 @@ export default {
|
|
|
65
61
|
<div class="row">
|
|
66
62
|
<h3>{{ t('cluster.privateRegistry.label') }}</h3>
|
|
67
63
|
</div>
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
:value="showCustomRegistryInput"
|
|
81
|
-
:mode="mode"
|
|
82
|
-
:label="t('cluster.privateRegistry.label')"
|
|
83
|
-
data-testid="registries-enable-checkbox"
|
|
84
|
-
@update:value="$emit('custom-registry-changed', $event)"
|
|
85
|
-
/>
|
|
86
|
-
</div>
|
|
87
|
-
<div
|
|
88
|
-
v-if="showCustomRegistryInput"
|
|
89
|
-
class="row mt-20"
|
|
90
|
-
>
|
|
91
|
-
<div class="col span-6">
|
|
92
|
-
<LabeledInput
|
|
93
|
-
:value="registryHost"
|
|
94
|
-
:mode="mode"
|
|
95
|
-
label-key="catalog.chart.registry.custom.inputLabel"
|
|
96
|
-
placeholder-key="catalog.chart.registry.custom.placeholder"
|
|
97
|
-
:min-height="30"
|
|
98
|
-
data-testid="registry-host-input"
|
|
99
|
-
@update:value="$emit('registry-host-changed', $event)"
|
|
100
|
-
/>
|
|
101
|
-
<SelectOrCreateAuthSecret
|
|
102
|
-
:value="registrySecret"
|
|
103
|
-
:register-before-hook="registerBeforeHook"
|
|
104
|
-
:hook-priority="1"
|
|
105
|
-
:mode="mode"
|
|
106
|
-
in-store="management"
|
|
107
|
-
:allow-ssh="false"
|
|
108
|
-
:allow-rke="true"
|
|
109
|
-
:vertical="true"
|
|
110
|
-
:namespace="value.metadata.namespace"
|
|
111
|
-
generate-name="registryconfig-auth-"
|
|
112
|
-
:cache-secrets="true"
|
|
113
|
-
@update:value="$emit('registry-secret-changed', $event)"
|
|
114
|
-
/>
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
64
|
+
<PrivateRegistry
|
|
65
|
+
:value="registryHost"
|
|
66
|
+
:enabled="showCustomRegistryInput"
|
|
67
|
+
:mode="mode"
|
|
68
|
+
:pull-secret="registrySecret"
|
|
69
|
+
:register-before-hook="registerBeforeHook"
|
|
70
|
+
checkbox-test-id="registries-enable-checkbox"
|
|
71
|
+
input-test-id="registry-host-input"
|
|
72
|
+
@update:value="$emit('registry-host-changed', $event)"
|
|
73
|
+
@update:enabled="$emit('custom-registry-changed', $event)"
|
|
74
|
+
@update:pull-secret="$emit('registry-secret-changed', $event)"
|
|
75
|
+
/>
|
|
117
76
|
<div
|
|
118
77
|
class="row"
|
|
119
78
|
>
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import ProvCluster from '@shell/models/provisioning.cattle.io.cluster';
|
|
2
2
|
import MgmtCluster from '@shell/models/management.cattle.io.cluster';
|
|
3
|
+
import { IMPORTED_DAY_2_OPS } from '@shell/config/features';
|
|
4
|
+
import { OPERATION_ANNOTATIONS } from '@shell/config/labels-annotations';
|
|
5
|
+
import { SETTING } from '@shell/config/settings';
|
|
6
|
+
import { MANAGEMENT, OPERATION } from '@shell/config/types';
|
|
7
|
+
import { createOperationCR } from '@shell/utils/operation-cr';
|
|
8
|
+
|
|
9
|
+
jest.mock('@shell/utils/operation-cr', () => ({ createOperationCR: jest.fn() }));
|
|
3
10
|
|
|
4
11
|
jest.mock('@shell/utils/provider', () => ({
|
|
5
12
|
isHostedProvider: jest.fn().mockImplementation((context, provider) => {
|
|
@@ -441,4 +448,153 @@ describe('class ProvCluster', () => {
|
|
|
441
448
|
jest.clearAllMocks();
|
|
442
449
|
});
|
|
443
450
|
});
|
|
451
|
+
describe('day 2 operations', () => {
|
|
452
|
+
const createContext = ({
|
|
453
|
+
byId = jest.fn(),
|
|
454
|
+
all = jest.fn(() => []),
|
|
455
|
+
schemaFor = jest.fn(() => ({})),
|
|
456
|
+
} = {}) => {
|
|
457
|
+
return {
|
|
458
|
+
getters: { schemaFor },
|
|
459
|
+
rootGetters: {
|
|
460
|
+
'management/byId': byId,
|
|
461
|
+
'management/all': all,
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
it('should return true when the day 2 operations feature is enabled', () => {
|
|
467
|
+
const byId = jest.fn().mockImplementation((type, id) => {
|
|
468
|
+
if (type === MANAGEMENT.FEATURE && id === IMPORTED_DAY_2_OPS) {
|
|
469
|
+
return { enabled: true };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return undefined;
|
|
473
|
+
});
|
|
474
|
+
const cluster = new ProvCluster({}, createContext({ byId }));
|
|
475
|
+
|
|
476
|
+
expect(cluster.isDayTwoOpsFeatureEnabled).toBe(true);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('should return true for imported RKE2 day 2 operations when the annotation is enabled', () => {
|
|
480
|
+
const byId = jest.fn().mockImplementation((type, id) => {
|
|
481
|
+
if (type === MANAGEMENT.FEATURE && id === IMPORTED_DAY_2_OPS) {
|
|
482
|
+
return { enabled: true };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return undefined;
|
|
486
|
+
});
|
|
487
|
+
const cluster = new ProvCluster({}, createContext({ byId }));
|
|
488
|
+
|
|
489
|
+
jest.spyOn(cluster, 'mgmt', 'get').mockReturnValue({ metadata: { annotations: { [OPERATION_ANNOTATIONS.ENABLED]: 'true' } } });
|
|
490
|
+
jest.spyOn(cluster, 'isImportedRke2K3s', 'get').mockReturnValue(true);
|
|
491
|
+
|
|
492
|
+
expect(cluster.isImportedWithDayTwoOps).toBe(true);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('should return true for imported RKE2 day 2 operations when the global setting is enabled', () => {
|
|
496
|
+
const byId = jest.fn().mockImplementation((type, id) => {
|
|
497
|
+
if (type === MANAGEMENT.FEATURE && id === IMPORTED_DAY_2_OPS) {
|
|
498
|
+
return { enabled: true };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (type === MANAGEMENT.SETTING && id === SETTING.IMPORTED_CLUSTER_DAY2_OPS_DEFAULT) {
|
|
502
|
+
return { value: 'true' };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return undefined;
|
|
506
|
+
});
|
|
507
|
+
const cluster = new ProvCluster({}, createContext({ byId }));
|
|
508
|
+
|
|
509
|
+
jest.spyOn(cluster, 'mgmt', 'get').mockReturnValue({ metadata: { annotations: {} } });
|
|
510
|
+
jest.spyOn(cluster, 'isImportedRke2K3s', 'get').mockReturnValue(true);
|
|
511
|
+
|
|
512
|
+
expect(cluster.isImportedWithDayTwoOps).toBe(true);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('should return false for imported RKE2 day 2 operations when the feature is disabled', () => {
|
|
516
|
+
const byId = jest.fn().mockImplementation((type, id) => {
|
|
517
|
+
if (type === MANAGEMENT.SETTING && id === SETTING.IMPORTED_CLUSTER_DAY2_OPS_DEFAULT) {
|
|
518
|
+
return { value: 'true' };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return undefined;
|
|
522
|
+
});
|
|
523
|
+
const cluster = new ProvCluster({}, createContext({ byId }));
|
|
524
|
+
|
|
525
|
+
jest.spyOn(cluster, 'mgmt', 'get').mockReturnValue({ metadata: { annotations: { [OPERATION_ANNOTATIONS.ENABLED]: 'true' } } });
|
|
526
|
+
jest.spyOn(cluster, 'isImportedRke2K3s', 'get').mockReturnValue(true);
|
|
527
|
+
|
|
528
|
+
expect(cluster.isImportedWithDayTwoOps).toBeFalsy();
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('should return false for imported RKE2 day 2 operations when operation schema is missing', () => {
|
|
532
|
+
const byId = jest.fn().mockImplementation((type, id) => {
|
|
533
|
+
if (type === MANAGEMENT.FEATURE && id === IMPORTED_DAY_2_OPS) {
|
|
534
|
+
return { enabled: true };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (type === MANAGEMENT.SETTING && id === SETTING.IMPORTED_CLUSTER_DAY2_OPS_DEFAULT) {
|
|
538
|
+
return { value: 'true' };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return undefined;
|
|
542
|
+
});
|
|
543
|
+
const schemaFor = jest.fn().mockReturnValue(undefined);
|
|
544
|
+
const cluster = new ProvCluster({}, createContext({ byId, schemaFor }));
|
|
545
|
+
|
|
546
|
+
jest.spyOn(cluster, 'mgmt', 'get').mockReturnValue({ metadata: { annotations: { [OPERATION_ANNOTATIONS.ENABLED]: 'true' } } });
|
|
547
|
+
jest.spyOn(cluster, 'isImportedRke2K3s', 'get').mockReturnValue(true);
|
|
548
|
+
|
|
549
|
+
expect(cluster.isImportedWithDayTwoOps).toBeFalsy();
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it('should filter etcd snapshots by management cluster fields for imported day 2 operations clusters', () => {
|
|
553
|
+
const snapshots = [
|
|
554
|
+
{
|
|
555
|
+
metadata: { namespace: 'c-m-1' },
|
|
556
|
+
spec: { clusterName: 'imported-cluster' }
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
metadata: { namespace: 'c-m-1' },
|
|
560
|
+
spec: { clusterName: 'other-cluster' }
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
metadata: { namespace: 'c-m-2' },
|
|
564
|
+
spec: { clusterName: 'imported-cluster' }
|
|
565
|
+
}
|
|
566
|
+
];
|
|
567
|
+
const all = jest.fn().mockReturnValue(snapshots);
|
|
568
|
+
const cluster = new ProvCluster({}, createContext({ all }));
|
|
569
|
+
|
|
570
|
+
jest.spyOn(cluster, 'isImportedWithDayTwoOps', 'get').mockReturnValue(true);
|
|
571
|
+
jest.spyOn(cluster, 'mgmt', 'get').mockReturnValue({ id: 'c-m-1', metadata: { name: 'imported-cluster' } });
|
|
572
|
+
|
|
573
|
+
expect(cluster.etcdSnapshots).toStrictEqual([snapshots[0]]);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('should create an operation CR when taking a snapshot on an imported RKE2 or K3s cluster', () => {
|
|
577
|
+
const cluster = new ProvCluster({}, createContext());
|
|
578
|
+
|
|
579
|
+
(createOperationCR as jest.Mock).mockResolvedValue(undefined);
|
|
580
|
+
|
|
581
|
+
jest.spyOn(cluster, 'isRke1', 'get').mockReturnValue(false);
|
|
582
|
+
jest.spyOn(cluster, 'isImportedWithDayTwoOps', 'get').mockReturnValue(true);
|
|
583
|
+
jest.spyOn(cluster, 'mgmt', 'get').mockReturnValue({ id: 'c-m-1', metadata: { name: 'imported-cluster' } });
|
|
584
|
+
|
|
585
|
+
cluster.takeSnapshot();
|
|
586
|
+
|
|
587
|
+
expect(createOperationCR).toHaveBeenCalledTimes(1);
|
|
588
|
+
expect((createOperationCR as jest.Mock).mock.calls[0][1]).toBe(OPERATION.ETCD_SNAPSHOT);
|
|
589
|
+
expect((createOperationCR as jest.Mock).mock.calls[0][2]).toStrictEqual({
|
|
590
|
+
clusterRef: {
|
|
591
|
+
apiVersion: 'management.cattle.io/v3',
|
|
592
|
+
kind: 'Cluster',
|
|
593
|
+
name: 'c-m-1',
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
expect((createOperationCR as jest.Mock).mock.calls[0][3]).toBe('c-m-1');
|
|
597
|
+
expect((createOperationCR as jest.Mock).mock.calls[0][4]).toBe('imported-cluster');
|
|
598
|
+
});
|
|
599
|
+
});
|
|
444
600
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import Secret from '@shell/models/secret';
|
|
2
|
-
import { SECRET_TYPES as TYPES } from '@shell/config/secret';
|
|
2
|
+
import { SECRET_TYPES as TYPES, GITHUB_APP_SECRET_KEYS } from '@shell/config/secret';
|
|
3
3
|
import { VIRTUAL_TYPES } from '@shell/config/types';
|
|
4
4
|
import { UI_PROJECT_SECRET } from '@shell/config/labels-annotations';
|
|
5
|
+
import { base64Encode } from '@shell/utils/crypto';
|
|
5
6
|
|
|
6
7
|
describe('class Secret', () => {
|
|
7
8
|
describe('detailLocation', () => {
|
|
@@ -132,4 +133,70 @@ ${ part }`;
|
|
|
132
133
|
expect(result).toBe(supported);
|
|
133
134
|
});
|
|
134
135
|
});
|
|
136
|
+
|
|
137
|
+
describe('isGithubApp', () => {
|
|
138
|
+
const githubAppData = {
|
|
139
|
+
github_app_id: 'MTI=',
|
|
140
|
+
github_app_installation_id: 'MzQ=',
|
|
141
|
+
github_app_private_key: 'a2V5',
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
it.each([
|
|
145
|
+
[
|
|
146
|
+
false,
|
|
147
|
+
'type is not Opaque',
|
|
148
|
+
TYPES.SSH,
|
|
149
|
+
githubAppData,
|
|
150
|
+
],
|
|
151
|
+
[
|
|
152
|
+
false,
|
|
153
|
+
'data is null',
|
|
154
|
+
TYPES.OPAQUE,
|
|
155
|
+
null,
|
|
156
|
+
],
|
|
157
|
+
[
|
|
158
|
+
false,
|
|
159
|
+
'Opaque but missing a GitHub App key',
|
|
160
|
+
TYPES.OPAQUE,
|
|
161
|
+
{ github_app_id: 'MTI=', github_app_installation_id: 'MzQ=' },
|
|
162
|
+
],
|
|
163
|
+
[
|
|
164
|
+
true,
|
|
165
|
+
'Opaque with all GitHub App keys',
|
|
166
|
+
TYPES.OPAQUE,
|
|
167
|
+
githubAppData,
|
|
168
|
+
],
|
|
169
|
+
])('is %p if %p', (
|
|
170
|
+
expected,
|
|
171
|
+
descr,
|
|
172
|
+
_type,
|
|
173
|
+
data
|
|
174
|
+
) => {
|
|
175
|
+
const secret = new Secret({ _type, data });
|
|
176
|
+
|
|
177
|
+
expect(secret.isGithubApp).toBe(expected);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('GitHub App display', () => {
|
|
182
|
+
const githubAppData = {
|
|
183
|
+
[GITHUB_APP_SECRET_KEYS.APP_ID]: base64Encode('12345'),
|
|
184
|
+
[GITHUB_APP_SECRET_KEYS.INSTALLATION_ID]: base64Encode('67890'),
|
|
185
|
+
[GITHUB_APP_SECRET_KEYS.PRIVATE_KEY]: base64Encode('a-private-key'),
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const ctx = { rootGetters: { 'i18n/withFallback': (_key: string, _args: any, fallback: string) => fallback } };
|
|
189
|
+
|
|
190
|
+
it('dataPreview should show the decoded App ID and Installation ID', () => {
|
|
191
|
+
const secret = new Secret({ _type: TYPES.OPAQUE, data: githubAppData }, ctx);
|
|
192
|
+
|
|
193
|
+
expect(secret.dataPreview).toBe('12345 / 67890');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('subTypeDisplay should resolve to the GitHub App label', () => {
|
|
197
|
+
const secret = new Secret({ _type: TYPES.OPAQUE, data: githubAppData }, ctx);
|
|
198
|
+
|
|
199
|
+
expect(secret.subTypeDisplay).toBe('GitHub App');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
135
202
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CATALOG, CLUSTER_BADGE, NODE_ARCHITECTURE } from '@shell/config/labels-annotations';
|
|
1
|
+
import { CATALOG, CLUSTER_BADGE, NODE_ARCHITECTURE, OPERATION_ANNOTATIONS } from '@shell/config/labels-annotations';
|
|
2
2
|
import {
|
|
3
3
|
NODE, FLEET, MANAGEMENT, CAPI, EXT,
|
|
4
4
|
NORMAN,
|
|
@@ -9,7 +9,7 @@ import { downloadFile } from '@shell/utils/download';
|
|
|
9
9
|
import { parseSi } from '@shell/utils/units';
|
|
10
10
|
import { parseColor, textColor } from '@shell/utils/color';
|
|
11
11
|
import { isEmpty, isEqual } from '@shell/utils/object';
|
|
12
|
-
import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
|
|
12
|
+
import { HARVESTER_NAME as HARVESTER, IMPORTED_DAY_2_OPS } from '@shell/config/features';
|
|
13
13
|
import { isHarvesterCluster } from '@shell/utils/cluster';
|
|
14
14
|
import SteveModel from '@shell/plugins/steve/steve-class';
|
|
15
15
|
import { LINUX, WINDOWS } from '@shell/store/catalog';
|
|
@@ -20,7 +20,7 @@ import { copyTextToClipboard } from '@shell/utils/clipboard';
|
|
|
20
20
|
import { isHostedProvider, isCAPIProvider } from '@shell/utils/provider';
|
|
21
21
|
import { ucFirst } from '@shell/utils/string';
|
|
22
22
|
import { sortBy } from '@shell/utils/sort';
|
|
23
|
-
|
|
23
|
+
import { SETTING } from '@shell/config/settings';
|
|
24
24
|
const DEFAULT_BADGE_COLOR = '#707070';
|
|
25
25
|
|
|
26
26
|
// See translation file cluster.providers for list of providers
|
|
@@ -389,6 +389,24 @@ export default class MgmtCluster extends SteveModel {
|
|
|
389
389
|
return this.spec?.internal === true;
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
+
get isDayTwoOpsFeatureEnabled() {
|
|
393
|
+
return this.$rootGetters['management/byId'](MANAGEMENT.FEATURE, IMPORTED_DAY_2_OPS)?.enabled || false;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Whether day 2 operations are enabled for this cluster.
|
|
398
|
+
* Reads the `operations.cattle.io/ops-enabled` annotation.
|
|
399
|
+
*/
|
|
400
|
+
get isDayTwoOpsEnabled() {
|
|
401
|
+
const isImportedRke2K3s = !this.isLocal && (this.isImportedK3s || this.isImportedRke2);
|
|
402
|
+
const annotationExists = typeof this.metadata?.annotations?.[OPERATION_ANNOTATIONS.ENABLED] !== 'undefined';
|
|
403
|
+
const annotationEnabled = this.metadata?.annotations?.[OPERATION_ANNOTATIONS.ENABLED] === 'true';
|
|
404
|
+
const globalDefaultIsTrue = this.$rootGetters['management/byId'](MANAGEMENT.SETTING, SETTING.IMPORTED_CLUSTER_DAY2_OPS_DEFAULT)?.value === 'true';
|
|
405
|
+
const annotationEnabledOrDefault = annotationExists ? annotationEnabled : globalDefaultIsTrue;
|
|
406
|
+
|
|
407
|
+
return this.isDayTwoOpsFeatureEnabled && isImportedRke2K3s && annotationEnabledOrDefault;
|
|
408
|
+
}
|
|
409
|
+
|
|
392
410
|
get isHarvester() {
|
|
393
411
|
return isHarvesterCluster(this);
|
|
394
412
|
}
|
package/models/pod.js
CHANGED
|
@@ -6,6 +6,7 @@ import WorkloadService from '@shell/models/workload.service';
|
|
|
6
6
|
import { deleteProperty } from '@shell/utils/object';
|
|
7
7
|
import { POD_RESTARTS_REG_EX } from '@shell/types/resources/pod';
|
|
8
8
|
import { useResourceCardRow } from '@shell/components/Resource/Detail/Card/StateCard/composables';
|
|
9
|
+
import { POD_SHELL } from '@shell/store/features';
|
|
9
10
|
|
|
10
11
|
export const WORKLOAD_PRIORITY = {
|
|
11
12
|
[WORKLOAD_TYPES.DEPLOYMENT]: 1,
|
|
@@ -64,11 +65,16 @@ export default class Pod extends WorkloadService {
|
|
|
64
65
|
|
|
65
66
|
get _availableActions() {
|
|
66
67
|
const out = super._availableActions;
|
|
68
|
+
const podShellFeatureEnabled = !!this.$rootGetters['features/get'](POD_SHELL);
|
|
67
69
|
|
|
68
70
|
// Add backwards, each one to the top
|
|
69
71
|
insertAt(out, 0, { divider: true });
|
|
70
72
|
insertAt(out, 0, this.openLogsMenuItem);
|
|
71
|
-
|
|
73
|
+
|
|
74
|
+
// Only add the menu item for the pod shell if the feature flag is enabled
|
|
75
|
+
if (podShellFeatureEnabled) {
|
|
76
|
+
insertAt(out, 0, this.openShellMenuItem);
|
|
77
|
+
}
|
|
72
78
|
|
|
73
79
|
return out;
|
|
74
80
|
}
|
|
@@ -95,9 +101,14 @@ export default class Pod extends WorkloadService {
|
|
|
95
101
|
|
|
96
102
|
get containerActions() {
|
|
97
103
|
const out = [];
|
|
104
|
+
const podShellFeatureEnabled = !!this.$rootGetters['features/get'](POD_SHELL);
|
|
98
105
|
|
|
99
106
|
insertAt(out, 0, this.openLogsMenuItem);
|
|
100
|
-
|
|
107
|
+
|
|
108
|
+
// Only add the menu item for the container shell if the feature flag is enabled
|
|
109
|
+
if (podShellFeatureEnabled) {
|
|
110
|
+
insertAt(out, 0, this.openShellMenuItem);
|
|
111
|
+
}
|
|
101
112
|
|
|
102
113
|
return out;
|
|
103
114
|
}
|