@rancher/shell 0.3.20 → 0.3.22

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 (74) hide show
  1. package/assets/translations/en-us.yaml +8 -2
  2. package/assets/translations/zh-hans.yaml +8 -1
  3. package/cloud-credential/__tests__/azure.test.ts +53 -0
  4. package/cloud-credential/azure.vue +6 -0
  5. package/components/GrowlManager.vue +33 -30
  6. package/components/Questions/Array.vue +2 -2
  7. package/components/Questions/Boolean.vue +7 -1
  8. package/components/Questions/CloudCredential.vue +1 -0
  9. package/components/Questions/Enum.vue +21 -2
  10. package/components/Questions/Float.vue +8 -3
  11. package/components/Questions/Int.vue +8 -3
  12. package/components/Questions/Question.js +72 -0
  13. package/components/Questions/QuestionMap.vue +2 -1
  14. package/components/Questions/Radio.vue +33 -0
  15. package/components/Questions/Reference.vue +2 -0
  16. package/components/Questions/String.vue +8 -3
  17. package/components/Questions/Yaml.vue +46 -0
  18. package/components/Questions/__tests__/Boolean.test.ts +123 -0
  19. package/components/Questions/__tests__/Float.test.ts +123 -0
  20. package/components/Questions/__tests__/Int.test.ts +123 -0
  21. package/components/Questions/__tests__/String.test.ts +123 -0
  22. package/components/Questions/__tests__/Yaml.test.ts +123 -0
  23. package/components/Questions/index.vue +8 -1
  24. package/components/ResourceTable.vue +6 -12
  25. package/components/SideNav.vue +634 -0
  26. package/components/__tests__/NamespaceFilter.test.ts +3 -4
  27. package/components/form/ResourceQuota/ProjectRow.vue +38 -15
  28. package/components/form/UnitInput.vue +1 -0
  29. package/components/form/__tests__/KeyValue.test.ts +2 -1
  30. package/components/form/__tests__/UnitInput.test.ts +2 -2
  31. package/components/formatter/ClusterProvider.vue +9 -3
  32. package/components/formatter/LinkName.vue +12 -1
  33. package/components/formatter/__tests__/ClusterProvider.test.ts +5 -1
  34. package/components/nav/Header.vue +1 -0
  35. package/components/nav/WorkspaceSwitcher.vue +4 -1
  36. package/config/settings.ts +59 -2
  37. package/config/types.js +2 -0
  38. package/core/plugin-helpers.js +4 -1
  39. package/core/types.ts +1 -0
  40. package/creators/pkg/files/.github/workflows/build-extension-catalog.yml +28 -0
  41. package/creators/pkg/files/.github/workflows/build-extension-charts.yml +26 -0
  42. package/creators/pkg/init +63 -4
  43. package/detail/provisioning.cattle.io.cluster.vue +4 -2
  44. package/edit/fleet.cattle.io.gitrepo.vue +8 -0
  45. package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -4
  46. package/layouts/default.vue +11 -597
  47. package/middleware/authenticated.js +2 -14
  48. package/mixins/__tests__/chart.test.ts +40 -0
  49. package/mixins/chart.js +5 -0
  50. package/models/catalog.cattle.io.clusterrepo.js +6 -2
  51. package/models/fleet.cattle.io.cluster.js +10 -2
  52. package/models/fleet.cattle.io.gitrepo.js +3 -1
  53. package/package.json +1 -1
  54. package/pages/c/_cluster/fleet/index.vue +4 -0
  55. package/pages/c/_cluster/gatekeeper/index.vue +10 -1
  56. package/pages/c/_cluster/uiplugins/index.vue +3 -3
  57. package/plugins/steve/__tests__/header-warnings.spec.ts +238 -0
  58. package/plugins/steve/actions.js +4 -23
  59. package/plugins/steve/header-warnings.ts +91 -0
  60. package/promptRemove/management.cattle.io.project.vue +9 -6
  61. package/rancher-components/components/Form/LabeledInput/LabeledInput.vue +8 -0
  62. package/rancher-components/components/Form/Radio/RadioButton.test.ts +7 -3
  63. package/scripts/extension/parse-tag-name +30 -0
  64. package/types/shell/index.d.ts +3 -0
  65. package/utils/auth.js +17 -0
  66. package/utils/object.js +0 -1
  67. package/utils/settings.ts +2 -17
  68. package/utils/validators/__tests__/cidr.test.ts +33 -0
  69. package/utils/validators/cidr.js +5 -0
  70. package/vue-config-helper.js +135 -0
  71. package/vue.config.js +23 -139
  72. package/yarn-error.log +200 -0
  73. package/creators/pkg/files/.github/workflows/build-container.yml +0 -64
  74. package/creators/pkg/files/.github/workflows/build-extension.yml +0 -110
