@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.
- package/assets/translations/en-us.yaml +8 -2
- package/assets/translations/zh-hans.yaml +8 -1
- package/cloud-credential/__tests__/azure.test.ts +53 -0
- package/cloud-credential/azure.vue +6 -0
- package/components/GrowlManager.vue +33 -30
- package/components/Questions/Array.vue +2 -2
- package/components/Questions/Boolean.vue +7 -1
- package/components/Questions/CloudCredential.vue +1 -0
- package/components/Questions/Enum.vue +21 -2
- package/components/Questions/Float.vue +8 -3
- package/components/Questions/Int.vue +8 -3
- package/components/Questions/Question.js +72 -0
- package/components/Questions/QuestionMap.vue +2 -1
- package/components/Questions/Radio.vue +33 -0
- package/components/Questions/Reference.vue +2 -0
- package/components/Questions/String.vue +8 -3
- package/components/Questions/Yaml.vue +46 -0
- package/components/Questions/__tests__/Boolean.test.ts +123 -0
- package/components/Questions/__tests__/Float.test.ts +123 -0
- package/components/Questions/__tests__/Int.test.ts +123 -0
- package/components/Questions/__tests__/String.test.ts +123 -0
- package/components/Questions/__tests__/Yaml.test.ts +123 -0
- package/components/Questions/index.vue +8 -1
- package/components/ResourceTable.vue +6 -12
- package/components/SideNav.vue +634 -0
- package/components/__tests__/NamespaceFilter.test.ts +3 -4
- package/components/form/ResourceQuota/ProjectRow.vue +38 -15
- package/components/form/UnitInput.vue +1 -0
- package/components/form/__tests__/KeyValue.test.ts +2 -1
- package/components/form/__tests__/UnitInput.test.ts +2 -2
- package/components/formatter/ClusterProvider.vue +9 -3
- package/components/formatter/LinkName.vue +12 -1
- package/components/formatter/__tests__/ClusterProvider.test.ts +5 -1
- package/components/nav/Header.vue +1 -0
- package/components/nav/WorkspaceSwitcher.vue +4 -1
- package/config/settings.ts +59 -2
- package/config/types.js +2 -0
- package/core/plugin-helpers.js +4 -1
- package/core/types.ts +1 -0
- package/creators/pkg/files/.github/workflows/build-extension-catalog.yml +28 -0
- package/creators/pkg/files/.github/workflows/build-extension-charts.yml +26 -0
- package/creators/pkg/init +63 -4
- package/detail/provisioning.cattle.io.cluster.vue +4 -2
- package/edit/fleet.cattle.io.gitrepo.vue +8 -0
- package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -4
- package/layouts/default.vue +11 -597
- package/middleware/authenticated.js +2 -14
- package/mixins/__tests__/chart.test.ts +40 -0
- package/mixins/chart.js +5 -0
- package/models/catalog.cattle.io.clusterrepo.js +6 -2
- package/models/fleet.cattle.io.cluster.js +10 -2
- package/models/fleet.cattle.io.gitrepo.js +3 -1
- package/package.json +1 -1
- package/pages/c/_cluster/fleet/index.vue +4 -0
- package/pages/c/_cluster/gatekeeper/index.vue +10 -1
- package/pages/c/_cluster/uiplugins/index.vue +3 -3
- package/plugins/steve/__tests__/header-warnings.spec.ts +238 -0
- package/plugins/steve/actions.js +4 -23
- package/plugins/steve/header-warnings.ts +91 -0
- package/promptRemove/management.cattle.io.project.vue +9 -6
- package/rancher-components/components/Form/LabeledInput/LabeledInput.vue +8 -0
- package/rancher-components/components/Form/Radio/RadioButton.test.ts +7 -3
- package/scripts/extension/parse-tag-name +30 -0
- package/types/shell/index.d.ts +3 -0
- package/utils/auth.js +17 -0
- package/utils/object.js +0 -1
- package/utils/settings.ts +2 -17
- package/utils/validators/__tests__/cidr.test.ts +33 -0
- package/utils/validators/cidr.js +5 -0
- package/vue-config-helper.js +135 -0
- package/vue.config.js +23 -139
- package/yarn-error.log +200 -0
- package/creators/pkg/files/.github/workflows/build-container.yml +0 -64
- 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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
@@ -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: {
|
|
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.
|
|
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
|
+
});
|
package/plugins/steve/actions.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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.
|
|
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="!
|
|
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 &&
|
|
112
|
+
v-if="filteredNamespaces.length > 0 && canManageNamespaces"
|
|
110
113
|
class="mt-20 remove-project-dialog"
|
|
111
114
|
>
|
|
112
115
|
<Checkbox
|