mirror of https://github.com/rancher/dashboard.git
Integrate new label select filtering (#12919)
* Test / Wire in new label filtering to limited places - Wired in to - Workload detail page - pods and services (broken due to reverse selector) - Service Detail - Pods List - Service create/edit - Pod selector tab - Also - Fix showPodRestarts in random location - TODOs - Lots of testing / validation required - reverse deployment --> service selector (not services given deployment, but for each service contains pods from deployment) - workload model usages of pods getter - services model usages of pods getter - many many more.... * wired in, NOT tested * improvement * sdfdsf * ryrty * werer * The great test off begins * testing and tidying * testing and tidying * First good run * tests, fixes and improvements * updates * Tidying up * Fixes (namespace orientated), tweaks, updates * Fix unit test * Fixes for vai off * Re-write `matching` - handle namespaces better (given if the resource is namespaced - better align with legacy matching fn (given specific scenarios that should return none or all) * Lots of fixes, but mainly vai off --> pods list --> deployment detail --> pods list updates correctly * fix lint, unit test * e2e fix * Fix weird plugins build validation failure (TS error on expected param for JS method with a default) * Fix workload services * changes following mini code review * Only show pods tab if workload type supports it, always show pod resource table if tab is shown
This commit is contained in:
parent
85a7667abf
commit
980d4b06d9
|
|
@ -545,7 +545,6 @@ export default {
|
|||
{{ value.createdBy.displayName }}
|
||||
</span>
|
||||
</span>
|
||||
<span v-if="value.showPodRestarts">{{ t("resourceDetail.masthead.restartCount") }}:<span class="live-data"> {{ value.restartCount }}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="right">
|
||||
|
|
|
|||
|
|
@ -25,7 +25,11 @@ export default {
|
|||
default: 'edit'
|
||||
},
|
||||
|
||||
// pod/node affinity types have different operator options
|
||||
/**
|
||||
* pod/node affinity types have different operator options
|
||||
*
|
||||
* Note - This prop should just be isNode
|
||||
*/
|
||||
type: {
|
||||
type: String,
|
||||
default: NODE
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
import { Banner } from '@components/Banner';
|
||||
import MatchExpressions from '@shell/components/form/MatchExpressions';
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { _EDIT } from '@shell/config/query-params';
|
||||
import { convert, matching, simplify } from '@shell/utils/selector';
|
||||
import { convert, simplify } from '@shell/utils/selector';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { COUNT } from '@shell/config/types';
|
||||
import { matching } from '@shell/utils/selector-typed';
|
||||
|
||||
export default {
|
||||
name: 'ResourceSelector',
|
||||
|
|
@ -36,11 +37,6 @@ export default {
|
|||
},
|
||||
|
||||
async fetch() {
|
||||
// Used in conjunction with `matches/match/label selectors`. Requires https://github.com/rancher/dashboard/issues/10417 to fix
|
||||
const hash = await allHash({ allResources: this.$store.dispatch('cluster/findAll', { type: this.type }) });
|
||||
|
||||
this.allResources = hash.allResources;
|
||||
|
||||
this.updateMatchingResources();
|
||||
},
|
||||
|
||||
|
|
@ -53,11 +49,10 @@ export default {
|
|||
sample: null,
|
||||
total: 0,
|
||||
},
|
||||
allResources: [],
|
||||
allResourcesInScope: [],
|
||||
tableHeaders: this.$store.getters['type-map/headersFor'](
|
||||
tableHeaders: this.$store.getters['type-map/headersFor'](
|
||||
this.$store.getters['cluster/schemaFor'](this.type)
|
||||
),
|
||||
inStore: this.$store.getters['currentProduct'].inStore,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -71,6 +66,9 @@ export default {
|
|||
schema() {
|
||||
return this.$store.getters['cluster/schemaFor'](this.type);
|
||||
},
|
||||
/**
|
||||
* of type matchExpression aka `KubeLabelSelectorExpression[]`
|
||||
*/
|
||||
selectorExpressions: {
|
||||
get() {
|
||||
return convert(
|
||||
|
|
@ -85,22 +83,27 @@ export default {
|
|||
this.value['matchExpressions'] = matchExpressions;
|
||||
}
|
||||
},
|
||||
allResourcesInScope() {
|
||||
const counts = this.$store.getters[`${ this.inStore }/all`](COUNT)?.[0]?.counts || {};
|
||||
|
||||
if (this.namespace) {
|
||||
return counts?.[this.type].namespaces[this.namespace]?.count || 0;
|
||||
}
|
||||
|
||||
return counts[this.type]?.summary?.count || 0;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateMatchingResources: throttle(function() {
|
||||
this.allResourcesInScope = this.namespace ? this.allResources.filter((res) => res.metadata.namespace === this.namespace) : this.allResources;
|
||||
const match = matching(this.allResourcesInScope, this.selectorExpressions);
|
||||
const matched = match.length || 0;
|
||||
const sample = match[0]?.nameDisplay;
|
||||
|
||||
this.matchingResources = {
|
||||
matched,
|
||||
matches: match,
|
||||
none: matched === 0,
|
||||
sample,
|
||||
total: this.allResourcesInScope.length,
|
||||
};
|
||||
updateMatchingResources: throttle(async function() {
|
||||
this.matchingResources = await matching({
|
||||
labelSelector: { matchExpressions: this.selectorExpressions },
|
||||
type: this.type,
|
||||
inStore: this.inStore,
|
||||
$store: this.$store,
|
||||
inScopeCount: this.allResourcesInScope,
|
||||
namespace: this.namespace,
|
||||
});
|
||||
}, 250, { leading: true }),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@ export default {
|
|||
|
||||
return {
|
||||
servicePortInfoHeaders,
|
||||
pods: [],
|
||||
podTableHeaders: this.$store.getters['type-map/headersFor'](
|
||||
this.$store.getters['cluster/schemaFor'](POD)
|
||||
),
|
||||
|
|
|
|||
|
|
@ -47,16 +47,20 @@ export default {
|
|||
} catch {}
|
||||
|
||||
const hash = {
|
||||
// Used in conjunction with `matches/match/label selectors`. Requires https://github.com/rancher/dashboard/issues/10417 to fix
|
||||
allPods: this.$store.dispatch('cluster/findAll', { type: POD }),
|
||||
// Used in conjunction with `matches/match/label selectors`. Requires https://github.com/rancher/dashboard/issues/10417 to fix
|
||||
allServices: this.$store.dispatch('cluster/findAll', { type: SERVICE }),
|
||||
allIngresses: this.$store.dispatch('cluster/findAll', { type: INGRESS }),
|
||||
// Nodes should be fetched because they may be referenced in the target
|
||||
// column of a service list item.
|
||||
allNodes: hasNodes ? this.$store.dispatch('cluster/findAll', { type: NODE }) : []
|
||||
};
|
||||
|
||||
if (this.podSchema) {
|
||||
hash.pods = this.value.fetchPods();
|
||||
}
|
||||
|
||||
if (this.serviceSchema) {
|
||||
hash.servicesInNamespace = this.$store.dispatch('cluster/findAll', { type: SERVICE, opt: { namespaced: this.value.metadata.namespace } });
|
||||
}
|
||||
|
||||
if (this.value.type === WORKLOAD_TYPES.CRON_JOB) {
|
||||
hash.jobs = this.value.matchingJobs();
|
||||
}
|
||||
|
|
@ -87,8 +91,7 @@ export default {
|
|||
|
||||
data() {
|
||||
return {
|
||||
allPods: [],
|
||||
allServices: [],
|
||||
servicesInNamespace: [],
|
||||
allIngresses: [],
|
||||
matchingServices: [],
|
||||
matchingIngresses: [],
|
||||
|
|
@ -180,22 +183,6 @@ export default {
|
|||
}, 0);
|
||||
},
|
||||
|
||||
podRestarts() {
|
||||
return this.value.pods.reduce((total, pod) => {
|
||||
const { status:{ containerStatuses = [] } } = pod;
|
||||
|
||||
if (containerStatuses.length) {
|
||||
total += containerStatuses.reduce((tot, container) => {
|
||||
tot += container.restartCount;
|
||||
|
||||
return tot;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return total;
|
||||
}, 0);
|
||||
},
|
||||
|
||||
podHeaders() {
|
||||
return this.$store.getters['type-map/headersFor'](this.podSchema).filter((h) => !h.name || h.name !== NAMESPACE_COL.name);
|
||||
},
|
||||
|
|
@ -213,12 +200,16 @@ export default {
|
|||
},
|
||||
|
||||
showPodGaugeCircles() {
|
||||
const podGauges = Object.values(this.value.podGauges);
|
||||
const podGauges = Object.values(this.podGauges);
|
||||
const total = this.value.pods.length;
|
||||
|
||||
return !podGauges.find((pg) => pg.count === total);
|
||||
},
|
||||
|
||||
podGauges() {
|
||||
return this.value.calcPodGauges(this.value.pods);
|
||||
},
|
||||
|
||||
showJobGaugeCircles() {
|
||||
const jobGauges = Object.values(this.value.jobGauges);
|
||||
const total = this.isCronJob ? this.totalRuns : this.value.pods.length;
|
||||
|
|
@ -230,6 +221,7 @@ export default {
|
|||
return !!SCALABLE_TYPES.includes(this.value.type) && this.value.canUpdate;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async scale(isUp) {
|
||||
try {
|
||||
|
|
@ -256,15 +248,13 @@ export default {
|
|||
if (!this.serviceSchema) {
|
||||
return [];
|
||||
}
|
||||
const matchingPods = this.value.pods;
|
||||
|
||||
// Find Services that have selectors that match this
|
||||
// workload's Pod(s).
|
||||
const matchingServices = this.allServices.filter((service) => {
|
||||
// Find Services that have selectors that match this workload's Pod(s).
|
||||
this.matchingServices = this.servicesInNamespace.filter((service) => {
|
||||
const selector = service.spec.selector;
|
||||
|
||||
for (let i = 0; i < matchingPods.length; i++) {
|
||||
const pod = matchingPods[i];
|
||||
for (let i = 0; i < this.value.pods.length; i++) {
|
||||
const pod = this.value.pods[i];
|
||||
|
||||
if (service.metadata?.namespace === this.value.metadata?.namespace && matches(pod, selector)) {
|
||||
return true;
|
||||
|
|
@ -273,8 +263,6 @@ export default {
|
|||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.matchingServices = matchingServices;
|
||||
},
|
||||
findMatchingIngresses() {
|
||||
if (!this.ingressSchema) {
|
||||
|
|
@ -358,7 +346,7 @@ export default {
|
|||
</template>
|
||||
<template v-else>
|
||||
<CountGauge
|
||||
v-for="(group, key) in value.podGauges"
|
||||
v-for="(group, key) in podGauges"
|
||||
:key="key"
|
||||
:total="value.pods.length"
|
||||
:useful="group.count || 0"
|
||||
|
|
@ -387,13 +375,12 @@ export default {
|
|||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-else
|
||||
v-else-if="value.podMatchExpression"
|
||||
name="pods"
|
||||
:label="t('tableHeaders.pods')"
|
||||
:weight="4"
|
||||
>
|
||||
<ResourceTable
|
||||
v-if="value.pods"
|
||||
:rows="value.pods"
|
||||
:headers="podHeaders"
|
||||
key-field="id"
|
||||
|
|
|
|||
|
|
@ -119,18 +119,24 @@ export default {
|
|||
},
|
||||
|
||||
async removeCatalogResources(catalog) {
|
||||
const selector = `${ UI_PLUGIN_LABELS.CATALOG_IMAGE }=${ catalog.name }`;
|
||||
const namespace = UI_PLUGIN_NAMESPACE;
|
||||
if ( catalog.name ) {
|
||||
const namespace = UI_PLUGIN_NAMESPACE;
|
||||
// of type KubeLabelSelector
|
||||
const labelSelector = { matchLabels: { [UI_PLUGIN_LABELS.CATALOG_IMAGE]: catalog.name } };
|
||||
|
||||
if ( selector ) {
|
||||
const hash = await allHash({
|
||||
deployment: this.$store.dispatch('management/findMatching', {
|
||||
type: WORKLOAD_TYPES.DEPLOYMENT, selector, namespace
|
||||
deployment: this.$store.dispatch('management/findLabelSelector', {
|
||||
type: WORKLOAD_TYPES.DEPLOYMENT,
|
||||
matching: { namespace, labelSelector }
|
||||
}),
|
||||
service: this.$store.dispatch('management/findMatching', {
|
||||
type: SERVICE, selector, namespace
|
||||
service: this.$store.dispatch('management/findLabelSelector', {
|
||||
type: SERVICE,
|
||||
matching: { namespace, labelSelector }
|
||||
}),
|
||||
repo: this.$store.dispatch('management/findMatching', { type: CATALOG.CLUSTER_REPO, selector })
|
||||
repo: this.$store.dispatch('management/findLabelSelector', {
|
||||
type: CATALOG.CLUSTER_REPO,
|
||||
matching: { labelSelector }
|
||||
})
|
||||
});
|
||||
|
||||
for ( const resource of Object.keys(hash) ) {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,8 @@ describe('service edit', () => {
|
|||
$store: {
|
||||
getters: {
|
||||
'management/all': jest.fn(),
|
||||
'i18n/t': jest.fn()
|
||||
'i18n/t': jest.fn(),
|
||||
currentStore: () => 'cluster',
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
|
|||
import { _EDIT } from '@shell/config/query-params';
|
||||
import PolicyRuleTarget from '@shell/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget';
|
||||
|
||||
// Components shown for Network Policy --> Ingress/Egress Rules --> Rule Type are...
|
||||
// Edit Network Policy --> `PolicyRules` 1 --> M `PolicyRule` 1 --> M `PolicyRuleTarget`
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ArrayListGrouped, PolicyRulePort, PolicyRuleTarget
|
||||
|
|
@ -28,18 +31,6 @@ export default {
|
|||
type: String,
|
||||
default: ''
|
||||
},
|
||||
allPods: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
allNamespaces: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { targetKey: this.type === 'ingress' ? 'from' : 'to' };
|
||||
|
|
@ -71,8 +62,6 @@ export default {
|
|||
:mode="mode"
|
||||
:type="type"
|
||||
:namespace="namespace"
|
||||
:all-namespaces="allNamespaces"
|
||||
:all-pods="allPods"
|
||||
:data-testid="`policy-rule-target-${props.i}`"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
|||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { _EDIT } from '@shell/config/query-params';
|
||||
import MatchExpressions from '@shell/components/form/MatchExpressions';
|
||||
import { convert, matching, simplify } from '@shell/utils/selector';
|
||||
import { POD } from '@shell/config/types';
|
||||
import { convert, simplify } from '@shell/utils/selector';
|
||||
import { NAMESPACE, POD } from '@shell/config/types';
|
||||
import ArrayList from '@shell/components/form/ArrayList';
|
||||
import { Banner } from '@components/Banner';
|
||||
import throttle from 'lodash/throttle';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { isValidCIDR } from '@shell/utils/validators/cidr';
|
||||
import { matching } from '@shell/utils/selector-typed';
|
||||
|
||||
const TARGET_OPTIONS = {
|
||||
IP_BLOCK: 'ipBlock',
|
||||
|
|
@ -18,6 +19,9 @@ const TARGET_OPTIONS = {
|
|||
NAMESPACE_AND_POD_SELECTOR: 'namespaceAndPodSelector',
|
||||
};
|
||||
|
||||
// Components shown for Network Policy --> Ingress/Egress Rules --> Rule Type are...
|
||||
// Edit Network Policy --> `PolicyRules` 1 --> M `PolicyRule` 1 --> M `PolicyRuleTarget`
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ArrayList, Banner, LabeledInput, LabeledSelect, MatchExpressions
|
||||
|
|
@ -41,18 +45,6 @@ export default {
|
|||
type: String,
|
||||
default: ''
|
||||
},
|
||||
allPods: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
allNamespaces: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
if (!this.value[TARGET_OPTIONS.IP_BLOCK] &&
|
||||
|
|
@ -66,18 +58,26 @@ export default {
|
|||
}
|
||||
|
||||
return {
|
||||
portOptions: ['TCP', 'UDP'],
|
||||
matchingPods: {},
|
||||
matchingNamespaces: {},
|
||||
invalidCidr: null,
|
||||
invalidCidrs: [],
|
||||
portOptions: ['TCP', 'UDP'],
|
||||
matchingPods: {
|
||||
matches: [], matched: 0, total: 0
|
||||
},
|
||||
matchingNamespaces: {
|
||||
matches: [], matched: 0, total: 0
|
||||
},
|
||||
invalidCidr: null,
|
||||
invalidCidrs: [],
|
||||
POD,
|
||||
TARGET_OPTIONS,
|
||||
targetOptions: Object.values(TARGET_OPTIONS),
|
||||
throttleTime: 250,
|
||||
targetOptions: Object.values(TARGET_OPTIONS),
|
||||
inStore: this.$store.getters['currentProduct'].inStore,
|
||||
debouncedUpdateMatches: debounce(this.updateMatches, 500)
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* of type matchExpression aka `KubeLabelSelectorExpression[]`
|
||||
*/
|
||||
podSelectorExpressions: {
|
||||
get() {
|
||||
return convert(
|
||||
|
|
@ -89,6 +89,9 @@ export default {
|
|||
this.value[TARGET_OPTIONS.POD_SELECTOR] = simplify(podSelectorExpressions);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* of type matchExpression aka `KubeLabelSelectorExpression[]`
|
||||
*/
|
||||
namespaceSelectorExpressions: {
|
||||
get() {
|
||||
return convert(
|
||||
|
|
@ -145,39 +148,40 @@ export default {
|
|||
},
|
||||
watch: {
|
||||
namespace: {
|
||||
handler: 'updateMatches',
|
||||
immediate: true
|
||||
},
|
||||
allNamespaces: {
|
||||
handler: 'updateMatches',
|
||||
handler: 'debouncedUpdateMatches',
|
||||
immediate: true
|
||||
},
|
||||
'value.podSelector': {
|
||||
handler: 'updateMatches',
|
||||
handler: 'debouncedUpdateMatches',
|
||||
immediate: true
|
||||
},
|
||||
'value.namespaceSelector': {
|
||||
handler: 'updateMatches',
|
||||
handler: 'debouncedUpdateMatches',
|
||||
immediate: true
|
||||
},
|
||||
'value.ipBlock.cidr': 'validateCIDR',
|
||||
'value.ipBlock.except': 'validateCIDR',
|
||||
podSelectorExpressions: {
|
||||
handler: 'updateMatches',
|
||||
handler: 'debouncedUpdateMatches',
|
||||
immediate: true
|
||||
},
|
||||
namespaceSelectorExpressions: {
|
||||
handler: 'updateMatches',
|
||||
handler: 'debouncedUpdateMatches',
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
|
||||
fetch() {
|
||||
this.debouncedUpdateMatches();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateMatches() {
|
||||
throttle(() => {
|
||||
this.matchingNamespaces = this.getMatchingNamespaces();
|
||||
this.matchingPods = this.getMatchingPods();
|
||||
}, this.throttle, { leading: true })();
|
||||
async updateMatches() {
|
||||
// Note - needs to be sequential as getMatchingPods requires matchingNamespaces to be up-to-date
|
||||
this.matchingNamespaces = await this.getMatchingNamespaces();
|
||||
this.matchingPods = await this.getMatchingPods();
|
||||
},
|
||||
|
||||
validateCIDR() {
|
||||
const exceptCidrs = this.value[TARGET_OPTIONS.IP_BLOCK]?.except || [];
|
||||
|
||||
|
|
@ -191,34 +195,25 @@ export default {
|
|||
this.invalidCidr = null;
|
||||
}
|
||||
},
|
||||
getMatchingPods() {
|
||||
const namespaces = this.targetType === TARGET_OPTIONS.NAMESPACE_AND_POD_SELECTOR ? this.matchingNamespaces.matches : [{ id: this.namespace }];
|
||||
const allInNamespace = this.allPods.filter((pod) => namespaces.some((ns) => ns.id === pod.metadata.namespace));
|
||||
const match = matching(allInNamespace, this.podSelectorExpressions);
|
||||
const matched = match.length || 0;
|
||||
const sample = match[0]?.nameDisplay;
|
||||
|
||||
return {
|
||||
matched,
|
||||
matches: match,
|
||||
none: matched === 0,
|
||||
sample,
|
||||
total: allInNamespace.length,
|
||||
};
|
||||
async getMatchingPods() {
|
||||
return await matching({
|
||||
labelSelector: { matchExpressions: this.podSelectorExpressions },
|
||||
type: POD,
|
||||
$store: this.$store,
|
||||
inStore: this.inStore,
|
||||
namespace: this.targetType === TARGET_OPTIONS.NAMESPACE_AND_POD_SELECTOR ? this.matchingNamespaces.matches.map((ns) => ns.id) : this.namespace,
|
||||
transient: true,
|
||||
});
|
||||
},
|
||||
getMatchingNamespaces() {
|
||||
const allNamespaces = this.allNamespaces;
|
||||
const match = matching(allNamespaces, this.namespaceSelectorExpressions);
|
||||
const matched = match.length || 0;
|
||||
const sample = match[0]?.nameDisplay;
|
||||
|
||||
return {
|
||||
matched,
|
||||
matches: match,
|
||||
none: matched === 0,
|
||||
sample,
|
||||
total: allNamespaces.length,
|
||||
};
|
||||
async getMatchingNamespaces() {
|
||||
return await matching({
|
||||
labelSelector: { matchExpressions: this.namespaceSelectorExpressions },
|
||||
type: NAMESPACE,
|
||||
$store: this.$store,
|
||||
inStore: this.inStore,
|
||||
transient: true,
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import Tab from '@shell/components/Tabbed/Tab';
|
|||
import Tabbed from '@shell/components/Tabbed';
|
||||
import { removeAt } from '@shell/utils/array';
|
||||
|
||||
// Components shown for Network Policy --> Ingress/Egress Rules --> Rule Type are...
|
||||
// Edit Network Policy --> `PolicyRules` 1 --> M `PolicyRule` 1 --> M `PolicyRuleTarget`
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PolicyRule, Tabbed, Tab
|
||||
|
|
@ -24,18 +27,6 @@ export default {
|
|||
type: String,
|
||||
default: 'ingress'
|
||||
},
|
||||
allPods: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
allNamespaces: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
if (!this.value.spec[this.type]) {
|
||||
|
|
@ -82,8 +73,6 @@ export default {
|
|||
:mode="mode"
|
||||
:type="type"
|
||||
:namespace="value.metadata.namespace"
|
||||
:all-namespaces="allNamespaces"
|
||||
:all-pods="allPods"
|
||||
/>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { mount } from '@vue/test-utils';
|
|||
import PolicyRuleTarget from '@shell/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget';
|
||||
import mock from '@shell/edit/networking.k8s.io.networkpolicy/__tests__/utils/mock.json';
|
||||
import { PolicyRuleTargetSelectors } from '@shell/edit/networking.k8s.io.networkpolicy/__tests__/utils/selectors.test.ts';
|
||||
import { nextTick } from 'vue';
|
||||
import { COUNT, NAMESPACE, POD } from '@shell/config/types';
|
||||
|
||||
type MatchData = {
|
||||
matched: number;
|
||||
|
|
@ -11,29 +13,6 @@ type MatchData = {
|
|||
sample?: string;
|
||||
}
|
||||
|
||||
const newNamespace = {
|
||||
id: 'new-namespace',
|
||||
type: 'namespace',
|
||||
kind: 'Namespace',
|
||||
spec: { finalizers: ['kubernetes'] },
|
||||
status: { phase: 'Active' },
|
||||
metadata: {
|
||||
annotations: { user: 'john' },
|
||||
name: 'default',
|
||||
creationTimestamp: '2024-01-31T10:24:03Z',
|
||||
fields: ['default', 'Active', '1d'],
|
||||
labels: { user: 'john' },
|
||||
relationships: null,
|
||||
resourceVersion: '1',
|
||||
state: {
|
||||
error: false,
|
||||
message: '',
|
||||
name: 'active',
|
||||
transitioning: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe.each([
|
||||
'view',
|
||||
'edit',
|
||||
|
|
@ -45,18 +24,60 @@ describe.each([
|
|||
return { throttleTime: 0 };
|
||||
},
|
||||
props: {
|
||||
namespace: mock.defaultNamespace,
|
||||
allNamespaces: mock.allNamespaces,
|
||||
allPods: mock.allPods,
|
||||
type: 'ingress',
|
||||
namespace: mock.defaultNamespace,
|
||||
type: 'ingress',
|
||||
mode
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$store: {
|
||||
dispatch: (action: string, { type }: { type: string}) => {
|
||||
switch (action) {
|
||||
case 'cluster/findAll':
|
||||
switch (type) {
|
||||
case NAMESPACE:
|
||||
return mock.allNamespaces;
|
||||
case POD:
|
||||
return mock.allPods;
|
||||
default:
|
||||
throw new Error(`unknown type ${ type }`);
|
||||
}
|
||||
default:
|
||||
throw new Error(`unknown action ${ action }`);
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
'i18n/exists': mockExists,
|
||||
'i18n/t': (key: string, matchData: MatchData) => matchData ? `${ key }-${ matchData.total }` : key,
|
||||
'i18n/exists': mockExists,
|
||||
'i18n/t': (key: string, matchData: MatchData) => matchData ? `${ key }-${ matchData.total }` : key,
|
||||
currentProduct: { inStore: 'cluster' },
|
||||
'cluster/all': (type: string) => {
|
||||
switch (type) {
|
||||
case COUNT:
|
||||
return mock.counts;
|
||||
default:
|
||||
throw new Error(`unknown type ${ type }`);
|
||||
}
|
||||
},
|
||||
'cluster/findAll': ({ type }: {type: string}) => {
|
||||
switch (type) {
|
||||
case NAMESPACE:
|
||||
return mock.allNamespaces;
|
||||
case POD:
|
||||
return mock.allPods;
|
||||
default:
|
||||
throw new Error(`unknown type ${ type }`);
|
||||
}
|
||||
},
|
||||
'cluster/schemaFor': (type: string) => {
|
||||
switch (type) {
|
||||
case NAMESPACE:
|
||||
return { attributes: { namespaced: false } };
|
||||
case POD:
|
||||
return { attributes: { namespaced: true } };
|
||||
default:
|
||||
throw new Error(`unknown type ${ type }`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -64,6 +85,20 @@ describe.each([
|
|||
});
|
||||
|
||||
describe(`${ mode } mode`, () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
const waitForUpdatedMatched = async() => {
|
||||
jest.advanceTimersByTime(1000); // Wait for debounced call to fetch updated cluster list
|
||||
await nextTick(); // Wait for changes to cluster list to trigger changes
|
||||
};
|
||||
|
||||
it('should display ip-block selector rule', async() => {
|
||||
const ipBlock = mock.selectors.ipBlock;
|
||||
|
||||
|
|
@ -83,6 +118,7 @@ describe.each([
|
|||
});
|
||||
|
||||
it('should display namespace selector rule', async() => {
|
||||
// This test needs improving - mock data needs to contain more than applicable to selector
|
||||
const namespaceSelector = mock.selectors.namespace;
|
||||
|
||||
await wrapper.setProps({ value: { namespaceSelector } });
|
||||
|
|
@ -92,6 +128,12 @@ describe.each([
|
|||
// Check rule type selector
|
||||
expect(wrapper.vm.targetType).toBe('namespaceSelector');
|
||||
|
||||
// Check the matching namespaces displayed by the banner
|
||||
expect(wrapper.vm.$data.matchingNamespaces.matched).toBe(0);
|
||||
|
||||
await wrapper.vm.updateMatches();
|
||||
await waitForUpdatedMatched();
|
||||
|
||||
// Check the matching namespaces displayed by the banner
|
||||
expect(wrapper.vm.$data.matchingNamespaces.matched).toBe(1);
|
||||
|
||||
|
|
@ -105,21 +147,10 @@ describe.each([
|
|||
expect(selectors.namespaceAndPod.podRule.exists()).toBe(false);
|
||||
|
||||
expect(selectors.namespace.element).toBeDefined();
|
||||
|
||||
// Updating allNamespace should update the matching namespaces message too
|
||||
await wrapper.setProps({
|
||||
allNamespaces: [
|
||||
...wrapper.vm.$props.allNamespaces,
|
||||
newNamespace
|
||||
]
|
||||
});
|
||||
|
||||
const expectedTotal = 3;
|
||||
|
||||
expect(wrapper.vm.$data.matchingNamespaces.total).toBe(expectedTotal);
|
||||
});
|
||||
|
||||
it('should display pod selector rule', async() => {
|
||||
// This test needs improving - mock data needs to contain more than applicable to selector
|
||||
const podSelector = mock.selectors.pod;
|
||||
|
||||
await wrapper.setProps({ value: { podSelector } });
|
||||
|
|
|
|||
|
|
@ -154,5 +154,21 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"counts": [{
|
||||
"counts": {
|
||||
"namespace": {
|
||||
"summary": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"pod": {
|
||||
"namespaces": {
|
||||
"default": {
|
||||
"count": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,15 @@ import Tabbed from '@shell/components/Tabbed';
|
|||
import CruResource from '@shell/components/CruResource';
|
||||
import { Banner } from '@components/Banner';
|
||||
import Labels from '@shell/components/form/Labels';
|
||||
import { NAMESPACE, POD } from '@shell/config/types';
|
||||
import { convert, matching, simplify } from '@shell/utils/selector';
|
||||
import { POD } from '@shell/config/types';
|
||||
import { convert, simplify } from '@shell/utils/selector';
|
||||
import { matching } from '@shell/utils/selector-typed';
|
||||
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import { addObject, removeObject } from '@shell/utils/array';
|
||||
import MatchExpressions from '@shell/components/form/MatchExpressions';
|
||||
import PolicyRules from '@shell/edit/networking.k8s.io.networkpolicy/PolicyRules';
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
|
||||
const POLICY_TYPES = {
|
||||
INGRESS: 'Ingress',
|
||||
|
|
@ -42,14 +43,6 @@ export default {
|
|||
mixins: [CreateEditView],
|
||||
|
||||
async fetch() {
|
||||
const hash = await allHash({
|
||||
allPods: this.$store.dispatch('cluster/findAll', { type: POD }), // Used in conjunction with `matches/match/label selectors`. Requires https://github.com/rancher/dashboard/issues/10417 to fix
|
||||
allNamespaces: this.$store.dispatch('cluster/findAll', { type: NAMESPACE }), // Used in conjunction with `matches/match/label selectors`. Requires https://github.com/rancher/dashboard/issues/10417 to fix
|
||||
});
|
||||
|
||||
this.allPods = hash.allPods; // Used in matchingPods, and PolicyRules --> PolicyRule --> PolicyRuleTarget
|
||||
this.allNamespaces = hash.allNamespaces; // Used in PolicyRules --> PolicyRule --> PolicyRuleTarget
|
||||
|
||||
this.updateMatchingPods();
|
||||
},
|
||||
|
||||
|
|
@ -75,11 +68,10 @@ export default {
|
|||
return {
|
||||
POD,
|
||||
matchingPods,
|
||||
allPods: [],
|
||||
allNamespaces: [],
|
||||
podTableHeaders: this.$store.getters['type-map/headersFor'](
|
||||
this.$store.getters['cluster/schemaFor'](POD)
|
||||
),
|
||||
inStore: this.$store.getters['currentProduct'].inStore,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -127,6 +119,9 @@ export default {
|
|||
this.value.spec['policyTypes'] = policyTypes;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* of type matchExpression aka `KubeLabelSelectorExpression[]`
|
||||
*/
|
||||
podSelectorExpressions: {
|
||||
get() {
|
||||
return convert(
|
||||
|
|
@ -146,19 +141,14 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
updateMatchingPods: throttle(function() {
|
||||
const allInNamespace = this.allPods.filter((pod) => pod.metadata.namespace === this.value.metadata.namespace);
|
||||
const match = matching(allInNamespace, this.podSelectorExpressions);
|
||||
const matched = match.length || 0;
|
||||
const sample = match[0]?.nameDisplay;
|
||||
|
||||
this.matchingPods = {
|
||||
matched,
|
||||
matches: match,
|
||||
none: matched === 0,
|
||||
sample,
|
||||
total: allInNamespace.length,
|
||||
};
|
||||
updateMatchingPods: throttle(async function() {
|
||||
this.matchingPods = await matching({
|
||||
labelSelector: { matchExpressions: this.podSelectorExpressions },
|
||||
type: POD,
|
||||
$store: this.$store,
|
||||
inStore: this.inStore,
|
||||
namespace: this.value.metadata.namespace,
|
||||
});
|
||||
}, 250, { leading: true }),
|
||||
},
|
||||
};
|
||||
|
|
@ -206,8 +196,7 @@ export default {
|
|||
:value="value"
|
||||
type="ingress"
|
||||
:mode="mode"
|
||||
:all-namespaces="allNamespaces"
|
||||
:all-pods="allPods"
|
||||
|
||||
@update:value="$emit('input', $event)"
|
||||
/>
|
||||
</Tab>
|
||||
|
|
@ -231,8 +220,7 @@ export default {
|
|||
:value="value"
|
||||
type="egress"
|
||||
:mode="mode"
|
||||
:all-namespaces="allNamespaces"
|
||||
:all-pods="allPods"
|
||||
|
||||
@update:value="$emit('input', $event)"
|
||||
/>
|
||||
</Tab>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import Labels from '@shell/components/form/Labels';
|
|||
import HarvesterServiceAddOnConfig from '@shell/components/HarvesterServiceAddOnConfig';
|
||||
import { clone } from '@shell/utils/object';
|
||||
import { POD, CAPI, HCI } from '@shell/config/types';
|
||||
import { matching } from '@shell/utils/selector';
|
||||
import { matching } from '@shell/utils/selector-typed';
|
||||
import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { isHarvesterSatisfiesVersion } from '@shell/utils/cluster';
|
||||
|
|
@ -98,7 +98,6 @@ export default {
|
|||
|
||||
return {
|
||||
matchingPods,
|
||||
allPods: [],
|
||||
defaultServiceTypes: DEFAULT_SERVICE_TYPES,
|
||||
saving: false,
|
||||
sessionAffinityActionLabels: Object.values(SESSION_AFFINITY_ACTION_LABELS)
|
||||
|
|
@ -109,7 +108,8 @@ export default {
|
|||
),
|
||||
fvFormRuleSets: [],
|
||||
fvReportedValidationPaths: ['spec'],
|
||||
closedErrorMessages: []
|
||||
closedErrorMessages: [],
|
||||
inStore: this.$store.getters['currentStore'](POD),
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -257,42 +257,27 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
updateMatchingPods: throttle(function() {
|
||||
updateMatchingPods: throttle(async function() {
|
||||
// https://kubernetes.io/docs/reference/kubernetes-api/service-resources/service-v1/#ServiceSpec
|
||||
const { value: { spec: { selector = { } } } } = this;
|
||||
const allInNamespace = this.allPods.filter((pod) => pod.metadata.namespace === this.value?.metadata?.namespace);
|
||||
|
||||
if (isEmpty(selector)) {
|
||||
this.matchingPods = {
|
||||
matched: 0,
|
||||
total: allInNamespace.length,
|
||||
none: true,
|
||||
sample: null,
|
||||
};
|
||||
} else {
|
||||
const match = matching(allInNamespace, selector);
|
||||
|
||||
this.matchingPods = {
|
||||
matched: match.length,
|
||||
total: allInNamespace.length,
|
||||
none: match.length === 0,
|
||||
sample: match[0] ? match[0].nameDisplay : null,
|
||||
};
|
||||
}
|
||||
this.matchingPods = await matching({
|
||||
labelSelector: { matchLabels: selector },
|
||||
type: POD,
|
||||
$store: this.$store,
|
||||
inStore: this.inStore,
|
||||
namespace: this.value?.metadata?.namespace,
|
||||
});
|
||||
}, 250, { leading: true }),
|
||||
|
||||
async loadPods() {
|
||||
try {
|
||||
const inStore = this.$store.getters['currentStore'](POD);
|
||||
|
||||
const hash = {
|
||||
provClusters: this.$store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER }),
|
||||
pods: this.$store.dispatch(`${ inStore }/findAll`, { type: POD }), // Used in conjunction with `matches/match/label selectors`. Requires https://github.com/rancher/dashboard/issues/10417 to fix
|
||||
harvesterConfigs: this.$store.dispatch(`management/findAll`, { type: HCI.HARVESTER_CONFIG }),
|
||||
};
|
||||
|
||||
const res = await allHash(hash);
|
||||
|
||||
this.allPods = res.pods;
|
||||
await allHash(hash);
|
||||
this.updateMatchingPods();
|
||||
} catch (e) { }
|
||||
},
|
||||
|
|
|
|||
|
|
@ -132,13 +132,18 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Fetch resources required to populate POD_RESTARTS and WORKLOAD_HEALTH_SCALE columns
|
||||
*/
|
||||
loadHeathResources() {
|
||||
// See https://github.com/rancher/dashboard/issues/10417, health comes from selectors applied locally to all pods (bad)
|
||||
if (this.paginationEnabled) {
|
||||
// Unfortunately with SSP enabled we cannot fetch all pods to then let each row find applicable pods by locally applied selectors (bad for scaling)
|
||||
// See https://github.com/rancher/dashboard/issues/14211
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch these in the background to populate workload health
|
||||
// Fetch these in the background
|
||||
if ( this.allTypes ) {
|
||||
this.$fetchType(POD);
|
||||
this.$fetchType(WORKLOAD_TYPES.JOB);
|
||||
|
|
|
|||
|
|
@ -408,6 +408,7 @@ export default class ClusterNode extends SteveModel {
|
|||
}
|
||||
|
||||
get pods() {
|
||||
// This fetches all pods that are in the store, rather than all pods in the cluster
|
||||
const allPods = this.$rootGetters['cluster/all'](POD);
|
||||
|
||||
return allPods.filter((pod) => pod.spec.nodeName === this.name);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import find from 'lodash/find';
|
||||
import { POD } from '@shell/config/types';
|
||||
import SteveModel from '@shell/plugins/steve/steve-class';
|
||||
import { parse } from '@shell/utils/selector';
|
||||
|
||||
// i18n-uses servicesPage.serviceTypes.clusterIp.*, servicesPage.serviceTypes.externalName.*, servicesPage.serviceTypes.headless.*
|
||||
// i18n-uses servicesPage.serviceTypes.loadBalancer.*, servicesPage.serviceTypes.nodePort.*
|
||||
|
|
@ -49,7 +50,7 @@ export const CLUSTERIP = (() => {
|
|||
return clusterIp.id;
|
||||
})();
|
||||
|
||||
export default class extends SteveModel {
|
||||
export default class Service extends SteveModel {
|
||||
get customValidationRules() {
|
||||
return [
|
||||
{
|
||||
|
|
@ -137,18 +138,27 @@ export default class extends SteveModel {
|
|||
}
|
||||
|
||||
async fetchPods() {
|
||||
if (this.podRelationship) {
|
||||
// Used in conjunction with `matches/match/label selectors`. Requires https://github.com/rancher/dashboard/issues/10417 to fix
|
||||
await this.$dispatch('cluster/findMatching', {
|
||||
type: POD,
|
||||
selector: this.podRelationship.selector,
|
||||
namespace: this.namespace
|
||||
}, { root: true });
|
||||
}
|
||||
return await this.$dispatch('findLabelSelector', {
|
||||
type: POD,
|
||||
matching: {
|
||||
namespace: this.metadata.namespace,
|
||||
labelSelector: { matchExpressions: parse(this.podRelationship?.selector) },
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This getter expects a superset of workload pods to have been fetched already
|
||||
*
|
||||
* It assumes fetchPods has been called and should be used instead of the response of fetchPods
|
||||
* (findAll --> findLabelSelector world results won't trigger change detection)
|
||||
*/
|
||||
get pods() {
|
||||
return this.podRelationship ? this.$getters.matching( POD, this.podRelationship.selector, this.namespace ) : [];
|
||||
if (this.podRelationship?.selector) {
|
||||
return this.$getters['matchingLabelSelector'](POD, { matchExpressions: parse(this.podRelationship?.selector) }, this.metadata.namespace);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
get serviceType() {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ import { TIMESTAMP, CATTLE_PUBLIC_ENDPOINTS } from '@shell/config/labels-annotat
|
|||
import { WORKLOAD_TYPES, SERVICE, POD } from '@shell/config/types';
|
||||
import { get, set } from '@shell/utils/object';
|
||||
import day from 'dayjs';
|
||||
import { convertSelectorObj, matching, matches } from '@shell/utils/selector';
|
||||
import { convertSelectorObj, parse } from '@shell/utils/selector';
|
||||
import { SEPARATOR } from '@shell/config/workload';
|
||||
import WorkloadService from '@shell/models/workload.service';
|
||||
import { matching } from '@shell/utils/selector-typed';
|
||||
|
||||
export const defaultContainer = {
|
||||
imagePullPolicy: 'Always',
|
||||
|
|
@ -18,6 +19,7 @@ export const defaultContainer = {
|
|||
},
|
||||
volumeMounts: []
|
||||
};
|
||||
|
||||
export default class Workload extends WorkloadService {
|
||||
// remove clone as yaml/edit as yaml until API supported
|
||||
get _availableActions() {
|
||||
|
|
@ -205,22 +207,20 @@ export default class Workload extends WorkloadService {
|
|||
return this.goToEdit({ sidecar: true });
|
||||
}
|
||||
|
||||
get showPodRestarts() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get restartCount() {
|
||||
const pods = this.pods;
|
||||
return this.pods.reduce((total, pod) => {
|
||||
const { status:{ containerStatuses = [] } } = pod;
|
||||
|
||||
let sum = 0;
|
||||
if (containerStatuses.length) {
|
||||
total += containerStatuses.reduce((tot, container) => {
|
||||
tot += container.restartCount || 0;
|
||||
|
||||
pods.forEach((pod) => {
|
||||
if (pod.status.containerStatuses) {
|
||||
sum += pod.status?.containerStatuses[0].restartCount || 0;
|
||||
return tot;
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
return sum;
|
||||
return total;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
get hasSidecars() {
|
||||
|
|
@ -334,6 +334,10 @@ export default class Workload extends WorkloadService {
|
|||
const type = this._type ? this._type : this.type;
|
||||
|
||||
const detailItem = {
|
||||
restarts: {
|
||||
label: this.t('resourceDetail.masthead.restartCount'),
|
||||
content: this.restartCount
|
||||
},
|
||||
endpoint: {
|
||||
label: 'Endpoints',
|
||||
content: this.endpoint,
|
||||
|
|
@ -400,10 +404,13 @@ export default class Workload extends WorkloadService {
|
|||
});
|
||||
}
|
||||
|
||||
out.push( {
|
||||
out.push({
|
||||
label: 'Image',
|
||||
content: this.imageNames,
|
||||
formatter: 'PodImages'
|
||||
}, {
|
||||
label: detailItem.restarts.label,
|
||||
content: detailItem.restarts.content,
|
||||
});
|
||||
|
||||
switch (type) {
|
||||
|
|
@ -550,30 +557,56 @@ export default class Workload extends WorkloadService {
|
|||
}
|
||||
}
|
||||
|
||||
get pods() {
|
||||
const relationships = this.metadata?.relationships || [];
|
||||
const podRelationship = relationships.filter((relationship) => relationship.toType === POD)[0];
|
||||
|
||||
if (podRelationship) {
|
||||
const pods = this.$getters['podsByNamespace'](this.metadata.namespace);
|
||||
|
||||
// Used in conjunction with `matches/match/label selectors`. Requires https://github.com/rancher/dashboard/issues/10417 to fix
|
||||
return pods.filter((obj) => {
|
||||
return matches(obj, podRelationship.selector);
|
||||
async fetchPods() {
|
||||
if (this.podMatchExpression) {
|
||||
return this.$dispatch('findLabelSelector', {
|
||||
type: POD,
|
||||
matching: {
|
||||
namespace: this.metadata.namespace,
|
||||
labelSelector: { matchExpressions: this.podMatchExpression },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* This getter expects a superset of workload pods to have been fetched already
|
||||
*
|
||||
* It assumes fetchPods has been called and should be used instead of the response of fetchPods
|
||||
* (findAll --> findLabelSelector world results won't trigger change detection)
|
||||
*/
|
||||
get pods() {
|
||||
if (this.podMatchExpression) {
|
||||
return this.$getters['matchingLabelSelector'](POD, { matchExpressions: this.podMatchExpression }, this.metadata.namespace);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
get podGauges() {
|
||||
/**
|
||||
* Return a string version of a matchLabel expression
|
||||
*/
|
||||
get podSelector() {
|
||||
const relationships = this.metadata?.relationships || [];
|
||||
const selector = relationships.filter((relationship) => relationship.toType === POD)[0]?.selector;
|
||||
|
||||
return selector;
|
||||
}
|
||||
|
||||
get podMatchExpression() {
|
||||
return this.podSelector ? parse(this.podSelector) : null;
|
||||
}
|
||||
|
||||
calcPodGauges(pods) {
|
||||
const out = { };
|
||||
|
||||
if (!this.pods) {
|
||||
if (!pods) {
|
||||
return out;
|
||||
}
|
||||
|
||||
this.pods.map((pod) => {
|
||||
pods.map((pod) => {
|
||||
const { stateColor, stateDisplay } = pod;
|
||||
|
||||
if (out[stateDisplay]) {
|
||||
|
|
@ -589,6 +622,10 @@ export default class Workload extends WorkloadService {
|
|||
return out;
|
||||
}
|
||||
|
||||
get podGauges() {
|
||||
return this.calcPodGauges(this.pods);
|
||||
}
|
||||
|
||||
// Job Specific
|
||||
get jobRelationships() {
|
||||
if (this.type !== WORKLOAD_TYPES.CRON_JOB) {
|
||||
|
|
@ -664,13 +701,15 @@ export default class Workload extends WorkloadService {
|
|||
}
|
||||
|
||||
async matchingPods() {
|
||||
// Used in conjunction with `matches/match/label selectors`. Requires https://github.com/rancher/dashboard/issues/10417 to fix
|
||||
const all = await this.$dispatch('findAll', { type: POD });
|
||||
const allInNamespace = all.filter((pod) => pod.metadata.namespace === this.metadata.namespace);
|
||||
const matchInfo = await matching({
|
||||
labelSelector: { matchExpressions: convertSelectorObj(this.spec.selector) },
|
||||
type: POD,
|
||||
$store: this.$store || { getters: this.$rootGetters, dispatch: (action, args) => this.$dispatch(action.split('/')[1], args) },
|
||||
inStore: this.$rootGetters['currentProduct'].inStore,
|
||||
namespace: this.metadata.namespace,
|
||||
});
|
||||
|
||||
const selector = convertSelectorObj(this.spec.selector);
|
||||
|
||||
return matching(allInNamespace, selector);
|
||||
return matchInfo.matches;
|
||||
}
|
||||
|
||||
cleanForSave(data) {
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ export default {
|
|||
|
||||
async fetch() {
|
||||
if ( this.$store.getters['cluster/schemaFor'](SERVICE) ) {
|
||||
this.uiServices = await this.$store.dispatch('cluster/findMatching', {
|
||||
this.uiServices = await this.$store.dispatch('cluster/findLabelSelector', {
|
||||
type: SERVICE,
|
||||
selector: 'app=longhorn-ui'
|
||||
matching: { labelSelector: { matchLabels: { app: 'longhorn-ui' } } }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -175,7 +175,9 @@ describe('dashboard-store: actions', () => {
|
|||
haveSelector: jest.fn(() => false),
|
||||
matching: jest.fn(() => 'getters.all'),
|
||||
urlFor: jest.fn(() => 'getters.urlFor'),
|
||||
urlOptions: jest.fn(() => 'getters.urlOptions')
|
||||
urlOptions: jest.fn(() => 'getters.urlOptions'),
|
||||
haveAll: jest.fn(() => false),
|
||||
all: jest.fn(() => []),
|
||||
};
|
||||
const rootGetters = { 'type-map/optionsFor': jest.fn() };
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import garbageCollect from '@shell/utils/gc/gc';
|
|||
import { addSchemaIndexFields } from '@shell/plugins/steve/schema.utils';
|
||||
import { addParam, parse } from '@shell/utils/url';
|
||||
import { conditionalDepaginate } from '@shell/store/type-map.utils';
|
||||
import { FilterArgs } from '@shell/types/store/pagination.types';
|
||||
import { isLabelSelectorEmpty, labelSelectorToSelector } from '@shell/utils/selector-typed';
|
||||
|
||||
export const _ALL = 'all';
|
||||
export const _MERGE = 'merge';
|
||||
|
|
@ -399,6 +401,9 @@ export default {
|
|||
},
|
||||
|
||||
/**
|
||||
* If result not already cached, make a http request to fetch a specific set of resources
|
||||
*
|
||||
* This accepts all the new sql-cache backed api features (sort, filter, etc)
|
||||
*
|
||||
* @param {*} ctx
|
||||
* @param { {type: string, opt: ActionFindPageArgs} } opt
|
||||
|
|
@ -478,11 +483,61 @@ export default {
|
|||
garbageCollect.gcUpdateLastAccessed(ctx, type);
|
||||
|
||||
return opt.transient ? {
|
||||
data: out.data,
|
||||
data: await dispatch('createMany', out.data), // Return classified objects
|
||||
pagination
|
||||
} : findAllGetter(getters, type, opt);
|
||||
},
|
||||
|
||||
/**
|
||||
* Find results matching a kube `labelSelector` object
|
||||
*
|
||||
* If result not already cached, make a http request to fetch resources matching the selector/s
|
||||
*
|
||||
* This is different if vai based pagination is on
|
||||
* a) Pagination Enabled - use the new sql-cache backed api - findPage
|
||||
* b) Pagination Disabled - use the old 'native kube api' - findMatching
|
||||
*
|
||||
* Filter is defined via the kube labelSelector object (see KubeLabelSelector)
|
||||
*/
|
||||
async findLabelSelector(ctx, {
|
||||
type,
|
||||
context,
|
||||
matching: {
|
||||
namespace,
|
||||
labelSelector
|
||||
},
|
||||
opt
|
||||
}) {
|
||||
const { getters, dispatch } = ctx;
|
||||
const args = {
|
||||
id: type,
|
||||
context,
|
||||
};
|
||||
|
||||
if (getters[`paginationEnabled`]?.(args)) {
|
||||
if (isLabelSelectorEmpty(labelSelector)) {
|
||||
throw new Error(`labelSelector must not be empty when using findLabelSelector (avoid fetching all resources)`);
|
||||
}
|
||||
|
||||
// opt of type ActionFindPageArgs
|
||||
return dispatch('findPage', {
|
||||
type,
|
||||
opt: {
|
||||
...(opt || {}),
|
||||
namespaced: namespace,
|
||||
pagination: new FilterArgs({ labelSelector }),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return dispatch('findMatching', {
|
||||
type,
|
||||
selector: labelSelectorToSelector(labelSelector),
|
||||
opt,
|
||||
namespace,
|
||||
});
|
||||
},
|
||||
|
||||
async findMatching(ctx, {
|
||||
type,
|
||||
selector,
|
||||
|
|
@ -500,7 +555,13 @@ export default {
|
|||
if ( !getters.typeRegistered(type) ) {
|
||||
commit('registerType', type);
|
||||
}
|
||||
if ( opt.force !== true && getters['haveSelector'](type, selector) ) {
|
||||
|
||||
if ( opt.force !== true && getters['haveSelector'](type, selector)) {
|
||||
return getters.all(type);
|
||||
}
|
||||
|
||||
// Optimisation - We can pretend like we've fetched a specific selectors worth instead of replacing ALL pods with only SOME
|
||||
if ( opt.force !== true && getters['haveAll'](type)) {
|
||||
return getters.matching( type, selector, namespace );
|
||||
}
|
||||
|
||||
|
|
@ -508,6 +569,7 @@ export default {
|
|||
|
||||
opt = opt || {};
|
||||
opt.labelSelector = selector;
|
||||
opt.namespaced = namespace;
|
||||
opt.url = getters.urlFor(type, null, opt);
|
||||
opt.depaginate = conditionalDepaginate(typeOptions?.depaginate, { ctx, args: { type, opt } });
|
||||
|
||||
|
|
@ -536,7 +598,7 @@ export default {
|
|||
|
||||
garbageCollect.gcUpdateLastAccessed(ctx, type);
|
||||
|
||||
return getters.matching( type, selector, namespace );
|
||||
return getters.all(type);
|
||||
},
|
||||
|
||||
// opt:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
import { SCHEMA, COUNT } from '@shell/config/types';
|
||||
import { SCHEMA, COUNT, POD } from '@shell/config/types';
|
||||
|
||||
import { matches } from '@shell/utils/selector';
|
||||
import { typeMunge, typeRef, SIMPLE_TYPES } from '@shell/utils/create-yaml';
|
||||
|
|
@ -9,6 +9,7 @@ import { keyFieldFor, normalizeType } from './normalize';
|
|||
import { lookup } from './model-loader';
|
||||
import garbageCollect from '@shell/utils/gc/gc';
|
||||
import paginationUtils from '@shell/utils/pagination-utils';
|
||||
import { labelSelectorToSelector } from '@shell/utils/selector-typed';
|
||||
|
||||
export const urlFor = (state, getters) => (type, id, opt) => {
|
||||
opt = opt || {};
|
||||
|
|
@ -91,12 +92,55 @@ export default {
|
|||
return state.types[type].list;
|
||||
},
|
||||
|
||||
/**
|
||||
* This is a _manual_ application of the selector against whatever is in the store
|
||||
*
|
||||
* The store must be populated with enough resources to make this valid
|
||||
*
|
||||
* It's like `matching` except
|
||||
* - it accepts a kube labelSelector object (see KubeLabelSelector)
|
||||
* - if the store already has the selector and only the selector just return it, otherwise run through `matching`
|
||||
*/
|
||||
matchingLabelSelector: (state, getters, rootState) => (type, labelSelector, namespace) => {
|
||||
type = getters.normalizeType(type);
|
||||
const selector = labelSelectorToSelector(labelSelector);
|
||||
const page = getters['havePage'](type, selector)?.request;
|
||||
|
||||
// Does the store contain the result of a vai backed api request?
|
||||
if (
|
||||
page?.namespace === namespace &&
|
||||
page?.pagination?.filters?.length === 0 &&
|
||||
page?.pagination.labelSelector &&
|
||||
selector === labelSelectorToSelector(page?.pagination.labelSelector
|
||||
)
|
||||
) {
|
||||
return getters.all(type);
|
||||
}
|
||||
|
||||
// Does the store contain the result of a specific labelSelector request?
|
||||
if (getters['haveSelector'](type, selector)) {
|
||||
return getters.all(type);
|
||||
}
|
||||
|
||||
// Does the store have all and we can pretend like it contains a result of a labelSelector?
|
||||
if (getters['haveAll'](type)) {
|
||||
return getters.matching( type, selector, namespace );
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
|
||||
/**
|
||||
* This is a _manual_ application of the selector against whatever is in the store
|
||||
*
|
||||
* The store must be populated with enough resources to make this valid
|
||||
*/
|
||||
matching: (state, getters, rootState) => (type, selector, namespace, config = { skipSelector: false }) => {
|
||||
let matching = getters['all'](type);
|
||||
|
||||
// Filter first by namespace if one is provided, since this is efficient
|
||||
if (namespace && typeof namespace === 'string') {
|
||||
matching = matching.filter((obj) => obj.namespace === namespace);
|
||||
matching = type === POD ? getters['podsByNamespace'](namespace) : matching.filter((obj) => obj.namespace === namespace);
|
||||
}
|
||||
|
||||
garbageCollect.gcUpdateLastAccessed({
|
||||
|
|
|
|||
|
|
@ -413,13 +413,18 @@ export default {
|
|||
loadSelector(state, {
|
||||
type, entries, ctx, selector, revision
|
||||
}) {
|
||||
const keyField = ctx.getters.keyFieldForType(type);
|
||||
const cache = registerType(state, type);
|
||||
const cachedArgs = createLoadArgs(ctx, entries?.[0]?.type);
|
||||
const proxies = reactive(entries.map((x) => classify(ctx, x)));
|
||||
|
||||
for ( const data of entries ) {
|
||||
load(state, {
|
||||
data, ctx, cachedArgs
|
||||
});
|
||||
clear(cache.list);
|
||||
cache.map.clear();
|
||||
cache.generation++;
|
||||
|
||||
addObjects(cache.list, proxies);
|
||||
|
||||
for ( let i = 0 ; i < proxies.length ; i++ ) {
|
||||
cache.map.set(proxies[i][keyField], proxies[i]);
|
||||
}
|
||||
|
||||
cache.haveSelector[selector] = true;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { markRaw } from 'vue';
|
|||
|
||||
import { ExtensionPoint, ActionLocation } from '@shell/core/types';
|
||||
import { getApplicableExtensionEnhancements } from '@shell/core/plugin-helpers';
|
||||
import { parse } from '@shell/utils/selector';
|
||||
|
||||
export const DNS_LIKE_TYPES = ['dnsLabel', 'dnsLabelRestricted', 'hostname'];
|
||||
|
||||
|
|
@ -1179,7 +1180,8 @@ export default class Resource {
|
|||
}
|
||||
|
||||
// @TODO remove this once the API maps steve _type <-> k8s type in both directions
|
||||
opt.data = this.toSave() || { ...this };
|
||||
// Completely disconnect the object we're going to save (and mangle in cleanForSave) and `this`
|
||||
opt.data = this.toSave() || JSON.parse(JSON.stringify(this));
|
||||
|
||||
if (opt.data._type) {
|
||||
opt.data.type = opt.data._type;
|
||||
|
|
@ -1791,6 +1793,7 @@ export default class Resource {
|
|||
}
|
||||
|
||||
if ( r.selector ) {
|
||||
// A selector is a stringified version of a matchLabel (https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go#L1010)
|
||||
addObjects(out.selectors, {
|
||||
type: r.toType,
|
||||
namespace: r.toNamespace,
|
||||
|
|
@ -1834,15 +1837,32 @@ export default class Resource {
|
|||
}
|
||||
|
||||
async _findRelationship(rel, direction) {
|
||||
// Find resources for this resource's metadata.relationships (steve prop)
|
||||
// These will either reference a selector (stringified matchLabels) OR specific resources (ids)
|
||||
const { selectors, ids } = this._relationshipsFor(rel, direction);
|
||||
const out = [];
|
||||
|
||||
// Find all the resources that match the selector
|
||||
for ( const sel of selectors ) {
|
||||
const matching = await this.$dispatch('findMatching', sel);
|
||||
const {
|
||||
type,
|
||||
selector,
|
||||
namespace,
|
||||
opt,
|
||||
} = sel;
|
||||
const matching = await this.$dispatch('findLabelSelector', {
|
||||
type,
|
||||
matching: {
|
||||
namespace,
|
||||
labelSelector: { matchExpressions: parse(selector) }
|
||||
},
|
||||
opts: opt
|
||||
});
|
||||
|
||||
addObjects(out, matching.data);
|
||||
addObjects(out, matching);
|
||||
}
|
||||
|
||||
// Find all the resources that match the required id's
|
||||
for ( const obj of ids ) {
|
||||
const { type, id } = obj;
|
||||
let matching = this.$getters['byId'](type, id);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { splitObjectPath } from '@shell/utils/string';
|
|||
import { parseType } from '@shell/models/schema';
|
||||
import {
|
||||
STEVE_AGE_COL,
|
||||
STEVE_ID_COL, STEVE_LIST_GROUPS, STEVE_NAMESPACE_COL, STEVE_STATE_COL
|
||||
STEVE_ID_COL, STEVE_LIST_GROUPS, STEVE_NAME_COL, STEVE_NAMESPACE_COL, STEVE_STATE_COL
|
||||
} from '@shell/config/pagination-table-headers';
|
||||
import { createHeaders } from '@shell/store/type-map.utils';
|
||||
import paginationUtils from '@shell/utils/pagination-utils';
|
||||
|
|
@ -326,6 +326,7 @@ export default {
|
|||
typeOptions: typeMapGetters['optionsFor'](schema, true),
|
||||
schema,
|
||||
columns: {
|
||||
name: STEVE_NAME_COL,
|
||||
state: STEVE_STATE_COL,
|
||||
namespace: STEVE_NAMESPACE_COL,
|
||||
age: STEVE_AGE_COL,
|
||||
|
|
|
|||
|
|
@ -59,4 +59,8 @@ export default class SteveModel extends HybridModel {
|
|||
|
||||
return val;
|
||||
}
|
||||
|
||||
paginationEnabled() {
|
||||
return this.$getters['paginationEnabled'](this.type);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
import { CAPI as CAPI_LAB_AND_ANO, CATTLE_PUBLIC_ENDPOINTS } from '@shell/config/labels-annotations';
|
||||
import { Schema } from '@shell/plugins/steve/schema';
|
||||
import { PaginationSettingsStore } from '@shell/types/resources/settings';
|
||||
import { KubeLabelSelector, KubeLabelSelectorExpression } from '@shell/types/kube/kube-api';
|
||||
|
||||
/**
|
||||
* This is a workaround for a ts build issue found in check-plugins-build.
|
||||
|
|
@ -401,6 +402,14 @@ class StevePaginationUtils extends NamespaceProjectFilters {
|
|||
}
|
||||
}
|
||||
|
||||
if (opt.pagination.labelSelector) {
|
||||
const filters = this.convertLabelSelectorPaginationParams(schema, opt.pagination.labelSelector);
|
||||
|
||||
if (filters) {
|
||||
params.push(filters);
|
||||
}
|
||||
}
|
||||
|
||||
// Note - There is a `limit` property that is by default 100,000. This can be disabled by using `limit=-1`,
|
||||
// but we shouldn't be fetching any pages big enough to exceed the default
|
||||
|
||||
|
|
@ -499,6 +508,111 @@ class StevePaginationUtils extends NamespaceProjectFilters {
|
|||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert kube labelSelector object into steve filter params
|
||||
*
|
||||
* A lot of the requirements and details are taken directly from
|
||||
* https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
|
||||
*/
|
||||
private convertLabelSelectorPaginationParams(schema: Schema, labelSelector: KubeLabelSelector): string {
|
||||
// Get a list of matchExpressions
|
||||
const expressions: KubeLabelSelectorExpression[] = labelSelector.matchExpressions ? [...labelSelector.matchExpressions] : [];
|
||||
|
||||
// matchLabels are just simpler versions of matchExpressions, for ease convert them
|
||||
if (labelSelector.matchLabels) {
|
||||
Object.entries(labelSelector.matchLabels).forEach(([key, value]) => {
|
||||
const expression: KubeLabelSelectorExpression = {
|
||||
key,
|
||||
values: [value],
|
||||
operator: 'In'
|
||||
};
|
||||
|
||||
expressions.push(expression);
|
||||
});
|
||||
}
|
||||
|
||||
// concert all matchExpressions into string params
|
||||
const filters: string[] = expressions.reduce((res, exp) => {
|
||||
const labelKey = `metadata.labels[${ exp.key }]`;
|
||||
|
||||
switch (exp.operator) {
|
||||
case 'In':
|
||||
if (!exp.values?.length) {
|
||||
console.error(`Skipping labelSelector to API filter param conversion for ${ exp.key }(IN) as no value was supplied`); // eslint-disable-line no-console
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// foo IN [bar] => ?filter=foo+IN+(bar)
|
||||
// foo IN [bar, baz2] => ?filter=foo+IN+(bar,baz2)
|
||||
res.push(`filter=${ labelKey } IN (${ exp.values.join(',') })`);
|
||||
break;
|
||||
case 'NotIn':
|
||||
|
||||
if (!exp.values?.length) {
|
||||
console.error(`Skipping labelSelector to API filter param conversion for ${ exp.key }(NOTIN) as no value was supplied`); // eslint-disable-line no-console
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// aaa NotIn [bar, baz2]=> ?filter=foo+NOTIN+(bar,baz2)
|
||||
res.push(`filter=${ labelKey } NOTIN (${ exp.values.join(',') })`);
|
||||
break;
|
||||
case 'Exists':
|
||||
|
||||
if (exp.values?.length) {
|
||||
console.error(`Skipping labelSelector to API filter param conversion for ${ exp.key }(Exists) as no value was supplied`); // eslint-disable-line no-console
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// bbb Exists=> ?filter=bbb
|
||||
res.push(`filter=${ labelKey }`);
|
||||
break;
|
||||
case 'DoesNotExist':
|
||||
if (exp.values?.length) {
|
||||
console.error(`Skipping labelSelector to API filter param conversion for ${ exp.key }(DoesNotExist) as no value was supplied`); // eslint-disable-line no-console
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// ccc DoesNotExist ?filter=!bbb. # or %21bbb
|
||||
res.push(`filter=!${ labelKey }`);
|
||||
break;
|
||||
case 'Gt':
|
||||
// Currently broken - see https://github.com/rancher/rancher/issues/50057
|
||||
// Only applicable to node affinity (atm) - https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#operators
|
||||
|
||||
if (typeof exp.values !== 'string') {
|
||||
console.error(`Skipping labelSelector to API filter param conversion for ${ exp.key }(Gt) as no value was supplied`); // eslint-disable-line no-console
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// ddd Gt 1=> ?filter=ddd+>+1
|
||||
res.push(`filter=${ labelKey } > (${ exp.values })`);
|
||||
break;
|
||||
case 'Lt':
|
||||
// Currently broken - see https://github.com/rancher/rancher/issues/50057
|
||||
// Only applicable to node affinity (atm) - https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#operators
|
||||
if (typeof exp.values !== 'string') {
|
||||
console.error(`Skipping labelSelector to API filter param conversion for ${ exp.key }(Lt) as no value was supplied`); // eslint-disable-line no-console
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// eee Lt 2=> ?filter=eee+<+2
|
||||
res.push(`filter=${ labelKey } < (${ exp.values })`);
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
}, [] as string[]);
|
||||
|
||||
// "All of the requirements, from both matchLabels and matchExpressions are ANDed together -- they must all be satisfied in order to match"
|
||||
return filters.join('&');
|
||||
}
|
||||
}
|
||||
|
||||
export const PAGINATION_SETTINGS_STORE_DEFAULTS: PaginationSettingsStore = {
|
||||
|
|
|
|||
|
|
@ -314,7 +314,9 @@ self.onmessage = (e) => {
|
|||
if (workerActions[action]) {
|
||||
workerActions[action](e?.data[action]);
|
||||
} else {
|
||||
console.warn('no associated action for:', action); // eslint-disable-line no-console
|
||||
// This catches any window sendMessage event. We're hitting this on hot-reload of code where somehow this file is loaded
|
||||
// Could be related to extensions, which have their own version of this
|
||||
console.debug('no associated action for:', action); // eslint-disable-line no-console
|
||||
}
|
||||
});
|
||||
}; // bind everything to the worker's onmessage handler via the workerActions
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ export function createHeaders(
|
|||
|
||||
// Always try to have an identifier
|
||||
if ( !hasName ) {
|
||||
insertAt(out, 1, idColumn || nameColumn);
|
||||
insertAt(out, 1, nameColumn || idColumn );
|
||||
if ( namespaced ) {
|
||||
insertAt(out, 2, namespaceColumn);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
export interface KubeLabelSelectorExpression {
|
||||
key: string,
|
||||
/**
|
||||
* Gt and Lt are only applicable to node selectors
|
||||
*/
|
||||
operator: 'In' | 'NotIn' | 'Exists' | 'DoesNotExist' | 'Gt' | 'Lt',
|
||||
/**
|
||||
* An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch.
|
||||
*/
|
||||
values?: string[],
|
||||
}
|
||||
|
||||
/**
|
||||
* https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#labelselector-v1-meta
|
||||
*/
|
||||
export interface KubeLabelSelector {
|
||||
matchExpressions?: KubeLabelSelectorExpression[],
|
||||
/**
|
||||
* matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed.
|
||||
*/
|
||||
matchLabels?: { [key: string]: string }
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { NAMESPACE_FILTER_NS_FULL_PREFIX, NAMESPACE_FILTER_P_FULL_PREFIX } from '@shell/utils/namespace-filter';
|
||||
import { KubeLabelSelector } from '@shell/types/kube/kube-api';
|
||||
|
||||
// Pagination Typing
|
||||
// These structures are designed to offer both convenience and flexibility based on a common structure and are
|
||||
|
|
@ -304,7 +305,7 @@ export class PaginationArgs {
|
|||
*/
|
||||
sort: PaginationSort[];
|
||||
/**
|
||||
* A collection of `filter` params
|
||||
* A collection of traditional `filter` params covering logic such as x is y, x is like y, x is not y
|
||||
*
|
||||
* For more info see {@link PaginationParamFilter}
|
||||
*/
|
||||
|
|
@ -316,6 +317,11 @@ export class PaginationArgs {
|
|||
*/
|
||||
projectsOrNamespaces: PaginationParamProjectOrNamespace[];
|
||||
|
||||
/**
|
||||
* Traditional Kube labelSelector consisting of matchLabels and matchExpressions
|
||||
*/
|
||||
labelSelector?: KubeLabelSelector;
|
||||
|
||||
/**
|
||||
* Creates an instance of PaginationArgs.
|
||||
*
|
||||
|
|
@ -327,6 +333,7 @@ export class PaginationArgs {
|
|||
sort = [],
|
||||
filters = [],
|
||||
projectsOrNamespaces = [],
|
||||
labelSelector = undefined,
|
||||
}:
|
||||
// This would be neater as just Partial<PaginationArgs> but we lose all jsdoc
|
||||
{
|
||||
|
|
@ -354,6 +361,10 @@ export class PaginationArgs {
|
|||
* For definition see {@link PaginationArgs} `projectsOrNamespaces`
|
||||
*/
|
||||
projectsOrNamespaces?: PaginationParamProjectOrNamespace | PaginationParamProjectOrNamespace[],
|
||||
/**
|
||||
* Traditional Kube labelSelector consisting of matchLabels and matchExpressions
|
||||
*/
|
||||
labelSelector?: KubeLabelSelector,
|
||||
}) {
|
||||
this.page = page;
|
||||
this.pageSize = pageSize;
|
||||
|
|
@ -368,6 +379,7 @@ export class PaginationArgs {
|
|||
} else {
|
||||
this.projectsOrNamespaces = [];
|
||||
}
|
||||
this.labelSelector = labelSelector;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -386,6 +398,7 @@ export class FilterArgs extends PaginationArgs {
|
|||
sort = [],
|
||||
filters = [],
|
||||
projectsOrNamespaces = [],
|
||||
labelSelector = undefined,
|
||||
}:
|
||||
// This would be neater as just Partial<PaginationArgs> but we lose all jsdoc
|
||||
{
|
||||
|
|
@ -405,9 +418,13 @@ export class FilterArgs extends PaginationArgs {
|
|||
* For definition see {@link PaginationArgs} `projectsOrNamespaces`
|
||||
*/
|
||||
projectsOrNamespaces?: PaginationParamProjectOrNamespace | PaginationParamProjectOrNamespace[],
|
||||
/**
|
||||
* Traditional Kube labelSelector consisting of matchLabels and matchExpressions
|
||||
*/
|
||||
labelSelector?: KubeLabelSelector
|
||||
}) {
|
||||
super({
|
||||
page: null, pageSize: null, sort, filters, projectsOrNamespaces
|
||||
page: null, pageSize: null, sort, filters, projectsOrNamespaces, labelSelector
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,205 @@
|
|||
import { COUNT } from '@shell/config/types';
|
||||
import { KubeLabelSelector, KubeLabelSelectorExpression } from '@shell/types/kube/kube-api';
|
||||
import { ActionFindPageArgs } from '@shell/types/store/dashboard-store.types';
|
||||
import { FilterArgs, PaginationFilterField, PaginationParamFilter } from '@shell/types/store/pagination.types';
|
||||
import { isEmpty } from '@shell/utils/object';
|
||||
import { convert, matching as rootMatching } from '@shell/utils/selector';
|
||||
|
||||
type MatchingResponse = {
|
||||
matched: number,
|
||||
matches: any[],
|
||||
none: boolean,
|
||||
sample: any,
|
||||
total: number,
|
||||
}
|
||||
|
||||
/**
|
||||
* Find resources that match a labelSelector. This behaves differently if vai based pagination is on
|
||||
* a) Pagination Enabled - fetch matching resources filtered on backend - findPage
|
||||
* b) Pagination Disabled - fetch all resources and then filter locally - findAll --> root `matching` fn
|
||||
*
|
||||
* This is a much smarter version of root matching fn `matching` from shell/utils/selector.js (which just does local filtering)
|
||||
*
|
||||
* If fetching all of a resource should be avoided or we don't want to mess around with the cache the action `findLabelSelector` should be used
|
||||
* - sometimes some legacy code expects all resources are fetched
|
||||
* - sometimes we want to fetch a resource but not override the cache
|
||||
* - already have a pods list cached, don't want to overwrite that when finding pods associated with a service
|
||||
*
|
||||
* Resources are returned in a common format which includes metadata
|
||||
*/
|
||||
export async function matching({
|
||||
labelSelector,
|
||||
type,
|
||||
inStore,
|
||||
$store,
|
||||
inScopeCount = undefined,
|
||||
namespace = undefined,
|
||||
transient = false,
|
||||
}: {
|
||||
/**
|
||||
* Standard kube label selector object.
|
||||
*
|
||||
* If this is 'empty' (no matchLabels or matchExpressions) it will return all results
|
||||
*
|
||||
* If this is 'null' it will return no results
|
||||
*/
|
||||
labelSelector: KubeLabelSelector,
|
||||
/**
|
||||
* Resource type
|
||||
*/
|
||||
type: string,
|
||||
/**
|
||||
* Store in which resources will be cached
|
||||
*/
|
||||
inStore: string,
|
||||
/**
|
||||
* Standard vuex store object
|
||||
*/
|
||||
$store: any,
|
||||
/**
|
||||
* Number of resources that are applicable when filtering.
|
||||
*
|
||||
* Used to skip any potential http request if we know the result will be zero
|
||||
*
|
||||
* If this property is not supplied we'll try and discover it from the COUNTS resource.
|
||||
*/
|
||||
inScopeCount?: number
|
||||
/**
|
||||
* Optional namespace or namespaces to apply selector to
|
||||
*
|
||||
* If this is undefined then namespaces will totally be ignored
|
||||
*
|
||||
* If this is provided all resources must be within them. If an empty array is provided then no resources will be matched
|
||||
*
|
||||
*/
|
||||
namespace?: string | string[],
|
||||
/**
|
||||
* Should the result bypass the store?
|
||||
*/
|
||||
transient?: boolean,
|
||||
}): Promise<MatchingResponse> {
|
||||
const isNamespaced = $store.getters[`${ inStore }/schemaFor`](type)?.attributes.namespaced;
|
||||
const safeNamespaces = Array.isArray(namespace) ? namespace : !!namespace ? [namespace] : [];
|
||||
const filterByNamespaces = isNamespaced && !!namespace ; // Result set must come from a resource in a namespace
|
||||
|
||||
// Determine if there's actually anything to filter on
|
||||
if (typeof inScopeCount === 'undefined') {
|
||||
const counts = $store.getters[`${ inStore }/all`](COUNT)?.[0]?.counts || {};
|
||||
|
||||
if (filterByNamespaces) {
|
||||
inScopeCount = 0;
|
||||
safeNamespaces.forEach((n) => {
|
||||
inScopeCount += counts?.[type]?.namespaces[n]?.count || 0;
|
||||
});
|
||||
} else {
|
||||
inScopeCount = counts?.[type]?.summary?.count || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Exit early if there are any situations that always return nothing
|
||||
const noCandidates = (inScopeCount || 0) === 0;
|
||||
const filterByNamespaceButNoNamespace = isNamespaced && !!namespace && (!safeNamespaces || safeNamespaces.length === 0);
|
||||
const explicityNullLabelSelector = labelSelector === null || (labelSelector?.matchLabels === null && !labelSelector.matchExpressions === null);
|
||||
|
||||
if (noCandidates || filterByNamespaceButNoNamespace || explicityNullLabelSelector) {
|
||||
return generateMatchingResponse([], inScopeCount || 0);
|
||||
}
|
||||
|
||||
if ($store.getters[`${ inStore }/paginationEnabled`]?.({ id: type })) {
|
||||
if (isLabelSelectorEmpty(labelSelector) && (!!namespace && !safeNamespaces?.length)) {
|
||||
// no namespaces - ALL resources are candidates
|
||||
// no labels - return all candidates
|
||||
// too many to fetch...
|
||||
throw new Error('Either populated labelSelector or namespace/s must be supplied in order to call findPage');
|
||||
}
|
||||
|
||||
const findPageArgs: ActionFindPageArgs = {
|
||||
pagination: new FilterArgs({
|
||||
labelSelector,
|
||||
filters: PaginationParamFilter.createMultipleFields(
|
||||
safeNamespaces.map(
|
||||
(n) => new PaginationFilterField({
|
||||
field: 'metadata.namespace', // API only compatible with steve atm...
|
||||
value: n,
|
||||
})
|
||||
)
|
||||
),
|
||||
}),
|
||||
transient,
|
||||
};
|
||||
|
||||
let match = await $store.dispatch(`${ inStore }/findPage`, { type, opt: findPageArgs });
|
||||
|
||||
if (transient) {
|
||||
match = match.data;
|
||||
}
|
||||
|
||||
return generateMatchingResponse(match, inScopeCount || 0);
|
||||
} else {
|
||||
// Start off with everything as a candidate
|
||||
let candidates = await $store.dispatch(`${ inStore }/findAll`, { type });
|
||||
|
||||
inScopeCount = candidates.length;
|
||||
|
||||
// Filter out namespace specific stuff
|
||||
if (isNamespaced && safeNamespaces?.length > 0) {
|
||||
candidates = candidates.filter((e: any) => safeNamespaces.includes(e.metadata?.namespace));
|
||||
inScopeCount = candidates.length;
|
||||
}
|
||||
|
||||
// Apply labelSelector
|
||||
if (labelSelector.matchLabels || labelSelector.matchExpressions) {
|
||||
candidates = matches(candidates, labelSelector, 'metadata.labels');
|
||||
}
|
||||
|
||||
return generateMatchingResponse(candidates, inScopeCount || 0);
|
||||
}
|
||||
}
|
||||
|
||||
const generateMatchingResponse = <T extends { [key: string]: any, nameDisplay: string}>(match: T[], inScopeCount: number): MatchingResponse => {
|
||||
const matched = match.length || 0;
|
||||
const sample = match[0]?.nameDisplay;
|
||||
|
||||
return {
|
||||
matched,
|
||||
matches: match,
|
||||
none: matched === 0,
|
||||
sample,
|
||||
total: inScopeCount || 0,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This is similar to shell/utils/selector.js `matches`, but accepts a kube labelSelector
|
||||
*/
|
||||
function matches<T = any>(candidates: T[], labelSelector: KubeLabelSelector, labelKey: string): T[] {
|
||||
const convertedObject = convert(labelSelector.matchLabels, labelSelector.matchExpressions);
|
||||
|
||||
return rootMatching(candidates, convertedObject, labelKey);
|
||||
}
|
||||
|
||||
export function isLabelSelectorEmpty(labelSelector?: KubeLabelSelector): boolean {
|
||||
return !labelSelector?.matchExpressions?.length && isEmpty(labelSelector?.matchLabels);
|
||||
}
|
||||
|
||||
export function labelSelectorToSelector(labelSelector?: KubeLabelSelector): string {
|
||||
if (isLabelSelectorEmpty(labelSelector)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const res: string[] = [];
|
||||
|
||||
Object.entries(labelSelector?.matchLabels || {}).forEach(([key, value]) => {
|
||||
res.push(`${ key }=${ value }`);
|
||||
});
|
||||
|
||||
(labelSelector?.matchExpressions || []).forEach((value: KubeLabelSelectorExpression) => {
|
||||
if (value.operator === 'In' && value.values?.length === 1) {
|
||||
res.push(`${ value.key }=${ value.values[0] }`);
|
||||
} else {
|
||||
throw new Error(`Unsupported matchExpression found when converting to selector string. ${ value }`);
|
||||
}
|
||||
});
|
||||
|
||||
return res.join(',');
|
||||
}
|
||||
|
|
@ -11,7 +11,14 @@ const OP_MAP = {
|
|||
'>': 'Gt',
|
||||
};
|
||||
|
||||
// Parse a labelSelector string
|
||||
/**
|
||||
* Convert a string to a matchExpression object
|
||||
*
|
||||
* *string* is from https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go#L1010.
|
||||
* However... it seems to contain more operators than just `=` which the labels.Selector `String` does not use
|
||||
*
|
||||
* matchExpression is an array of 'key' 'operator' 'values'
|
||||
*/
|
||||
export function parse(labelSelector) {
|
||||
// matchLabels:
|
||||
// comma-separated list, all rules ANDed together
|
||||
|
|
@ -32,6 +39,10 @@ export function parse(labelSelector) {
|
|||
// operator: In, NotIn, Exists, or DoesNotExist
|
||||
// values: [array, of, values, even, if, only, one]
|
||||
|
||||
if (!labelSelector) {
|
||||
return [];
|
||||
}
|
||||
|
||||
labelSelector = labelSelector.replace(/\+/g, ' ');
|
||||
|
||||
if ( parseCache[labelSelector] ) {
|
||||
|
|
@ -101,13 +112,17 @@ export function parse(labelSelector) {
|
|||
return out;
|
||||
}
|
||||
|
||||
// Convert a Selector object to matchExpressions
|
||||
/**
|
||||
* Convert a Selector object to matchExpressions
|
||||
*/
|
||||
export function convertSelectorObj(obj) {
|
||||
return convert(obj.matchLabels || {}, obj.matchExpressions || []);
|
||||
}
|
||||
|
||||
// Convert matchLabels to matchExpressions
|
||||
// Optionally combining with an existing set of matchExpressions
|
||||
/**
|
||||
* Convert matchLabels to matchExpressions
|
||||
* Optionally combining with an existing set of matchExpressions
|
||||
*/
|
||||
export function convert(matchLabelsObj, matchExpressions) {
|
||||
const keys = Object.keys(matchLabelsObj || {});
|
||||
const out = matchExpressions || [];
|
||||
|
|
@ -130,8 +145,10 @@ export function convert(matchLabelsObj, matchExpressions) {
|
|||
return out;
|
||||
}
|
||||
|
||||
// Convert matchExpressions to matchLabels when possible,
|
||||
// returning the simplest combination of them.
|
||||
/**
|
||||
* Convert matchExpressions to matchLabels when possible,
|
||||
* returning the simplest combination of them.
|
||||
*/
|
||||
export function simplify(matchExpressionsInput) {
|
||||
const matchLabels = {};
|
||||
const matchExpressions = [];
|
||||
|
|
@ -163,6 +180,12 @@ export function simplify(matchExpressionsInput) {
|
|||
return { matchLabels, matchExpressions };
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* For a fn that accepts a kube labelSelector object see shell/utils/selector-typed.ts `matches`
|
||||
*
|
||||
* @param {*} selector Is NOT a labelSelector object
|
||||
*/
|
||||
export function matches(obj, selector, labelKey = 'metadata.labels') {
|
||||
let rules = [];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue