diff --git a/charts/headlamp/templates/deployment.yaml b/charts/headlamp/templates/deployment.yaml index bfe7831f2ca..0e85d98b0d4 100644 --- a/charts/headlamp/templates/deployment.yaml +++ b/charts/headlamp/templates/deployment.yaml @@ -212,11 +212,11 @@ spec: {{- end }} {{- if $oidc.useAccessToken }} - name: OIDC_USE_ACCESS_TOKEN - value: {{ $oidc.useAccessToken }} + value: {{ $oidc.useAccessToken | quote }} {{- end }} {{- if $oidc.usePKCE }} - name: OIDC_USE_PKCE - value: {{ $oidc.usePKCE }} + value: {{ $oidc.usePKCE | quote }} {{- end }} {{- if $oidc.meUserInfoURL }} - name: ME_USER_INFO_URL diff --git a/charts/headlamp/templates/secret.yaml b/charts/headlamp/templates/secret.yaml index afda05afa01..c2a763106e1 100644 --- a/charts/headlamp/templates/secret.yaml +++ b/charts/headlamp/templates/secret.yaml @@ -34,5 +34,8 @@ data: {{- with .usePKCE }} usePKCE: {{ . | toString | b64enc | quote }} {{- end }} +{{- with .meUserInfoURL }} + meUserInfoURL: {{ . | b64enc | quote }} +{{- end }} {{- end }} {{- end }} diff --git a/charts/headlamp/tests/expected_templates/me-user-info-url.yaml b/charts/headlamp/tests/expected_templates/me-user-info-url.yaml index 2043f0d7c9e..6b7175bfc2c 100644 --- a/charts/headlamp/tests/expected_templates/me-user-info-url.yaml +++ b/charts/headlamp/tests/expected_templates/me-user-info-url.yaml @@ -20,6 +20,7 @@ metadata: namespace: default type: Opaque data: + meUserInfoURL: "L29hdXRoMi91c2VyaW5mb2N1c3RvbTI=" --- # Source: headlamp/templates/clusterrolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 diff --git a/charts/headlamp/tests/expected_templates/oidc-pkce.yaml b/charts/headlamp/tests/expected_templates/oidc-pkce.yaml index 1cbd3638905..5fe12a85b9f 100644 --- a/charts/headlamp/tests/expected_templates/oidc-pkce.yaml +++ b/charts/headlamp/tests/expected_templates/oidc-pkce.yaml @@ -107,7 +107,7 @@ spec: - name: OIDC_SCOPES value: testScope - name: OIDC_USE_PKCE - value: true + value: "true" args: - "-in-cluster" - "-plugins-dir=/headlamp/plugins" diff --git a/charts/headlamp/tests/expected_templates/oidc-validator-overrides.yaml b/charts/headlamp/tests/expected_templates/oidc-validator-overrides.yaml index 79fc3f6ad47..9a402653b83 100644 --- a/charts/headlamp/tests/expected_templates/oidc-validator-overrides.yaml +++ b/charts/headlamp/tests/expected_templates/oidc-validator-overrides.yaml @@ -111,7 +111,7 @@ spec: - name: OIDC_VALIDATOR_ISSUER_URL value: overriddenIssuerURL - name: OIDC_USE_ACCESS_TOKEN - value: true + value: "true" args: - "-in-cluster" - "-plugins-dir=/headlamp/plugins" diff --git a/frontend/src/components/App/RouteSwitcher.tsx b/frontend/src/components/App/RouteSwitcher.tsx index 06ee2e6f9f8..304e33f13d0 100644 --- a/frontend/src/components/App/RouteSwitcher.tsx +++ b/frontend/src/components/App/RouteSwitcher.tsx @@ -155,7 +155,7 @@ function AuthRoute(props: AuthRouteProps) { computedMatch = {}, ...other } = props; - const redirectRoute = getCluster() ? 'login' : 'chooser'; + const redirectRoute = getCluster() ? 'token' : 'chooser'; useSidebarItem(sidebar, computedMatch); const cluster = useCluster(); const query = useQuery({ diff --git a/frontend/src/components/account/Auth.tsx b/frontend/src/components/account/Auth.tsx index e3e8e03af97..159f7edd82d 100644 --- a/frontend/src/components/account/Auth.tsx +++ b/frontend/src/components/account/Auth.tsx @@ -25,7 +25,7 @@ import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import React from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { generatePath, useHistory } from 'react-router-dom'; +import { generatePath, useHistory, useLocation } from 'react-router-dom'; import { setToken } from '../../lib/auth'; import { getCluster, getClusterPrefixedPath } from '../../lib/cluster'; import { useClustersConf } from '../../lib/k8s'; @@ -37,6 +37,7 @@ import HeadlampLink from '../common/Link'; export default function AuthToken() { const history = useHistory(); + const location = useLocation<{ from: { pathname: string; search: string } }>(); const clusterConf = useClustersConf(); const [token, setToken] = React.useState(''); const [showError, setShowError] = React.useState(false); @@ -45,13 +46,17 @@ export default function AuthToken() { function onAuthClicked() { loginWithToken(token).then(code => { - // If successful, redirect. + // If successful, redirect if (code === 200) { - history.replace( - generatePath(getClusterPrefixedPath(), { - cluster: getCluster() as string, - }) - ); + if (location.state && location.state.from) { + history.replace(location.state.from); + } else { + history.replace( + generatePath(getClusterPrefixedPath(), { + cluster: getCluster() as string, + }) + ); + } } else { setToken(''); setShowError(true); diff --git a/frontend/src/components/common/Resource/EnvVarDisplay.stories.tsx b/frontend/src/components/common/Resource/EnvVarDisplay.stories.tsx new file mode 100644 index 00000000000..1a85ca57d9a --- /dev/null +++ b/frontend/src/components/common/Resource/EnvVarDisplay.stories.tsx @@ -0,0 +1,90 @@ +/* + * 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 { Meta, StoryFn } from '@storybook/react'; +import { TestContext } from '../../../test'; +import { EnvVarGrid } from './EnvVarDisplay'; + +export default { + title: 'Resource/EnvVarDisplay', + component: EnvVarGrid, + decorators: [ + Story => ( + + + + ), + ], + argTypes: { + namespace: { control: 'text' }, + cluster: { control: 'text' }, + }, +} as Meta; + +const Template: StoryFn> = args => ; + +export const PlainValues = Template.bind({}); +PlainValues.args = { + namespace: 'default', + cluster: 'minikube', + envVars: [ + { name: 'NODE_ENV', value: 'production' }, + { name: 'DEBUG', value: 'true' }, + ], +}; + +export const ComplexReferences = Template.bind({}); +ComplexReferences.args = { + namespace: 'default', + cluster: 'minikube', + envVars: [ + { name: 'DB_HOST', value: '127.0.0.1' }, + { + name: 'API_KEY', + valueFrom: { + secretKeyRef: { name: 'my-secret', key: 'api-key' }, + }, + }, + { + name: 'APP_CONFIG', + valueFrom: { + configMapKeyRef: { name: 'app-config', key: 'config.json' }, + }, + }, + { + name: 'MY_POD_IP', + valueFrom: { + fieldRef: { fieldPath: 'status.podIP' }, + }, + }, + { + name: 'CPU_LIMIT', + valueFrom: { + resourceFieldRef: { resource: 'limits.cpu' }, + }, + }, + ], +}; + +export const ManyVariables = Template.bind({}); +ManyVariables.args = { + namespace: 'default', + cluster: 'minikube', + envVars: Array.from({ length: 35 }, (_, i) => ({ + name: `VAR_${i}`, + value: `value-${i}`, + })), +}; diff --git a/frontend/src/components/common/Resource/EnvVarDisplay.tsx b/frontend/src/components/common/Resource/EnvVarDisplay.tsx new file mode 100644 index 00000000000..e24866b81c7 --- /dev/null +++ b/frontend/src/components/common/Resource/EnvVarDisplay.tsx @@ -0,0 +1,131 @@ +/* + * 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 { Icon } from '@iconify/react'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import { Theme } from '@mui/material/styles'; +import Typography from '@mui/material/Typography'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import Link from '../Link'; + +interface EnvVarGridProps { + envVars: any[]; + namespace: string; + cluster: string; +} + +export function EnvVarGrid(props: EnvVarGridProps) { + const { envVars = [], namespace, cluster } = props; + const { t } = useTranslation(); + const [expanded, setExpanded] = React.useState(false); + const defaultNumShown = 20; + + const envEntryStyle = (theme: Theme) => ({ + color: theme.palette.text.primary, + borderRadius: theme.shape.borderRadius + 'px', + backgroundColor: theme.palette.background.muted, + border: '1px solid', + borderColor: theme.palette.divider, + fontSize: theme.typography.pxToRem(14), + padding: '4px 8px', + marginRight: theme.spacing(1), + whiteSpace: 'nowrap', + display: 'inline-block', + }); + + const renderEnvVar = (envVar: any) => { + // Secret Key: + if (envVar.valueFrom?.secretKeyRef) { + const { name: secretName, key: secretKey } = envVar.valueFrom.secretKeyRef; + const secretUrl = `/c/${cluster}/secrets/${namespace}/${secretName}`; + + return ( + + {envVar.name}:{' '} + + Secret: {secretName} (Key: {secretKey}) + + + ); + } + + // Config Map: + if (envVar.valueFrom?.configMapKeyRef) { + const { name: cmName, key: cmKey } = envVar.valueFrom.configMapKeyRef; + const secretUrl = `/c/${cluster}/secrets/${namespace}/${cmName}`; + return ( + + {envVar.name}:{' '} + + ConfigMap: {cmName} (Key: {cmKey}) + + + ); + } + + // FieldRef: + if (envVar.valueFrom?.fieldRef) { + const { fieldPath } = envVar.valueFrom.fieldRef; + return ( + + {envVar.name}:FieldRef ({fieldPath}) + + ); + } + + // ResourceFieldRef: + if (envVar.valueFrom?.resourceFieldRef) { + const { resource } = envVar.valueFrom.resourceFieldRef; + return ( + + {envVar.name}: ResourceField ({resource}) + + ); + } + + // Plaintext + return ( + + {envVar.name}: {envVar.value} + + ); + }; + + return ( + + + {envVars + .slice(0, expanded ? envVars.length : defaultNumShown) + .map(env => renderEnvVar(env))} + + {envVars.length > defaultNumShown && ( + + )} + + ); +} diff --git a/frontend/src/components/common/Resource/Resource.tsx b/frontend/src/components/common/Resource/Resource.tsx index 7a5fc902fdd..2139cbbd80f 100644 --- a/frontend/src/components/common/Resource/Resource.tsx +++ b/frontend/src/components/common/Resource/Resource.tsx @@ -64,6 +64,7 @@ import InnerTable from '../InnerTable'; import { DateLabel, HoverInfoLabel, StatusLabel, StatusLabelProps, ValueLabel } from '../Label'; import Link, { LinkProps } from '../Link'; import { metadataStyles } from '.'; +import { EnvVarGrid } from './EnvVarDisplay'; import { MainInfoSection, MainInfoSectionProps } from './MainInfoSection/MainInfoSection'; import { MainInfoHeader } from './MainInfoSection/MainInfoSectionHeader'; import { MetadataDictGrid, MetadataDisplay } from './MetadataDisplay'; @@ -981,8 +982,14 @@ export function ContainerInfo(props: ContainerInfoProps) { }, { name: t('Environment'), - value: , - hide: _.isEmpty(env), + value: ( + + ), + hide: !container.env || container.env?.length === 0, }, { name: t('Liveness Probes'), diff --git a/frontend/src/components/common/Resource/__snapshots__/EnvDisplay.ComplexReferences.stories.storyshot b/frontend/src/components/common/Resource/__snapshots__/EnvDisplay.ComplexReferences.stories.storyshot new file mode 100644 index 00000000000..e1eb4066e96 --- /dev/null +++ b/frontend/src/components/common/Resource/__snapshots__/EnvDisplay.ComplexReferences.stories.storyshot @@ -0,0 +1,71 @@ + +
+
+
+ + DB_HOST + : + 127.0.0.1 + + + API_KEY + : + + + Secret: + my-secret + (Key: + api-key + ) + + +

