Skip to content
Merged
66 changes: 57 additions & 9 deletions Localize/locales/en/plugin-translation.json

Large diffs are not rendered by default.

1,230 changes: 647 additions & 583 deletions plugins/aks-desktop/locales/cs/translation.json

Large diffs are not rendered by default.

1,204 changes: 623 additions & 581 deletions plugins/aks-desktop/locales/de/translation.json

Large diffs are not rendered by default.

1,210 changes: 624 additions & 586 deletions plugins/aks-desktop/locales/en/translation.json

Large diffs are not rendered by default.

1,227 changes: 640 additions & 587 deletions plugins/aks-desktop/locales/es/translation.json

Large diffs are not rendered by default.

1,231 changes: 642 additions & 589 deletions plugins/aks-desktop/locales/fr/translation.json

Large diffs are not rendered by default.

1,208 changes: 625 additions & 583 deletions plugins/aks-desktop/locales/hu/translation.json

Large diffs are not rendered by default.

1,210 changes: 626 additions & 584 deletions plugins/aks-desktop/locales/id/translation.json

Large diffs are not rendered by default.

1,219 changes: 636 additions & 583 deletions plugins/aks-desktop/locales/it/translation.json

Large diffs are not rendered by default.

1,210 changes: 626 additions & 584 deletions plugins/aks-desktop/locales/ja/translation.json

Large diffs are not rendered by default.

1,210 changes: 626 additions & 584 deletions plugins/aks-desktop/locales/ko/translation.json

Large diffs are not rendered by default.

1,208 changes: 625 additions & 583 deletions plugins/aks-desktop/locales/nl/translation.json

Large diffs are not rendered by default.

1,230 changes: 647 additions & 583 deletions plugins/aks-desktop/locales/pl/translation.json

Large diffs are not rendered by default.

1,215 changes: 634 additions & 581 deletions plugins/aks-desktop/locales/pt-BR/translation.json

Large diffs are not rendered by default.

1,211 changes: 632 additions & 579 deletions plugins/aks-desktop/locales/pt-PT/translation.json

Large diffs are not rendered by default.

1,220 changes: 642 additions & 578 deletions plugins/aks-desktop/locales/ru/translation.json

Large diffs are not rendered by default.

1,196 changes: 619 additions & 577 deletions plugins/aks-desktop/locales/sv/translation.json

Large diffs are not rendered by default.

1,208 changes: 625 additions & 583 deletions plugins/aks-desktop/locales/tr/translation.json

Large diffs are not rendered by default.

1,210 changes: 626 additions & 584 deletions plugins/aks-desktop/locales/zh-Hans/translation.json

Large diffs are not rendered by default.

1,210 changes: 626 additions & 584 deletions plugins/aks-desktop/locales/zh-Hant/translation.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import { visuallyHidden } from '@mui/utils';
import React, { useMemo, useState } from 'react';
import { useAzureContext } from '../../../hooks/useAzureContext';
import { useNamespaceCapabilities } from '../../../hooks/useNamespaceCapabilities';
import type { GitHubRepo } from '../../../types/github';
import { openExternalUrl } from '../../../utils/shared/openExternalUrl';
import DeployWizard from '../../DeployWizard/DeployWizard';
Expand Down Expand Up @@ -92,6 +93,12 @@ export function ClusterDeployCard({ cluster, namespace, pipelineEnabled }: Clust
const [manualDeployOpen, setManualDeployOpen] = useState(false);
const [pipelineDeployRepo, setPipelineDeployRepo] = useState<GitHubRepo | null>(null);
const [editingDeployment, setEditingDeployment] = useState<DeploymentStatus | null>(null);
const { isManagedNamespace, azureRbacEnabled } = useNamespaceCapabilities({
subscriptionId: azureContext?.subscriptionId,
resourceGroup: azureContext?.resourceGroup,
clusterName: cluster,
namespace,
});

const editingContainerConfig: Partial<ContainerConfig> | undefined = useMemo(() => {
if (!editingDeployment?.rawDeployment) return undefined;
Expand Down Expand Up @@ -279,6 +286,17 @@ export function ClusterDeployCard({ cluster, namespace, pipelineEnabled }: Clust
initialApplicationName={editingDeployment?.name}
initialContainerConfig={editingContainerConfig}
onClose={handleCloseManualDeploy}
azureContext={
azureContext
? {
subscriptionId: azureContext.subscriptionId,
resourceGroup: azureContext.resourceGroup,
clusterName: cluster,
isManagedNamespace,
azureRbacEnabled,
}
: undefined
}
/>
</Dialog>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ describe('extractContainerConfigFromDeployment', () => {
expect(result.targetPort).toBe(8080);

expect(result.envVars).toEqual([
{ key: 'NODE_ENV', value: 'production' },
{ key: 'PORT', value: '8080' },
{ key: 'NODE_ENV', value: 'production', isSecret: false },
{ key: 'PORT', value: '8080', isSecret: false },
]);

expect(result.enableResources).toBe(true);
Expand Down Expand Up @@ -249,4 +249,74 @@ describe('extractContainerConfigFromDeployment', () => {
expect(result.containerImage).toBe('first:v1');
expect(result.targetPort).toBe(8080);
});

it('extracts secret env vars from secretKeyRef', () => {
const deployment = {
metadata: { name: 'my-app' },
spec: {
replicas: 1,
template: {
spec: {
containers: [
{
image: 'nginx:latest',
env: [
{ name: 'PLAIN_VAR', value: 'hello' },
{
name: 'SECRET_VAR',
valueFrom: { secretKeyRef: { name: 'my-secret', key: 'SECRET_VAR' } },
},
],
},
],
},
},
},
};
const result = extractContainerConfigFromDeployment(deployment);
expect(result.envVars).toEqual([
{ key: 'PLAIN_VAR', value: 'hello', isSecret: false },
{ key: 'SECRET_VAR', value: '', isSecret: true },
]);
});

it('extracts workload identity config from pod labels and serviceAccountName', () => {
const deployment = {
metadata: { name: 'my-app' },
spec: {
replicas: 1,
template: {
metadata: {
labels: { app: 'my-app', 'azure.workload.identity/use': 'true' },
},
spec: {
containers: [{ image: 'nginx:latest' }],
serviceAccountName: 'my-app-sa',
},
},
},
};
const result = extractContainerConfigFromDeployment(deployment);
expect(result.enableWorkloadIdentity).toBe(true);
expect(result.workloadIdentityServiceAccount).toBe('my-app-sa');
});

it('does not enable workload identity when serviceAccountName is missing', () => {
const deployment = {
metadata: { name: 'my-app' },
spec: {
replicas: 1,
template: {
metadata: {
labels: { app: 'my-app', 'azure.workload.identity/use': 'true' },
},
spec: {
containers: [{ image: 'nginx:latest' }],
},
},
},
};
const result = extractContainerConfigFromDeployment(deployment);
expect(result.enableWorkloadIdentity).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache 2.0.

import type { KubeContainer as HeadlampKubeContainer } from '@kinvolk/headlamp-plugin/lib/lib/k8s/cluster';
import type { ContainerConfig } from '../../DeployWizard/hooks/useContainerConfiguration';

interface KubeContainer {
image?: string;
ports?: Array<{ containerPort?: number }>;
env?: Array<{ name?: string; value?: string }>;
resources?: {
requests?: { cpu?: string; memory?: string };
limits?: { cpu?: string; memory?: string };
};
securityContext?: {
runAsNonRoot?: boolean;
readOnlyRootFilesystem?: boolean;
allowPrivilegeEscalation?: boolean;
};
livenessProbe?: KubeProbe;
readinessProbe?: KubeProbe;
startupProbe?: KubeProbe;
}

interface KubeProbe {
httpGet?: { path?: string };
exec?: { command?: string[] };
tcpSocket?: { port?: number };
initialDelaySeconds?: number;
periodSeconds?: number;
timeoutSeconds?: number;
failureThreshold?: number;
successThreshold?: number;
}

type KubeContainer = Partial<Omit<HeadlampKubeContainer, 'livenessProbe' | 'readinessProbe'>> & {
livenessProbe?: KubeProbe;
readinessProbe?: KubeProbe;
startupProbe?: KubeProbe;
securityContext?: {
runAsNonRoot?: boolean;
readOnlyRootFilesystem?: boolean;
allowPrivilegeEscalation?: boolean;
};
};

interface KubeDeploymentInput {
metadata?: { name?: string };
spec?: {
replicas?: number;
template?: {
metadata?: {
labels?: Record<string, string>;
};
spec?: {
containers?: KubeContainer[];
serviceAccountName?: string;
securityContext?: { runAsNonRoot?: boolean };
affinity?: { podAntiAffinity?: unknown };
topologySpreadConstraints?: unknown[];
Expand Down Expand Up @@ -82,7 +82,11 @@ export function extractContainerConfigFromDeployment(
if (Array.isArray(container.env) && container.env.length > 0) {
result.envVars = container.env
.filter(e => e.name)
.map(e => ({ key: e.name!, value: e.value ?? '' }));
.map(e => ({
key: e.name!,
value: e.valueFrom?.secretKeyRef ? '' : e.value ?? '',
isSecret: !!e.valueFrom?.secretKeyRef,
}));
}

const resources = container.resources;
Expand Down Expand Up @@ -113,6 +117,12 @@ export function extractContainerConfigFromDeployment(
result.runAsNonRoot = true;
}

const podLabels = spec?.template?.metadata?.labels ?? {};
if (podLabels['azure.workload.identity/use'] === 'true' && templateSpec.serviceAccountName) {
result.enableWorkloadIdentity = true;
result.workloadIdentityServiceAccount = templateSpec.serviceAccountName;
}

result.enablePodAntiAffinity = !!templateSpec.affinity?.podAntiAffinity;
result.enableTopologySpreadConstraints =
Array.isArray(templateSpec.topologySpreadConstraints) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import React from 'react';
import ConfigureContainer from './components/ConfigureContainer';
import type { DeployAzureContext } from './components/configureContainerUtils';
import ConfigureYAML from './components/ConfigureYAML';
import Deploy from './components/Deploy';
import DeployWizardPure from './components/DeployWizardPure';
Expand All @@ -21,6 +22,8 @@ type DeployWizardProps = {
initialContainerConfig?: Partial<ContainerConfig>;
/** Called when the user clicks "Close" after a deploy result. */
onClose?: () => void;
/** Azure context for workload identity setup */
azureContext?: DeployAzureContext;
};

/**
Expand Down Expand Up @@ -64,7 +67,11 @@ export default function DeployWizard(props: DeployWizardProps) {
onYamlErrorChange={err => setYamlError(err)}
/>
) : (
<ConfigureContainer containerConfig={containerConfig} />
<ConfigureContainer
containerConfig={containerConfig}
azureContext={props.azureContext}
namespace={props.namespace}
/>
)}
</React.Fragment>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache 2.0.

import { useTranslation } from '@kinvolk/headlamp-plugin/lib';
import { Box, Button, FormControlLabel, Switch, Typography } from '@mui/material';
import React from 'react';
import { CONTAINER_STEPS, type ContainerConfig } from '../hooks/useContainerConfiguration';
import { ContainerConfigProp, LabelWithInfo } from './configureContainerUtils';

interface AdvancedStepProps {
containerConfig: ContainerConfigProp;
}

function SwitchWithDescription({
checked,
onChange,
label,
infoText,
description,
}: {
checked: boolean;
onChange: (checked: boolean) => void;
label: string;
infoText: string;
description: string;
}) {
return (
<>
<FormControlLabel
control={<Switch checked={checked} onChange={e => onChange(e.target.checked)} />}
label={<LabelWithInfo label={label} infoText={infoText} />}
/>
<Typography variant="caption" color="text.secondary" sx={{ ml: 5, display: 'block', mt: -1 }}>
{description}
</Typography>
</>
);
}

export default function AdvancedStep({ containerConfig }: AdvancedStepProps) {
const { t } = useTranslation();
const set = <K extends keyof ContainerConfig>(key: K) => {
return (val: ContainerConfig[K]) => containerConfig.setConfig(c => ({ ...c, [key]: val }));
};

return (
<>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t('Configure security context settings for the container.')}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<SwitchWithDescription
checked={containerConfig.config.runAsNonRoot}
onChange={set('runAsNonRoot')}
label={t('Run as non root user')}
infoText={t(
'Ensures the container runs as a non-root user (UID != 0) for better security. This prevents privilege escalation attacks.'
)}
description={t('Ensures the container runs as a non-root user for better security.')}
/>
<SwitchWithDescription
checked={containerConfig.config.readOnlyRootFilesystem}
onChange={set('readOnlyRootFilesystem')}
label={t('Read only root filesystem')}
infoText={t(
"Mounts the container's root filesystem as read-only to prevent write operations. This enhances security by preventing malicious code from modifying system files."
)}
description={t(
"Mounts the container's root filesystem as read-only to prevent write operations."
)}
/>
<SwitchWithDescription
checked={containerConfig.config.allowPrivilegeEscalation}
onChange={set('allowPrivilegeEscalation')}
label={t('Allow privilege escalation')}
infoText={t(
'Controls whether a process can gain more privileges than its parent process. Disabling this (recommended) prevents privilege escalation attacks.'
)}
description={t(
'Controls whether a process can gain more privileges than its parent process.'
)}
/>
<SwitchWithDescription
checked={containerConfig.config.enablePodAntiAffinity}
onChange={set('enablePodAntiAffinity')}
label={t('Enable pod anti-affinity')}
infoText={t(
'Prefer scheduling pods on different nodes to improve availability and fault tolerance. This helps ensure pods are distributed across the cluster.'
)}
description={t(
'Prefer scheduling pods on different nodes to improve availability and fault tolerance.'
)}
/>
<SwitchWithDescription
checked={containerConfig.config.enableTopologySpreadConstraints}
onChange={set('enableTopologySpreadConstraints')}
label={t('Enable topology spread constraints')}
infoText={t(
'Distributes pods evenly across nodes, zones, or other topology domains to improve workload distribution and availability.'
)}
description={t('Distributes pods evenly across nodes to improve workload distribution.')}
/>
</Box>
<Box sx={{ mt: 2, display: 'flex', gap: 1 }}>
<Button
variant="outlined"
onClick={() =>
containerConfig.setConfig(c => ({
...c,
containerStep: CONTAINER_STEPS.WORKLOAD_IDENTITY,
}))
}
>
{t('Back')}
</Button>
</Box>
</>
);
}
Loading
Loading