@@ -15,6 +15,7 @@ import dynamicPluginLoader from '@shell/pkg/dynamic-plugin-loader';
15
15
  import { AFTER_LOGIN_ROUTE, WORKSPACE } from '@shell/store/prefs';
16
16
  import { BACK_TO } from '@shell/config/local-storage';
17
17
  import { NAME as FLEET_NAME } from '@shell/config/product/fleet.js';
18
+ import { canViewResource } from '@shell/utils/auth';
18
19
 
19
20
  const getPackageFromRoute = (route) => {
20
21
  if (!route?.meta) {
@@ -133,20 +134,7 @@ function invalidResource(store, to, redirect) {
133
134
  return false;
134
135
  }
135
136
 
136
- // Note - don't use the current products store... because products can override stores for resources with `typeStoreMap`
137
- const inStore = store.getters['currentStore'](resource);
138
- // There's a chance we're in an extension's product who's store could be anything, so confirm schemaFor exists
139
- const schemaFor = store.getters[`${ inStore }/schemaFor`];
140
-
141
- // In order to check a resource is valid we need these
142
- if (!inStore || !schemaFor) {
143
- return false;
144
- }
145
-
146
- // Resource is valid if a schema exists for it (standard resource, spoofed resource) or it's a virtual resource
147
- const validResource = schemaFor(resource) || store.getters['type-map/isVirtual'](resource);
148
-
149
- if (validResource) {
137
+ if (canViewResource(store, resource)) {
150
138
  return false;
151
139
  }
152
140
 
@@ -0,0 +1,40 @@
1
+ import { createLocalVue } from '@vue/test-utils';
2
+ import Vuex from 'vuex';
3
+ import ChartMixin from '@shell/mixins/chart';
4
+ import { OPA_GATE_KEEPER_ID } from '@shell/pages/c/_cluster/gatekeeper/index.vue';
5
+
6
+ describe('chartMixin', () => {
7
+ const testCases = [[null, 0], [OPA_GATE_KEEPER_ID, 1], ['any_other_id', 0]];
8
+
9
+ it.each(testCases)(
10
+ 'should add OPA deprecation warning properly', (chartId, expected) => {
11
+ const localVue = createLocalVue();
12
+
13
+ localVue.use(Vuex);
14
+ localVue.mixin(ChartMixin);
15
+
16
+ const store = new Vuex.Store({
17
+ getters: {
18
+ currentCluster: () => {},
19
+ isRancher: () => true,
20
+ 'catalog/repo': () => {
21
+ return () => 'repo';
22
+ },
23
+ 'catalog/chart': () => {
24
+ return () => ({ id: chartId });
25
+ },
26
+ 'i18n/t': () => jest.fn()
27
+ }
28
+ });
29
+
30
+ const vm = localVue.extend({});
31
+ const instance = new vm({ store });
32
+
33
+ instance.$route = { query: { chart: 'chart_name' } };
34
+
35
+ const warnings = instance.warnings;
36
+
37
+ expect(warnings).toHaveLength(expected);
38
+ }
39
+ );
40
+ });
package/mixins/chart.js CHANGED
@@ -8,6 +8,7 @@ import { CATALOG as CATALOG_ANNOTATIONS } from '@shell/config/labels-annotations
8
8
  import { SHOW_PRE_RELEASE, mapPref } from '@shell/store/prefs';
9
9
  import { NAME as EXPLORER } from '@shell/config/product/explorer';
10
10
  import { NAME as MANAGER } from '@shell/config/product/manager';
11
+ import { OPA_GATE_KEEPER_ID } from '@shell/pages/c/_cluster/gatekeeper/index.vue';
11
12
 
12
13
  import { formatSi, parseSi } from '@shell/utils/units';
13
14
  import { CAPI, CATALOG } from '@shell/config/types';
@@ -185,6 +186,10 @@ export default {
185
186
  }
186
187
  }
187
188
 
189
+ if (this.chart?.id === OPA_GATE_KEEPER_ID) {
190
+ warnings.unshift(this.t('gatekeeperIndex.deprecated', {}, true));
191
+ }
192
+
188
193
  return warnings;
189
194
  },
190
195
 
@@ -29,11 +29,15 @@ export default class ClusterRepo extends SteveModel {
29
29
  return out;
30
30
  }
31
31
 
32
- refresh() {
32
+ async refresh() {
33
33
  const now = (new Date()).toISOString().replace(/\.\d+Z$/, 'Z');
34
34
 
35
35
  this.spec.forceUpdate = now;
36
- this.save();
36
+ await this.save();
37
+
38
+ await this.waitForState('active', 10000, 1000);
39
+
40
+ this.$dispatch('catalog/load', { force: true, reset: true }, { root: true });
37
41
  }
38
42
 
39
43
  get isGit() {
@@ -1,4 +1,4 @@
1
- import { MANAGEMENT, NORMAN } from '@shell/config/types';
1
+ import { LOCAL_CLUSTER, MANAGEMENT, NORMAN } from '@shell/config/types';
2
2
  import { CAPI, FLEET as FLEET_LABELS } from '@shell/config/labels-annotations';
3
3
  import { _RKE2 } from '@shell/store/prefs';
4
4
  import SteveModel from '@shell/plugins/steve/steve-class';
@@ -34,7 +34,7 @@ export default class FleetCluster extends SteveModel {
34
34
  enabled: !!this.links.update
35
35
  });
36
36
 
37
- if (!this.isRke2) {
37
+ if (this.canChangeWorkspace) {
38
38
  insertAt(out, 3, {
39
39
  action: 'assignTo',
40
40
  label: 'Change workspace',
@@ -79,6 +79,14 @@ export default class FleetCluster extends SteveModel {
79
79
  return false;
80
80
  }
81
81
 
82
+ get canChangeWorkspace() {
83
+ return !this.isRke2 && !this.isLocal;
84
+ }
85
+
86
+ get isLocal() {
87
+ return this.metadata.name === LOCAL_CLUSTER || this.metadata?.labels?.[FLEET_LABELS.CLUSTER_NAME] === LOCAL_CLUSTER;
88
+ }
89
+
82
90
  get isRke2() {
83
91
  const provider = this?.metadata?.labels?.[CAPI.PROVIDER] || this?.status?.provider;
84
92
 
@@ -1,3 +1,4 @@
1
+ import Vue from 'vue';
1
2
  import { convert, matching, convertSelectorObj } from '@shell/utils/selector';
2
3
  import jsyaml from 'js-yaml';
3
4
  import { escapeHtml, randomStr } from '@shell/utils/string';
@@ -32,7 +33,8 @@ export default class GitRepo extends SteveModel {
32
33
 
33
34
  spec.paths = spec.paths || [];
34
35
  spec.clientSecretName = spec.clientSecretName || null;
35
- spec.correctDrift = { enabled: false };
36
+
37
+ Vue.set(spec, 'correctDrift', { enabled: false });
36
38
 
37
39
  set(this, 'spec', spec);
38
40
  set(this, 'metadata', meta);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rancher/shell",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
4
4
  "description": "Rancher Dashboard Shell",
5
5
  "repository": "https://github.com/rancherlabs/dashboard",
6
6
  "license": "Apache-2.0",
@@ -377,6 +377,7 @@ export default {
377
377
  :title="`${t('resourceDetail.masthead.workspace')}: ${ws.nameDisplay}`"
378
378
  :is-collapsed="isCollapsed[ws.id]"
379
379
  :is-title-clickable="true"
380
+ :data-testid="`collapsible-card-${ ws.id }`"
380
381
  @toggleCollapse="toggleCollapse($event, ws.id)"
381
382
  @titleClick="setWorkspaceFilterAndLinkToGitRepo(ws.id)"
382
383
  >
@@ -411,6 +412,7 @@ export default {
411
412
  <span v-if="ws.type === 'namespace'"> - </span>
412
413
  <CompoundStatusBadge
413
414
  v-else
415
+ data-testid="clusters-ready"
414
416
  :tooltip-text="getTooltipInfo('clusters', row)"
415
417
  :badge-class="getStatusInfo('clusters', row).badgeClass"
416
418
  :icon="getStatusInfo('clusters', row).icon"
@@ -421,6 +423,7 @@ export default {
421
423
  <span v-if="ws.type === 'namespace'"> - </span>
422
424
  <CompoundStatusBadge
423
425
  v-else
426
+ data-testid="bundles-ready"
424
427
  :tooltip-text="getTooltipInfo('bundles', row)"
425
428
  :badge-class="getStatusInfo('bundles', row).badgeClass"
426
429
  :icon="getStatusInfo('bundles', row).icon"
@@ -429,6 +432,7 @@ export default {
429
432
  </template>
430
433
  <template #cell:resourcesReady="{row}">
431
434
  <CompoundStatusBadge
435
+ data-testid="resources-ready"
432
436
  :tooltip-text="getTooltipInfo('resources', row)"
433
437
  :badge-class="getStatusInfo('resources', row).badgeClass"
434
438
  :icon="getStatusInfo('resources', row).icon"
@@ -3,10 +3,16 @@ import { NAME, CHART_NAME } from '@shell/config/product/gatekeeper';
3
3
  import InstallRedirect from '@shell/utils/install-redirect';
4
4
  import ChartHeading from '@shell/components/ChartHeading';
5
5
  import SortableTable from '@shell/components/SortableTable';
6
+ import { Banner } from '@components/Banner';
6
7
  import { CONSTRAINT_VIOLATION_CONSTRAINT_LINK, CONSTRAINT_VIOLATION_COUNT, CONSTRAINT_VIOLATION_TEMPLATE_LINK } from '@shell/config/table-headers';
7
8
  import { GATEKEEPER } from '@shell/config/types';
9
+
10
+ export const OPA_GATE_KEEPER_ID = 'cluster/rancher-charts/rancher-gatekeeper';
11
+
8
12
  export default {
9
- components: { ChartHeading, SortableTable },
13
+ components: {
14
+ ChartHeading, SortableTable, Banner
15
+ },
10
16
  middleware: InstallRedirect(NAME, CHART_NAME),
11
17
  async fetch() {
12
18
  const constraints = this.constraint ? [this.constraint] : await this.$store.dispatch('cluster/findAll', { type: GATEKEEPER.SPOOFED.CONSTRAINT });
@@ -49,6 +55,9 @@ export default {
49
55
  :label="t('gatekeeperIndex.poweredBy')"
50
56
  url="https://github.com/open-policy-agent/gatekeeper"
51
57
  />
58
+ <Banner color="warning">
59
+ <span v-clean-html="t('gatekeeperIndex.deprecated', {}, true)" />
60
+ </Banner>
52
61
  <div class="spacer" />
53
62
  <div class="mb-10">
54
63
  <h2><t k="gatekeeperIndex.violations" /></h2>
@@ -243,9 +243,9 @@ export default {
243
243
 
244
244
  all = all.map((chart) => {
245
245
  // Label can be overridden by chart annotation
246
- const label = uiPluginAnnotation(UI_PLUGIN_CHART_ANNOTATIONS.DISPLAY_NAME) || chart.chartNameDisplay;
246
+ const label = uiPluginAnnotation(chart, UI_PLUGIN_CHART_ANNOTATIONS.DISPLAY_NAME) || chart.chartNameDisplay;
247
247
  const item = {
248
- name: chart.chartNameDisplay,
248
+ name: chart.chartName,
249
249
  label,
250
250
  description: chart.chartDescription,
251
251
  id: chart.id,
@@ -305,7 +305,7 @@ export default {
305
305
  if (!chart) {
306
306
  // A plugin is loaded, but there is no chart, so add an item so that it shows up
307
307
  const rancher = typeof p.metadata?.rancher === 'object' ? p.metadata.rancher : {};
308
- const label = rancher[UI_PLUGIN_CHART_ANNOTATIONS.DISPLAY_NAME] || p.name;
308
+ const label = rancher.annotations?.[UI_PLUGIN_CHART_ANNOTATIONS.DISPLAY_NAME] || p.name;
309
309
  const item = {
310
310
  name: p.name,
311
311
  label,
@@ -0,0 +1,238 @@
1
+ import { handleKubeApiHeaderWarnings } from '@shell/plugins/steve/header-warnings';
2
+ import { DEFAULT_PERF_SETTING } from '@shell/config/settings';
3
+
4
+ describe('steve: header-warnings', () => {
5
+ // eslint-disable-next-line jest/no-hooks
6
+
7
+ function setupMocks(settings = {
8
+ separator: '299 - ',
9
+ notificationBlockList: ['299 - unknown field']
10
+ }) {
11
+ return {
12
+ dispatch: jest.fn(),
13
+ consoleWarn: jest.spyOn(console, 'warn').mockImplementation(),
14
+ consoleDebug: jest.spyOn(console, 'debug').mockImplementation(),
15
+ rootGetter: {
16
+ 'management/byId': (resource: string, id: string): { value: string} => {
17
+ return {
18
+ value: JSON.stringify({
19
+ ...DEFAULT_PERF_SETTING,
20
+ kubeAPI: { warningHeader: settings }
21
+ })
22
+ };
23
+ },
24
+ 'i18n/t': (key: string, { resourceType }: any) => {
25
+ return `${ key }_${ resourceType }`;
26
+ }
27
+ }
28
+ };
29
+ }
30
+
31
+ function createMockRes( headers = {}, data = { type: inputResourceType }) {
32
+ return {
33
+ headers, config: { url: 'unit/test' }, data
34
+ };
35
+ }
36
+
37
+ function createGrowlResponse(message: string, action = updateKey) {
38
+ return [
39
+ 'growl/warning',
40
+ {
41
+ message, timeout: 0, title: `${ action }_${ inputResourceType }`
42
+ },
43
+ { root: true }
44
+ ];
45
+ }
46
+
47
+ function createConsoleResponse(warnings: string) {
48
+ return `Validation Warnings for unit/test\n\n${ warnings }`;
49
+ }
50
+
51
+ const inputResourceType = 'abc';
52
+ const updateKey = 'growl.kubeApiHeaderWarning.titleUpdate';
53
+ const createKey = 'growl.kubeApiHeaderWarning.titleCreate';
54
+ const podSecurity = '299 - would violate PodSecurity "restricted:latest": unrestricted capabilities (container "container-0" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (container "container-0" must not set securityContext.runAsNonRoot=false), seccompProfile (pod or container "container-0" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")';
55
+ const deprecated = "299 - i'm deprecated";
56
+ const validation = '299 - unknown field "spec.containers[0].__active"';
57
+
58
+ describe('no warnings', () => {
59
+ it('put, no header warning', () => {
60
+ const mocks = setupMocks();
61
+
62
+ handleKubeApiHeaderWarnings(createMockRes(),
63
+ mocks.dispatch,
64
+ mocks.rootGetter,
65
+ 'put',
66
+ true,
67
+ );
68
+
69
+ expect(mocks.dispatch).not.toHaveBeenCalled();
70
+ expect(mocks.consoleWarn).not.toHaveBeenCalled();
71
+ expect(mocks.consoleDebug).not.toHaveBeenCalled();
72
+ });
73
+
74
+ it('post, no header warning', () => {
75
+ const mocks = setupMocks();
76
+
77
+ handleKubeApiHeaderWarnings(createMockRes(),
78
+ mocks.dispatch,
79
+ mocks.rootGetter,
80
+ 'post',
81
+ true,
82
+ );
83
+
84
+ expect(mocks.dispatch).not.toHaveBeenCalled();
85
+ expect(mocks.consoleWarn).not.toHaveBeenCalled();
86
+ expect(mocks.consoleDebug).not.toHaveBeenCalled();
87
+ });
88
+
89
+ it('patch, no header warning', () => {
90
+ const mocks = setupMocks();
91
+
92
+ handleKubeApiHeaderWarnings(createMockRes(),
93
+ mocks.dispatch,
94
+ mocks.rootGetter,
95
+ 'patch',
96
+ true,
97
+ );
98
+
99
+ expect(mocks.dispatch).not.toHaveBeenCalled();
100
+ expect(mocks.consoleWarn).not.toHaveBeenCalled();
101
+ expect(mocks.consoleDebug).not.toHaveBeenCalled();
102
+ });
103
+
104
+ it('get, no header warning', () => {
105
+ const mocks = setupMocks();
106
+
107
+ handleKubeApiHeaderWarnings(createMockRes(),
108
+ mocks.dispatch,
109
+ mocks.rootGetter,
110
+ 'get',
111
+ true,
112
+ );
113
+
114
+ expect(mocks.dispatch).not.toHaveBeenCalled();
115
+ expect(mocks.consoleWarn).not.toHaveBeenCalled();
116
+ expect(mocks.consoleDebug).not.toHaveBeenCalled();
117
+ });
118
+
119
+ it('get, warnings', () => {
120
+ const mocks = setupMocks();
121
+
122
+ handleKubeApiHeaderWarnings(createMockRes( { warnings: ['erg'] }),
123
+ mocks.dispatch,
124
+ mocks.rootGetter,
125
+ 'get',
126
+ true,
127
+ );
128
+
129
+ expect(mocks.dispatch).not.toHaveBeenCalled();
130
+ expect(mocks.consoleWarn).not.toHaveBeenCalled();
131
+ expect(mocks.consoleDebug).not.toHaveBeenCalled();
132
+ });
133
+
134
+ it('blocked (via default config)', () => {
135
+ const mocks = setupMocks();
136
+
137
+ handleKubeApiHeaderWarnings(
138
+ createMockRes({ warning: validation }),
139
+ mocks.dispatch,
140
+ mocks.rootGetter,
141
+ 'put',
142
+ true
143
+ );
144
+
145
+ expect(mocks.dispatch).not.toHaveBeenCalled();
146
+ expect(mocks.consoleWarn).not.toHaveBeenCalled();
147
+ expect(mocks.consoleDebug).toHaveBeenCalledWith(createConsoleResponse(validation));
148
+ });
149
+
150
+ it('blocked (via custom config)', () => {
151
+ const mocks = setupMocks({
152
+ separator: '299 - ',
153
+ notificationBlockList: [deprecated]
154
+ });
155
+
156
+ handleKubeApiHeaderWarnings(
157
+ createMockRes({ warning: deprecated }),
158
+ mocks.dispatch,
159
+ mocks.rootGetter,
160
+ 'put',
161
+ true
162
+ );
163
+
164
+ expect(mocks.dispatch).not.toHaveBeenCalled();
165
+ expect(mocks.consoleWarn).not.toHaveBeenCalled();
166
+ expect(mocks.consoleDebug).toHaveBeenCalledWith(createConsoleResponse(deprecated));
167
+ });
168
+
169
+ it('multi blocked (via custom config)', () => {
170
+ const mocks = setupMocks({
171
+ separator: '299 - ',
172
+ notificationBlockList: [deprecated, podSecurity]
173
+ });
174
+
175
+ handleKubeApiHeaderWarnings(
176
+ createMockRes({ warning: `${ deprecated },${ podSecurity }` }),
177
+ mocks.dispatch,
178
+ mocks.rootGetter,
179
+ 'put',
180
+ true
181
+ );
182
+
183
+ expect(mocks.dispatch).not.toHaveBeenCalled();
184
+ expect(mocks.consoleWarn).not.toHaveBeenCalled();
185
+ expect(mocks.consoleDebug).toHaveBeenCalledWith(createConsoleResponse(`${ deprecated }\n${ podSecurity }`));
186
+ });
187
+ });
188
+
189
+ describe('warnings', () => {
190
+ it('deprecated', () => {
191
+ const mocks = setupMocks();
192
+
193
+ handleKubeApiHeaderWarnings(
194
+ createMockRes({ warning: deprecated }),
195
+ mocks.dispatch,
196
+ mocks.rootGetter,
197
+ 'put',
198
+ true,
199
+ );
200
+
201
+ expect(mocks.dispatch).toHaveBeenCalledWith(...createGrowlResponse(deprecated));
202
+ expect(mocks.consoleWarn).not.toHaveBeenCalled();
203
+ expect(mocks.consoleDebug).toHaveBeenCalledWith(createConsoleResponse(deprecated));
204
+ });
205
+
206
+ it('pod security', () => {
207
+ const mocks = setupMocks();
208
+
209
+ handleKubeApiHeaderWarnings(
210
+ createMockRes({ warning: podSecurity }),
211
+ mocks.dispatch,
212
+ mocks.rootGetter,
213
+ 'post',
214
+ true,
215
+ );
216
+
217
+ expect(mocks.dispatch).toHaveBeenCalledWith(...createGrowlResponse(podSecurity, createKey));
218
+ expect(mocks.consoleWarn).not.toHaveBeenCalled();
219
+ expect(mocks.consoleDebug).toHaveBeenCalledWith(createConsoleResponse(podSecurity));
220
+ });
221
+
222
+ it('deprecated & pod security', () => {
223
+ const mocks = setupMocks();
224
+
225
+ handleKubeApiHeaderWarnings(
226
+ createMockRes({ warning: `${ deprecated },${ podSecurity }` }),
227
+ mocks.dispatch,
228
+ mocks.rootGetter,
229
+ 'put',
230
+ true,
231
+ );
232
+
233
+ expect(mocks.dispatch).toHaveBeenCalledWith(...createGrowlResponse(`${ deprecated }, ${ podSecurity }` ));
234
+ expect(mocks.consoleWarn).not.toHaveBeenCalled();
235
+ expect(mocks.consoleDebug).toHaveBeenCalledWith(createConsoleResponse(`${ deprecated }\n${ podSecurity }`));
236
+ });
237
+ });
238
+ });
@@ -8,6 +8,7 @@ import isObject from 'lodash/isObject';
8
8
  import { classify } from '@shell/plugins/dashboard-store/classify';
9
9
  import { NAMESPACE } from '@shell/config/types';
10
10
  import jsyaml from 'js-yaml';
11
+ import { handleKubeApiHeaderWarnings } from '@shell/plugins/steve/header-warnings';
11
12
 
12
13
  export default {
13
14
 
@@ -85,7 +86,7 @@ export default {
85
86
 
86
87
  while (true) {
87
88
  try {
88
- const out = await makeRequest(this, opt);
89
+ const out = await makeRequest(this, opt, rootGetters);
89
90
 
90
91
  if (!opt.depaginate) {
91
92
  return out;
@@ -116,7 +117,7 @@ export default {
116
117
  }
117
118
  }
118
119
 
119
- function makeRequest(that, opt) {
120
+ function makeRequest(that, opt, rootGetters) {
120
121
  return that.$axios(opt).then((res) => {
121
122
  let out;
122
123
 
@@ -128,9 +129,7 @@ export default {
128
129
 
129
130
  finishDeferred(key, 'resolve', out);
130
131
 
131
- if (opt.method === 'post' || opt.method === 'put') {
132
- handleValidationWarnings(res);
133
- }
132
+ handleKubeApiHeaderWarnings(res, dispatch, rootGetters, opt.method);
134
133
 
135
134
  return out;
136
135
  });
@@ -196,24 +195,6 @@ export default {
196
195
 
197
196
  return Promise.reject(out);
198
197
  }
199
-
200
- function handleValidationWarnings(res) {
201
- const warnings = (res.headers?.warning || '').split(',');
202
-
203
- if (!warnings.length || !warnings[0]) {
204
- return;
205
- }
206
-
207
- const message = warnings.reduce((message, warning) => {
208
- return `${ message }\n${ warning.trim() }`;
209
- }, `Validation Warnings for ${ opt.url }\n`);
210
-
211
- if (process.env.dev) {
212
- console.warn(`${ message }\n\n`, res.data); // eslint-disable-line no-console
213
- } else {
214
- console.debug(message); // eslint-disable-line no-console
215
- }
216
- }
217
198
  },
218
199
 
219
200
  promptMove({ commit, state }, resources) {
@@ -0,0 +1,91 @@
1
+ import { PerfSettingsWarningHeaders } from '@shell/config/settings';
2
+ import { getPerformanceSetting } from '@shell/utils/settings';
3
+
4
+ interface HttpResponse {
5
+ headers?: { [key: string]: string},
6
+ data?: any,
7
+ config: {
8
+ url: string,
9
+ }
10
+ }
11
+
12
+ /**
13
+ * Cache the kube api warning header settings that will determine if they are growled or not
14
+ */
15
+ let warningHeaderSettings: PerfSettingsWarningHeaders;
16
+
17
+ /**
18
+ * Extract sanitised warnings from the warnings header string
19
+ */
20
+ function kubeApiHeaderWarnings(allWarnings: string): string[] {
21
+ // Find each warning.
22
+ // Each warning is separated by `,`... however... this can appear within the warning itself so can't `split` on it
23
+ // Instead provide a configurable way to split (default 299 - )
24
+ const warnings = allWarnings.split(warningHeaderSettings.separator) || [];
25
+
26
+ // Trim and remove effects of split
27
+ return warnings.reduce((res, warning) => {
28
+ const trimmedWarning = warning.trim();
29
+
30
+ if (!trimmedWarning) {
31
+ return res;
32
+ }
33
+
34
+ const fixedWarning = trimmedWarning.endsWith(',') ? trimmedWarning.slice(0, -1) : trimmedWarning;
35
+
36
+ // Why add the separator again? It's almost certainly `299 - ` which is important info to include
37
+ res.push(warningHeaderSettings.separator + fixedWarning);
38
+
39
+ return res;
40
+ }, [] as string[]);
41
+ }
42
+
43
+ /**
44
+ * Take action given the `warnings` in the response header of a kube api request
45
+ */
46
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
47
+ export function handleKubeApiHeaderWarnings(res: HttpResponse, dispatch: any, rootGetters: any, method: string, refreshCache = false): void {
48
+ const safeMethod = method?.toLowerCase(); // Some requests have this as uppercase
49
+
50
+ // Exit early if there's no warnings
51
+ if ((safeMethod !== 'post' && safeMethod !== 'put') || !res.headers?.warning) {
52
+ return;
53
+ }
54
+
55
+ // Grab the required settings
56
+ if (!warningHeaderSettings || refreshCache) {
57
+ const settings = getPerformanceSetting(rootGetters);
58
+
59
+ // Cache this, we don't need to react to changes within the same session
60
+ warningHeaderSettings = settings?.kubeAPI.warningHeader;
61
+ }
62
+
63
+ // Determine each warning
64
+ const sanitisedWarnings = kubeApiHeaderWarnings(res.headers?.warning);
65
+
66
+ if (!sanitisedWarnings.length) {
67
+ return;
68
+ }
69
+
70
+ // Shows warnings as growls
71
+ const growlWarnings = sanitisedWarnings.filter((w) => !warningHeaderSettings.notificationBlockList.find((blocked) => w.startsWith(blocked)));
72
+
73
+ if (growlWarnings.length) {
74
+ const resourceType = res.data?.type || res.data?.kind || rootGetters['i18n/t']('generic.resource', { count: 1 });
75
+
76
+ dispatch('growl/warning', {
77
+ title: method === 'put' ? rootGetters['i18n/t']('growl.kubeApiHeaderWarning.titleUpdate', { resourceType }) : rootGetters['i18n/t']('growl.kubeApiHeaderWarning.titleCreate', { resourceType }),
78
+ message: growlWarnings.join(', '),
79
+ timeout: 0,
80
+ }, { root: true });
81
+ }
82
+
83
+ // Print warnings to console
84
+ const message = `Validation Warnings for ${ res.config.url }\n\n${ sanitisedWarnings.join('\n') }`;
85
+
86
+ if (process.env.dev) {
87
+ console.warn(`${ message }\n\n`, res.data); // eslint-disable-line no-console
88
+ } else {
89
+ console.debug(message); // eslint-disable-line no-console
90
+ }
91
+ }
@@ -71,9 +71,12 @@ export default {
71
71
  names() {
72
72
  return this.filteredNamespaces.map((obj) => obj.nameDisplay).slice(0, 5);
73
73
  },
74
- // Only admins and cluster owners can see namespaces outside of projects
75
- canSeeProjectlessNamespaces() {
76
- return this.currentCluster.canUpdate;
74
+
75
+ canManageNamespaces() {
76
+ // Only admins and cluster owners can see namespaces outside of projects
77
+ // BUT cluster members can also manage projects and namespaces and may want to not delete the namespaces associated with the project
78
+ // as per https://github.com/rancher/dashboard/issues/9517 despite the namespaces cannot be seen afterwards (projectless)
79
+ return this.currentCluster.canUpdate || (this.currentProject.canDelete && this.filteredNamespaces.length && this.filteredNamespaces[0]?.canDelete);
77
80
  }
78
81
  },
79
82
  methods: {
@@ -81,7 +84,7 @@ export default {
81
84
  remove() {
82
85
  // Delete all of thre namespaces and return false - this tells the prompt remove dialog to continue and delete the project
83
86
  // Delete all namespaces if the user wouldn't be able to see them after deleting the project
84
- if (this.deleteProjectNamespaces || !this.canSeeProjectlessNamespaces) {
87
+ if (this.deleteProjectNamespaces || !this.canManageNamespaces) {
85
88
  return Promise.all(this.filteredNamespaces.map((n) => n.remove())).then(() => false);
86
89
  }
87
90
 
@@ -97,7 +100,7 @@ export default {
97
100
  <div>
98
101
  <div class="mb-10">
99
102
  {{ t('promptRemove.attemptingToRemove', { type }) }} <span class="display-name">{{ `${displayName}.` }}</span>
100
- <template v-if="!canSeeProjectlessNamespaces">
103
+ <template v-if="!canManageNamespaces">
101
104
  <span class="delete-warning"> {{ t('promptRemove.willDeleteAssociatedNamespaces') }}</span> <br>
102
105
  <div
103
106
  v-clean-html="resourceNames(names, plusMore, t)"
@@ -106,7 +109,7 @@ export default {
106
109
  </template>
107
110
  </div>
108
111
  <div
109
- v-if="filteredNamespaces.length > 0 && canSeeProjectlessNamespaces"
112
+ v-if="filteredNamespaces.length > 0 && canManageNamespaces"
110
113
  class="mt-20 remove-project-dialog"
111
114
  >
112
115
  <Checkbox