@rancher/shell 0.5.2 → 0.5.3

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 (66) hide show
  1. package/assets/translations/en-us.yaml +8 -4
  2. package/components/ClusterIconMenu.vue +24 -9
  3. package/components/CodeMirror.vue +79 -18
  4. package/components/FixedBanner.vue +1 -0
  5. package/components/ResourceDetail/index.vue +1 -4
  6. package/components/ResourceYaml.vue +29 -5
  7. package/components/SideNav.vue +42 -64
  8. package/components/SortableTable/index.vue +1 -1
  9. package/components/YamlEditor.vue +1 -0
  10. package/components/__tests__/CodeMirror.spec.ts +99 -0
  11. package/components/form/BannerSettings.vue +3 -0
  12. package/components/form/FileSelector.vue +1 -0
  13. package/components/form/KeyValue.vue +1 -0
  14. package/components/formatter/WorkloadDetailEndpoints.vue +12 -22
  15. package/components/formatter/__tests__/WorkloadDetailEndpoints.test.ts +81 -0
  16. package/components/nav/Header.vue +1 -0
  17. package/components/nav/Jump.vue +19 -9
  18. package/components/nav/TopLevelMenu.vue +37 -15
  19. package/components/nav/Type.vue +15 -4
  20. package/components/nav/__tests__/TopLevelMenu.test.ts +1 -1
  21. package/components/nav/__tests__/Type.test.ts +30 -0
  22. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +77 -0
  23. package/detail/fleet.cattle.io.bundle.vue +1 -1
  24. package/detail/provisioning.cattle.io.cluster.vue +19 -4
  25. package/edit/management.cattle.io.setting.vue +1 -0
  26. package/edit/monitoring.coreos.com.alertmanagerconfig/types/opsgenie.vue +1 -1
  27. package/edit/monitoring.coreos.com.alertmanagerconfig/types/pagerduty.vue +1 -2
  28. package/edit/monitoring.coreos.com.alertmanagerconfig/types/slack.vue +1 -1
  29. package/edit/provisioning.cattle.io.cluster/index.vue +14 -7
  30. package/edit/provisioning.cattle.io.cluster/rke2.vue +22 -50
  31. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +9 -11
  32. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +3 -1
  33. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +3 -0
  34. package/edit/token.vue +1 -0
  35. package/list/catalog.cattle.io.app.vue +1 -0
  36. package/list/management.cattle.io.setting.vue +1 -0
  37. package/machine-config/amazonec2.vue +1 -0
  38. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +151 -0
  39. package/models/__tests__/secret.test.ts +37 -0
  40. package/models/__tests__/storage.k8s.io.storageclass.test.ts +22 -0
  41. package/models/provisioning.cattle.io.cluster.js +36 -1
  42. package/models/secret.js +9 -0
  43. package/models/storage.k8s.io.storageclass.js +1 -1
  44. package/package.json +1 -1
  45. package/pages/c/_cluster/settings/DefaultLinksEditor.vue +1 -0
  46. package/pages/c/_cluster/settings/brand.vue +3 -0
  47. package/pages/c/_cluster/uiplugins/AddExtensionRepos.vue +4 -4
  48. package/pages/c/_cluster/uiplugins/SetupUIPlugins.vue +5 -2
  49. package/pages/c/_cluster/uiplugins/__tests__/AddExtensionRepos.test.ts +96 -0
  50. package/pages/c/_cluster/uiplugins/__tests__/SetupUIPlugins.test.ts +128 -0
  51. package/plugins/dashboard-store/__tests__/actions.test.ts +196 -111
  52. package/plugins/dashboard-store/actions.js +4 -6
  53. package/plugins/dashboard-store/getters.js +60 -2
  54. package/plugins/dashboard-store/resource-class.js +6 -2
  55. package/plugins/steve/__tests__/getters.spec.ts +10 -0
  56. package/plugins/steve/__tests__/resource-utils.test.ts +159 -0
  57. package/plugins/steve/actions.js +3 -37
  58. package/plugins/steve/getters.js +6 -0
  59. package/plugins/steve/resource-utils.ts +38 -0
  60. package/store/__tests__/type-map.test.ts +1122 -0
  61. package/store/index.js +3 -2
  62. package/store/type-map.js +145 -75
  63. package/types/shell/index.d.ts +2 -0
  64. package/utils/__tests__/create-yaml.test.ts +10 -0
  65. package/utils/create-yaml.js +5 -1
  66. package/utils/object.js +10 -0