+ APP_CONFIG + : + + + ConfigMap: + app-config + (Key: + config.json + ) + +

+ + MY_POD_IP + :FieldRef ( + status.podIP + ) + + + CPU_LIMIT + : ResourceField ( + limits.cpu + ) + +
+
+
+ \ No newline at end of file diff --git a/frontend/src/components/common/Resource/__snapshots__/EnvDisplay.ManyVariables.stories.storyshot b/frontend/src/components/common/Resource/__snapshots__/EnvDisplay.ManyVariables.stories.storyshot new file mode 100644 index 00000000000..57915b80990 --- /dev/null +++ b/frontend/src/components/common/Resource/__snapshots__/EnvDisplay.ManyVariables.stories.storyshot @@ -0,0 +1,165 @@ + +
+
+
+ + VAR_0 + : + value-0 + + + VAR_1 + : + value-1 + + + VAR_2 + : + value-2 + + + VAR_3 + : + value-3 + + + VAR_4 + : + value-4 + + + VAR_5 + : + value-5 + + + VAR_6 + : + value-6 + + + VAR_7 + : + value-7 + + + VAR_8 + : + value-8 + + + VAR_9 + : + value-9 + + + VAR_10 + : + value-10 + + + VAR_11 + : + value-11 + + + VAR_12 + : + value-12 + + + VAR_13 + : + value-13 + + + VAR_14 + : + value-14 + + + VAR_15 + : + value-15 + + + VAR_16 + : + value-16 + + + VAR_17 + : + value-17 + + + VAR_18 + : + value-18 + + + VAR_19 + : + value-19 + +
+ +
+
+ \ No newline at end of file diff --git a/frontend/src/components/common/Resource/__snapshots__/EnvDisplay.PlainValues.stories.storyshot b/frontend/src/components/common/Resource/__snapshots__/EnvDisplay.PlainValues.stories.storyshot new file mode 100644 index 00000000000..ab255d300eb --- /dev/null +++ b/frontend/src/components/common/Resource/__snapshots__/EnvDisplay.PlainValues.stories.storyshot @@ -0,0 +1,26 @@ + +
+
+
+ + NODE_ENV + : + production + + + DEBUG + : + true + +
+
+
+ \ No newline at end of file diff --git a/frontend/src/components/common/Resource/__snapshots__/EnvVarDisplay.ComplexReferences.stories.storyshot b/frontend/src/components/common/Resource/__snapshots__/EnvVarDisplay.ComplexReferences.stories.storyshot new file mode 100644 index 00000000000..ff1397f2cc6 --- /dev/null +++ b/frontend/src/components/common/Resource/__snapshots__/EnvVarDisplay.ComplexReferences.stories.storyshot @@ -0,0 +1,71 @@ + +
+
+
+ + DB_HOST + : + 127.0.0.1 + + + API_KEY + : + + + Secret: + my-secret + (Key: + api-key + ) + + +

