@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
|
@@ -66,4 +66,126 @@ describe('component: SelectOrCreateAuthSecret', () => {
|
|
|
66
66
|
|
|
67
67
|
expect(passwordLabeledInput!.props('labelKey')).toBe(expectedLabelKey);
|
|
68
68
|
});
|
|
69
|
+
|
|
70
|
+
describe('GitHub App auth', () => {
|
|
71
|
+
const githubAppSetup = () => mount(SelectOrCreateAuthSecret, {
|
|
72
|
+
...requiredSetup(),
|
|
73
|
+
props: {
|
|
74
|
+
mode: _EDIT,
|
|
75
|
+
namespace: 'default',
|
|
76
|
+
value: {},
|
|
77
|
+
allowGithubApp: true,
|
|
78
|
+
registerBeforeHook: () => {},
|
|
79
|
+
},
|
|
80
|
+
data() {
|
|
81
|
+
return { selected: AUTH_TYPE._GITHUB_APP } as any;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should render the three GitHub App fields when selected', () => {
|
|
86
|
+
const wrapper = githubAppSetup();
|
|
87
|
+
|
|
88
|
+
expect(wrapper.find('[data-testid="auth-secret-github-app-id"]').exists()).toBe(true);
|
|
89
|
+
expect(wrapper.find('[data-testid="auth-secret-github-app-installation-id"]').exists()).toBe(true);
|
|
90
|
+
expect(wrapper.find('[data-testid="auth-secret-github-app-private-key"]').exists()).toBe(true);
|
|
91
|
+
expect(wrapper.find('[data-testid="auth-secret-github-app-private-key-file"]').exists()).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should not narrow the select column for GitHub App (keeps span-6)', () => {
|
|
95
|
+
const wrapper = githubAppSetup();
|
|
96
|
+
|
|
97
|
+
expect((wrapper.vm as any).firstCol).toBe('col span-6');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should emit inputauthval with the GitHub App field values', async() => {
|
|
101
|
+
const wrapper = githubAppSetup();
|
|
102
|
+
|
|
103
|
+
await wrapper.setData({
|
|
104
|
+
githubAppId: 'app-id',
|
|
105
|
+
githubAppInstallationId: 'installation-id',
|
|
106
|
+
githubAppPrivateKey: 'private-key',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const emitted = wrapper.emitted('inputauthval') as any[];
|
|
110
|
+
const last = emitted[emitted.length - 1][0];
|
|
111
|
+
|
|
112
|
+
expect(last).toStrictEqual({
|
|
113
|
+
selected: AUTH_TYPE._GITHUB_APP,
|
|
114
|
+
publicKey: '',
|
|
115
|
+
privateKey: '',
|
|
116
|
+
githubAppId: 'app-id',
|
|
117
|
+
githubAppInstallationId: 'installation-id',
|
|
118
|
+
githubAppPrivateKey: 'private-key',
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should populate the private key when a file is read', async() => {
|
|
123
|
+
const wrapper = githubAppSetup();
|
|
124
|
+
|
|
125
|
+
const fileSelector = wrapper.findComponent({ name: 'FileSelector' });
|
|
126
|
+
|
|
127
|
+
await fileSelector.vm.$emit('selected', 'key-from-file');
|
|
128
|
+
|
|
129
|
+
expect((wrapper.vm as any).githubAppPrivateKey).toBe('key-from-file');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it.each([
|
|
133
|
+
['offer', true],
|
|
134
|
+
['not offer', false],
|
|
135
|
+
])('should %s the GitHub App create option when allowGithubApp is %p', (_, allowGithubApp) => {
|
|
136
|
+
const wrapper = mount(SelectOrCreateAuthSecret, {
|
|
137
|
+
...requiredSetup(),
|
|
138
|
+
props: {
|
|
139
|
+
mode: _EDIT,
|
|
140
|
+
namespace: 'default',
|
|
141
|
+
value: {},
|
|
142
|
+
allowGithubApp,
|
|
143
|
+
registerBeforeHook: () => {},
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const hasGithubAppOption = (wrapper.vm as any).options
|
|
148
|
+
.some((o: any) => o.value === AUTH_TYPE._GITHUB_APP);
|
|
149
|
+
|
|
150
|
+
expect(hasGithubAppOption).toBe(allowGithubApp);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should list existing GitHub App secrets but exclude plain Opaque secrets', () => {
|
|
154
|
+
const githubAppSecret = {
|
|
155
|
+
_type: 'Opaque',
|
|
156
|
+
isGithubApp: true,
|
|
157
|
+
id: 'default/gh-app',
|
|
158
|
+
dataPreview: '3 keys',
|
|
159
|
+
subTypeDisplay: 'Opaque',
|
|
160
|
+
metadata: { name: 'gh-app', namespace: 'default' },
|
|
161
|
+
};
|
|
162
|
+
const plainOpaqueSecret = {
|
|
163
|
+
_type: 'Opaque',
|
|
164
|
+
isGithubApp: false,
|
|
165
|
+
id: 'default/plain',
|
|
166
|
+
dataPreview: '1 key',
|
|
167
|
+
subTypeDisplay: 'Opaque',
|
|
168
|
+
metadata: { name: 'plain', namespace: 'default' },
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const wrapper = mount(SelectOrCreateAuthSecret, {
|
|
172
|
+
...requiredSetup(),
|
|
173
|
+
props: {
|
|
174
|
+
mode: _EDIT,
|
|
175
|
+
namespace: 'default',
|
|
176
|
+
value: {},
|
|
177
|
+
allowGithubApp: true,
|
|
178
|
+
registerBeforeHook: () => {},
|
|
179
|
+
},
|
|
180
|
+
data() {
|
|
181
|
+
return { allSecrets: [githubAppSecret, plainOpaqueSecret] } as any;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const optionValues = (wrapper.vm as any).options.map((o: any) => o.value);
|
|
186
|
+
|
|
187
|
+
expect(optionValues).toContain('default/gh-app');
|
|
188
|
+
expect(optionValues).not.toContain('default/plain');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
69
191
|
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { get } from '@shell/utils/object';
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
props: {
|
|
6
|
+
value: {
|
|
7
|
+
type: String,
|
|
8
|
+
default: ''
|
|
9
|
+
},
|
|
10
|
+
row: {
|
|
11
|
+
type: Object,
|
|
12
|
+
required: true
|
|
13
|
+
},
|
|
14
|
+
col: {
|
|
15
|
+
type: Object,
|
|
16
|
+
default: () => ({})
|
|
17
|
+
},
|
|
18
|
+
reference: {
|
|
19
|
+
type: String,
|
|
20
|
+
default: null,
|
|
21
|
+
},
|
|
22
|
+
getCustomDetailLink: {
|
|
23
|
+
type: Function,
|
|
24
|
+
default: null
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
computed: {
|
|
29
|
+
to() {
|
|
30
|
+
if (this.getCustomDetailLink) {
|
|
31
|
+
return this.getCustomDetailLink(this.row);
|
|
32
|
+
}
|
|
33
|
+
if ( this.row && this.reference ) {
|
|
34
|
+
return get(this.row, this.reference);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return this.row?.detailLocation;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
showPredatesImportIcon() {
|
|
41
|
+
return !!this.row?.isSnapshotTooOld;
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
predatesImportMessage() {
|
|
45
|
+
return this.t('cluster.snapshot.predatesImportTooltip');
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<span>
|
|
53
|
+
<router-link
|
|
54
|
+
v-if="to"
|
|
55
|
+
:to="to"
|
|
56
|
+
>
|
|
57
|
+
{{ value }}
|
|
58
|
+
</router-link>
|
|
59
|
+
<span v-else>
|
|
60
|
+
{{ value }}
|
|
61
|
+
<template v-if="!value && col.dashIfEmpty">
|
|
62
|
+
<span class="text-muted">—</span>
|
|
63
|
+
</template>
|
|
64
|
+
</span>
|
|
65
|
+
<i
|
|
66
|
+
v-if="showPredatesImportIcon"
|
|
67
|
+
v-clean-tooltip="{ content: predatesImportMessage, triggers: ['hover', 'touch', 'focus'] }"
|
|
68
|
+
v-stripped-aria-label="predatesImportMessage"
|
|
69
|
+
tabindex="0"
|
|
70
|
+
class="icon icon-error text-error ml-5"
|
|
71
|
+
/>
|
|
72
|
+
</span>
|
|
73
|
+
</template>
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
RcDropdownTrigger
|
|
31
31
|
} from '@components/RcDropdown';
|
|
32
32
|
import { SLO_AUTH_PROVIDERS } from '@shell/store/auth';
|
|
33
|
+
import { CLUSTER_SHELL } from '@shell/store/features';
|
|
33
34
|
|
|
34
35
|
export default {
|
|
35
36
|
|
|
@@ -147,10 +148,16 @@ export default {
|
|
|
147
148
|
return true;
|
|
148
149
|
},
|
|
149
150
|
|
|
151
|
+
// Does the user have permissions to use the shell
|
|
150
152
|
shellEnabled() {
|
|
151
153
|
return !!this.currentCluster?.links?.shell;
|
|
152
154
|
},
|
|
153
155
|
|
|
156
|
+
// Is the feature flag enabled for cluster shell access?
|
|
157
|
+
shellFeatureEnabled() {
|
|
158
|
+
return !!this.$store.getters['features/get'](CLUSTER_SHELL);
|
|
159
|
+
},
|
|
160
|
+
|
|
154
161
|
showKubeShell() {
|
|
155
162
|
return !this.rootProduct?.hideKubeShell;
|
|
156
163
|
},
|
|
@@ -629,7 +636,7 @@ export default {
|
|
|
629
636
|
</button>
|
|
630
637
|
|
|
631
638
|
<button
|
|
632
|
-
v-if="showKubeShell"
|
|
639
|
+
v-if="showKubeShell && shellFeatureEnabled"
|
|
633
640
|
id="btn-kubectl"
|
|
634
641
|
v-clean-tooltip="t('nav.shellShortcut', {key: shellShortcut})"
|
|
635
642
|
v-shortkey="{windows: ['ctrl', '`'], mac: ['meta', '`']}"
|
|
@@ -25,6 +25,7 @@ import { getClusterFromRoute, getProductFromRoute } from '@shell/utils/router';
|
|
|
25
25
|
import SideNav from '@shell/components/SideNav';
|
|
26
26
|
import { Layout } from '@shell/types/window-manager';
|
|
27
27
|
import { RcButton } from '@components/RcButton';
|
|
28
|
+
import { CLUSTER_SHELL } from '@shell/store/features';
|
|
28
29
|
|
|
29
30
|
const SET_LOGIN_ACTION = 'set-as-login';
|
|
30
31
|
|
|
@@ -140,6 +141,7 @@ export default {
|
|
|
140
141
|
debugger;
|
|
141
142
|
},
|
|
142
143
|
|
|
144
|
+
// Open the shell for the current cluster if the user has permissions and the feature is enabled (invoked via keyboard shortcut)
|
|
143
145
|
async toggleShell() {
|
|
144
146
|
const clusterId = this.$route.params.cluster;
|
|
145
147
|
|
|
@@ -147,6 +149,11 @@ export default {
|
|
|
147
149
|
return;
|
|
148
150
|
}
|
|
149
151
|
|
|
152
|
+
// Cluster shell is disabled via feature flag
|
|
153
|
+
if (!this.$store.getters['features/get'](CLUSTER_SHELL)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
150
157
|
const cluster = await this.$store.dispatch('management/find', {
|
|
151
158
|
type: MANAGEMENT.CLUSTER,
|
|
152
159
|
id: clusterId,
|
package/config/features.js
CHANGED
|
@@ -150,6 +150,8 @@ export const RKE = { EXTERNAL_IP: 'rke.cattle.io/external-ip' };
|
|
|
150
150
|
|
|
151
151
|
export const SNAPSHOT = { CLUSTER_NAME: 'rke.cattle.io/cluster-name' };
|
|
152
152
|
|
|
153
|
+
export const OPERATION_ANNOTATIONS = { ENABLED: 'operations.cattle.io/ops-enabled' };
|
|
154
|
+
|
|
153
155
|
export const ISTIO = { AUTO_INJECTION: 'istio-injection' };
|
|
154
156
|
|
|
155
157
|
const CATTLE_REGEX = /cattle\.io\//;
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
HCI,
|
|
11
11
|
MANAGEMENT,
|
|
12
12
|
SNAPSHOT,
|
|
13
|
+
OPERATION,
|
|
13
14
|
VIRTUAL_TYPES,
|
|
14
15
|
HOSTED_PROVIDER,
|
|
15
16
|
SAVED_COUNTS
|
|
@@ -80,6 +81,11 @@ export function init(store) {
|
|
|
80
81
|
configureType(SNAPSHOT, { depaginate: true });
|
|
81
82
|
configureType(CATALOG.CLUSTER_REPO, { listCreateButtonLabelKey: 'catalog.repo.add' });
|
|
82
83
|
|
|
84
|
+
// Day 2 operation CRDs - read-only, not user-creatable or editable
|
|
85
|
+
configureType(OPERATION.ETCD_SNAPSHOT, { isCreatable: false, isEditable: false });
|
|
86
|
+
configureType(OPERATION.ETCD_SNAPSHOT_RESTORE, { isCreatable: false, isEditable: false });
|
|
87
|
+
configureType(OPERATION.ENCRYPTION_KEY_ROTATE, { isCreatable: false, isEditable: false });
|
|
88
|
+
|
|
83
89
|
configureType(CAPI.RANCHER_CLUSTER, {
|
|
84
90
|
showListMasthead: false, namespaced: false, alias: [HCI.CLUSTER]
|
|
85
91
|
});
|
package/config/secret.ts
CHANGED
|
@@ -13,3 +13,13 @@ export const SECRET_TYPES = {
|
|
|
13
13
|
RKE_AUTH_CONFIG: 'rke.cattle.io/auth-config',
|
|
14
14
|
FLEET_OCI_STORAGE: 'fleet.cattle.io/bundle-oci-storage/v1alpha1'
|
|
15
15
|
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Data keys for a Fleet GitHub App authentication secret (stored as an Opaque secret).
|
|
19
|
+
* Shared so the create flows stay in sync.
|
|
20
|
+
*/
|
|
21
|
+
export const GITHUB_APP_SECRET_KEYS = {
|
|
22
|
+
APP_ID: 'github_app_id',
|
|
23
|
+
INSTALLATION_ID: 'github_app_installation_id',
|
|
24
|
+
PRIVATE_KEY: 'github_app_private_key',
|
|
25
|
+
};
|
package/config/settings.ts
CHANGED
|
@@ -67,6 +67,7 @@ export const SETTING = {
|
|
|
67
67
|
UI_DASHBOARD_HARVESTER_LEGACY_PLUGIN: 'ui-dashboard-harvester-legacy-plugin',
|
|
68
68
|
UI_OFFLINE_PREFERRED: 'ui-offline-preferred',
|
|
69
69
|
SYSTEM_DEFAULT_REGISTRY: 'system-default-registry',
|
|
70
|
+
SYSTEM_DEFAULT_REGISTRY_PULL_SECRETS: 'system-default-registry-pull-secrets',
|
|
70
71
|
UI_ISSUES: 'ui-issues',
|
|
71
72
|
PL: 'ui-pl',
|
|
72
73
|
PL_RANCHER_VALUE: 'rancher',
|
|
@@ -124,6 +125,7 @@ export const SETTING = {
|
|
|
124
125
|
*/
|
|
125
126
|
DYNAMIC_CONTENT_ENABLED: 'ui-content-enabled',
|
|
126
127
|
DYNAMIC_CONTENT_ENDPOINT: 'ui-content-endpoint',
|
|
128
|
+
IMPORTED_CLUSTER_DAY2_OPS_DEFAULT: 'imported-cluster-day2-ops-enabled'
|
|
127
129
|
} as const;
|
|
128
130
|
|
|
129
131
|
// These are the settings that are allowed to be edited via the UI
|
|
@@ -163,6 +165,7 @@ export const ALLOWED_SETTINGS: GlobalSetting = {
|
|
|
163
165
|
[SETTING.SERVER_URL]: { kind: 'url', canReset: true },
|
|
164
166
|
[SETTING.RKE_METADATA_CONFIG]: { kind: 'json' },
|
|
165
167
|
[SETTING.SYSTEM_DEFAULT_REGISTRY]: {},
|
|
168
|
+
[SETTING.SYSTEM_DEFAULT_REGISTRY_PULL_SECRETS]: {},
|
|
166
169
|
[SETTING.UI_DASHBOARD_INDEX]: {},
|
|
167
170
|
[SETTING.UI_OFFLINE_PREFERRED]: {
|
|
168
171
|
kind: 'enum',
|
|
@@ -184,12 +187,12 @@ export const ALLOWED_SETTINGS: GlobalSetting = {
|
|
|
184
187
|
ruleSet: [{ name: 'minValue', factoryArg: 1 }]
|
|
185
188
|
},
|
|
186
189
|
[SETTING.IMPORTED_CLUSTER_VERSION_MANAGEMENT]: { kind: 'boolean' },
|
|
190
|
+
[SETTING.IMPORTED_CLUSTER_DAY2_OPS_DEFAULT]: { kind: 'boolean' },
|
|
187
191
|
// Configuration setup for agent configuration. Setting this up will activate the specific banner configuration.
|
|
188
192
|
[SETTING.CLUSTER_AGENT_DEFAULT_PRIORITY_CLASS]: { kind: 'json', agent: AGENT_CONFIGURATION_TYPES.CLUSTER },
|
|
189
193
|
[SETTING.CLUSTER_AGENT_DEFAULT_POD_DISTRIBUTION_BUDGET]: { kind: 'json', agent: AGENT_CONFIGURATION_TYPES.CLUSTER },
|
|
190
194
|
[SETTING.FLEET_AGENT_DEFAULT_PRIORITY_CLASS]: { kind: 'json', agent: AGENT_CONFIGURATION_TYPES.FLEET },
|
|
191
|
-
[SETTING.FLEET_AGENT_DEFAULT_POD_DISTRIBUTION_BUDGET]: { kind: 'json', agent: AGENT_CONFIGURATION_TYPES.FLEET }
|
|
192
|
-
|
|
195
|
+
[SETTING.FLEET_AGENT_DEFAULT_POD_DISTRIBUTION_BUDGET]: { kind: 'json', agent: AGENT_CONFIGURATION_TYPES.FLEET },
|
|
193
196
|
};
|
|
194
197
|
|
|
195
198
|
/**
|
|
@@ -206,6 +209,7 @@ export const PROVISIONING_SETTINGS = [
|
|
|
206
209
|
SETTING.CLUSTER_AGENT_DEFAULT_POD_DISTRIBUTION_BUDGET,
|
|
207
210
|
SETTING.FLEET_AGENT_DEFAULT_PRIORITY_CLASS,
|
|
208
211
|
SETTING.FLEET_AGENT_DEFAULT_POD_DISTRIBUTION_BUDGET,
|
|
212
|
+
SETTING.IMPORTED_CLUSTER_DAY2_OPS_DEFAULT
|
|
209
213
|
];
|
|
210
214
|
|
|
211
215
|
/**
|
package/config/types.js
CHANGED
|
@@ -219,6 +219,12 @@ export const LONGHORN_VERSION_V2 = 'LonghornV2';
|
|
|
219
219
|
|
|
220
220
|
export const SNAPSHOT = 'rke.cattle.io.etcdsnapshot';
|
|
221
221
|
|
|
222
|
+
export const OPERATION = {
|
|
223
|
+
ETCD_SNAPSHOT: 'operation.cattle.io.etcdsnapshotsave',
|
|
224
|
+
ETCD_SNAPSHOT_RESTORE: 'operation.cattle.io.etcdsnapshotrestore',
|
|
225
|
+
ENCRYPTION_KEY_ROTATE: 'operation.cattle.io.encryptionkeyrotation',
|
|
226
|
+
};
|
|
227
|
+
|
|
222
228
|
// --------------------------------------
|
|
223
229
|
// 2. Only if Rancher is installed
|
|
224
230
|
// --------------------------------------
|
|
@@ -400,6 +406,7 @@ export const AUTH_TYPE = {
|
|
|
400
406
|
_S3: '_S3',
|
|
401
407
|
_RKE: '_RKE',
|
|
402
408
|
_IMAGE_PULL_SECRET: '_IPS',
|
|
409
|
+
_GITHUB_APP: '_GITHUB_APP',
|
|
403
410
|
};
|
|
404
411
|
|
|
405
412
|
export const LOCAL_CLUSTER = 'local';
|
|
@@ -6,7 +6,9 @@ import SortableTable from '@shell/components/SortableTable';
|
|
|
6
6
|
import CopyCode from '@shell/components/CopyCode';
|
|
7
7
|
import Tab from '@shell/components/Tabbed/Tab';
|
|
8
8
|
import { allHash } from '@shell/utils/promise';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
CAPI, MANAGEMENT, NORMAN, SNAPSHOT, OPERATION
|
|
11
|
+
} from '@shell/config/types';
|
|
10
12
|
import {
|
|
11
13
|
STATE, NAME as NAME_COL, AGE, INTERNAL_EXTERNAL_IP, STATE_NORMAN, ROLES, MACHINE_NODE_OS, MANAGEMENT_NODE_OS, NAME,
|
|
12
14
|
} from '@shell/config/table-headers';
|
|
@@ -149,6 +151,21 @@ export default {
|
|
|
149
151
|
fetchOne.snapshots = this.$store.dispatch('management/findAll', { type: SNAPSHOT });
|
|
150
152
|
}
|
|
151
153
|
|
|
154
|
+
// Fetch operation CRDs for clusters with day 2 ops enabled
|
|
155
|
+
if ( this.value.isImportedWithDayTwoOps ) {
|
|
156
|
+
const operationTypes = [
|
|
157
|
+
OPERATION.ETCD_SNAPSHOT,
|
|
158
|
+
OPERATION.ETCD_SNAPSHOT_RESTORE,
|
|
159
|
+
OPERATION.ENCRYPTION_KEY_ROTATE,
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
for (const opType of operationTypes) {
|
|
163
|
+
if (this.$store.getters['management/canList'](opType)) {
|
|
164
|
+
fetchOne[`operations_${ opType }`] = this.$store.dispatch('management/findAll', { type: opType });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
152
169
|
if ( this.value.isImported || this.value.isCustom || this.value.isHostedKubernetesProvider ) {
|
|
153
170
|
fetchOne.clusterToken = this.value.getOrCreateToken();
|
|
154
171
|
}
|
|
@@ -292,6 +309,7 @@ export default {
|
|
|
292
309
|
registration: true, // in this component
|
|
293
310
|
snapshots: true, // in this component
|
|
294
311
|
autoscaler: true, // in this component
|
|
312
|
+
operations: true, // in this component
|
|
295
313
|
related: true, // in ResourceTabs
|
|
296
314
|
events: true, // in ResourceTabs
|
|
297
315
|
conditions: true, // in ResourceTabs
|
|
@@ -477,13 +495,54 @@ export default {
|
|
|
477
495
|
showSnapshots() {
|
|
478
496
|
if (this.value.isRke1) {
|
|
479
497
|
return false;
|
|
480
|
-
} else if (this.value.isRke2) {
|
|
498
|
+
} else if (this.value.isRke2 || (this.value.isImported && this.value.isDayTwoOpsEnabled)) {
|
|
481
499
|
return this.$store.getters['management/canList'](SNAPSHOT) && this.extDetailTabs.snapshots;
|
|
482
500
|
}
|
|
483
501
|
|
|
484
502
|
return false;
|
|
485
503
|
},
|
|
486
504
|
|
|
505
|
+
showOperations() {
|
|
506
|
+
return this.value.isImportedWithDayTwoOps && this.extDetailTabs.operations;
|
|
507
|
+
},
|
|
508
|
+
|
|
509
|
+
clusterOperations() {
|
|
510
|
+
if (!this.value.isImportedWithDayTwoOps) {
|
|
511
|
+
return [];
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const mgmtId = this.value.mgmt?.id;
|
|
515
|
+
const operationTypes = [
|
|
516
|
+
OPERATION.ETCD_SNAPSHOT,
|
|
517
|
+
OPERATION.ETCD_SNAPSHOT_RESTORE,
|
|
518
|
+
OPERATION.ENCRYPTION_KEY_ROTATE,
|
|
519
|
+
];
|
|
520
|
+
|
|
521
|
+
const allOps = [];
|
|
522
|
+
|
|
523
|
+
for (const opType of operationTypes) {
|
|
524
|
+
const resources = this.$store.getters['management/all'](opType) || [];
|
|
525
|
+
|
|
526
|
+
allOps.push(...resources.filter((op) => op.spec?.clusterRef?.name === mgmtId));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return allOps;
|
|
530
|
+
},
|
|
531
|
+
|
|
532
|
+
operationHeaders() {
|
|
533
|
+
return [
|
|
534
|
+
STATE,
|
|
535
|
+
NAME,
|
|
536
|
+
{
|
|
537
|
+
name: 'type',
|
|
538
|
+
labelKey: 'tableHeaders.type',
|
|
539
|
+
value: 'type',
|
|
540
|
+
sort: 'type',
|
|
541
|
+
},
|
|
542
|
+
AGE,
|
|
543
|
+
];
|
|
544
|
+
},
|
|
545
|
+
|
|
487
546
|
isRke1() {
|
|
488
547
|
return this.value.isRke1;
|
|
489
548
|
},
|
|
@@ -566,7 +625,10 @@ export default {
|
|
|
566
625
|
{
|
|
567
626
|
...STATE_NORMAN, value: 'snapshotFile.status', formatterOpts: { arbitrary: true }
|
|
568
627
|
},
|
|
569
|
-
|
|
628
|
+
{
|
|
629
|
+
...NAME,
|
|
630
|
+
formatter: 'EtcdSnapshotName',
|
|
631
|
+
},
|
|
570
632
|
{
|
|
571
633
|
name: 'size',
|
|
572
634
|
labelKey: 'tableHeaders.size',
|
|
@@ -1150,6 +1212,20 @@ export default {
|
|
|
1150
1212
|
</template>
|
|
1151
1213
|
</SortableTable>
|
|
1152
1214
|
</Tab>
|
|
1215
|
+
<Tab
|
|
1216
|
+
v-if="showOperations"
|
|
1217
|
+
name="operations"
|
|
1218
|
+
:label="t('cluster.tabs.operations')"
|
|
1219
|
+
:weight="0"
|
|
1220
|
+
>
|
|
1221
|
+
<SortableTable
|
|
1222
|
+
:headers="operationHeaders"
|
|
1223
|
+
default-sort-by="age"
|
|
1224
|
+
:table-actions="false"
|
|
1225
|
+
:rows="clusterOperations"
|
|
1226
|
+
:search="false"
|
|
1227
|
+
/>
|
|
1228
|
+
</Tab>
|
|
1153
1229
|
<AutoscalerTab
|
|
1154
1230
|
v-if="showAutoScalerTab"
|
|
1155
1231
|
:value="value"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { SNAPSHOT } from '@shell/config/types';
|
|
2
|
+
import { SNAPSHOT, OPERATION } from '@shell/config/types';
|
|
3
3
|
import AsyncButton from '@shell/components/AsyncButton';
|
|
4
4
|
import { Card } from '@components/Card';
|
|
5
5
|
import { Banner } from '@components/Banner';
|
|
@@ -10,7 +10,7 @@ import day from 'dayjs';
|
|
|
10
10
|
import { escapeHtml } from '@shell/utils/string';
|
|
11
11
|
import { DATE_FORMAT, TIME_FORMAT } from '@shell/store/prefs';
|
|
12
12
|
import { set } from '@shell/utils/object';
|
|
13
|
-
|
|
13
|
+
import { createOperationCR } from '@shell/utils/operation-cr';
|
|
14
14
|
export default {
|
|
15
15
|
emits: ['close'],
|
|
16
16
|
|
|
@@ -66,6 +66,10 @@ export default {
|
|
|
66
66
|
async getEtcdBackups() {
|
|
67
67
|
let etcdBackups = await this.$store.dispatch('management/findAll', { type: SNAPSHOT });
|
|
68
68
|
|
|
69
|
+
if (this.cluster.isImportedWithDayTwoOps) {
|
|
70
|
+
return etcdBackups
|
|
71
|
+
.filter((backup) => backup.metadata.namespace === this.cluster.mgmt?.id && backup.spec?.clusterName === this.cluster.mgmt?.metadata?.name );
|
|
72
|
+
}
|
|
69
73
|
etcdBackups = etcdBackups.filter((backup) => backup.clusterId === this.cluster.id);
|
|
70
74
|
|
|
71
75
|
return etcdBackups;
|
|
@@ -82,12 +86,32 @@ export default {
|
|
|
82
86
|
|
|
83
87
|
async apply(buttonDone) {
|
|
84
88
|
try {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
const isImportedWithDayTwoOps = this.cluster?.isImportedWithDayTwoOps || this.cluster?.mgmt?.isDayTwoOpsEnabled;
|
|
90
|
+
|
|
91
|
+
if (isImportedWithDayTwoOps) {
|
|
92
|
+
if (!isImportedWithDayTwoOps) {
|
|
93
|
+
throw new Error(this.t('promptRotateEncryptionKey.error.unableToResolveTargetCluster'));
|
|
94
|
+
}
|
|
95
|
+
// For imported clusters with day 2 ops, create an encryption key rotation operation CR
|
|
96
|
+
const namespace = this.cluster.mgmt?.id;
|
|
97
|
+
const safePrefix = this.cluster.mgmt?.id;
|
|
98
|
+
const spec = {
|
|
99
|
+
clusterRef: {
|
|
100
|
+
apiVersion: 'management.cattle.io/v3',
|
|
101
|
+
kind: 'Cluster',
|
|
102
|
+
name: this.cluster.mgmt?.id,
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
createOperationCR(this.$store.dispatch, OPERATION.ENCRYPTION_KEY_ROTATE, spec, namespace, safePrefix);
|
|
107
|
+
} else {
|
|
108
|
+
const currentGeneration = this.cluster.spec?.rkeConfig?.rotateEncryptionKeys?.generation || 0;
|
|
109
|
+
|
|
110
|
+
// To rotate the encryption keys, increment
|
|
111
|
+
// rkeConfig.rotateEncyrptionKeys.generation in the YAML.
|
|
112
|
+
set(this.cluster, 'spec.rkeConfig.rotateEncryptionKeys.generation', currentGeneration + 1);
|
|
113
|
+
await this.cluster.save();
|
|
114
|
+
}
|
|
91
115
|
|
|
92
116
|
this.close(buttonDone);
|
|
93
117
|
} catch (err) {
|
|
@@ -128,7 +152,7 @@ export default {
|
|
|
128
152
|
<Banner
|
|
129
153
|
v-else
|
|
130
154
|
color="error"
|
|
131
|
-
label-key="promptRotateEncryptionKey.error"
|
|
155
|
+
label-key="promptRotateEncryptionKey.error.noBackup"
|
|
132
156
|
/>
|
|
133
157
|
</div>
|
|
134
158
|
</div>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { shallowMount } from '@vue/test-utils';
|
|
2
|
+
import RotateEncryptionKeyDialog from '@shell/dialog/RotateEncryptionKeyDialog.vue';
|
|
3
|
+
import { OPERATION } from '@shell/config/types';
|
|
4
|
+
import { createOperationCR } from '@shell/utils/operation-cr';
|
|
5
|
+
|
|
6
|
+
jest.mock('@shell/utils/operation-cr', () => ({ createOperationCR: jest.fn() }));
|
|
7
|
+
|
|
8
|
+
describe('component: RotateEncryptionKeyDialog', () => {
|
|
9
|
+
const t = (key: string) => key;
|
|
10
|
+
|
|
11
|
+
const createWrapper = (cluster: any, dispatch = jest.fn()) => {
|
|
12
|
+
return shallowMount(RotateEncryptionKeyDialog, {
|
|
13
|
+
propsData: { cluster },
|
|
14
|
+
global: {
|
|
15
|
+
mocks: {
|
|
16
|
+
t,
|
|
17
|
+
$store: {
|
|
18
|
+
dispatch,
|
|
19
|
+
getters: {
|
|
20
|
+
isRancher: true,
|
|
21
|
+
'prefs/get': jest.fn((key: string) => (key.includes('date') ? 'YYYY-MM-DD' : 'HH:mm:ss')),
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
directives: { 'clean-html': true }
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
jest.clearAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should create operation CR for imported day 2 ops clusters', async() => {
|
|
35
|
+
(createOperationCR as jest.Mock).mockResolvedValue(undefined);
|
|
36
|
+
|
|
37
|
+
const cluster = {
|
|
38
|
+
isImportedWithDayTwoOps: true,
|
|
39
|
+
mgmt: { id: 'c-m-1' },
|
|
40
|
+
save: jest.fn(),
|
|
41
|
+
spec: { rkeConfig: {} }
|
|
42
|
+
};
|
|
43
|
+
const dispatch = jest.fn();
|
|
44
|
+
const buttonDone = jest.fn();
|
|
45
|
+
const wrapper = createWrapper(cluster, dispatch);
|
|
46
|
+
|
|
47
|
+
await wrapper.vm.apply(buttonDone);
|
|
48
|
+
|
|
49
|
+
expect(createOperationCR).toHaveBeenCalledWith(dispatch, OPERATION.ENCRYPTION_KEY_ROTATE, {
|
|
50
|
+
clusterRef: {
|
|
51
|
+
apiVersion: 'management.cattle.io/v3',
|
|
52
|
+
kind: 'Cluster',
|
|
53
|
+
name: 'c-m-1',
|
|
54
|
+
}
|
|
55
|
+
}, 'c-m-1', 'c-m-1');
|
|
56
|
+
expect(cluster.save).not.toHaveBeenCalled();
|
|
57
|
+
expect(buttonDone).toHaveBeenCalledWith(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should update generation and save for non-day-2 clusters', async() => {
|
|
61
|
+
const save = jest.fn().mockResolvedValue(undefined);
|
|
62
|
+
const cluster = {
|
|
63
|
+
isImportedWithDayTwoOps: false,
|
|
64
|
+
mgmt: { id: 'c-m-1' },
|
|
65
|
+
save,
|
|
66
|
+
spec: { rkeConfig: { rotateEncryptionKeys: { generation: 4 } } }
|
|
67
|
+
};
|
|
68
|
+
const buttonDone = jest.fn();
|
|
69
|
+
const wrapper = createWrapper(cluster);
|
|
70
|
+
|
|
71
|
+
await wrapper.vm.apply(buttonDone);
|
|
72
|
+
|
|
73
|
+
expect(createOperationCR).not.toHaveBeenCalled();
|
|
74
|
+
expect(cluster.spec.rkeConfig.rotateEncryptionKeys.generation).toBe(5);
|
|
75
|
+
expect(save).toHaveBeenCalledWith();
|
|
76
|
+
expect(buttonDone).toHaveBeenCalledWith(true);
|
|
77
|
+
});
|
|
78
|
+
});
|