@@ -189,11 +236,12 @@ export default {
label-key="performance.experimental"
/>
diff --git a/shell/plugins/dashboard-store/actions.js b/shell/plugins/dashboard-store/actions.js
index f61caf30653..7c1660cc355 100644
--- a/shell/plugins/dashboard-store/actions.js
+++ b/shell/plugins/dashboard-store/actions.js
@@ -317,13 +317,15 @@ export default {
// ToDo: SM if we start a "bigger" watch (such as watch without a namespace vs a watch with a namespace), we should stop the stop the "smaller" watch so we don't have duplicate events coming back
if ( opt.watch !== false ) {
- dispatch('watch', {
+ const args = {
type,
revision: out.revision,
namespace: opt.watchNamespace || opt.namespaced, // it could be either apparently
// ToDo: SM namespaced is sometimes a boolean and sometimes a string, I don't see it as especially broken but we should refactor that in the future
force: opt.forceWatch === true,
- });
+ };
+
+ dispatch('watch', args);
}
const all = getters.all(type);
diff --git a/shell/plugins/dashboard-store/getters.js b/shell/plugins/dashboard-store/getters.js
index e7d21e583f6..857dc7ee0a2 100644
--- a/shell/plugins/dashboard-store/getters.js
+++ b/shell/plugins/dashboard-store/getters.js
@@ -274,6 +274,12 @@ export default {
return false;
},
+ haveNamespace: (state, getters) => (type) => {
+ type = getters.normalizeType(type);
+
+ return state.types[type]?.haveNamespace || null;
+ },
+
haveSelector: (state, getters) => (type, selector) => {
type = getters.normalizeType(type);
const entry = state.types[type];
diff --git a/shell/plugins/steve/actions.js b/shell/plugins/steve/actions.js
index 722e934c48f..ba1a5742100 100644
--- a/shell/plugins/steve/actions.js
+++ b/shell/plugins/steve/actions.js
@@ -11,7 +11,7 @@ import jsyaml from 'js-yaml';
export default {
- // Need to override this, so that thhe 'this' context is correct (this class not the base class)
+ // Need to override this, so that the 'this' context is correct (this class not the base class)
async loadSchemas(ctx, watch = true) {
return await loadSchemas(ctx, watch);
},
diff --git a/shell/plugins/steve/getters.js b/shell/plugins/steve/getters.js
index 2c4908fad54..f64ade14dbf 100644
--- a/shell/plugins/steve/getters.js
+++ b/shell/plugins/steve/getters.js
@@ -8,6 +8,7 @@ import HybridModel, { cleanHybridResources } from './hybrid-class';
import NormanModel from './norman-class';
import { urlFor } from '@shell/plugins/dashboard-store/getters';
import { normalizeType } from '@shell/plugins/dashboard-store/normalize';
+import pAndNFiltering from '@shell/utils/projectAndNamespaceFiltering.utils';
export const STEVE_MODEL_TYPES = {
NORMAN: 'norman',
@@ -42,6 +43,15 @@ export default {
});
});
}
+
+ // `opt.namespaced` is either
+ // - a string representing a single namespace - add restriction to the url
+ // - an array of namespaces or projects - add restriction as a param
+ const namespaceProjectFilter = pAndNFiltering.checkAndCreateParam(opt);
+
+ if (namespaceProjectFilter) {
+ url += `${ (url.includes('?') ? '&' : '?') + namespaceProjectFilter }`;
+ }
// End: Filter
// Limit
@@ -72,12 +82,13 @@ export default {
urlFor: (state, getters) => (type, id, opt) => {
let url = urlFor(state, getters)(type, id, opt);
- if (opt.namespaced) {
+ // `namespaced` is either
+ // - a string representing a single namespace - add restriction to the url
+ // - an array of namespaces or projects - add restriction as a param
+ if (opt.namespaced && !pAndNFiltering.isApplicable(opt)) {
const parts = url.split('/');
url = `${ parts.join('/') }/${ opt.namespaced }`;
-
- return url;
}
return url;
diff --git a/shell/plugins/steve/subscribe.js b/shell/plugins/steve/subscribe.js
index 9256179ee21..f3e7f22983f 100644
--- a/shell/plugins/steve/subscribe.js
+++ b/shell/plugins/steve/subscribe.js
@@ -31,6 +31,7 @@ import { escapeHtml } from '@shell/utils/string';
import { keyForSubscribe } from '@shell/plugins/steve/resourceWatcher';
import { waitFor } from '@shell/utils/async';
import { WORKER_MODES } from './worker';
+import pAndNFiltering from '@shell/utils/projectAndNamespaceFiltering.utils';
import { BLANK_CLUSTER } from '@shell/store/index.js';
import { STORE } from '@shell/store/store-types';
@@ -104,7 +105,7 @@ export async function createWorker(store, ctx) {
}
},
batchChanges: (batch) => {
- dispatch('batchChanges', batch);
+ dispatch('batchChanges', namespaceHandler.validateBatchChange(ctx, batch));
},
dispatch: (msg) => {
dispatch(`ws.${ msg.name }`, msg);
@@ -178,7 +179,67 @@ export function equivalentWatch(a, b) {
return true;
}
-function queueChange({ getters, state }, { data, revision }, load, label) {
+/**
+ * Sockets will not be able to subscribe to more than one namespace. If this is requested we pretend to handle it
+ * - Changes to all resources are monitored (no namespace provided in sub)
+ * - We ignore any events not from a required namespace (we have the conversion of project --> namespaces already)
+ */
+const namespaceHandler = {
+ /**
+ * Note - namespace can be a list of projects or namespaces
+ */
+ subscribeNamespace: (namespace) => {
+ if (pAndNFiltering.isApplicable({ namespaced: namespace }) && namespace.length) {
+ return undefined; // AKA sub to everything
+ }
+
+ return namespace;
+ },
+
+ validChange: ({ getters, rootGetters }, type, data) => {
+ const haveNamespace = getters.haveNamespace(type);
+
+ if (haveNamespace?.length) {
+ const namespaces = rootGetters.activeNamespaceCache;
+
+ if (!namespaces[data.metadata.namespace]) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ validateBatchChange: ({ getters, rootGetters }, batch) => {
+ const namespaces = rootGetters.activeNamespaceCache;
+
+ Object.entries(batch).forEach(([type, entries]) => {
+ const haveNamespace = getters.haveNamespace(type);
+
+ if (!haveNamespace?.length) {
+ return;
+ }
+
+ const schema = getters.schemaFor(type);
+
+ if (!schema?.attributes?.namespaced) {
+ return;
+ }
+
+ Object.keys(entries).forEach((id) => {
+ const namespace = id.split('/')[0];
+
+ if (!namespace || !namespaces[namespace]) {
+ delete entries[id];
+ }
+ });
+ });
+
+ return batch;
+ }
+};
+
+function queueChange({ getters, state, rootGetters }, { data, revision }, load, label) {
const type = getters.normalizeType(data.type);
const entry = getters.typeEntry(type);
@@ -191,6 +252,10 @@ function queueChange({ getters, state }, { data, revision }, load, label) {
// console.log(`${ label } Event [${ state.config.namespace }]`, data.type, data.id); // eslint-disable-line no-console
+ if (!namespaceHandler.validChange({ getters, rootGetters }, type, data)) {
+ return;
+ }
+
if ( load ) {
state.queue.push({
action: 'dispatch',
@@ -335,6 +400,7 @@ const sharedActions = {
type, selector, id, revision, namespace, stop, force
} = params;
+ namespace = namespaceHandler.subscribeNamespace(namespace);
type = getters.normalizeType(type);
if (rootGetters['type-map/isSpoofed'](type)) {
@@ -413,6 +479,8 @@ const sharedActions = {
const { commit, getters, dispatch } = ctx;
if (getters['schemaFor'](type)) {
+ namespace = namespaceHandler.subscribeNamespace(namespace);
+
const obj = {
type,
id,
diff --git a/shell/store/index.js b/shell/store/index.js
index 6860885189f..163501872be 100644
--- a/shell/store/index.js
+++ b/shell/store/index.js
@@ -27,6 +27,7 @@ import {
NAMESPACE_FILTER_NAMESPACED_PREFIX as NAMESPACED_PREFIX,
NAMESPACE_FILTER_NAMESPACED_YES as NAMESPACED_YES,
splitNamespaceFilterKey,
+ NAMESPACE_FILTER_NS_FULL_PREFIX,
} from '@shell/utils/namespace-filter';
import { allHash, allHashSettled } from '@shell/utils/promise';
import { sortBy } from '@shell/utils/sort';
@@ -242,7 +243,6 @@ export const state = () => {
serverVersion: null,
systemNamespaces: [],
isSingleProduct: undefined,
- namespaceFilterMode: null,
};
};
@@ -279,15 +279,6 @@ export const getters = {
return state.systemNamespaces;
},
- /**
- * Namespace Filter Mode supplies a resource type to the NamespaceFilter.
- *
- * Only one of the resource type is allowed to be selected
- */
- namespaceFilterMode(state) {
- return state.namespaceFilterMode;
- },
-
currentCluster(state, getters) {
return getters['management/byId'](MANAGEMENT.CLUSTER, state.clusterId);
},
@@ -373,34 +364,6 @@ export const getters = {
return state.namespaceFilters.filter(x => !`${ x }`.startsWith(NAMESPACED_PREFIX)).length === 0;
},
- isSingleNamespace(state, getters) {
- const product = getters['currentProduct'];
-
- if ( !product ) {
- return false;
- }
-
- if ( product.showWorkspaceSwitcher ) {
- return false;
- }
-
- if ( getters.isAllNamespaces ) {
- return false;
- }
-
- const filters = state.namespaceFilters;
-
- if ( filters.length !== 1 ) {
- return false;
- }
-
- if (filters[0].startsWith('ns://')) {
- return filters[0];
- }
-
- return false;
- },
-
isMultipleNamespaces(state, getters) {
const product = getters['currentProduct'];
@@ -422,7 +385,7 @@ export const getters = {
return true;
}
- return !filters[0].startsWith('ns://');
+ return !filters[0].startsWith(NAMESPACE_FILTER_NS_FULL_PREFIX);
},
namespaceFilters(state) {
@@ -622,10 +585,6 @@ export const mutations = {
state.allNamespaces = namespace;
},
- setNamespaceFilterMode(state, mode) {
- state.namespaceFilterMode = mode;
- },
-
pageActions(state, pageActions) {
state.pageActions = pageActions;
},
@@ -968,10 +927,6 @@ export const actions = {
commit('updateNamespaces', { filters: ids });
},
- setNamespaceFilterMode({ commit }, mode) {
- commit('setNamespaceFilterMode', mode);
- },
-
async cleanNamespaces({ getters, dispatch }) {
// Initialise / Remove any filters that the user no-longer has access to
await dispatch('management/findAll', { type: MANAGEMENT.CLUSTER }); // So they can be got byId below
diff --git a/shell/utils/namespace-filter.js b/shell/utils/namespace-filter.js
index 6fae34ba427..a039510485d 100644
--- a/shell/utils/namespace-filter.js
+++ b/shell/utils/namespace-filter.js
@@ -1,13 +1,25 @@
-export const NAMESPACE_FILTER_SPECIAL = 'special';
+export const NAMESPACE_FILTER_ALL_PREFIX = 'all';
+export const NAMESPACE_FILTER_NS_PREFIX = 'ns';
+export const NAMESPACE_FILTER_P_PREFIX = 'project';
-export const NAMESPACE_FILTER_ALL = 'all';
-export const NAMESPACE_FILTER_ALL_SYSTEM = 'all://system';
-export const NAMESPACE_FILTER_ALL_USER = 'all://user';
-export const NAMESPACE_FILTER_ALL_ORPHANS = 'all://orphans';
+export const NAMESPACE_FILTER_NS_FULL_PREFIX = `${ NAMESPACE_FILTER_NS_PREFIX }://`;
+export const NAMESPACE_FILTER_P_FULL_PREFIX = `${ NAMESPACE_FILTER_P_PREFIX }://`;
+
+export const NAMESPACE_FILTER_ALL = NAMESPACE_FILTER_ALL_PREFIX;
+export const NAMESPACE_FILTER_ALL_SYSTEM = `${ NAMESPACE_FILTER_ALL_PREFIX }://system`;
+export const NAMESPACE_FILTER_ALL_USER = `${ NAMESPACE_FILTER_ALL_PREFIX }://user`;
+export const NAMESPACE_FILTER_ALL_ORPHANS = `${ NAMESPACE_FILTER_ALL_PREFIX }://orphans`;
export const NAMESPACE_FILTER_NAMESPACED_PREFIX = 'namespaced://';
export const NAMESPACE_FILTER_NAMESPACED_YES = 'namespaced://true';
export const NAMESPACE_FILTER_NAMESPACED_NO = 'namespaced://false';
+export const NAMESPACE_FILTER_KINDS = {
+ DIVIDER: 'divider',
+ PROJECT: 'project',
+ NAMESPACE: 'namespace',
+ SPECIAL: 'special'
+};
+
const SEPARATOR = '__%%__';
export const createNamespaceFilterKey = (clusterId, product) => {
diff --git a/shell/utils/projectAndNamespaceFiltering.utils.ts b/shell/utils/projectAndNamespaceFiltering.utils.ts
new file mode 100644
index 00000000000..d7284cbf74c
--- /dev/null
+++ b/shell/utils/projectAndNamespaceFiltering.utils.ts
@@ -0,0 +1,62 @@
+import { NAMESPACE_FILTER_NS_FULL_PREFIX, NAMESPACE_FILTER_P_FULL_PREFIX } from '@shell/utils/namespace-filter';
+import { getPerformanceSetting } from '@shell/utils/settings';
+
+type Opt = { [key: string]: any, namespaced?: string[]}
+
+class ProjectAndNamespaceFiltering {
+ static param = 'projectsornamespaces'
+
+ /**
+ * Does the request `opt` definition require resources are fetched from a specific set namespaces/projects?
+ */
+ isApplicable(opt: Opt): boolean {
+ return Array.isArray(opt.namespaced);
+ }
+
+ isEnabled(rootGetters: any): boolean {
+ const currentProduct = rootGetters['currentProduct'];
+
+ // Only enable for the cluster store at the moment. In theory this should work in management as well, as they're both 'steve' stores
+ if (currentProduct?.inStore !== 'cluster') {
+ return false;
+ }
+
+ if (currentProduct?.showWorkspaceSwitcher) {
+ return false;
+ }
+
+ const perfConfig = getPerformanceSetting(rootGetters);
+
+ if (!perfConfig.forceNsFilterV2?.enabled) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if `opt` requires resources from specific ns/projects, if so return the required query param (x=y)
+ */
+ checkAndCreateParam(opt: Opt): string {
+ if (!this.isApplicable(opt)) {
+ return '';
+ }
+
+ return this.createParam(opt.namespaced);
+ }
+
+ private createParam(namespaceFilter: string[] | undefined): string {
+ if (!namespaceFilter || !namespaceFilter.length) {
+ return '';
+ }
+
+ const projectsOrNamespaces = namespaceFilter
+ .map(f => f.replace(NAMESPACE_FILTER_NS_FULL_PREFIX, '')
+ .replace(NAMESPACE_FILTER_P_FULL_PREFIX, ''))
+ .join(',');
+
+ return `${ ProjectAndNamespaceFiltering.param }=${ projectsOrNamespaces }`;
+ }
+}
+
+export default new ProjectAndNamespaceFiltering();
diff --git a/shell/utils/settings.ts b/shell/utils/settings.ts
index 9e76555daa8..55727d77e6d 100644
--- a/shell/utils/settings.ts
+++ b/shell/utils/settings.ts
@@ -44,19 +44,17 @@ export const setSetting = async(store: Store
, id: string, val: string): Pro
};
export const getPerformanceSetting = (rootGetters: Record any>) => {
- const perfSetting = rootGetters['management/byId'](MANAGEMENT.SETTING, SETTING.UI_PERFORMANCE);
- let perfConfig = {};
+ const perfSettingResource = rootGetters['management/byId'](MANAGEMENT.SETTING, SETTING.UI_PERFORMANCE);
+ let perfSetting = {};
- if (perfSetting && perfSetting.value) {
+ if (perfSettingResource?.value) {
try {
- perfConfig = JSON.parse(perfSetting.value);
+ perfSetting = JSON.parse(perfSettingResource.value);
} catch (e) {
console.warn('ui-performance setting contains invalid data'); // eslint-disable-line no-console
}
}
// Start with the default and overwrite the values from the setting - ensures we have defaults for newly added options
- perfConfig = Object.assign(DEFAULT_PERF_SETTING, perfConfig);
-
- return perfConfig;
+ return Object.assign(DEFAULT_PERF_SETTING, perfSetting || {});
};