+ APP_CONFIG + : + + + ConfigMap: + app-config + (Key: + config.json + ) + +

+ + MY_POD_IP + :FieldRef ( + status.podIP + ) + + + CPU_LIMIT + : ResourceField ( + limits.cpu + ) + +
+
+
+ \ No newline at end of file diff --git a/frontend/src/components/common/Resource/__snapshots__/EnvVarDisplay.ManyVariables.stories.storyshot b/frontend/src/components/common/Resource/__snapshots__/EnvVarDisplay.ManyVariables.stories.storyshot new file mode 100644 index 00000000000..57915b80990 --- /dev/null +++ b/frontend/src/components/common/Resource/__snapshots__/EnvVarDisplay.ManyVariables.stories.storyshot @@ -0,0 +1,165 @@ + +
+
+
+ + VAR_0 + : + value-0 + + + VAR_1 + : + value-1 + + + VAR_2 + : + value-2 + + + VAR_3 + : + value-3 + + + VAR_4 + : + value-4 + + + VAR_5 + : + value-5 + + + VAR_6 + : + value-6 + + + VAR_7 + : + value-7 + + + VAR_8 + : + value-8 + + + VAR_9 + : + value-9 + + + VAR_10 + : + value-10 + + + VAR_11 + : + value-11 + + + VAR_12 + : + value-12 + + + VAR_13 + : + value-13 + + + VAR_14 + : + value-14 + + + VAR_15 + : + value-15 + + + VAR_16 + : + value-16 + + + VAR_17 + : + value-17 + + + VAR_18 + : + value-18 + + + VAR_19 + : + value-19 + +
+ +
+
+ \ No newline at end of file diff --git a/frontend/src/components/common/Resource/__snapshots__/EnvVarDisplay.PlainValues.stories.storyshot b/frontend/src/components/common/Resource/__snapshots__/EnvVarDisplay.PlainValues.stories.storyshot new file mode 100644 index 00000000000..ab255d300eb --- /dev/null +++ b/frontend/src/components/common/Resource/__snapshots__/EnvVarDisplay.PlainValues.stories.storyshot @@ -0,0 +1,26 @@ + +
+
+
+ + NODE_ENV + : + production + + + DEBUG + : + true + +
+
+
+ \ No newline at end of file diff --git a/frontend/src/components/common/Resource/index.test.ts b/frontend/src/components/common/Resource/index.test.ts index 00ed707dfdb..82878a455d0 100644 --- a/frontend/src/components/common/Resource/index.test.ts +++ b/frontend/src/components/common/Resource/index.test.ts @@ -38,6 +38,7 @@ const checkExports = [ 'DocsViewer', 'EditButton', 'EditorDialog', + 'EnvVarDisplay', 'MainInfoSection', 'MatchExpressions', 'MetadataDisplay', diff --git a/frontend/src/components/common/Resource/index.tsx b/frontend/src/components/common/Resource/index.tsx index 53b70b21708..8900726695b 100644 --- a/frontend/src/components/common/Resource/index.tsx +++ b/frontend/src/components/common/Resource/index.tsx @@ -16,6 +16,7 @@ export * from './CircularChart'; export * from './MetadataDisplay'; +export * from './EnvVarDisplay'; export * from './Resource'; export * as ResourceTable from './ResourceTable'; export * from './ResourceListView'; diff --git a/frontend/src/components/pod/Details.tsx b/frontend/src/components/pod/Details.tsx index aaebcac3325..f6677f37d08 100644 --- a/frontend/src/components/pod/Details.tsx +++ b/frontend/src/components/pod/Details.tsx @@ -26,7 +26,7 @@ import { Terminal as XTerminal } from '@xterm/xterm'; import _ from 'lodash'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router-dom'; import { KubeContainerStatus } from '../../lib/k8s/cluster'; import Pod from '../../lib/k8s/pod'; import { DefaultHeaderAction } from '../../redux/actionButtonsSlice'; @@ -490,6 +490,30 @@ export default function PodDetails(props: PodDetailsProps) { const { name = params.name, namespace = params.namespace, cluster } = props; const { t } = useTranslation('glossary'); const dispatchHeadlampEvent = useEventCallback(); + // Get query parameters: + const location = useLocation(); + const query = new URLSearchParams(location.search); + const autoLaunchView = query.get('view'); + // Only Launch Once: + const hasAutoLaunched = React.useRef(false); + + // Helper to launch logs (button and deep link): + const launchLogs = (item: Pod) => { + Activity.launch({ + id: 'logs-' + item.metadata.uid, + title: t('Logs') + ': ' + item.metadata.name, + cluster: item.cluster, + icon: , + location: 'full', + content: {}} />, + }); + dispatchHeadlampEvent({ + type: HeadlampEventType.LOGS, + data: { + status: EventStatus.OPENED, + }, + }); + }; function prepareExtraInfo(item: Pod | null) { let extraInfo: { @@ -589,8 +613,16 @@ export default function PodDetails(props: PodDetailsProps) { namespace={namespace} cluster={cluster} withEvents - actions={item => - item && [ + actions={item => { + if (item && autoLaunchView === 'logs' && !hasAutoLaunched.current) { + hasAutoLaunched.current = true; + //Set Timeout to prevent Rendering Issues: + setTimeout(() => launchLogs(item), 0); + } + + if (!item) return null; + + return [ { id: DefaultHeaderAction.POD_LOGS, action: ( @@ -598,24 +630,7 @@ export default function PodDetails(props: PodDetailsProps) { { - Activity.launch({ - id: 'logs-' + item.metadata.uid, - title: t('Logs') + ': ' + item.metadata.name, - cluster: item.cluster, - icon: ( - - ), - location: 'full', - content: {}} />, - }); - dispatchHeadlampEvent({ - type: HeadlampEventType.LOGS, - data: { - status: EventStatus.OPENED, - }, - }); - }} + onClick={() => launchLogs(item)} /> ), @@ -678,8 +693,8 @@ export default function PodDetails(props: PodDetailsProps) { ), }, - ] - } + ]; + }} extraInfo={item => prepareExtraInfo(item)} extraSections={item => item && [ diff --git a/frontend/src/plugin/__snapshots__/pluginLib.snapshot b/frontend/src/plugin/__snapshots__/pluginLib.snapshot index b82fb2f521c..fc0a126e146 100644 --- a/frontend/src/plugin/__snapshots__/pluginLib.snapshot +++ b/frontend/src/plugin/__snapshots__/pluginLib.snapshot @@ -60,6 +60,7 @@ "EditButton": [Function], "EditorDialog": [Function], "EmptyContent": [Function], + "EnvVarGrid": [Function], "ErrorPage": [Function], "HeaderLabel": [Function], "HoverInfoLabel": [Function], @@ -106,6 +107,7 @@ "DocsViewer": [Function], "EditButton": [Function], "EditorDialog": [Function], + "EnvVarGrid": [Function], "LivenessProbes": [Function], "LogsButton": [Function], "MainInfoSection": [Function],