@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
195 lines (194 loc) • 10.7 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/*
* Copyright 2025 The Kubernetes Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InlineIcon } from '@iconify/react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import { styled } from '@mui/system';
import _ from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { generatePath, useHistory, useLocation } from 'react-router-dom';
import { getAppUrl } from '../../helpers/getAppUrl';
import { getCluster, getClusterPrefixedPath } from '../../lib/cluster';
import { useClustersConf } from '../../lib/k8s';
import { testAuth } from '../../lib/k8s/apiProxy';
import { createRouteURL, getRoute, getRoutePath } from '../../lib/router';
import { setConfig } from '../../redux/configSlice';
import { ClusterDialog } from '../cluster/Chooser';
import { Link, Loader } from '../common';
import { DialogTitle } from '../common/Dialog';
import Empty from '../common/EmptyContent';
import OauthPopup from '../oidcauth/OauthPopup';
const ColorButton = styled(Button)(({ theme }) => ({
color: theme.palette.primary.contrastText,
backgroundColor: theme.palette.primaryColor,
width: '14rem',
padding: '0.5rem 2rem',
'&:hover': {
opacity: '0.8',
backgroundColor: theme.palette.text.primary,
},
}));
function AuthChooser({ children }) {
const history = useHistory();
const location = useLocation();
const clusters = useClustersConf();
const dispatch = useDispatch();
const [testingAuth, setTestingAuth] = React.useState(true);
const [error, setError] = React.useState(null);
const { from = { pathname: createRouteURL('cluster') } } = (location.state ||
{});
const clusterName = getCluster();
const { t } = useTranslation();
const clustersRef = React.useRef(null);
const cancelledRef = React.useRef(false);
let clusterAuthType = '';
if (clusters && clusters[clusterName]) {
clusterAuthType = clusters[clusterName].auth_type;
}
const numClusters = Object.keys(clusters || {}).length;
function runTestAuthAgain() {
setError(null);
clustersRef.current = null;
}
React.useEffect(() => {
const sameClusters = _.isEqual(clustersRef.current, clusters);
if (!sameClusters) {
clustersRef.current = clusters;
}
const clusterName = getCluster();
// Reset the testing auth state just to prevent the early return from this function
// without actually testing auth, which would cause the auth chooser to never show up.
setTestingAuth(false);
if (!clusterName || !clusters || sameClusters || error || numClusters === 0) {
return;
}
const cluster = clusters[clusterName];
if (!cluster) {
return;
}
// If we haven't yet figured whether we need to use a token for the current
// cluster, then we check here.
// With clusterAuthType == oidc,
// they are presented with a choice of login or enter token.
if (clusterAuthType !== 'oidc' && cluster.useToken === undefined) {
let useToken = true;
setTestingAuth(true);
let errorObj = null;
console.debug('Testing auth at authchooser');
testAuth(clusterName)
.then(() => {
console.debug('Not requiring token as testing auth succeeded');
useToken = false;
})
.catch(err => {
if (!cancelledRef.current) {
console.debug(`Requiring token for ${clusterName} as testing auth failed:`, err);
// Ideally we'd only not assign the error if it was 401 or 403 (so we let the logic
// proceed to request a token), but let's first check whether this is all we get
// from clusters that require a token.
if ([408, 504, 502].includes(err.status)) {
errorObj = err;
}
setTestingAuth(false);
}
})
.finally(() => {
if (!cancelledRef.current) {
setTestingAuth(false);
if (!!errorObj) {
if (!_.isEqual(errorObj, error)) {
setError(errorObj);
}
return;
}
else {
setError(null);
}
cluster.useToken = useToken;
dispatch(setConfig({ clusters: { ...clusters } }));
// If we don't require a token, then we just move to the attempted URL or root.
if (!useToken) {
history.replace(from);
}
// If we reach this point, then we know whether or not we need a token. If we don't,
// just redirect.
if (cluster.useToken === false) {
history.replace(from);
}
else if (!clusterAuthType) {
// we know that it requires token and also doesn't have oidc configured
// so let's redirect to token page
history.replace({
pathname: generatePath(getClusterPrefixedPath('token'), {
cluster: clusterName,
}),
});
}
}
});
}
else if (cluster.useToken) {
history.replace({
pathname: generatePath(getClusterPrefixedPath('token'), {
cluster: clusterName,
}),
});
}
},
// eslint-disable-next-line
[clusters, error]);
// Ensure we have a way to know in the testAuth result whether this component is no longer
// mounted.
React.useEffect(() => {
return function cleanup() {
cancelledRef.current = true;
};
}, []);
return (_jsx(PureAuthChooser, { clusterName: clusterName, testingTitle: numClusters > 1
? t('Getting auth info: {{ clusterName }}', { clusterName })
: t('Getting auth info'), testingAuth: testingAuth, title: numClusters > 1
? t('Authentication: {{ clusterName }}', { clusterName })
: t('Authentication'), error: error, oauthUrl: `${getAppUrl()}oidc?dt=${Date()}&cluster=${getCluster()}`, clusterAuthType: clusterAuthType, handleTryAgain: runTestAuthAgain, handleOidcAuth: () => {
history.replace({
pathname: generatePath(getClusterPrefixedPath(), {
cluster: clusterName,
}),
});
}, handleBackButtonPress: () => {
numClusters > 1 ? history.goBack() : history.push('/');
}, handleTokenAuth: () => {
history.push({
pathname: generatePath(getRoutePath(getRoute('token')), {
cluster: clusterName,
}),
});
}, children: children }));
}
export function PureAuthChooser({ title, testingTitle, testingAuth, error, oauthUrl, clusterAuthType, handleOidcAuth, handleTokenAuth, handleTryAgain, handleBackButtonPress, children, clusterName, }) {
const { t } = useTranslation();
function onClose() {
// Do nothing because we're not supposed to close on backdrop click or escape.
}
return (_jsxs(ClusterDialog, { useCover: true, onClose: onClose, "aria-labelledby": "authchooser-dialog-title", children: [testingAuth ? (_jsxs(Box, { component: "main", textAlign: "center", children: [_jsx(DialogTitle, { id: "authchooser-dialog-title", focusTitle: true, children: testingTitle }), _jsx(Loader, { title: t('Testing auth') })] })) : (_jsxs(Box, { component: "main", display: "flex", flexDirection: "column", alignItems: "center", children: [_jsx(DialogTitle, { id: "authchooser-dialog-title", focusTitle: true, children: title }), !error ? (_jsxs(Box, { children: [clusterAuthType === 'oidc' ? (_jsx(Box, { m: 2, children: _jsx(OauthPopup, { onCode: handleOidcAuth, url: oauthUrl, title: t('Headlamp Cluster Authentication'), button: ColorButton, children: t('Sign In') }) })) : null, _jsx(Box, { m: 2, children: _jsx(ColorButton, { onClick: handleTokenAuth, children: t('Use A Token') }) }), _jsx(Box, { m: 2, textAlign: "center", children: _jsx(Link, { routeName: "settingsCluster", params: { clusterID: clusterName }, children: t('translation|Cluster settings') }) })] })) : (_jsxs(Box, { alignItems: "center", textAlign: "center", children: [_jsxs(Box, { m: 2, children: [_jsx(Empty, { children: error && error.message === 'Bad Gateway'
? t('Failed to connect. Please make sure the Kubernetes cluster is running and accessible. Error: {{ errorMessage }}', { errorMessage: error.message })
: t('Failed to get authentication information: {{ errorMessage }}', {
errorMessage: error.message,
}) }), _jsx(Link, { routeName: "settingsClusterHomeContext", children: t('translation|Cluster settings') })] }), _jsx(Button, { variant: "contained", color: "primary", onClick: handleTryAgain, children: t('translation|Try Again') })] }))] })), _jsx(Box, { display: "flex", flexDirection: "column", alignItems: "center", children: _jsxs(Box, { m: 2, display: "flex", alignItems: "center", style: { cursor: 'pointer' }, onClick: handleBackButtonPress, role: "button", children: [_jsx(Box, { pt: 0.5, children: _jsx(InlineIcon, { icon: "mdi:chevron-left", height: 20, width: 20 }) }), _jsx(Box, { fontSize: 14, style: { textTransform: 'uppercase' }, children: t('translation|Back') })] }) }), children] }));
}
export default AuthChooser;