@rancher/shell 3.0.12-rc.4 → 3.0.12-rc.6

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 (159) hide show
  1. package/apis/intf/resources-api/resource-base.ts +42 -2
  2. package/apis/intf/resources-api/resource-instance.ts +101 -0
  3. package/apis/intf/resources-api/resources-api.ts +177 -23
  4. package/apis/intf/resources.ts +8 -6
  5. package/apis/resources/__tests__/resources-api-class.test.ts +309 -28
  6. package/apis/resources/resources-api-class.ts +232 -43
  7. package/assets/images/providers/traefik.png +0 -0
  8. package/assets/styles/global/_button.scss +1 -1
  9. package/assets/translations/en-us.yaml +49 -14
  10. package/chart/__tests__/rancher-monitoring-dashboards-index.test.ts +269 -0
  11. package/chart/rancher-monitoring-dashboards/index.vue +155 -0
  12. package/cloud-credential/__tests__/generic.test.ts +132 -0
  13. package/cloud-credential/generic.vue +33 -3
  14. package/components/ActionDropdownShell.vue +5 -3
  15. package/components/ButtonGroup.vue +26 -1
  16. package/components/CruResource.vue +51 -2
  17. package/components/EtcdInfoBanner.vue +2 -12
  18. package/components/GrafanaDashboard.vue +2 -8
  19. package/components/PromptRestore.vue +93 -32
  20. package/components/Questions/index.vue +1 -0
  21. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +68 -14
  22. package/components/Resource/Detail/Masthead/index.vue +8 -3
  23. package/components/ResourceTable.vue +1 -0
  24. package/components/SortableTable/index.vue +4 -3
  25. package/components/Wizard.vue +14 -1
  26. package/components/__tests__/ButtonGroup.test.ts +56 -0
  27. package/components/__tests__/PromptRestore.test.ts +169 -19
  28. package/components/fleet/GitRepoAdvancedTab.vue +1 -0
  29. package/components/fleet/GitRepoMetadataTab.vue +5 -0
  30. package/components/fleet/HelmOpAppCoConfigTab.vue +4 -0
  31. package/components/fleet/HelmOpMetadataTab.vue +5 -0
  32. package/components/form/FileSelector.vue +39 -1
  33. package/components/form/PrivateRegistry.constants.ts +7 -0
  34. package/components/form/PrivateRegistry.vue +253 -18
  35. package/components/form/Security.vue +1 -1
  36. package/components/form/SelectOrCreateAuthSecret.vue +140 -17
  37. package/components/form/__tests__/FileSelector.test.ts +23 -0
  38. package/components/form/__tests__/PrivateRegistry.test.ts +463 -73
  39. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +122 -0
  40. package/components/formatter/EtcdSnapshotName.vue +73 -0
  41. package/components/formatter/Translate.vue +22 -1
  42. package/components/nav/Group.vue +5 -10
  43. package/components/nav/Header.vue +8 -1
  44. package/components/nav/TopLevelMenu.vue +1 -30
  45. package/components/nav/__tests__/Group.test.ts +61 -0
  46. package/components/templates/default.vue +7 -0
  47. package/config/features.js +1 -0
  48. package/config/labels-annotations.js +3 -0
  49. package/config/product/manager.js +6 -0
  50. package/config/secret.ts +10 -0
  51. package/config/settings.ts +6 -2
  52. package/config/table-headers.js +5 -2
  53. package/config/types.js +7 -0
  54. package/core/__tests__/plugin-products-apply.test.ts +787 -0
  55. package/core/__tests__/plugin-products-extend.test.ts +189 -0
  56. package/core/__tests__/plugin-products-helpers.test.ts +81 -78
  57. package/core/__tests__/plugin-products-new.test.ts +389 -0
  58. package/core/__tests__/plugin-products-scenarios.test.ts +980 -0
  59. package/core/__tests__/plugin-products-side-menu.test.ts +1935 -0
  60. package/core/plugin-products-base.ts +201 -93
  61. package/core/plugin-products-extending.ts +1 -1
  62. package/core/plugin-products-external.ts +380 -0
  63. package/core/plugin-products-helpers.ts +28 -24
  64. package/core/plugin-products-internal.ts +207 -0
  65. package/core/plugin-products-top-level.ts +10 -5
  66. package/core/plugin-products-type-guards.ts +17 -6
  67. package/core/plugin-products.ts +5 -7
  68. package/core/plugin-types.ts +1 -389
  69. package/core/plugin.ts +10 -11
  70. package/core/types.ts +6 -202
  71. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +52 -0
  72. package/detail/pod.vue +1 -1
  73. package/detail/provisioning.cattle.io.cluster.vue +79 -5
  74. package/dialog/RotateEncryptionKeyDialog.vue +33 -9
  75. package/dialog/__tests__/RotateEncryptionKeyDialog.test.ts +78 -0
  76. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +92 -0
  77. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +101 -0
  78. package/edit/__tests__/management.cattle.io.setting.test.ts +2 -1
  79. package/edit/auth/AuthProviderWarningBanners.vue +12 -0
  80. package/edit/auth/__tests__/AuthProviderWarningBanners.test.ts +10 -1
  81. package/edit/auth/__tests__/azuread.test.ts +1 -0
  82. package/edit/auth/__tests__/github.test.ts +1 -0
  83. package/edit/auth/__tests__/oidc.test.ts +1 -0
  84. package/edit/auth/__tests__/saml.test.ts +1 -0
  85. package/edit/auth/ldap/__tests__/index.test.ts +1 -0
  86. package/edit/compliance.cattle.io.clusterscanprofile.vue +39 -41
  87. package/edit/fleet.cattle.io.gitrepo.vue +70 -16
  88. package/edit/fleet.cattle.io.helmop.vue +51 -5
  89. package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
  90. package/edit/{management.cattle.io.setting.vue → management.cattle.io.setting/index.vue} +32 -9
  91. package/edit/management.cattle.io.setting/system-default-registry-pull-secrets.vue +81 -0
  92. package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +3 -12
  93. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +18 -0
  94. package/edit/provisioning.cattle.io.cluster/index.vue +1 -1
  95. package/edit/provisioning.cattle.io.cluster/rke2.vue +6 -2
  96. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +0 -1
  97. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +2 -2
  98. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +14 -55
  99. package/list/management.cattle.io.user.vue +24 -5
  100. package/list/utils/management.cattle.io.cluster.utils.ts +1 -1
  101. package/machine-config/__tests__/generic.test.ts +163 -0
  102. package/machine-config/components/EC2Networking.vue +31 -7
  103. package/machine-config/components/__tests__/EC2Networking.test.ts +38 -15
  104. package/machine-config/generic.vue +32 -5
  105. package/mixins/resource-fetch.js +1 -1
  106. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +156 -0
  107. package/models/__tests__/rke-machine.cattle.io.vmwarevspheremachinetemplate.test.ts +22 -0
  108. package/models/__tests__/secret.test.ts +68 -1
  109. package/models/management.cattle.io.cluster.js +21 -3
  110. package/models/pod.js +25 -12
  111. package/models/provisioning.cattle.io.cluster.js +59 -9
  112. package/models/rke-machine.cattle.io.vmwarevspheremachinetemplate.js +9 -0
  113. package/models/rke.cattle.io.etcdsnapshot.js +17 -9
  114. package/models/secret.js +19 -0
  115. package/models/workload.js +12 -7
  116. package/package.json +1 -1
  117. package/pages/auth/login.vue +1 -1
  118. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +569 -81
  119. package/pages/c/_cluster/apps/charts/install.vue +143 -28
  120. package/pages/c/_cluster/auth/config/index.vue +11 -4
  121. package/pages/c/_cluster/monitoring/index.vue +6 -1
  122. package/pkg/require-asset.lib.js +25 -0
  123. package/pkg/vue.config.js +7 -0
  124. package/plugins/dashboard-store/__tests__/resource-class.test.ts +84 -0
  125. package/plugins/dashboard-store/getters.js +0 -1
  126. package/plugins/dashboard-store/resource-class.js +62 -15
  127. package/plugins/steve/__tests__/steve-class-resource-api.test.ts +147 -0
  128. package/plugins/steve/steve-class.js +43 -0
  129. package/plugins/steve/subscribe.js +11 -0
  130. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +115 -0
  131. package/rancher-components/Form/LabeledInput/LabeledInput.vue +83 -3
  132. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +30 -0
  133. package/rancher-components/Form/TextArea/__tests__/TextAreaAutoGrow.test.ts +95 -0
  134. package/rancher-components/RcButton/index.ts +1 -1
  135. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +6 -1
  136. package/store/__tests__/auth.test.ts +18 -0
  137. package/store/__tests__/features.test.ts +131 -0
  138. package/store/__tests__/growl.test.ts +374 -0
  139. package/store/__tests__/modal.test.ts +131 -0
  140. package/store/__tests__/notifications.test.ts +466 -1
  141. package/store/__tests__/slideInPanel.test.ts +88 -0
  142. package/store/__tests__/type-map.utils.test.ts +433 -0
  143. package/store/__tests__/ui-context.test.ts +255 -0
  144. package/store/auth.js +11 -1
  145. package/store/features.js +5 -0
  146. package/store/plugins.js +6 -0
  147. package/types/rancher/steve.api.ts +2 -2
  148. package/types/shell/index.d.ts +120 -0
  149. package/types/store/dashboard-store.types.ts +2 -2
  150. package/types/store/type-map.ts +262 -1
  151. package/utils/__tests__/operation-cr.test.ts +34 -0
  152. package/utils/async.ts +2 -0
  153. package/utils/grafana.js +2 -17
  154. package/utils/monitoring.js +38 -1
  155. package/utils/operation-cr.js +19 -0
  156. package/utils/require-asset.ts +7 -0
  157. package/utils/validators/__tests__/private-registry.test.ts +27 -15
  158. package/utils/validators/private-registry.ts +15 -4
  159. package/core/__tests__/plugin-products.test.ts +0 -4694
