Skip to content

Commit

Permalink
Handle fetching from inventory for users that don't have permissions …
Browse files Browse the repository at this point in the history
…to list all namespaces

Reference: #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 <[email protected]>
  • Loading branch information
sgratch committed Aug 21, 2024
1 parent 509aa55 commit 9fdf819
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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
}

Expand All @@ -32,31 +43,43 @@ 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<ProvidersInventoryList | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | null>(null);
const oldDataRef = useRef(null);
const oldErrorRef = useRef(null);
const canList: boolean = useFlag('CAN_LIST_NS');
const k8Providers: V1beta1Provider[] = !canList ? GetK8sProvidersForNamespace(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);
Expand All @@ -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 GetK8sProvidersForNamespace(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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -183,7 +188,7 @@ const ProvidersListPage: React.FC<{
inventory,
loading: inventoryLoading,
error: inventoryError,
} = useProvidersInventoryList({});
} = useProvidersInventoryList({ namespace });

const permissions = useGetDeleteAndEditAccessReview({
model: ProviderModel,
Expand Down Expand Up @@ -220,9 +225,9 @@ const ProvidersListPage: React.FC<{
title={t('Providers')}
userSettings={userSettings}
alerts={
!inventoryLoading && inventoryError
? [<InventoryNotReachable key={'inventoryNotReachable'} />]
: undefined
providersLoadError
? handleK8sError(providersLoaded, providersLoadError)
: handleInventoryErrorTypes(inventoryLoading, inventoryError)
}
customNoResultsFound={EmptyState}
page={1}
Expand All @@ -239,6 +244,27 @@ const ModernizeMigration = () => (
<img src={modernizeMigration} className="forklift-empty-state__icon" />
);

const handleK8sError = (providersLoaded: boolean, providersLoadError: Error) => {
if (!providersLoaded && providersLoadError)
return [<ProvidersK8sNotReachable key={'providersK8sNotReachable'} />];
};

const handleInventoryErrorTypes = (inventoryLoading: boolean, inventoryError: Error) => {
if (!inventoryLoading && inventoryError) {
switch (inventoryError?.message) {
case 'namespaceNotAccessible':
return [<NamespaceNotAccessible key={'namespaceNotAccessible'} />];
case 'providersK8sNotReachable':
return [<ProvidersK8sNotReachable key={'providersK8sNotReachable'} />];
case 'inventoryNotReachable':
default:
return [<InventoryNotReachable key={'inventoryNotReachable'} />];
}
} else {
return undefined;
}
};

const EmptyState_: React.FC<EmptyStateProps> = ({ AddButton, namespace }) => {
const { t } = useForkliftTranslation();

Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Alert title={t('Namespace')} variant="warning">
<TextContent>
<Text component={TextVariants.p}>
{t(
'Namespace is not accessible. To troubleshoot, check the Forklift controller pod logs.',
)}
</Text>
</TextContent>
</Alert>
);
};

export default NamespaceNotAccessible;
Original file line number Diff line number Diff line change
@@ -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 (
<Alert title={t('Providers')} variant="warning">
<TextContent>
<Text component={TextVariants.p}>
{t(
'Providers are not accessible. To troubleshoot, check the Forklift controller pod logs.',
)}
</Text>
</TextContent>
</Alert>
);
};

export default ProvidersK8sNotReachable;
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit 9fdf819

Please sign in to comment.