@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
@@ -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, DEFAULT_WORKSPACE } from '@shell/config/types';
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: NORMAN.CLOUD_CREDENTIAL,
54
- metadata: {
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"
@@ -40,7 +40,6 @@ export default {
40
40
  type: Function,
41
41
  required: true,
42
42
  },
43
-
44
43
  },
45
44
 
46
45
  computed: {
@@ -1,8 +1,6 @@
1
1
  <script>
2
- import { LabeledInput } from '@components/Form/LabeledInput';
3
2
  import { Banner } from '@components/Banner';
4
- import { Checkbox } from '@components/Form/Checkbox';
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
- Checkbox,
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
- <div class="row">
69
- <div class="col span-12">
70
- <Banner
71
- :closable="false"
72
- class="cluster-tools-tip"
73
- color="info"
74
- label-key="cluster.privateRegistry.description"
75
- />
76
- </div>
77
- </div>
78
- <div class="row">
79
- <Checkbox
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
- insertAt(out, 0, this.openShellMenuItem);
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
- insertAt(out, 0, this.openShellMenuItem);
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
  }