@rancher/shell 3.0.8 → 3.0.9-rc.2
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/apis/intf/modal.ts +38 -0
- package/apis/intf/slide-in.ts +3 -1
- package/apis/shell/__tests__/slide-in.test.ts +36 -0
- package/apis/shell/slide-in.ts +5 -1
- package/assets/styles/base/_color.scss +1 -0
- package/assets/styles/base/_typography.scss +14 -5
- package/assets/styles/themes/_light.scss +1 -1
- package/assets/styles/themes/_modern.scss +1 -1
- package/assets/translations/en-us.yaml +94 -33
- package/assets/translations/zh-hans.yaml +0 -2
- package/components/ActionMenuShell.vue +4 -4
- package/components/CodeMirror.vue +4 -3
- package/components/DetailText.vue +54 -7
- package/components/Drawer/Chrome.vue +11 -4
- package/components/Drawer/DrawerCard.vue +19 -0
- package/components/Drawer/ResourceDetailDrawer/ConfigTab.vue +3 -11
- package/components/Drawer/ResourceDetailDrawer/__tests__/ConfigTab.test.ts +2 -2
- package/components/Drawer/ResourceDetailDrawer/index.vue +3 -20
- package/components/Drawer/types.ts +1 -0
- package/components/DynamicContent/DynamicContentCloseButton.vue +2 -2
- package/components/LocaleSelector.vue +1 -1
- package/components/Markdown.vue +1 -1
- package/components/PopoverCard.vue +3 -3
- package/components/Resource/Detail/Card/ExtrasCard.vue +39 -0
- package/components/Resource/Detail/Card/StateCard/__tests__/composables.test.ts +142 -0
- package/components/Resource/Detail/Card/StateCard/composables.ts +41 -11
- package/components/Resource/Detail/Card/StateCard/index.vue +3 -9
- package/components/Resource/Detail/Card/StateCard/types.ts +6 -0
- package/components/Resource/Detail/Card/{PodsCard → StatusCard}/index.vue +11 -10
- package/components/Resource/Detail/Card/__tests__/PodsCard.test.ts +24 -25
- package/components/Resource/Detail/Cards.vue +27 -0
- package/components/Resource/Detail/Masthead/__tests__/index.test.ts +70 -0
- package/components/Resource/Detail/Masthead/index.vue +5 -0
- package/components/Resource/Detail/Metadata/KeyValueRow.vue +4 -2
- package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +2 -2
- package/components/Resource/Detail/ResourceRow.types.ts +14 -0
- package/components/Resource/Detail/ResourceRow.vue +23 -35
- package/components/Resource/Detail/StatusRow.vue +5 -2
- package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +38 -7
- package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +106 -2
- package/components/Resource/Detail/TitleBar/composables.ts +2 -1
- package/components/Resource/Detail/TitleBar/index.vue +41 -6
- package/components/ResourceDetail/Masthead/__tests__/index.test.ts +49 -1
- package/components/ResourceDetail/Masthead/__tests__/latest.test.ts +85 -0
- package/components/ResourceDetail/Masthead/index.vue +1 -0
- package/components/ResourceDetail/Masthead/latest.vue +8 -1
- package/components/ResourceDetail/Masthead/legacy.vue +1 -1
- package/components/Setting.vue +1 -1
- package/components/SortableTable/index.vue +25 -0
- package/components/SortableTable/selection.js +25 -12
- package/components/SortableTable/sorting.js +1 -1
- package/components/Tabbed/Tab.vue +1 -0
- package/components/Tabbed/index.vue +29 -6
- package/components/Window/ContainerShell.vue +10 -13
- package/components/fleet/FleetClusterTargets/TargetsList.vue +47 -29
- package/components/fleet/FleetClusterTargets/index.vue +82 -29
- package/components/fleet/FleetClusters.vue +26 -12
- package/components/fleet/FleetGitRepoPaths.vue +2 -2
- package/components/fleet/FleetResources.vue +14 -0
- package/components/fleet/FleetValuesFrom.vue +2 -2
- package/components/fleet/__tests__/FleetClusterTargets.test.ts +531 -0
- package/components/fleet/__tests__/FleetClusters.test.ts +576 -0
- package/components/fleet/dashboard/ResourceDetails.vue +96 -123
- package/components/form/Conditions.vue +1 -15
- package/components/form/HookOption.vue +5 -0
- package/components/form/LabeledSelect.vue +1 -1
- package/components/form/LifecycleHooks.vue +2 -6
- package/components/form/ResourceLabeledSelect.vue +12 -1
- package/components/form/SeccompProfile.vue +113 -0
- package/components/form/Security.vue +244 -133
- package/components/form/__tests__/LabeledSelect.test.ts +1 -1
- package/components/form/__tests__/SeccompProfile.test.js +124 -0
- package/components/form/__tests__/Security.test.ts +125 -37
- package/components/formatter/Autoscaler.vue +2 -2
- package/components/formatter/FleetSummaryGraph.vue +4 -1
- package/components/nav/Group.vue +5 -0
- package/components/nav/Header.vue +3 -3
- package/components/nav/HeaderPageActionMenu.vue +1 -1
- package/components/nav/NamespaceFilter.vue +6 -6
- package/components/nav/NotificationCenter/index.vue +1 -1
- package/components/nav/TopLevelMenu.helper.ts +41 -16
- package/components/nav/TopLevelMenu.vue +45 -25
- package/components/nav/WorkspaceSwitcher.vue +1 -1
- package/components/nav/__tests__/TopLevelMenu.helper.test.ts +277 -0
- package/components/nav/__tests__/TopLevelMenu.test.ts +160 -4
- package/components/templates/default.vue +0 -3
- package/components/templates/home.vue +0 -3
- package/components/templates/plain.vue +0 -3
- package/composables/useClickOutside.ts +1 -1
- package/config/product/explorer.js +1 -2
- package/config/types.js +41 -8
- package/detail/__tests__/workload.test.ts +8 -16
- package/detail/catalog.cattle.io.app.vue +6 -0
- package/detail/fleet.cattle.io.cluster.vue +6 -0
- package/detail/workload/index.vue +7 -109
- package/edit/__tests__/projectsecret.test.ts +42 -0
- package/edit/auth/__tests__/oidc.test.ts +50 -0
- package/edit/auth/oidc.vue +68 -44
- package/edit/autoscaling.horizontalpodautoscaler/index.vue +140 -59
- package/edit/autoscaling.horizontalpodautoscaler/metrics-row.vue +41 -5
- package/edit/projectsecret.vue +29 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/Basics.test.ts +89 -200
- package/edit/provisioning.cattle.io.cluster/__tests__/Networking.test.ts +58 -17
- package/edit/provisioning.cattle.io.cluster/rke2.vue +11 -0
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +3 -63
- package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +82 -14
- package/edit/workload/__tests__/index.test.ts +122 -85
- package/edit/workload/index.vue +48 -29
- package/edit/workload/mixins/workload.js +85 -32
- package/list/catalog.cattle.io.clusterrepo.vue +1 -1
- package/list/projectsecret.vue +2 -2
- package/machine-config/__tests__/vmwarevsphere.test.ts +64 -0
- package/machine-config/amazonec2.vue +2 -2
- package/machine-config/vmwarevsphere.vue +58 -4
- package/mixins/__tests__/brand.spec.ts +18 -13
- package/mixins/__tests__/chart.test.ts +63 -0
- package/mixins/chart.js +56 -51
- package/models/__tests__/catalog.cattle.io.app.test.ts +33 -0
- package/models/__tests__/workload.test.ts +333 -0
- package/models/catalog.cattle.io.app.js +8 -0
- package/models/pod.js +14 -0
- package/models/secret.js +1 -1
- package/models/workload.js +93 -27
- package/package.json +4 -4
- package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +91 -0
- package/pages/c/_cluster/apps/charts/install.vue +4 -4
- package/pages/c/_cluster/explorer/EventsTable.vue +2 -2
- package/pages/c/_cluster/fleet/index.vue +18 -12
- package/pages/c/_cluster/manager/hostedprovider/index.vue +1 -19
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -1
- package/pages/c/_cluster/uiplugins/index.vue +1 -1
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +234 -0
- package/plugins/dashboard-store/actions.js +9 -8
- package/plugins/dashboard-store/resource-class.js +97 -1
- package/plugins/steve/__tests__/revision.test.ts +84 -0
- package/plugins/steve/__tests__/steve-pagination-utils.test.ts +30 -0
- package/plugins/steve/__tests__/subscribe.spec.ts +134 -0
- package/plugins/steve/mutations.js +9 -0
- package/plugins/steve/revision.ts +26 -0
- package/plugins/steve/steve-pagination-utils.ts +6 -5
- package/plugins/steve/subscribe.js +211 -51
- package/plugins/subscribe-events.ts +2 -2
- package/rancher-components/Form/Checkbox/Checkbox.vue +13 -0
- package/rancher-components/LabeledTooltip/LabeledTooltip.vue +1 -1
- package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +1 -1
- package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +3 -1
- package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +3 -1
- package/rancher-components/Pill/RcTag/RcTag.vue +1 -1
- package/rancher-components/Pill/index.ts +4 -0
- package/rancher-components/RcButton/RcButton.test.ts +53 -9
- package/rancher-components/RcButton/RcButton.vue +217 -25
- package/rancher-components/RcButton/types.ts +27 -1
- package/rancher-components/RcDropdown/RcDropdownMenu.vue +4 -4
- package/rancher-components/RcDropdown/types.ts +3 -3
- package/rancher-components/RcIcon/RcIcon.test.ts +42 -0
- package/rancher-components/RcIcon/RcIcon.vue +9 -6
- package/rancher-components/RcIcon/types.ts +13 -9
- package/rancher-components/utils/status.test.ts +10 -15
- package/rancher-components/utils/status.ts +5 -6
- package/store/aws.js +18 -12
- package/store/index.js +4 -8
- package/store/type-map.utils.ts +1 -1
- package/types/kube/kube-api.ts +29 -3
- package/types/rancher/steve.api.ts +40 -0
- package/types/shell/index.d.ts +99 -0
- package/types/store/dashboard-store.types.ts +29 -7
- package/types/store/pagination.types.ts +1 -0
- package/types/store/subscribe-events.types.ts +1 -0
- package/utils/__tests__/azure.test.ts +56 -0
- package/utils/__tests__/back-off.test.ts +364 -245
- package/utils/__tests__/error.test.ts +44 -0
- package/utils/__tests__/fleet.test.ts +8 -1
- package/utils/__tests__/pagination-wrapper.test.ts +167 -0
- package/utils/__tests__/version.test.ts +55 -1
- package/utils/azure.js +12 -0
- package/utils/back-off.ts +302 -69
- package/utils/cspAdaptor.ts +32 -14
- package/utils/dynamic-content/__tests__/index.test.ts +1 -1
- package/utils/dynamic-content/__tests__/new-release.test.ts +48 -7
- package/utils/dynamic-content/__tests__/support-notice.test.ts +1 -4
- package/utils/dynamic-content/index.ts +1 -6
- package/utils/dynamic-content/new-release.ts +5 -3
- package/utils/dynamic-content/types.d.ts +0 -1
- package/utils/error.js +9 -0
- package/utils/fleet.ts +2 -2
- package/utils/inactivity.ts +2 -3
- package/utils/pagination-wrapper.ts +101 -17
- package/utils/validators/formRules/index.ts +3 -0
- package/utils/version.js +38 -0
- package/components/auth/AzureWarning.vue +0 -77
- /package/components/Resource/Detail/{Card/PodsCard/Bubble.vue → Bubble.vue} +0 -0
- /package/components/Resource/Detail/Card/{PodsCard → StatusCard}/composable.ts +0 -0
package/mixins/chart.js
CHANGED
|
@@ -303,57 +303,6 @@ export default {
|
|
|
303
303
|
|
|
304
304
|
this.fetchStoreChart();
|
|
305
305
|
|
|
306
|
-
if ( this.query.appNamespace && this.query.appName ) {
|
|
307
|
-
// First check the URL query for an app name and namespace.
|
|
308
|
-
// Use those values to check for a catalog app resource.
|
|
309
|
-
// If found, set the form to edit mode. If not, set the
|
|
310
|
-
// form to create mode.
|
|
311
|
-
|
|
312
|
-
try {
|
|
313
|
-
this.existing = await this.$store.dispatch('cluster/find', {
|
|
314
|
-
type: CATALOG.APP,
|
|
315
|
-
id: `${ this.query.appNamespace }/${ this.query.appName }`,
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
await this.existing?.fetchValues(true);
|
|
319
|
-
|
|
320
|
-
this.mode = _EDIT;
|
|
321
|
-
} catch (e) {
|
|
322
|
-
this.mode = _CREATE;
|
|
323
|
-
this.existing = null;
|
|
324
|
-
}
|
|
325
|
-
} else if ( this.chart?.targetNamespace && this.chart?.targetName ) {
|
|
326
|
-
// If the app name and namespace values are not provided in the
|
|
327
|
-
// query, fall back on target values defined in the Helm chart itself.
|
|
328
|
-
|
|
329
|
-
// Ask to install a special chart with fixed namespace/name
|
|
330
|
-
// or edit it if there's an existing install.
|
|
331
|
-
|
|
332
|
-
try {
|
|
333
|
-
this.existing = await this.$store.dispatch('cluster/find', {
|
|
334
|
-
type: CATALOG.APP,
|
|
335
|
-
id: `${ this.chart.targetNamespace }/${ this.chart.targetName }`,
|
|
336
|
-
});
|
|
337
|
-
this.mode = _EDIT;
|
|
338
|
-
} catch (e) {
|
|
339
|
-
this.mode = _CREATE;
|
|
340
|
-
this.existing = null;
|
|
341
|
-
}
|
|
342
|
-
} else if (this.chart) {
|
|
343
|
-
const matching = this.chart.matchingInstalledApps;
|
|
344
|
-
|
|
345
|
-
if (matching.length === 1) {
|
|
346
|
-
this.existing = matching[0];
|
|
347
|
-
this.mode = _EDIT;
|
|
348
|
-
} else {
|
|
349
|
-
this.mode = _CREATE;
|
|
350
|
-
}
|
|
351
|
-
} else {
|
|
352
|
-
// Regular create
|
|
353
|
-
|
|
354
|
-
this.mode = _CREATE;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
306
|
if ( !this.chart ) {
|
|
358
307
|
return;
|
|
359
308
|
}
|
|
@@ -418,6 +367,62 @@ export default {
|
|
|
418
367
|
|
|
419
368
|
console.error('Unable to fetch VersionInfo: ', e); // eslint-disable-line no-console
|
|
420
369
|
}
|
|
370
|
+
|
|
371
|
+
if ( this.query.appNamespace && this.query.appName ) {
|
|
372
|
+
// First check the URL query for an app name and namespace.
|
|
373
|
+
// Use those values to check for a catalog app resource.
|
|
374
|
+
// If found, set the form to edit mode. If not, set the
|
|
375
|
+
// form to create mode.
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
this.existing = await this.$store.dispatch('cluster/find', {
|
|
379
|
+
type: CATALOG.APP,
|
|
380
|
+
id: `${ this.query.appNamespace }/${ this.query.appName }`,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
await this.existing?.fetchValues(true);
|
|
384
|
+
|
|
385
|
+
this.mode = _EDIT;
|
|
386
|
+
} catch (e) {
|
|
387
|
+
this.mode = _CREATE;
|
|
388
|
+
this.existing = null;
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
const targetNamespace = this.version?.annotations?.[CATALOG_ANNOTATIONS.NAMESPACE];
|
|
392
|
+
const targetName = this.version?.annotations?.[CATALOG_ANNOTATIONS.RELEASE_NAME];
|
|
393
|
+
|
|
394
|
+
if ( targetNamespace && targetName ) {
|
|
395
|
+
// If the app name and namespace values are not provided in the
|
|
396
|
+
// query, fall back on target values defined in the Helm chart itself.
|
|
397
|
+
|
|
398
|
+
// Ask to install a special chart with fixed namespace/name
|
|
399
|
+
// or edit it if there's an existing install.
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
this.existing = await this.$store.dispatch('cluster/find', {
|
|
403
|
+
type: CATALOG.APP,
|
|
404
|
+
id: `${ targetNamespace }/${ targetName }`,
|
|
405
|
+
});
|
|
406
|
+
this.mode = _EDIT;
|
|
407
|
+
} catch (e) {
|
|
408
|
+
this.mode = _CREATE;
|
|
409
|
+
this.existing = null;
|
|
410
|
+
}
|
|
411
|
+
} else if (this.chart) {
|
|
412
|
+
const matching = this.chart.matchingInstalledApps;
|
|
413
|
+
|
|
414
|
+
if (matching.length === 1) {
|
|
415
|
+
this.existing = matching[0];
|
|
416
|
+
this.mode = _EDIT;
|
|
417
|
+
} else {
|
|
418
|
+
this.mode = _CREATE;
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
// Regular create
|
|
422
|
+
|
|
423
|
+
this.mode = _CREATE;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
421
426
|
}, // End of fetchChart
|
|
422
427
|
|
|
423
428
|
// Charts have an annotation that specifies any additional charts that should be installed at the same time eg CRDs
|
|
@@ -145,4 +145,37 @@ describe('class CatalogApp', () => {
|
|
|
145
145
|
expect(catalogApp.upgradeAvailable).toBe(expected);
|
|
146
146
|
});
|
|
147
147
|
});
|
|
148
|
+
|
|
149
|
+
describe('valuesLoaded', () => {
|
|
150
|
+
it('should be false if data is missing (e.g. secret)', () => {
|
|
151
|
+
const catalogApp = new CatalogApp({});
|
|
152
|
+
|
|
153
|
+
jest.spyOn(catalogApp, '_secret', 'get').mockReturnValue(null);
|
|
154
|
+
|
|
155
|
+
expect(catalogApp.valuesLoaded).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should be false if part of the data is missing', () => {
|
|
159
|
+
const catalogApp = new CatalogApp({});
|
|
160
|
+
|
|
161
|
+
jest.spyOn(catalogApp, '_secret', 'get').mockReturnValue({ data: {} });
|
|
162
|
+
|
|
163
|
+
expect(catalogApp.valuesLoaded).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should be true if all required data is present', () => {
|
|
167
|
+
const catalogApp = new CatalogApp({});
|
|
168
|
+
|
|
169
|
+
jest.spyOn(catalogApp, '_secret', 'get').mockReturnValue({
|
|
170
|
+
data: {
|
|
171
|
+
release: {
|
|
172
|
+
config: { foo: 'bar' },
|
|
173
|
+
chart: { values: { baz: 'qux' } }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(catalogApp.valuesLoaded).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
148
181
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Workload from '@shell/models/workload.js';
|
|
2
2
|
import { steveClassJunkObject } from '@shell/plugins/steve/__tests__/utils/steve-mocks';
|
|
3
|
+
import { WORKLOAD_TYPES, SERVICE } from '@shell/config/types';
|
|
3
4
|
|
|
4
5
|
describe('class: Workload', () => {
|
|
5
6
|
describe('given custom workload keys', () => {
|
|
@@ -89,4 +90,336 @@ describe('class: Workload', () => {
|
|
|
89
90
|
});
|
|
90
91
|
});
|
|
91
92
|
});
|
|
93
|
+
|
|
94
|
+
describe('method: scale', () => {
|
|
95
|
+
it('should call scaleUp when isUp is true', async() => {
|
|
96
|
+
const scaleUpMock = jest.fn().mockResolvedValue(undefined);
|
|
97
|
+
const workload = new Workload({
|
|
98
|
+
type: WORKLOAD_TYPES.DEPLOYMENT,
|
|
99
|
+
metadata: { name: 'test', namespace: 'default' },
|
|
100
|
+
spec: { replicas: 1 }
|
|
101
|
+
}, {
|
|
102
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
103
|
+
dispatch: jest.fn(),
|
|
104
|
+
rootGetters: { 'i18n/t': jest.fn() },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
workload.scaleUp = scaleUpMock;
|
|
108
|
+
|
|
109
|
+
await workload.scale(true);
|
|
110
|
+
|
|
111
|
+
expect(scaleUpMock).toHaveBeenCalledWith();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should call scaleDown when isUp is false', async() => {
|
|
115
|
+
const scaleDownMock = jest.fn().mockResolvedValue(undefined);
|
|
116
|
+
const workload = new Workload({
|
|
117
|
+
type: WORKLOAD_TYPES.DEPLOYMENT,
|
|
118
|
+
metadata: { name: 'test', namespace: 'default' },
|
|
119
|
+
spec: { replicas: 2 }
|
|
120
|
+
}, {
|
|
121
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
122
|
+
dispatch: jest.fn(),
|
|
123
|
+
rootGetters: { 'i18n/t': jest.fn() },
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
workload.scaleDown = scaleDownMock;
|
|
127
|
+
|
|
128
|
+
await workload.scale(false);
|
|
129
|
+
|
|
130
|
+
expect(scaleDownMock).toHaveBeenCalledWith();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should dispatch growl error on failure', async() => {
|
|
134
|
+
const dispatchMock = jest.fn();
|
|
135
|
+
const scaleUpMock = jest.fn().mockRejectedValue(new Error('Scale failed'));
|
|
136
|
+
const workload = new Workload({
|
|
137
|
+
type: WORKLOAD_TYPES.DEPLOYMENT,
|
|
138
|
+
metadata: { name: 'test-workload', namespace: 'default' },
|
|
139
|
+
spec: { replicas: 1 }
|
|
140
|
+
}, {
|
|
141
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
142
|
+
dispatch: dispatchMock,
|
|
143
|
+
rootGetters: { 'i18n/t': (key: string) => key },
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
workload.scaleUp = scaleUpMock;
|
|
147
|
+
workload.$store = { dispatch: dispatchMock };
|
|
148
|
+
|
|
149
|
+
await workload.scale(true);
|
|
150
|
+
|
|
151
|
+
expect(dispatchMock).toHaveBeenCalledWith(
|
|
152
|
+
'growl/fromError',
|
|
153
|
+
expect.objectContaining({
|
|
154
|
+
title: expect.stringContaining('workload.list.errorCannotScale'),
|
|
155
|
+
err: expect.any(Error)
|
|
156
|
+
}),
|
|
157
|
+
{ root: true }
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('getter: relatedServices', () => {
|
|
163
|
+
it('should return services that match workload pods', () => {
|
|
164
|
+
const mockPod = {
|
|
165
|
+
metadata: {
|
|
166
|
+
name: 'pod-1', namespace: 'default', labels: { app: 'my-app' }
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const mockService = {
|
|
170
|
+
metadata: { name: 'my-service', namespace: 'default' },
|
|
171
|
+
spec: { selector: { app: 'my-app' } }
|
|
172
|
+
};
|
|
173
|
+
const workload = new Workload({
|
|
174
|
+
type: WORKLOAD_TYPES.DEPLOYMENT,
|
|
175
|
+
metadata: { name: 'test', namespace: 'default' },
|
|
176
|
+
spec: {}
|
|
177
|
+
}, {
|
|
178
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
179
|
+
dispatch: jest.fn(),
|
|
180
|
+
rootGetters: {
|
|
181
|
+
'i18n/t': jest.fn(),
|
|
182
|
+
'cluster/all': (type: string) => (type === SERVICE ? [mockService] : [])
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Mock pods getter
|
|
187
|
+
Object.defineProperty(workload, 'pods', { get: () => [mockPod] });
|
|
188
|
+
|
|
189
|
+
const related = workload.relatedServices;
|
|
190
|
+
|
|
191
|
+
expect(related).toContain(mockService);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should not return services from different namespace', () => {
|
|
195
|
+
const mockPod = {
|
|
196
|
+
metadata: {
|
|
197
|
+
name: 'pod-1', namespace: 'default', labels: { app: 'my-app' }
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const mockService = {
|
|
201
|
+
metadata: { name: 'my-service', namespace: 'other-namespace' },
|
|
202
|
+
spec: { selector: { app: 'my-app' } }
|
|
203
|
+
};
|
|
204
|
+
const workload = new Workload({
|
|
205
|
+
type: WORKLOAD_TYPES.DEPLOYMENT,
|
|
206
|
+
metadata: { name: 'test', namespace: 'default' },
|
|
207
|
+
spec: {}
|
|
208
|
+
}, {
|
|
209
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
210
|
+
dispatch: jest.fn(),
|
|
211
|
+
rootGetters: {
|
|
212
|
+
'i18n/t': jest.fn(),
|
|
213
|
+
'cluster/all': (type: string) => (type === SERVICE ? [mockService] : [])
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
Object.defineProperty(workload, 'pods', { get: () => [mockPod] });
|
|
218
|
+
|
|
219
|
+
const related = workload.relatedServices;
|
|
220
|
+
|
|
221
|
+
expect(related).toHaveLength(0);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should not return services with non-matching selectors', () => {
|
|
225
|
+
const mockPod = {
|
|
226
|
+
metadata: {
|
|
227
|
+
name: 'pod-1', namespace: 'default', labels: { app: 'my-app' }
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const mockService = {
|
|
231
|
+
metadata: { name: 'my-service', namespace: 'default' },
|
|
232
|
+
spec: { selector: { app: 'different-app' } }
|
|
233
|
+
};
|
|
234
|
+
const workload = new Workload({
|
|
235
|
+
type: WORKLOAD_TYPES.DEPLOYMENT,
|
|
236
|
+
metadata: { name: 'test', namespace: 'default' },
|
|
237
|
+
spec: {}
|
|
238
|
+
}, {
|
|
239
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
240
|
+
dispatch: jest.fn(),
|
|
241
|
+
rootGetters: {
|
|
242
|
+
'i18n/t': jest.fn(),
|
|
243
|
+
'cluster/all': (type: string) => (type === SERVICE ? [mockService] : [])
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
Object.defineProperty(workload, 'pods', { get: () => [mockPod] });
|
|
248
|
+
|
|
249
|
+
const related = workload.relatedServices;
|
|
250
|
+
|
|
251
|
+
expect(related).toHaveLength(0);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('getter: podsCard', () => {
|
|
256
|
+
it('should return card for Deployment type', () => {
|
|
257
|
+
const workload = new Workload({
|
|
258
|
+
type: WORKLOAD_TYPES.DEPLOYMENT,
|
|
259
|
+
metadata: { name: 'test', namespace: 'default' },
|
|
260
|
+
spec: {}
|
|
261
|
+
}, {
|
|
262
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
263
|
+
dispatch: jest.fn(),
|
|
264
|
+
rootGetters: { 'i18n/t': (key: string) => key },
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
Object.defineProperty(workload, 'pods', { get: () => [] });
|
|
268
|
+
Object.defineProperty(workload, 'canUpdate', { get: () => true });
|
|
269
|
+
|
|
270
|
+
const card = workload.podsCard;
|
|
271
|
+
|
|
272
|
+
expect(card).not.toBeNull();
|
|
273
|
+
expect(card.props.title).toBe('component.resource.detail.card.podsCard.title');
|
|
274
|
+
expect(card.props.showScaling).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should return card for DaemonSet type without scaling', () => {
|
|
278
|
+
const workload = new Workload({
|
|
279
|
+
type: WORKLOAD_TYPES.DAEMON_SET,
|
|
280
|
+
metadata: { name: 'test', namespace: 'default' },
|
|
281
|
+
spec: {}
|
|
282
|
+
}, {
|
|
283
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
284
|
+
dispatch: jest.fn(),
|
|
285
|
+
rootGetters: { 'i18n/t': (key: string) => key },
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
Object.defineProperty(workload, 'pods', { get: () => [] });
|
|
289
|
+
Object.defineProperty(workload, 'canUpdate', { get: () => true });
|
|
290
|
+
|
|
291
|
+
const card = workload.podsCard;
|
|
292
|
+
|
|
293
|
+
expect(card).not.toBeNull();
|
|
294
|
+
expect(card.props.showScaling).toBe(false);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should return null for unsupported types like CronJob', () => {
|
|
298
|
+
const workload = new Workload({
|
|
299
|
+
type: WORKLOAD_TYPES.CRON_JOB,
|
|
300
|
+
metadata: { name: 'test', namespace: 'default' },
|
|
301
|
+
spec: {}
|
|
302
|
+
}, {
|
|
303
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
304
|
+
dispatch: jest.fn(),
|
|
305
|
+
rootGetters: { 'i18n/t': (key: string) => key },
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const card = workload.podsCard;
|
|
309
|
+
|
|
310
|
+
expect(card).toBeNull();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should hide scaling when canUpdate is false', () => {
|
|
314
|
+
const workload = new Workload({
|
|
315
|
+
type: WORKLOAD_TYPES.DEPLOYMENT,
|
|
316
|
+
metadata: { name: 'test', namespace: 'default' },
|
|
317
|
+
spec: {}
|
|
318
|
+
}, {
|
|
319
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
320
|
+
dispatch: jest.fn(),
|
|
321
|
+
rootGetters: { 'i18n/t': (key: string) => key },
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
Object.defineProperty(workload, 'pods', { get: () => [] });
|
|
325
|
+
Object.defineProperty(workload, 'canUpdate', { get: () => false });
|
|
326
|
+
|
|
327
|
+
const card = workload.podsCard;
|
|
328
|
+
|
|
329
|
+
expect(card.props.showScaling).toBe(false);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe('getter: jobsCard', () => {
|
|
334
|
+
it('should return card for CronJob type', () => {
|
|
335
|
+
const workload = new Workload({
|
|
336
|
+
type: WORKLOAD_TYPES.CRON_JOB,
|
|
337
|
+
metadata: { name: 'test', namespace: 'default' },
|
|
338
|
+
spec: {}
|
|
339
|
+
}, {
|
|
340
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
341
|
+
dispatch: jest.fn(),
|
|
342
|
+
rootGetters: { 'i18n/t': (key: string) => key },
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
Object.defineProperty(workload, 'jobs', { get: () => [] });
|
|
346
|
+
|
|
347
|
+
const card = workload.jobsCard;
|
|
348
|
+
|
|
349
|
+
expect(card).not.toBeNull();
|
|
350
|
+
expect(card.props.title).toBe('component.resource.detail.card.jobsCard.title');
|
|
351
|
+
expect(card.props.showScaling).toBe(false);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('should return null for non-CronJob types', () => {
|
|
355
|
+
const workload = new Workload({
|
|
356
|
+
type: WORKLOAD_TYPES.DEPLOYMENT,
|
|
357
|
+
metadata: { name: 'test', namespace: 'default' },
|
|
358
|
+
spec: {}
|
|
359
|
+
}, {
|
|
360
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
361
|
+
dispatch: jest.fn(),
|
|
362
|
+
rootGetters: { 'i18n/t': (key: string) => key },
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const card = workload.jobsCard;
|
|
366
|
+
|
|
367
|
+
expect(card).toBeNull();
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
describe('getter: cards', () => {
|
|
372
|
+
it('should include podsCard for Deployment', () => {
|
|
373
|
+
const workload = new Workload({
|
|
374
|
+
type: WORKLOAD_TYPES.DEPLOYMENT,
|
|
375
|
+
metadata: { name: 'test', namespace: 'default' },
|
|
376
|
+
spec: {},
|
|
377
|
+
status: {}
|
|
378
|
+
}, {
|
|
379
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
380
|
+
dispatch: jest.fn(),
|
|
381
|
+
rootGetters: {
|
|
382
|
+
'i18n/t': (key: string) => key,
|
|
383
|
+
'cluster/all': () => []
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
Object.defineProperty(workload, 'pods', { get: () => [] });
|
|
388
|
+
Object.defineProperty(workload, 'canUpdate', { get: () => true });
|
|
389
|
+
|
|
390
|
+
const cards = workload.cards;
|
|
391
|
+
|
|
392
|
+
// Cards should include podsCard (not null), jobsCard (null for deployment), and _cards from parent
|
|
393
|
+
const nonNullCards = cards.filter((c: any) => c !== null);
|
|
394
|
+
|
|
395
|
+
expect(nonNullCards.length).toBeGreaterThanOrEqual(1);
|
|
396
|
+
expect(nonNullCards[0].props.title).toBe('component.resource.detail.card.podsCard.title');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('should include jobsCard for CronJob', () => {
|
|
400
|
+
const workload = new Workload({
|
|
401
|
+
type: WORKLOAD_TYPES.CRON_JOB,
|
|
402
|
+
metadata: { name: 'test', namespace: 'default' },
|
|
403
|
+
spec: {},
|
|
404
|
+
status: {}
|
|
405
|
+
}, {
|
|
406
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
407
|
+
dispatch: jest.fn(),
|
|
408
|
+
rootGetters: {
|
|
409
|
+
'i18n/t': (key: string) => key,
|
|
410
|
+
'cluster/all': () => []
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
Object.defineProperty(workload, 'jobs', { get: () => [] });
|
|
415
|
+
|
|
416
|
+
const cards = workload.cards;
|
|
417
|
+
const nonNullCards = cards.filter((c: any) => c !== null);
|
|
418
|
+
|
|
419
|
+
// Should have jobsCard and insight card from parent
|
|
420
|
+
const jobsCard = nonNullCards.find((c: any) => c.props.title === 'component.resource.detail.card.jobsCard.title');
|
|
421
|
+
|
|
422
|
+
expect(jobsCard).toBeDefined();
|
|
423
|
+
});
|
|
424
|
+
});
|
|
92
425
|
});
|
|
@@ -442,6 +442,14 @@ export default class CatalogApp extends SteveModel {
|
|
|
442
442
|
}
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Safely checks if the required data is loaded.
|
|
447
|
+
* This avoids the exceptions thrown by `values` and `chartValues` when the data (e.g. secret) is missing.
|
|
448
|
+
*/
|
|
449
|
+
get valuesLoaded() {
|
|
450
|
+
return !!this._values && !!this._chartValues;
|
|
451
|
+
}
|
|
452
|
+
|
|
445
453
|
/**
|
|
446
454
|
* The user's helm values
|
|
447
455
|
*/
|
package/models/pod.js
CHANGED
|
@@ -46,6 +46,20 @@ export default class Pod extends WorkloadService {
|
|
|
46
46
|
return this.$getters['byId'](NODE, this.spec.nodeName);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
get customValidationRules() {
|
|
50
|
+
const out = [
|
|
51
|
+
{
|
|
52
|
+
nullable: false,
|
|
53
|
+
path: 'metadata.name',
|
|
54
|
+
required: true,
|
|
55
|
+
translationKey: 'generic.name',
|
|
56
|
+
type: 'subDomain',
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
49
63
|
get _availableActions() {
|
|
50
64
|
const out = super._availableActions;
|
|
51
65
|
|
package/models/secret.js
CHANGED
|
@@ -573,7 +573,7 @@ export default class Secret extends SteveModel {
|
|
|
573
573
|
const id = this.id?.replace(/.*\//, '');
|
|
574
574
|
|
|
575
575
|
return {
|
|
576
|
-
name: `c-cluster-product
|
|
576
|
+
name: `c-cluster-product-resource-namespace-id`,
|
|
577
577
|
params: {
|
|
578
578
|
product: this.$rootGetters['productId'],
|
|
579
579
|
cluster: this.$rootGetters['clusterId'],
|
package/models/workload.js
CHANGED
|
@@ -3,10 +3,12 @@ import { CATTLE_PUBLIC_ENDPOINTS } from '@shell/config/labels-annotations';
|
|
|
3
3
|
import { WORKLOAD_TYPES, SERVICE, POD } from '@shell/config/types';
|
|
4
4
|
import { set } from '@shell/utils/object';
|
|
5
5
|
import day from 'dayjs';
|
|
6
|
-
import { convertSelectorObj, parse } from '@shell/utils/selector';
|
|
6
|
+
import { convertSelectorObj, parse, matches } from '@shell/utils/selector';
|
|
7
7
|
import { SEPARATOR } from '@shell/config/workload';
|
|
8
8
|
import WorkloadService from '@shell/models/workload.service';
|
|
9
9
|
import { matching } from '@shell/utils/selector-typed';
|
|
10
|
+
import { defineAsyncComponent, markRaw } from 'vue';
|
|
11
|
+
import { useResourceCardRow } from '@shell/components/Resource/Detail/Card/StateCard/composables';
|
|
10
12
|
|
|
11
13
|
export const defaultContainer = {
|
|
12
14
|
imagePullPolicy: 'Always',
|
|
@@ -179,6 +181,22 @@ export default class Workload extends WorkloadService {
|
|
|
179
181
|
await this.save();
|
|
180
182
|
}
|
|
181
183
|
|
|
184
|
+
async scale(isUp) {
|
|
185
|
+
try {
|
|
186
|
+
if (isUp) {
|
|
187
|
+
await this.scaleUp();
|
|
188
|
+
} else {
|
|
189
|
+
await this.scaleDown();
|
|
190
|
+
}
|
|
191
|
+
} catch (err) {
|
|
192
|
+
this.$store.dispatch('growl/fromError', {
|
|
193
|
+
title: this.t('workload.list.errorCannotScale', { direction: isUp ? 'up' : 'down', workloadName: this.name }),
|
|
194
|
+
err
|
|
195
|
+
},
|
|
196
|
+
{ root: true });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
182
200
|
get state() {
|
|
183
201
|
if ( this.spec?.paused === true ) {
|
|
184
202
|
return 'paused';
|
|
@@ -665,32 +683,6 @@ export default class Workload extends WorkloadService {
|
|
|
665
683
|
}).filter((x) => !!x);
|
|
666
684
|
}
|
|
667
685
|
|
|
668
|
-
get jobGauges() {
|
|
669
|
-
const out = {
|
|
670
|
-
succeeded: { color: 'success', count: 0 }, running: { color: 'info', count: 0 }, failed: { color: 'error', count: 0 }
|
|
671
|
-
};
|
|
672
|
-
|
|
673
|
-
if (this.type === WORKLOAD_TYPES.CRON_JOB) {
|
|
674
|
-
this.jobs.forEach((job) => {
|
|
675
|
-
const { status = {} } = job;
|
|
676
|
-
|
|
677
|
-
out.running.count += status.active || 0;
|
|
678
|
-
out.succeeded.count += status.succeeded || 0;
|
|
679
|
-
out.failed.count += status.failed || 0;
|
|
680
|
-
});
|
|
681
|
-
} else if (this.type === WORKLOAD_TYPES.JOB) {
|
|
682
|
-
const { status = {} } = this;
|
|
683
|
-
|
|
684
|
-
out.running.count = status.active || 0;
|
|
685
|
-
out.succeeded.count = status.succeeded || 0;
|
|
686
|
-
out.failed.count = status.failed || 0;
|
|
687
|
-
} else {
|
|
688
|
-
return null;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
return out;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
686
|
get currentRevisionNumber() {
|
|
695
687
|
if (this.ownedByWorkload || this.kind === 'Job' || this.kind === 'CronJob') {
|
|
696
688
|
return undefined;
|
|
@@ -731,4 +723,78 @@ export default class Workload extends WorkloadService {
|
|
|
731
723
|
|
|
732
724
|
return val;
|
|
733
725
|
}
|
|
726
|
+
|
|
727
|
+
get servicesInNamespace() {
|
|
728
|
+
return this.$rootGetters['cluster/all'](SERVICE).filter((s) => s.metadata.namespace === this.metadata.namespace);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
get relatedServices() {
|
|
732
|
+
// Find Services that have selectors that match this workload's Pod(s).
|
|
733
|
+
return this.servicesInNamespace.filter((service) => {
|
|
734
|
+
const selector = service.spec.selector;
|
|
735
|
+
|
|
736
|
+
for (let i = 0; i < this.pods.length; i++) {
|
|
737
|
+
const pod = this.pods[i];
|
|
738
|
+
|
|
739
|
+
if (service.metadata?.namespace === this.metadata?.namespace && matches(pod, selector)) {
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return false;
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
get resourcesCardRows() {
|
|
749
|
+
return [
|
|
750
|
+
useResourceCardRow(this.t('component.resource.detail.card.resourcesCard.rows.services'), this.relatedServices, undefined, undefined, '#services'),
|
|
751
|
+
...this._resourcesCardRows,
|
|
752
|
+
];
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
get podsCard() {
|
|
756
|
+
const supportedTypes = [WORKLOAD_TYPES.DEPLOYMENT, WORKLOAD_TYPES.DAEMON_SET, WORKLOAD_TYPES.JOB, WORKLOAD_TYPES.STATEFUL_SET];
|
|
757
|
+
|
|
758
|
+
if (!supportedTypes.includes(this.type)) {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const scalingTypes = [WORKLOAD_TYPES.DEPLOYMENT, WORKLOAD_TYPES.STATEFUL_SET];
|
|
763
|
+
|
|
764
|
+
return {
|
|
765
|
+
component: markRaw(defineAsyncComponent(() => import('@shell/components/Resource/Detail/Card/StatusCard/index.vue'))),
|
|
766
|
+
props: {
|
|
767
|
+
title: this.t('component.resource.detail.card.podsCard.title'),
|
|
768
|
+
resources: this.pods,
|
|
769
|
+
showScaling: this.canUpdate && scalingTypes.includes(this.type),
|
|
770
|
+
onIncrease: () => this.scale(true),
|
|
771
|
+
onDecrease: () => this.scale(false)
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
get jobsCard() {
|
|
777
|
+
const supportedTypes = [WORKLOAD_TYPES.CRON_JOB];
|
|
778
|
+
|
|
779
|
+
if (!supportedTypes.includes(this.type)) {
|
|
780
|
+
return null;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return {
|
|
784
|
+
component: markRaw(defineAsyncComponent(() => import('@shell/components/Resource/Detail/Card/StatusCard/index.vue'))),
|
|
785
|
+
props: {
|
|
786
|
+
title: this.t('component.resource.detail.card.jobsCard.title'),
|
|
787
|
+
resources: this.jobs,
|
|
788
|
+
showScaling: false,
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
get cards() {
|
|
794
|
+
return [
|
|
795
|
+
this.podsCard,
|
|
796
|
+
this.jobsCard,
|
|
797
|
+
...this._cards
|
|
798
|
+
];
|
|
799
|
+
}
|
|
734
800
|
}
|