@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.
Files changed (81) hide show
  1. package/assets/styles/global/_button.scss +1 -1
  2. package/assets/translations/en-us.yaml +39 -10
  3. package/components/ActionDropdownShell.vue +5 -3
  4. package/components/ButtonGroup.vue +26 -1
  5. package/components/CruResource.vue +51 -2
  6. package/components/PromptRestore.vue +93 -32
  7. package/components/Questions/index.vue +1 -0
  8. package/components/ResourceTable.vue +1 -0
  9. package/components/SortableTable/index.vue +4 -3
  10. package/components/Wizard.vue +14 -1
  11. package/components/__tests__/ButtonGroup.test.ts +56 -0
  12. package/components/__tests__/PromptRestore.test.ts +169 -19
  13. package/components/fleet/GitRepoAdvancedTab.vue +1 -0
  14. package/components/fleet/GitRepoMetadataTab.vue +5 -0
  15. package/components/fleet/HelmOpAppCoConfigTab.vue +4 -0
  16. package/components/fleet/HelmOpMetadataTab.vue +5 -0
  17. package/components/form/FileSelector.vue +39 -1
  18. package/components/form/PrivateRegistry.constants.ts +7 -0
  19. package/components/form/PrivateRegistry.vue +253 -18
  20. package/components/form/SelectOrCreateAuthSecret.vue +140 -17
  21. package/components/form/__tests__/FileSelector.test.ts +23 -0
  22. package/components/form/__tests__/PrivateRegistry.test.ts +463 -73
  23. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +122 -0
  24. package/components/formatter/EtcdSnapshotName.vue +73 -0
  25. package/components/nav/Header.vue +8 -1
  26. package/components/templates/default.vue +7 -0
  27. package/config/features.js +1 -0
  28. package/config/labels-annotations.js +2 -0
  29. package/config/product/manager.js +6 -0
  30. package/config/secret.ts +10 -0
  31. package/config/settings.ts +6 -2
  32. package/config/types.js +7 -0
  33. package/detail/provisioning.cattle.io.cluster.vue +79 -3
  34. package/dialog/RotateEncryptionKeyDialog.vue +33 -9
  35. package/dialog/__tests__/RotateEncryptionKeyDialog.test.ts +78 -0
  36. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +92 -0
  37. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +101 -0
  38. package/edit/__tests__/management.cattle.io.setting.test.ts +2 -1
  39. package/edit/compliance.cattle.io.clusterscanprofile.vue +39 -41
  40. package/edit/fleet.cattle.io.gitrepo.vue +70 -16
  41. package/edit/fleet.cattle.io.helmop.vue +51 -5
  42. package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
  43. package/edit/{management.cattle.io.setting.vue → management.cattle.io.setting/index.vue} +32 -9
  44. package/edit/management.cattle.io.setting/system-default-registry-pull-secrets.vue +81 -0
  45. package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +3 -12
  46. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +18 -0
  47. package/edit/provisioning.cattle.io.cluster/rke2.vue +5 -1
  48. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +0 -1
  49. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +14 -55
  50. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +156 -0
  51. package/models/__tests__/secret.test.ts +68 -1
  52. package/models/management.cattle.io.cluster.js +21 -3
  53. package/models/pod.js +13 -2
  54. package/models/provisioning.cattle.io.cluster.js +59 -9
  55. package/models/rke.cattle.io.etcdsnapshot.js +17 -9
  56. package/models/secret.js +19 -0
  57. package/models/workload.js +12 -7
  58. package/package.json +1 -1
  59. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +485 -107
  60. package/pages/c/_cluster/apps/charts/install.vue +114 -28
  61. package/pkg/require-asset.lib.js +25 -0
  62. package/pkg/vue.config.js +7 -0
  63. package/plugins/dashboard-store/__tests__/resource-class.test.ts +84 -0
  64. package/plugins/dashboard-store/getters.js +0 -1
  65. package/plugins/dashboard-store/resource-class.js +52 -12
  66. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +30 -0
  67. package/rancher-components/Form/TextArea/__tests__/TextAreaAutoGrow.test.ts +95 -0
  68. package/rancher-components/RcButton/index.ts +1 -1
  69. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +6 -1
  70. package/store/__tests__/features.test.ts +131 -0
  71. package/store/__tests__/growl.test.ts +374 -0
  72. package/store/__tests__/modal.test.ts +131 -0
  73. package/store/__tests__/slideInPanel.test.ts +88 -0
  74. package/store/__tests__/type-map.utils.test.ts +433 -0
  75. package/store/features.js +4 -0
  76. package/types/shell/index.d.ts +62 -0
  77. package/utils/__tests__/operation-cr.test.ts +34 -0
  78. package/utils/operation-cr.js +19 -0
  79. package/utils/require-asset.ts +7 -0
  80. package/utils/validators/__tests__/private-registry.test.ts +27 -15
  81. 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">&mdash;</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,
@@ -4,3 +4,4 @@ export const ONE_WAY = [
4
4
 
5
5
  export const HARVESTER_NAME = 'harvester';
6
6
  export const SCHEDULING_CUSTOMIZATION = 'cluster-agent-scheduling-customization';
7
+ export const IMPORTED_DAY_2_OPS = 'imported-day-2-ops';
@@ -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
+ };
@@ -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 { CAPI, MANAGEMENT, NORMAN, SNAPSHOT } from '@shell/config/types';
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
- NAME,
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 currentGeneration = this.cluster.spec?.rkeConfig?.rotateEncryptionKeys?.generation || 0;
86
-
87
- // To rotate the encryption keys, increment
88
- // rkeConfig.rotateEncyrptionKeys.generation in the YAML.
89
- set(this.cluster, 'spec.rkeConfig.rotateEncryptionKeys.generation', currentGeneration + 1);
90
- await this.cluster.save();
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
+ });