From c7fd8a0a867fa90531e4685bbff8349b5b50b1d8 Mon Sep 17 00:00:00 2001 From: Sharon Gratch Date: Wed, 14 Aug 2024 11:32:06 +0300 Subject: [PATCH] Handle fetching from inventory for users that don't have permissions to list all namespaces Reference: https://github.com/kubev2v/forklift-console-plugin/issues/1293 Handle providers fetching from inventory for users with limited namespace roles, who can only list their namespaces content and don't have get permissions to all namespaces. Before the fix, when those users tried to fetch providers inventory data, regarldess to current namespace, the "inventory server is not reachable" error was always displayed. After this fix, those users will not fetch all namepsaces but only ones which they allow to list. So the fetching should now succeed and no error should be displayed. Signed-off-by: Sharon Gratch --- .../en/plugin__forklift-console-plugin.json | 2 + .../hooks/useProvidersInventoryList.ts | 108 ++++++++++++++++-- .../views/list/ProvidersListPage.tsx | 36 +++++- .../components/NamespaceNotAccessible.tsx | 21 ++++ .../components/ProvidersK8sNotReachable.tsx | 21 ++++ .../Providers/views/list/components/index.ts | 2 + 6 files changed, 176 insertions(+), 14 deletions(-) create mode 100644 packages/forklift-console-plugin/src/modules/Providers/views/list/components/NamespaceNotAccessible.tsx create mode 100644 packages/forklift-console-plugin/src/modules/Providers/views/list/components/ProvidersK8sNotReachable.tsx diff --git a/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json b/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json index 370642153..3fc4c0757 100644 --- a/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json +++ b/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json @@ -260,6 +260,7 @@ "Namespace": "Namespace", "Namespace defines the space within which each name must be unique.\n An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation.\n Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.": "Namespace defines the space within which each name must be unique.\n An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation.\n Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.", "Namespace defines the space within which each name must be unique.\n An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation.\n Not all objects are required to be scoped to a namespace -\n the value of this field for those objects will be empty.": "Namespace defines the space within which each name must be unique.\n An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation.\n Not all objects are required to be scoped to a namespace -\n the value of this field for those objects will be empty.", + "Namespace is not accessible. To troubleshoot, check the Forklift controller pod logs.": "Namespace is not accessible. To troubleshoot, check the Forklift controller pod logs.", "Namespace is not defined": "Namespace is not defined", "Network for data transfer": "Network for data transfer", "Network interfaces": "Network interfaces", @@ -379,6 +380,7 @@ "Provider web UI link": "Provider web UI link", "Provider YAML": "Provider YAML", "Providers": "Providers", + "Providers are not accessible. To troubleshoot, check the Forklift controller pod logs.": "Providers are not accessible. To troubleshoot, check the Forklift controller pod logs.", "Providers default": "Providers default", "Providers for virtualization": "Providers for virtualization", "Ready": "Ready", diff --git a/packages/forklift-console-plugin/src/modules/Providers/hooks/useProvidersInventoryList.ts b/packages/forklift-console-plugin/src/modules/Providers/hooks/useProvidersInventoryList.ts index 3ad506af0..c6c7ef40b 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/hooks/useProvidersInventoryList.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/hooks/useProvidersInventoryList.ts @@ -1,7 +1,16 @@ import { useEffect, useRef, useState } from 'react'; -import { ProviderInventory, ProvidersInventoryList } from '@kubev2v/types'; -import { consoleFetchJSON } from '@openshift-console/dynamic-plugin-sdk'; +import { + ProviderInventory, + ProviderModelGroupVersionKind, + ProvidersInventoryList, + V1beta1Provider, +} from '@kubev2v/types'; +import { + consoleFetchJSON, + useFlag, + useK8sWatchResource, +} from '@openshift-console/dynamic-plugin-sdk'; import { getInventoryApiUrl, hasObjectChangedInGivenFields } from '../utils/helpers'; @@ -12,9 +21,11 @@ const INVENTORY_TYPES: string[] = ['openshift', 'openstack', 'ovirt', 'vsphere', /** * Configuration parameters for useProvidersInventoryList hook. * @interface + * @property {string} namespace - namespace for fetching inventory's providers data for. Used only for users with limited namespaces privileges. * @property {number} interval - Polling interval in milliseconds. */ interface UseInventoryParams { + namespace?: string; interval?: number; // Polling interval in milliseconds } @@ -32,15 +43,19 @@ interface UseInventoryResult { } /** - * A React hook to fetch and maintain an up-to-date list of providers' inventory data. + * A React hook to fetch and maintain an up-to-date list of providers' inventory data, belongs to a given namespace or to all namespaces + * (based on the namespace parameter). + * For users with limited namespaces privileges, only the given namespace's providers inventory data are fetched. * It fetches data on mount and then at the specified interval. * * @param {UseInventoryParams} params - Configuration parameters for the hook. + * @param {string} namespace - namespace to fetch providers' inventory data for. if set to null, then fetch for all namespaces. * @param {number} [params.interval=10000] - Interval (in milliseconds) to fetch new data at. * * @returns {UseInventoryResult} result - Contains the inventory data, the loading state, and the error state. */ export const useProvidersInventoryList = ({ + namespace = null, interval = 20000, }: UseInventoryParams): UseInventoryResult => { const [inventory, setInventory] = useState(null); @@ -48,15 +63,23 @@ export const useProvidersInventoryList = ({ const [error, setError] = useState(null); const oldDataRef = useRef(null); const oldErrorRef = useRef(null); + const canList: boolean = useFlag('CAN_LIST_NS'); + const k8Providers: V1beta1Provider[] = !canList ? useK8sProvidersForNamespace(namespace) : null; useEffect(() => { const fetchData = async () => { try { - const newInventory: ProvidersInventoryList = await consoleFetchJSON( - getInventoryApiUrl(`providers?detail=1`), - ); - - updateInventoryIfChanged(newInventory, DEFAULT_FIELDS_TO_COMPARE); + // Fetch all providers + if (canList) { + const newInventory: ProvidersInventoryList = await consoleFetchJSON( + getInventoryApiUrl(`providers?detail=1`), + ); + updateInventoryIfChanged(newInventory, DEFAULT_FIELDS_TO_COMPARE); + } + // Fetch single namespace's providers + else { + await getAndUpdateInventoryProviders(k8Providers); + } handleError(null); } catch (e) { handleError(e); @@ -67,7 +90,74 @@ export const useProvidersInventoryList = ({ const intervalId = setInterval(fetchData, interval); return () => clearInterval(intervalId); - }, [interval]); + }, [interval, namespace, k8Providers]); + + /** + * For users with limited namespaces privileges, Fetch only the given namespace's k8s providers + * and handle errors if exist. + * + * @param {string} namespace namespace to fetch providers' inventory data for. + * @returns {V1beta1Provider[]} list of providers' k8s data, belongs to the given namespace, or null in case of a failure. + */ + function useK8sProvidersForNamespace(namespace: string): V1beta1Provider[] { + const [k8sProviders, providersLoaded, providersLoadError] = useK8sWatchResource< + V1beta1Provider[] + >({ + groupVersionKind: ProviderModelGroupVersionKind, + namespaced: true, + isList: true, + namespace, + }); + + if (namespace == null) + // Fetching all namespaces is not accessible for non privileged users + handleError(new Error('namespaceNotAccessible')); + else if (!providersLoaded && providersLoadError) + // In case of any other error, return a message that K8s is not reachable + handleError(new Error('providersK8sNotReachable')); + + return k8sProviders; + } + + /** + * For users with limited namespaces privileges, Fetch only the given namespace's inventory providers + * , handle errors if exist and update the UI if changed. + * + * @param {V1beta1Provider[]} k8Providers providers to fetch inventory data for. + * @returns {void} + */ + const getAndUpdateInventoryProviders = async (k8Providers: V1beta1Provider[]) => { + const newInventory: ProvidersInventoryList = { + openshift: [], + openstack: [], + ovirt: [], + vsphere: [], + ova: [], + }; + + const allPromises = Promise.all( + k8Providers + ?.filter((provider) => provider.status.phase === 'Ready') + .map(async (provider) => { + return await consoleFetchJSON( + getInventoryApiUrl(`providers/${provider.spec.type}/${provider.metadata.uid}`), + ); + }), + ); + + allPromises + .then((newInventoryProviders) => { + newInventoryProviders.map((newInventoryProvider) => + newInventory[newInventoryProvider.type].push(newInventoryProvider), + ); + }) + .catch((error) => { + handleError(error); + }) + .finally(() => { + updateInventoryIfChanged(newInventory, DEFAULT_FIELDS_TO_COMPARE); + }); + }; /** * Handles any errors thrown when trying to fetch the inventory. diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/list/ProvidersListPage.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/list/ProvidersListPage.tsx index af4b8036d..5b32ce842 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/list/ProvidersListPage.tsx +++ b/packages/forklift-console-plugin/src/modules/Providers/views/list/ProvidersListPage.tsx @@ -23,7 +23,12 @@ import modernizeMigration from '../../images/modernizeMigration.svg'; import { findInventoryByID } from '../../utils'; import { InventoryNotReachable } from './components/InventoryNotReachable'; -import { ProvidersAddButton, ProvidersEmptyState } from './components'; +import { + NamespaceNotAccessible, + ProvidersAddButton, + ProvidersEmptyState, + ProvidersK8sNotReachable, +} from './components'; import ProviderRow from './ProviderRow'; import './ProvidersListPage.style.css'; @@ -183,7 +188,7 @@ const ProvidersListPage: React.FC<{ inventory, loading: inventoryLoading, error: inventoryError, - } = useProvidersInventoryList({}); + } = useProvidersInventoryList({ namespace }); const permissions = useGetDeleteAndEditAccessReview({ model: ProviderModel, @@ -220,9 +225,9 @@ const ProvidersListPage: React.FC<{ title={t('Providers')} userSettings={userSettings} alerts={ - !inventoryLoading && inventoryError - ? [] - : undefined + providersLoadError + ? handleK8sError(providersLoaded, providersLoadError) + : handleInventoryErrorTypes(inventoryLoading, inventoryError) } customNoResultsFound={EmptyState} page={1} @@ -239,6 +244,27 @@ const ModernizeMigration = () => ( ); +const handleK8sError = (providersLoaded: boolean, providersLoadError: Error) => { + if (!providersLoaded && providersLoadError) + return []; +}; + +const handleInventoryErrorTypes = (inventoryLoading: boolean, inventoryError: Error) => { + if (!inventoryLoading && inventoryError) { + switch (inventoryError?.message) { + case 'namespaceNotAccessible': + return []; + case 'providersK8sNotReachable': + return []; + case 'inventoryNotReachable': + default: + return []; + } + } else { + return undefined; + } +}; + const EmptyState_: React.FC = ({ AddButton, namespace }) => { const { t } = useForkliftTranslation(); diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/list/components/NamespaceNotAccessible.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/list/components/NamespaceNotAccessible.tsx new file mode 100644 index 000000000..f8a3e929f --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/list/components/NamespaceNotAccessible.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Alert, Text, TextContent, TextVariants } from '@patternfly/react-core'; + +export const NamespaceNotAccessible: React.FC = () => { + const { t } = useTranslation(); + return ( + + + + {t( + 'Namespace is not accessible. To troubleshoot, check the Forklift controller pod logs.', + )} + + + + ); +}; + +export default NamespaceNotAccessible; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/list/components/ProvidersK8sNotReachable.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/list/components/ProvidersK8sNotReachable.tsx new file mode 100644 index 000000000..5d0f0ba56 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/list/components/ProvidersK8sNotReachable.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Alert, Text, TextContent, TextVariants } from '@patternfly/react-core'; + +export const ProvidersK8sNotReachable: React.FC = () => { + const { t } = useTranslation(); + return ( + + + + {t( + 'Providers are not accessible. To troubleshoot, check the Forklift controller pod logs.', + )} + + + + ); +}; + +export default ProvidersK8sNotReachable; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/list/components/index.ts b/packages/forklift-console-plugin/src/modules/Providers/views/list/components/index.ts index 7b4a43165..6b9b28041 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/list/components/index.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/views/list/components/index.ts @@ -3,11 +3,13 @@ export * from './CellProps'; export * from './InventoryCellFactory'; export * from './InventoryNotReachable'; export * from './NamespaceCell'; +export * from './NamespaceNotAccessible'; export * from './OpenshiftNetworkCell'; export * from './ProviderCriticalCondition'; export * from './ProviderLinkCell'; export * from './ProvidersAddButton'; export * from './ProvidersEmptyState'; +export * from './ProvidersK8sNotReachable'; export * from './StatusCell'; export * from './TypeCell'; export * from './URLCell';