@@ -126,7 +126,7 @@ export default {
126
126
  </div>
127
127
  <div class="col span-6">
128
128
  <LabeledInput
129
- v-model="value.httpConfig.proxyUrl"
129
+ v-model="value.httpConfig.proxyURL"
130
130
  :mode="mode"
131
131
  label="Proxy URL"
132
132
  placeholder="e.g. http://my-proxy/"
@@ -305,7 +305,7 @@ export default {
305
305
 
306
306
  const templates = this.templateOptions;
307
307
  const vueKontainerTypes = getters['plugins/clusterDrivers'];
308
- const machineTypes = this.nodeDrivers.filter((x) => x.spec.active && x.state === 'active').map((x) => x.spec.displayName || x.id);
308
+ const machineTypes = this.nodeDrivers.filter((x) => x.spec.active && x.state === 'active');
309
309
 
310
310
  this.kontainerDrivers.filter((x) => (isImport ? x.showImport : x.showCreate)).forEach((obj) => {
311
311
  if ( vueKontainerTypes.includes(obj.driverName) ) {
@@ -330,14 +330,18 @@ export default {
330
330
  });
331
331
 
332
332
  if (this.isRke1 ) {
333
- machineTypes.forEach((id) => {
334
- addType(id, _RKE1, false, `/g/clusters/add/launch/${ id }`, this.iconClasses[id]);
333
+ machineTypes.forEach((type) => {
334
+ const id = type.spec.displayName || type.id;
335
+
336
+ addType(id, _RKE1, false, `/g/clusters/add/launch/${ id }`, this.iconClasses[id], type);
335
337
  });
336
338
 
337
339
  addType('custom', 'custom1', false, '/g/clusters/add/launch/custom');
338
340
  } else {
339
- machineTypes.forEach((id) => {
340
- addType(id, _RKE2, false);
341
+ machineTypes.forEach((type) => {
342
+ const id = type.spec.displayName || type.id;
343
+
344
+ addType(id, _RKE2, false, null, undefined, type);
341
345
  });
342
346
 
343
347
  addType('custom', 'custom2', false);
@@ -392,7 +396,7 @@ export default {
392
396
  out.push(subtype);
393
397
  }
394
398
 
395
- function addType(id, group, disabled = false, emberLink = null, iconClass = undefined) {
399
+ function addType(id, group, disabled = false, emberLink = null, iconClass = undefined, providerConfig = undefined) {
396
400
  const label = getters['i18n/withFallback'](`cluster.provider."${ id }"`, null, id);
397
401
  const description = getters['i18n/withFallback'](`cluster.providerDescription."${ id }"`, null, '');
398
402
  const tag = '';
@@ -418,7 +422,8 @@ export default {
418
422
  group,
419
423
  disabled,
420
424
  emberLink,
421
- tag
425
+ tag,
426
+ providerConfig
422
427
  };
423
428
 
424
429
  out.push(subtype);
@@ -638,6 +643,7 @@ export default {
638
643
  :live-value="liveValue"
639
644
  :mode="mode"
640
645
  :provider="subType"
646
+ :provider-config="selectedSubType.providerConfig"
641
647
  />
642
648
  <Rke2Config
643
649
  v-else
@@ -646,6 +652,7 @@ export default {
646
652
  :live-value="liveValue"
647
653
  :mode="mode"
648
654
  :provider="subType"
655
+ :provider-config="selectedSubType.providerConfig"
649
656
  />
650
657
  </template>
651
658
 
@@ -126,6 +126,11 @@ export default {
126
126
  type: String,
127
127
  required: true,
128
128
  },
129
+
130
+ providerConfig: {
131
+ type: Object,
132
+ default: () => null
133
+ }
129
134
  },
130
135
 
131
136
  async fetch() {
@@ -211,7 +216,6 @@ export default {
211
216
  machinePoolValidation: {}, // map of validation states for each machine pool
212
217
  machinePoolErrors: {},
213
218
  allNamespaces: [],
214
- initialCloudProvider: this.value?.agentConfig?.['cloud-provider-name'] || '',
215
219
  extensionTabs: getApplicableExtensionEnhancements(this, ExtensionPoint.TAB, TabLocation.CLUSTER_CREATE_RKE2, this.$route, this),
216
220
  };
217
221
  },
@@ -326,6 +330,14 @@ export default {
326
330
 
327
331
  const out = findBy(this.versionOptions, 'value', str);
328
332
 
333
+ // Adding the option 'none' to Container Network select (used in Basics component)
334
+ // https://github.com/rancher/dashboard/issues/10338
335
+ // there's an update loop on refresh that might include 'none'
336
+ // multiple times... Prevent that
337
+ if (out.serverArgs?.cni?.options && !out.serverArgs?.cni?.options.includes('none')) {
338
+ out.serverArgs.cni.options.push('none');
339
+ }
340
+
329
341
  return out;
330
342
  },
331
343
 
@@ -351,7 +363,7 @@ export default {
351
363
  },
352
364
 
353
365
  needCredential() {
354
- if ( this.provider === 'custom' || this.provider === 'import' || this.isElementalCluster || this.mode === _VIEW ) {
366
+ if ( this.provider === 'custom' || this.provider === 'import' || this.isElementalCluster || this.mode === _VIEW || (this.providerConfig?.spec?.builtin === false && this.providerConfig?.spec?.addCloudCredential === false) ) {
355
367
  return false;
356
368
  }
357
369
 
@@ -577,30 +589,8 @@ export default {
577
589
 
578
590
  const cur = this.agentConfig?.['cloud-provider-name'];
579
591
 
580
- if (cur && !out.find((x) => x.value === cur)) {
581
- // Localization missing
582
- // Look up cur in the localization file
583
- const label = this.$store.getters['i18n/withFallback'](`cluster.cloudProvider."${ cur }".label`, null, cur);
584
-
585
- out.unshift({
586
- label: `${ label } (Current)`,
587
- value: cur,
588
- unsupported: true,
589
- disabled: true
590
- });
591
- }
592
-
593
- const initial = this.initialCloudProvider;
594
-
595
- if (cur !== initial && initial && !out.find((x) => x.value === initial)) {
596
- const label = this.$store.getters['i18n/withFallback'](`cluster.cloudProvider."${ initial }".label`, null, initial);
597
-
598
- out.unshift({
599
- label: `${ label } (Current)`,
600
- value: initial,
601
- unsupported: true,
602
- disabled: true
603
- });
592
+ if ( cur && !out.find((x) => x.value === cur) ) {
593
+ out.unshift({ label: `${ cur } (Current)`, value: cur });
604
594
  }
605
595
 
606
596
  return out;
@@ -708,14 +698,6 @@ export default {
708
698
 
709
699
  return validRequiredPools && base;
710
700
  },
711
- unsupportedCloudProvider() {
712
- // The current cloud provider
713
- const cur = this.initialCloudProvider;
714
-
715
- const provider = cur && this.cloudProviderOptions.find((x) => x.value === cur);
716
-
717
- return !!provider?.unsupported;
718
- },
719
701
  },
720
702
 
721
703
  watch: {
@@ -1431,7 +1413,9 @@ export default {
1431
1413
  for ( const chartName of this.addonNames ) {
1432
1414
  const entry = this.chartVersions[chartName];
1433
1415
 
1434
- if ( this.versionInfo[chartName] ) {
1416
+ // prevent fetching of addon config for 'none' CNI option
1417
+ // https://github.com/rancher/dashboard/issues/10338
1418
+ if ( this.versionInfo[chartName] || chartName.includes('none')) {
1435
1419
  continue;
1436
1420
  }
1437
1421
 
@@ -1568,8 +1552,7 @@ export default {
1568
1552
  set(regs, 'mirrors', {});
1569
1553
  }
1570
1554
 
1571
- const hostname = Object.keys(regs.configs)[0];
1572
- const config = regs.configs[hostname];
1555
+ const config = regs.configs[this.registryHost];
1573
1556
 
1574
1557
  if ( config ) {
1575
1558
  registrySecret = config.authConfigSecretName;
@@ -1866,18 +1849,6 @@ export default {
1866
1849
  if (this.isHarvesterDriver && this.mode === _CREATE && this.isHarvesterIncompatible) {
1867
1850
  this.setHarvesterDefaultCloudProvider();
1868
1851
  }
1869
-
1870
- // Cloud Provider check
1871
- // If the cloud provider is unsupported, switch provider to 'external'
1872
- if (this.unsupportedCloudProvider) {
1873
- set(this.agentConfig, 'cloud-provider-name', 'external');
1874
- } else {
1875
- // Switch the cloud provider back to the initial value
1876
- // Use changed the Kubernetes version back to a version where the initial cloud provider is valid - so switch back to this one
1877
- // to undo the change to external that we may have made
1878
- // Note: Cloud Provider can only be changed on edit when the initial provider is no longer supported
1879
- set(this.agentConfig, 'cloud-provider-name', this.initialCloudProvider);
1880
- }
1881
1852
  }
1882
1853
  },
1883
1854
 
@@ -2048,11 +2019,13 @@ export default {
2048
2019
  :cancel="cancelCredential"
2049
2020
  :showing-form="showForm"
2050
2021
  :default-on-cancel="true"
2022
+ data-testid="select-credential"
2051
2023
  class="mt-20"
2052
2024
  />
2053
2025
 
2054
2026
  <div
2055
2027
  v-if="showForm"
2028
+ data-testid="form"
2056
2029
  class="mt-20"
2057
2030
  >
2058
2031
  <NameNsDescription
@@ -2182,7 +2155,6 @@ export default {
2182
2155
  :have-arg-info="haveArgInfo"
2183
2156
  :show-cni="showCni"
2184
2157
  :show-cloud-provider="showCloudProvider"
2185
- :unsupported-cloud-provider="unsupportedCloudProvider"
2186
2158
  :cloud-provider-options="cloudProviderOptions"
2187
2159
  @cilium-values-changed="handleCiliumValuesChanged"
2188
2160
  @enabled-system-services-changed="handleEnabledSystemServicesChanged"
@@ -103,10 +103,6 @@ export default {
103
103
  type: Boolean,
104
104
  required: true
105
105
  },
106
- unsupportedCloudProvider: {
107
- type: Boolean,
108
- required: true
109
- },
110
106
  cloudProviderOptions: {
111
107
  type: Array,
112
108
  required: true
@@ -369,7 +365,7 @@ export default {
369
365
  },
370
366
 
371
367
  canNotEditCloudProvider() {
372
- const canNotEdit = this.isEdit && !this.unsupportedCloudProvider;
368
+ const canNotEdit = this.isEdit;
373
369
 
374
370
  return canNotEdit;
375
371
  },
@@ -421,12 +417,20 @@ export default {
421
417
  >
422
418
  <span v-clean-html="t('cluster.banner.cloudProviderAddConfig', {}, true)" />
423
419
  </Banner>
420
+ <Banner
421
+ v-if="serverConfig.cni === 'none'"
422
+ color="warning"
423
+ data-testid="clusterBasics__noneOptionSelectedForCni"
424
+ >
425
+ <span v-clean-html="t('cluster.rke2.cni.cniNoneBanner', {}, true)" />
426
+ </Banner>
424
427
  <div class="row mb-10">
425
428
  <div class="col span-6">
426
429
  <LabeledSelect
427
430
  v-model="value.spec.kubernetesVersion"
428
431
  :mode="mode"
429
432
  :options="versionOptions"
433
+ data-testid="clusterBasics__kubernetesVersions"
430
434
  label-key="cluster.kubernetesVersion.label"
431
435
  @input="$emit('kubernetes-changed', $event)"
432
436
  />
@@ -495,12 +499,6 @@ export default {
495
499
  <div class="spacer" />
496
500
 
497
501
  <div class="col span-12">
498
- <Banner
499
- v-if="unsupportedCloudProvider"
500
- class="error mt-5"
501
- >
502
- {{ t('cluster.rke2.cloudProvider.unsupported') }}
503
- </Banner>
504
502
  <h3>
505
503
  {{ t('cluster.rke2.cloudProvider.header') }}
506
504
  </h3>
@@ -138,13 +138,14 @@ export default {
138
138
  :mode="mode"
139
139
  @input="update"
140
140
  >
141
- <template #default="{row}">
141
+ <template #default="{row, i}">
142
142
  <div class="row">
143
143
  <div class="col span-6">
144
144
  <LabeledInput
145
145
  v-model="row.value.hostname"
146
146
  label="Registry Hostname"
147
147
  :mode="mode"
148
+ :data-testid="`registry-auth-host-input-${i}`"
148
149
  />
149
150
 
150
151
  <SelectOrCreateAuthSecret
@@ -158,6 +159,7 @@ export default {
158
159
  :namespace="value.metadata.namespace"
159
160
  :mode="mode"
160
161
  generate-name="registryconfig-auth-"
162
+ :data-testid="`registry-auth-select-or-create-${i}`"
161
163
  />
162
164
  </div>
163
165
  <div class="col span-6">
@@ -79,6 +79,7 @@ export default {
79
79
  :value="showCustomRegistryInput"
80
80
  class="mb-20"
81
81
  :label="t('cluster.privateRegistry.label')"
82
+ data-testid="registries-enable-checkbox"
82
83
  @input="$emit('custom-registry-changed', $event)"
83
84
  />
84
85
  </div>
@@ -92,6 +93,7 @@ export default {
92
93
  label-key="catalog.chart.registry.custom.inputLabel"
93
94
  placeholder-key="catalog.chart.registry.custom.placeholder"
94
95
  :min-height="30"
96
+ data-testid="registry-host-input"
95
97
  @input="$emit('registry-host-changed', $event)"
96
98
  />
97
99
  <SelectOrCreateAuthSecret
@@ -118,6 +120,7 @@ export default {
118
120
  class="col span-12 advanced"
119
121
  :is-open-by-default="showCustomRegistryAdvancedInput"
120
122
  :mode="mode"
123
+ data-testid="registries-advanced-section"
121
124
  >
122
125
  <Banner
123
126
  :closable="false"
package/edit/token.vue CHANGED
@@ -199,6 +199,7 @@ export default {
199
199
  <RadioGroup
200
200
  v-model="form.expiryType"
201
201
  :options="expiryOptions"
202
+ data-testid="expiry__options"
202
203
  class="mr-20"
203
204
  name="expiryGroup"
204
205
  />
@@ -39,6 +39,7 @@ export default {
39
39
  :loading="loading"
40
40
  :use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
41
41
  :force-update-live-and-delayed="forceUpdateLiveAndDelayed"
42
+ data-testid="installed-app-catalog-list"
42
43
  >
43
44
  <template #cell:upgrade="{row}">
44
45
  <span
@@ -92,6 +92,7 @@ export default {
92
92
  v-for="setting in settings"
93
93
  :key="setting.id"
94
94
  class="advanced-setting mb-20"
95
+ :data-testid="`advanced-setting__option-${setting.id}`"
95
96
  >
96
97
  <div class="header">
97
98
  <div class="title">
@@ -475,6 +475,7 @@ export default {
475
475
  :disabled="disabled"
476
476
  :placeholder="t('cluster.machineConfig.amazonEc2.selectedNetwork.placeholder')"
477
477
  :label="t('cluster.machineConfig.amazonEc2.selectedNetwork.label')"
478
+ data-testid="amazonEc2__selectedNetwork"
478
479
  option-key="value"
479
480
  @input="updateNetwork($event)"
480
481
  >
@@ -26,6 +26,56 @@ describe('class ProvCluster', () => {
26
26
  clusterName: 'test', provisioner: 'rke2', mgmt: { isLocal: false, providerForEmberParam: 'import' }, spec: { rkeConfig: {} }
27
27
  };
28
28
 
29
+ const gkeClusterWithPrivateEndpoint = {
30
+ clusterName: 'test',
31
+ provisioner: 'GKE',
32
+ spec: { rkeConfig: {} },
33
+ mgmt: { spec: { gkeConfig: { privateClusterConfig: { enablePrivateEndpoint: true } } } }
34
+ };
35
+
36
+ const eksClusterWithPrivateEndpoint = {
37
+ clusterName: 'test',
38
+ provisioner: 'EKS',
39
+ spec: { rkeConfig: {} },
40
+ mgmt: { spec: { eksConfig: { privateAccess: true } } }
41
+ };
42
+
43
+ const aksClusterWithPrivateEndpoint = {
44
+ clusterName: 'test',
45
+ provisioner: 'AKS',
46
+ spec: { rkeConfig: {} },
47
+ mgmt: { spec: { aksConfig: { privateCluster: true } } }
48
+ };
49
+
50
+ // Related to https://github.com/rancher/dashboard/issues/9402
51
+ describe('isHostedKubernetesProvider + isPrivateHostedProvider', () => {
52
+ const testCases = [
53
+ [gkeClusterWithPrivateEndpoint, true],
54
+ [eksClusterWithPrivateEndpoint, true],
55
+ [aksClusterWithPrivateEndpoint, true],
56
+ ];
57
+ const resetMocks = () => {
58
+ // Clear all mock function calls:
59
+ jest.clearAllMocks();
60
+ };
61
+
62
+ it.each(testCases)('should return the isHostedKubernetesProvider and isPrivateHostedProvider values properly based on the props data', (clusterData: Object, expected: Boolean) => {
63
+ const cluster = new ProvCluster({ spec: clusterData.spec });
64
+
65
+ jest.spyOn(cluster, 'mgmt', 'get').mockReturnValue(
66
+ clusterData.mgmt
67
+ );
68
+ jest.spyOn(cluster, 'provisioner', 'get').mockReturnValue(
69
+ clusterData.provisioner
70
+ );
71
+
72
+ expect(cluster.isRke2).toBe(expected);
73
+ expect(cluster.isHostedKubernetesProvider).toBe(expected);
74
+ expect(cluster.isPrivateHostedProvider).toBe(expected);
75
+ resetMocks();
76
+ });
77
+ });
78
+
29
79
  describe('isImported', () => {
30
80
  const testCases = [
31
81
  [importedClusterInfo, true],
@@ -84,6 +134,107 @@ describe('class ProvCluster', () => {
84
134
 
85
135
  expect(cluster.mgmt).toBe(expected);
86
136
  resetMocks();
137
+ });
138
+ });
139
+
140
+ describe('hasError', () => {
141
+ const conditionsWithoutError = [
142
+ {
143
+ error: false,
144
+ lastUpdateTime: '2022-10-17T23:09:15Z',
145
+ status: 'True',
146
+ transitioning: false,
147
+ type: 'Ready'
148
+ },
149
+ ];
150
+
151
+ const conditionsWithoutReady = [
152
+ {
153
+ error: true,
154
+ lastUpdateTime: '2022-10-17T23:09:15Z',
155
+ status: 'False',
156
+ message: 'some-error-message',
157
+ transitioning: false,
158
+ type: 'Pending'
159
+ },
160
+ ];
161
+
162
+ const noConditions:[] = [];
163
+
164
+ const conditionsWithReadyLatest = [
165
+ {
166
+ error: true,
167
+ lastUpdateTime: '2022-10-17T23:09:15Z',
168
+ status: 'False',
169
+ message: 'some-error-message',
170
+ transitioning: false,
171
+ type: 'Pending'
172
+ },
173
+ {
174
+ error: false,
175
+ lastUpdateTime: '2023-10-17T23:09:15Z',
176
+ status: 'True',
177
+ transitioning: false,
178
+ type: 'Ready'
179
+ }
180
+ ];
181
+
182
+ const conditionsWithErrorLatest = [
183
+ {
184
+ error: false,
185
+ lastUpdateTime: '2022-10-17T23:09:15Z',
186
+ status: 'True',
187
+ transitioning: false,
188
+ type: 'Ready'
189
+ },
190
+ {
191
+ error: true,
192
+ lastUpdateTime: '2023-10-17T23:09:15Z',
193
+ status: 'False',
194
+ message: 'some-error-message',
195
+ transitioning: false,
196
+ type: 'Pending'
197
+ }
198
+ ];
199
+
200
+ const conditionsWithProblemInLastUpdateTimeProp = [
201
+ {
202
+ error: true,
203
+ lastUpdateTime: '',
204
+ status: 'False',
205
+ message: 'some-error-message',
206
+ transitioning: false,
207
+ type: 'Pending'
208
+ },
209
+ {
210
+ error: false,
211
+ lastUpdateTime: '2023-10-17T23:09:15Z',
212
+ status: 'True',
213
+ transitioning: false,
214
+ type: 'Ready'
215
+ }
216
+ ];
217
+
218
+ const testCases = [
219
+ ['conditionsWithoutError', conditionsWithoutError, false],
220
+ ['conditionsWithoutReady', conditionsWithoutReady, true],
221
+ ['noConditions', noConditions, false],
222
+ ['conditionsWithReadyLatest', conditionsWithReadyLatest, false],
223
+ ['conditionsWithErrorLatest', conditionsWithErrorLatest, true],
224
+ ['conditionsWithProblemInLastUpdateTimeProp', conditionsWithProblemInLastUpdateTimeProp, false],
225
+ ];
226
+
227
+ const resetMocks = () => {
228
+ // Clear all mock function calls
229
+ jest.clearAllMocks();
230
+ };
231
+
232
+ it.each(testCases)('should return the hasError value properly based on the "status.conditions" props data for testcase %p', (testName: string, conditions: Array, expected: Boolean) => {
233
+ const ctx = { rootGetters: { 'management/byId': jest.fn() } };
234
+ const cluster = new ProvCluster({ status: { conditions } }, ctx);
235
+
236
+ expect(cluster.hasError).toBe(expected);
237
+ resetMocks();
87
238
  }
88
239
  );
89
240
  });
@@ -0,0 +1,37 @@
1
+ import Secret from '@shell/models/secret';
2
+
3
+ describe('class Secret', () => {
4
+ it('should contains the type attribute if cleanForDownload', async() => {
5
+ const secret = new Secret({});
6
+ const yaml = `apiVersion: v1
7
+ kind: Secret
8
+ metadata:
9
+ name: my-secret
10
+ type: Opaque
11
+ `;
12
+ const cleanYaml = await secret.cleanForDownload(yaml);
13
+
14
+ expect(cleanYaml).toBe(yaml);
15
+ });
16
+
17
+ it('should remove id, links and actions keys if cleanForDownload', async() => {
18
+ const secret = new Secret({});
19
+ const expectedYamlStr = `apiVersion: v1
20
+ kind: Secret
21
+ metadata:
22
+ name: my-secret
23
+ namespace: default
24
+ type: Opaque
25
+ `;
26
+ const part = `id: test_id
27
+ links:
28
+ view: https://example.com
29
+ actions:
30
+ remove: https://example.com`;
31
+ const yaml = `${ expectedYamlStr }
32
+ ${ part }`;
33
+ const cleanYaml = await secret.cleanForDownload(yaml);
34
+
35
+ expect(cleanYaml).toBe(expectedYamlStr);
36
+ });
37
+ });
@@ -0,0 +1,22 @@
1
+ import StorageClass, { PROVISIONER_OPTIONS } from '@shell/models/storage.k8s.io.storageclass';
2
+
3
+ describe('class StorageClass', () => {
4
+ describe('checking if provisionerDisplay', () => {
5
+ it.each([
6
+ ['kubernetes.io/azure-disk', true],
7
+ ['kubernetes.io/portworx-volume', true],
8
+ ['rancher.io/local-path', false],
9
+ ['some-random-string-as-provisioner', false],
10
+ ])('should NOT show a suffix IF they are built-in (on the PROVISIONER_OPTIONS list)', (provisioner, expectation) => {
11
+ const storageClass = new StorageClass({
12
+ metadata: {},
13
+ spec: {},
14
+ provisioner
15
+ });
16
+
17
+ jest.spyOn(storageClass, '$rootGetters', 'get').mockReturnValue({ 'i18n/t': jest.fn() });
18
+
19
+ expect(!!PROVISIONER_OPTIONS.find((opt) => opt.value === provisioner)).toBe(expectation);
20
+ });
21
+ });
22
+ });
@@ -249,6 +249,21 @@ export default class ProvCluster extends SteveModel {
249
249
  return providers.includes(this.provisioner);
250
250
  }
251
251
 
252
+ get isPrivateHostedProvider() {
253
+ if (this.isHostedKubernetesProvider && this.mgmt && this.provisioner) {
254
+ switch (this.provisioner.toLowerCase()) {
255
+ case 'gke':
256
+ return this.mgmt.spec?.gkeConfig?.privateClusterConfig?.enablePrivateEndpoint;
257
+ case 'eks':
258
+ return this.mgmt.spec?.eksConfig?.privateAccess;
259
+ case 'aks':
260
+ return this.mgmt.spec?.aksConfig?.privateCluster;
261
+ }
262
+ }
263
+
264
+ return false;
265
+ }
266
+
252
267
  get isLocal() {
253
268
  return this.mgmt?.isLocal;
254
269
  }
@@ -891,7 +906,27 @@ export default class ProvCluster extends SteveModel {
891
906
  }
892
907
 
893
908
  get hasError() {
894
- return this.status?.conditions?.some((condition) => condition.error === true);
909
+ // Before we were just checking for this.status?.conditions?.some((condition) => condition.error === true)
910
+ // but this is wrong as an error might exist but it might not be meaningful in the context of readiness of a cluster
911
+ // which is what this 'hasError' is used for.
912
+ // We now check if there's a ready condition after an error, which helps dictate the readiness of a cluster
913
+ // Based on the findings in https://github.com/rancher/dashboard/issues/10043
914
+ if (this.status?.conditions && this.status?.conditions.length) {
915
+ // if there are errors, we compare with how recent the "Ready" condition is compared to that error, otherwise we just move on
916
+ if (this.status?.conditions.some((c) => c.error === true)) {
917
+ // there's no ready condition and has an error, mark it
918
+ if (!this.status?.conditions.some((c) => c.type === 'Ready')) {
919
+ return true;
920
+ }
921
+
922
+ const filteredConditions = this.status?.conditions.filter((c) => c.error === true || c.type === 'Ready');
923
+ const mostRecentCondition = filteredConditions.reduce((a, b) => ((a.lastUpdateTime > b.lastUpdateTime) ? a : b));
924
+
925
+ return mostRecentCondition.error;
926
+ }
927
+ }
928
+
929
+ return false;
895
930
  }
896
931
 
897
932
  get namespaceLocation() {
package/models/secret.js CHANGED
@@ -9,6 +9,7 @@ import SteveModel from '@shell/plugins/steve/steve-class';
9
9
  import { colorForState, stateDisplay, STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';
10
10
  import { diffFrom } from '@shell/utils/time';
11
11
  import day from 'dayjs';
12
+ import { steveCleanForDownload } from '@shell/plugins/steve/resource-utils';
12
13
 
13
14
  export const TYPES = {
14
15
  OPAQUE: 'Opaque',
@@ -456,4 +457,12 @@ export default class Secret extends SteveModel {
456
457
 
457
458
  return val;
458
459
  }
460
+
461
+ async cleanForDownload(yaml) {
462
+ // secret resource contains the type attribute
463
+ // ref: https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/secret-v1/
464
+ // ref: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types
465
+
466
+ return steveCleanForDownload(yaml, { rootKeys: ['id', 'links', 'actions'] });
467
+ }
459
468
  }
@@ -84,7 +84,7 @@ export const PROVISIONER_OPTIONS = [
84
84
  export default class extends SteveModel {
85
85
  get provisionerDisplay() {
86
86
  const option = PROVISIONER_OPTIONS.find((o) => o.value === this.provisioner);
87
- const fallback = `${ this.provisioner } ${ this.t('persistentVolume.csi.drivers.suffix') }`;
87
+ const fallback = `${ this.provisioner } ${ this.t('persistentVolume.csi.suffix') }`;
88
88
 
89
89
  return option ? this.t(option.labelKey) : this.$rootGetters['i18n/withFallback'](`persistentVolume.csi.drivers.${ this.provisioner.replaceAll('.', '-') }`, null, fallback);
90
90
  }