@@ -40,9 +40,12 @@ import {
40
40
  import { ignoreVariables } from './install.helpers';
41
41
  import { findBy, insertAt } from '@shell/utils/array';
42
42
  import { saferDump } from '@shell/utils/create-yaml';
43
+ import { addParam } from '@shell/utils/url';
43
44
  import { WINDOWS } from '@shell/store/catalog';
44
45
  import { SETTING } from '@shell/config/settings';
45
46
  import SelectOrCreateAuthSecret from '@shell/components/form/SelectOrCreateAuthSecret.vue';
47
+ import PrivateRegistry from '@shell/components/form/PrivateRegistry.vue';
48
+ import { PRIVATE_REGISTRY_CONTEXT } from '@shell/components/form/PrivateRegistry.constants';
46
49
  import { generateRandomAlphaString } from '@shell/utils/string';
47
50
 
48
51
  const VALUES_STATE = {
@@ -95,7 +98,8 @@ export default {
95
98
  UnitInput,
96
99
  YamlEditor,
97
100
  Wizard,
98
- SelectOrCreateAuthSecret
101
+ SelectOrCreateAuthSecret,
102
+ PrivateRegistry
99
103
  },
100
104
 
101
105
  mixins: [
@@ -357,6 +361,15 @@ export default {
357
361
  this.showCustomRegistryInput = !!this.customRegistrySetting;
358
362
  }
359
363
 
364
+ // On upgrade, pre-select a single existing image pull secret in the dropdown
365
+ if (this.existing && this.showRegistryPullSecrets) {
366
+ const existingPullSecrets = this.chartValues?.global?.imagePullSecrets;
367
+
368
+ if (Array.isArray(existingPullSecrets) && existingPullSecrets.length === 1) {
369
+ this.registryPullSecret = existingPullSecrets[0];
370
+ }
371
+ }
372
+
360
373
  /* Serializes an object as a YAML document */
361
374
  this.valuesYaml = saferDump(this.chartValues);
362
375
 
@@ -453,6 +466,9 @@ export default {
453
466
  appCoDataFetched: false,
454
467
  AUTH_TYPE,
455
468
  CLUSTER_REPO_APPCO_AUTH_GENERATE_NAME,
469
+ PRIVATE_REGISTRY_CONTEXT,
470
+ skipPullSecrets: false,
471
+ registryPullSecret: null,
456
472
  stepBasic: {
457
473
  name: 'basics',
458
474
  label: this.t('catalog.install.steps.basics.label'),
@@ -499,6 +515,29 @@ export default {
499
515
  ...mapGetters({ inStore: 'catalog/inStore', features: 'features/get' }),
500
516
  mcm: mapFeature(MULTI_CLUSTER),
501
517
 
518
+ showMonitoringBanner() {
519
+ const annotations = this.version?.annotations || {};
520
+ const releaseName = annotations[CATALOG_ANNOTATIONS.RELEASE_NAME];
521
+ const certified = annotations[CATALOG_ANNOTATIONS.CERTIFIED];
522
+
523
+ // Only show monitoring banners for the Rancher-certified charts —
524
+ // a third-party chart that happens to reuse the release name should not
525
+ // trigger our migration prompts.
526
+ if (certified !== 'rancher') {
527
+ return null;
528
+ }
529
+
530
+ if (releaseName === 'rancher-monitoring') {
531
+ return this.t('catalog.install.steps.basics.oldMonitoringChartWarning');
532
+ }
533
+
534
+ if (!this.existing && releaseName === 'rancher-monitoring-dashboards') {
535
+ return this.t('catalog.install.steps.basics.newMonitoringChartWarning');
536
+ }
537
+
538
+ return null;
539
+ },
540
+
502
541
  /**
503
542
  * Return list of variables to filter chart questions
504
543
  */
@@ -783,6 +822,29 @@ export default {
783
822
  return global.systemDefaultRegistry !== undefined || global.cattle?.systemDefaultRegistry !== undefined;
784
823
  },
785
824
 
825
+ showRegistryPullSecrets() {
826
+ return !!this.repo?.spec?.defaultImagePullSecrets?.length;
827
+ },
828
+
829
+ existingValuesPullSecrets() {
830
+ if (!this.existing) {
831
+ return [];
832
+ }
833
+
834
+ const pullSecrets = this.chartValues?.global?.imagePullSecrets;
835
+
836
+ return Array.isArray(pullSecrets) ? pullSecrets.filter(Boolean) : [];
837
+ },
838
+
839
+ /**
840
+ * if the system-default-pull-image-secrets global setting is set OR the current cluster has system default registry pull secrets configured
841
+ * the Rancher cluster repo will automatically be populated with
842
+ * copies of the secrets referenced in the global setting
843
+ */
844
+ repoDefaultPullSecretNames() {
845
+ return (this.repo?.spec?.defaultImagePullSecrets || []).map((s) => s.name).filter(Boolean);
846
+ },
847
+
786
848
  setImagePullSecretDataTrigger() {
787
849
  return `
788
850
  ${ this.defaultImagePullSecret?.name }
@@ -974,11 +1036,30 @@ export default {
974
1036
  }
975
1037
  }
976
1038
  },
1039
+
977
1040
  async getClusterRegistry() {
1041
+ const mgmCluster = this.$store.getters['currentCluster'];
1042
+
1043
+ // For local, imported, and hosted (AKS, EKS, GKE, ALI) clusters,
1044
+ // the cluster-scoped private registry is on the norman cluster's importedConfig.
1045
+ if (mgmCluster?.isLocal || mgmCluster?.isImported || mgmCluster?.isHostedKubernetesProvider) {
1046
+ try {
1047
+ const normanCluster = await mgmCluster.findNormanCluster();
1048
+ const importedRegistryURL = normanCluster?.importedConfig?.privateRegistryURL;
1049
+
1050
+ if (importedRegistryURL) {
1051
+ return importedRegistryURL;
1052
+ }
1053
+ } catch (e) {
1054
+ console.warn('Unable to fetch norman cluster for registry lookup: ', e); // eslint-disable-line no-console
1055
+ }
1056
+
1057
+ return;
1058
+ }
1059
+
978
1060
  const hasPermissionToSeeProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER);
979
1061
 
980
1062
  if (hasPermissionToSeeProvCluster) {
981
- const mgmCluster = this.$store.getters['currentCluster'];
982
1063
  const provClusterId = mgmCluster?.provClusterId;
983
1064
  let provCluster;
984
1065
 
@@ -1148,14 +1229,22 @@ export default {
1148
1229
  const isUpgrade = !!this.existing;
1149
1230
 
1150
1231
  this.errors = [];
1232
+ // Create namespace if it doesn't exist
1233
+ // this is done before save hooks so that image pull secrets can be created in the target namespace
1234
+ await this.createNamespaceIfNeeded();
1151
1235
 
1152
- // Create namespace if it doesn't exist (before hooks run)
1153
- // And only if it is SUSE APP Collection, overall should just do the same flow
1154
- if (!isUpgrade && this.isNamespaceNew && this.repo?.isSuseAppCollection) {
1155
- await this.createNamespaceIfNeeded();
1156
- }
1236
+ const hookResults = await this.applyHooks(BEFORE_SAVE_HOOKS);
1157
1237
 
1158
- await this.applyHooks(BEFORE_SAVE_HOOKS);
1238
+ // When a new pull secret is created by SelectOrCreateAuthSecret inside
1239
+ // PrivateRegistry, the emit chain does not propagate the secret name
1240
+ // back to registryPullSecret in time. Read it from the hook result.
1241
+ if (this.showRegistryPullSecrets && !this.skipPullSecrets && !this.registryPullSecret) {
1242
+ const createdSecret = hookResults?.registerAuthSecret;
1243
+
1244
+ if (createdSecret?.metadata?.name) {
1245
+ this.registryPullSecret = createdSecret.metadata.name;
1246
+ }
1247
+ }
1159
1248
 
1160
1249
  const { errors, input } = this.actionInput(isUpgrade);
1161
1250
 
@@ -1166,7 +1255,16 @@ export default {
1166
1255
  return;
1167
1256
  }
1168
1257
 
1169
- const res = await this.repo.doAction((isUpgrade ? 'upgrade' : 'install'), input);
1258
+ const actionName = isUpgrade ? 'upgrade' : 'install';
1259
+ const actionOpt = {};
1260
+
1261
+ if (this.skipPullSecrets) {
1262
+ const baseUrl = this.repo.actionLinkFor(actionName);
1263
+
1264
+ actionOpt.url = addParam(baseUrl, 'skipPullSecrets', 'true');
1265
+ }
1266
+
1267
+ const res = await this.repo.doAction(actionName, input, actionOpt);
1170
1268
  const operationId = `${ res.operationNamespace }/${ res.operationName }`;
1171
1269
 
1172
1270
  // Non-admins without a cluster won't be able to fetch operations immediately
@@ -1227,6 +1325,15 @@ export default {
1227
1325
  set(global, 'systemDefaultRegistry', this.customRegistrySetting);
1228
1326
  }
1229
1327
 
1328
+ if (this.showRegistryPullSecrets && this.registryPullSecret) {
1329
+ // User explicitly selected or created a pull secret
1330
+ set(global, 'imagePullSecrets', [this.registryPullSecret]);
1331
+ } else if (this.showRegistryPullSecrets) {
1332
+ // User chose "skip" or "use default" — remove explicit imagePullSecrets
1333
+ // so the backend falls back to the repo/global defaults
1334
+ delete global.imagePullSecrets;
1335
+ }
1336
+
1230
1337
  setIfNotSet(global, 'cattle.systemProjectId', systemProjectId);
1231
1338
  setIfNotSet(cattle, 'url', serverUrl);
1232
1339
  setIfNotSet(cattle, 'rkePathPrefix', pathPrefix);
@@ -1329,7 +1436,6 @@ export default {
1329
1436
  */
1330
1437
 
1331
1438
  this.addGlobalValuesTo(values);
1332
-
1333
1439
  const form = JSON.parse(JSON.stringify(this.value));
1334
1440
 
1335
1441
  /*
@@ -1488,6 +1594,8 @@ export default {
1488
1594
  }
1489
1595
  },
1490
1596
 
1597
+ // not the same as PrivateRegistry pull secrets which are created based off the global/cluster system default registry hostname value not the repo url directly
1598
+ // those secrets will be created in a beforeSaveHook managed by SelectOrCreateAuthSecret
1491
1599
  async createImagePullSecret() {
1492
1600
  if (!this.repo?.isSuseAppCollection) {
1493
1601
  return;
@@ -1593,6 +1701,12 @@ export default {
1593
1701
  </template>
1594
1702
  <template #basics>
1595
1703
  <div class="step__basic">
1704
+ <Banner
1705
+ v-if="showMonitoringBanner"
1706
+ color="warning"
1707
+ >
1708
+ {{ showMonitoringBanner }}
1709
+ </Banner>
1596
1710
  <Banner
1597
1711
  v-if="step1Description"
1598
1712
  color="info"
@@ -1753,26 +1867,27 @@ export default {
1753
1867
  :label="t('catalog.install.steps.helmCli.checkbox', { action: action.name, existing: !!existing })"
1754
1868
  />
1755
1869
 
1756
- <Checkbox
1870
+ <PrivateRegistry
1757
1871
  v-if="showCustomRegistry"
1758
- v-model:value="showCustomRegistryInput"
1759
- class="mb-20"
1760
- data-testid="custom-registry-checkbox"
1761
- :label="t('catalog.chart.registry.custom.checkBoxLabel')"
1762
- :tooltip="t('catalog.chart.registry.tooltip')"
1872
+ :context="PRIVATE_REGISTRY_CONTEXT.CHARTS"
1873
+ :value="customRegistrySetting"
1874
+ :enabled="showCustomRegistryInput"
1875
+ :default-registry="defaultRegistrySetting"
1876
+ :namespace="targetNamespace"
1877
+ in-store="cluster"
1878
+ :register-before-hook="registerBeforeHook"
1879
+ :show-pull-secrets="showRegistryPullSecrets"
1880
+ :repo-default-pull-secrets="repoDefaultPullSecretNames"
1881
+ :existing-values-pull-secrets="existingValuesPullSecrets"
1882
+ :pull-secret="registryPullSecret"
1883
+ :skip-pull-secrets="skipPullSecrets"
1884
+ checkbox-test-id="custom-registry-checkbox"
1885
+ input-test-id="custom-registry-input"
1886
+ @update:value="(val) => customRegistrySetting = val"
1887
+ @update:enabled="(val) => showCustomRegistryInput = val"
1888
+ @update:pull-secret="(val) => registryPullSecret = val"
1889
+ @update:skip-pull-secrets="(val) => skipPullSecrets = val"
1763
1890
  />
1764
- <div class="row">
1765
- <div class="col span-6">
1766
- <LabeledInput
1767
- v-if="showCustomRegistryInput"
1768
- v-model:value="customRegistrySetting"
1769
- data-testid="custom-registry-input"
1770
- label-key="catalog.chart.registry.custom.inputLabel"
1771
- placeholder-key="catalog.chart.registry.custom.placeholder"
1772
- :min-height="30"
1773
- />
1774
- </div>
1775
- </div>
1776
1891
  <div
1777
1892
  class="step__values__controls--spacer"
1778
1893
  style="flex:1"
@@ -6,6 +6,7 @@ import { MODE, _EDIT } from '@shell/config/query-params';
6
6
  import { authProvidersInfo } from '@shell/utils/auth';
7
7
  import { Banner } from '@components/Banner';
8
8
  import Loading from '@shell/components/Loading';
9
+ import { HIDE_LOCAL_AUTH_PROVIDER } from '@shell/store/features';
9
10
 
10
11
  const resource = MANAGEMENT.AUTH_CONFIG;
11
12
 
@@ -28,8 +29,9 @@ export default {
28
29
  data() {
29
30
  return {
30
31
  // Provided by fetch later
31
- enabled: false,
32
- nonLocal: null,
32
+ enabled: false,
33
+ nonLocal: null,
34
+ disableLocalAuth: this.$store.getters['features/get'](HIDE_LOCAL_AUTH_PROVIDER)
33
35
  };
34
36
  },
35
37
 
@@ -100,8 +102,13 @@ export default {
100
102
  <router-link :to="localUsersRoute">
101
103
  {{ t('authConfig.manageLocal') }}
102
104
  </router-link>
103
- <br>
104
- {{ t('authConfig.noneEnabled') }}
105
+ <br><br>
106
+ <template v-if="disableLocalAuth">
107
+ {{ t('authConfig.bannerEnableAuthProvider', null, true) }}
108
+ </template>
109
+ <template v-else>
110
+ {{ t('authConfig.noneEnabled') }}
111
+ </template>
105
112
  </div>
106
113
  </Banner>
107
114
  <SelectIconGrid
@@ -106,7 +106,12 @@ export default {
106
106
  const grafanaMatch = findBy(externalLinks, 'group', 'grafana');
107
107
  // Generate Grafana link
108
108
  const currentCluster = this.$store.getters['currentCluster'];
109
- const rancherMonitoring = !isEmpty(res.apps) ? findBy(res.apps, 'id', 'cattle-monitoring-system/rancher-monitoring') : '';
109
+ let rancherMonitoring = !isEmpty(res.apps) ? findBy(res.apps, 'id', 'cattle-monitoring-system/rancher-monitoring-dashboards') : '';
110
+
111
+ if (!rancherMonitoring) {
112
+ rancherMonitoring = !isEmpty(res.apps) ? findBy(res.apps, 'id', 'cattle-monitoring-system/rancher-monitoring') : '';
113
+ }
114
+
110
115
  const clusterPrefix = getClusterPrefix(rancherMonitoring?.currentVersion || '', currentCluster.id);
111
116
 
112
117
  grafanaMatch.link = `${ clusterPrefix }/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/`;
@@ -0,0 +1,25 @@
1
+ // Stub for extension library builds — prevents require.context() from bundling
2
+ // all shell images into every extension. At runtime, delegates to the host
3
+ // dashboard's asset resolver (exposed on window by the real require-asset.ts).
4
+
5
+ export function toContextKey(path) {
6
+ return `./${ path.replace(/^[~@]shell\/assets\//, '') }`;
7
+ }
8
+
9
+ export function requireAsset(path) {
10
+ if (typeof window !== 'undefined' && window.__shell_requireAsset) {
11
+ return window.__shell_requireAsset(path);
12
+ }
13
+
14
+ throw new Error(`Asset context not available for: ${ path }`);
15
+ }
16
+
17
+ export function requireJson(path) {
18
+ if (typeof window !== 'undefined' && window.__shell_requireJson) {
19
+ return window.__shell_requireJson(path);
20
+ }
21
+
22
+ throw new Error(`JSON context not available for: ${ path }`);
23
+ }
24
+
25
+ export function _setContexts() {}
package/pkg/vue.config.js CHANGED
@@ -72,11 +72,18 @@ module.exports = function(dir) {
72
72
  resource.request = fs.existsSync(pkgModelLoaderRequire) ? pkgModelLoaderRequire : path.join(__dirname, fileName);
73
73
  });
74
74
 
75
+ // Prevent require.context('@shell/assets') from bundling all shell images into extensions.
76
+ // The stub delegates to the host dashboard's asset resolver at runtime via window.__shell_requireAsset.
77
+ const requireAssetOverride = new webpack.NormalModuleReplacementPlugin(/require-asset$/, (resource) => {
78
+ resource.request = path.join(__dirname, 'require-asset.lib.js');
79
+ });
80
+
75
81
  // Auto-generate module to import the types (model, detail, edit etc)
76
82
  const autoImportPlugin = new VirtualModulesPlugin({ 'node_modules/@rancher/auto-import': generateTypeImport('@pkg', dir) });
77
83
 
78
84
  config.plugins.unshift(dynamicImporterOverride);
79
85
  config.plugins.unshift(modelLoaderImporterOverride);
86
+ config.plugins.unshift(requireAssetOverride);
80
87
  config.plugins.unshift(autoImportPlugin);
81
88
  config.plugins.unshift(new NodePolyfillPlugin()); // required from Webpack 5 to polyfill node modules
82
89
  // config.plugins.unshift(debug);
@@ -678,4 +678,88 @@ describe('class: Resource', () => {
678
678
  expect(viewYaml.enabled).toBe(true);
679
679
  });
680
680
  });
681
+
682
+ describe('method: dryRunCreate', () => {
683
+ const collectionUrl = '/v1/test.resources';
684
+
685
+ it('should dispatch a request with dryRun=All query param', async() => {
686
+ const dispatch = jest.fn().mockResolvedValue({});
687
+ const resource = new Resource({
688
+ type: 'test.resource',
689
+ metadata: {
690
+ name: 'my-resource',
691
+ namespace: 'my-ns',
692
+ },
693
+ }, {
694
+ getters: {
695
+ schemaFor: () => ({
696
+ linkFor: (link: string) => (link === 'collection' ? collectionUrl : ''),
697
+ attributes: { namespaced: true },
698
+ })
699
+ },
700
+ dispatch,
701
+ rootGetters: { 'i18n/t': jest.fn() },
702
+ });
703
+
704
+ await resource.dryRunCreate();
705
+
706
+ expect(dispatch).toHaveBeenCalledWith('request', {
707
+ opt: expect.objectContaining({
708
+ method: 'post',
709
+ url: `${ collectionUrl }/my-ns?dryRun=All`,
710
+ }),
711
+ type: 'test.resource'
712
+ });
713
+ });
714
+
715
+ it('should use provided data instead of resource state when given', async() => {
716
+ const dispatch = jest.fn().mockResolvedValue({});
717
+ const resource = new Resource({
718
+ type: 'test.resource',
719
+ metadata: { name: 'original', namespace: 'ns' },
720
+ }, {
721
+ getters: {
722
+ schemaFor: () => ({
723
+ linkFor: () => collectionUrl,
724
+ attributes: { namespaced: true },
725
+ })
726
+ },
727
+ dispatch,
728
+ rootGetters: { 'i18n/t': jest.fn() },
729
+ });
730
+
731
+ const customData = {
732
+ type: 'test.resource',
733
+ metadata: { name: 'custom' },
734
+ spec: {}
735
+ };
736
+
737
+ await resource.dryRunCreate(customData);
738
+
739
+ expect(dispatch).toHaveBeenCalledWith('request', {
740
+ opt: expect.objectContaining({ data: customData }),
741
+ type: 'test.resource'
742
+ });
743
+ });
744
+
745
+ it('should propagate API errors', async() => {
746
+ const apiError = { _status: 409, message: 'already exists' };
747
+ const dispatch = jest.fn().mockRejectedValue(apiError);
748
+ const resource = new Resource({
749
+ type: 'test.resource',
750
+ metadata: { name: 'dup', namespace: 'ns' },
751
+ }, {
752
+ getters: {
753
+ schemaFor: () => ({
754
+ linkFor: () => collectionUrl,
755
+ attributes: { namespaced: true },
756
+ })
757
+ },
758
+ dispatch,
759
+ rootGetters: { 'i18n/t': jest.fn() },
760
+ });
761
+
762
+ await expect(resource.dryRunCreate()).rejects.toStrictEqual(apiError);
763
+ });
764
+ });
681
765
  });
@@ -278,7 +278,6 @@ export default {
278
278
  const schemas = state.types[SCHEMA];
279
279
 
280
280
  type = getters.normalizeType(type);
281
-
282
281
  if ( !schemas ) {
283
282
  if ( allowThrow ) {
284
283
  throw new Error("Schemas aren't loaded yet");
@@ -79,6 +79,7 @@ export const STATES_ENUM = {
79
79
  BUILDING: 'building',
80
80
  COMPLETED: 'completed',
81
81
  CORDONED: 'cordoned',
82
+ CANCELLED: 'cancelled',
82
83
  COUNT: 'count',
83
84
  CREATED: 'created',
84
85
  CREATING: 'creating',
@@ -207,6 +208,9 @@ export const STATES = {
207
208
  [STATES_ENUM.CORDONED]: {
208
209
  color: 'info', icon: 'tag', label: 'Cordoned', compoundIcon: 'info'
209
210
  },
211
+ [STATES_ENUM.CANCELLED]: {
212
+ color: 'warning', icon: 'error', label: 'Cancelled', compoundIcon: 'warning'
213
+ },
210
214
  [STATES_ENUM.COUNT]: {
211
215
  color: 'success', icon: 'dot-open', label: 'Count', compoundIcon: 'checkmark'
212
216
  },
@@ -1191,6 +1195,46 @@ export default class Resource {
1191
1195
  return this._save(...arguments);
1192
1196
  }
1193
1197
 
1198
+ _collectionUrl() {
1199
+ const schema = this.$getters['schemaFor'](this.type);
1200
+
1201
+ if ( !schema ) {
1202
+ // Schema not found - likely due to lack of permissions to view this resource type
1203
+ throw new Error(`${ this.type }: ${ this.t('validation.createResourceFailed', { type: this.typeDisplay }, true) }`);
1204
+ }
1205
+
1206
+ let url = schema.linkFor('collection');
1207
+
1208
+ if ( schema.attributes && schema.attributes.namespaced && this.metadata && this.metadata.namespace ) {
1209
+ url += `/${ this.metadata.namespace }`;
1210
+ }
1211
+
1212
+ return url;
1213
+ }
1214
+
1215
+ async dryRunCreate(data) {
1216
+ try {
1217
+ const url = this._collectionUrl();
1218
+ const separator = url.includes('?') ? '&' : '?';
1219
+ const body = data || this.cleanForSave(this.toSave() || JSON.parse(JSON.stringify(this)), true);
1220
+
1221
+ return this.$dispatch('request', {
1222
+ opt: {
1223
+ method: 'post',
1224
+ url: `${ url }${ separator }dryRun=All`,
1225
+ data: body,
1226
+ headers: {
1227
+ 'content-type': 'application/json',
1228
+ accept: 'application/json'
1229
+ }
1230
+ },
1231
+ type: this.type
1232
+ });
1233
+ } catch (e) {
1234
+ return Promise.reject(e);
1235
+ }
1236
+ }
1237
+
1194
1238
  /**
1195
1239
  * Remove any unwanted properties from the object that will be saved
1196
1240
  */
@@ -1209,8 +1253,11 @@ export default class Resource {
1209
1253
 
1210
1254
  async _save(opt = { }) {
1211
1255
  const forNew = !this.id;
1256
+ let errors;
1212
1257
 
1213
- const errors = this.validationErrors(this, opt);
1258
+ if (!opt.skipUIValidation) {
1259
+ errors = this.validationErrors(this, opt);
1260
+ }
1214
1261
 
1215
1262
  if (!isEmpty(errors)) {
1216
1263
  return Promise.reject(errors);
@@ -1219,20 +1266,16 @@ export default class Resource {
1219
1266
  if ( this.metadata?.resourceVersion ) {
1220
1267
  this.metadata.resourceVersion = `${ this.metadata.resourceVersion }`;
1221
1268
  }
1222
-
1223
- if ( !opt.url ) {
1224
- if ( forNew ) {
1225
- const schema = this.$getters['schemaFor'](this.type);
1226
- let url = schema.linkFor('collection');
1227
-
1228
- if ( schema.attributes && schema.attributes.namespaced && this.metadata && this.metadata.namespace ) {
1229
- url += `/${ this.metadata.namespace }`;
1269
+ try {
1270
+ if ( !opt.url ) {
1271
+ if ( forNew ) {
1272
+ opt.url = this._collectionUrl();
1273
+ } else {
1274
+ opt.url = this.linkFor('update') || this.linkFor('self');
1230
1275
  }
1231
-
1232
- opt.url = url;
1233
- } else {
1234
- opt.url = this.linkFor('update') || this.linkFor('self');
1235
1276
  }
1277
+ } catch (e) {
1278
+ return Promise.reject(e);
1236
1279
  }
1237
1280
 
1238
1281
  if ( !opt.method ) {
@@ -1254,7 +1297,9 @@ export default class Resource {
1254
1297
  // @TODO remove this once the API maps steve _type <-> k8s type in both directions
1255
1298
  // `JSON.parse(JSON.stringify` - Completely disconnect the object we're going to send and `this`. This ensures that properties
1256
1299
  // removed from opt.data before sending (as part of cleanForSave) are not stripped from where they're still needed (`this`)
1257
- opt.data = this.toSave() || JSON.parse(JSON.stringify(this));
1300
+ if (!(opt.method === 'patch')) {
1301
+ opt.data = this.toSave() || JSON.parse(JSON.stringify(this));
1302
+ }
1258
1303
 
1259
1304
  if (opt.data._type) {
1260
1305
  opt.data.type = opt.data._type;
@@ -1272,7 +1317,9 @@ export default class Resource {
1272
1317
  opt.data.annotations = opt.data._annotations;
1273
1318
  }
1274
1319
 
1275
- opt.data = this.cleanForSave(opt.data, forNew);
1320
+ if (!(opt.method === 'patch')) {
1321
+ opt.data = this.cleanForSave(opt.data, forNew);
1322
+ }
1276
1323
 
1277
1324
  // handle "replace" opt as a query param _replace=true for norman PUT requests
1278
1325
  if (opt?.replace && opt.method === 'put') {