Merge branch 'backstage:master' into go/expose-detecterrors
This commit is contained in:
@@ -32,9 +32,7 @@ export const isKubernetesAvailable = (entity: Entity) =>
|
||||
entity.metadata.annotations?.[KUBERNETES_LABEL_SELECTOR_QUERY_ANNOTATION],
|
||||
);
|
||||
|
||||
type Props = {};
|
||||
|
||||
export const Router = (_props: Props) => {
|
||||
export const Router = (props: { refreshIntervalMs?: number }) => {
|
||||
const { entity } = useEntity();
|
||||
|
||||
const kubernetesAnnotationValue =
|
||||
@@ -49,7 +47,15 @@ export const Router = (_props: Props) => {
|
||||
) {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<KubernetesContent entity={entity} />} />
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<KubernetesContent
|
||||
entity={entity}
|
||||
refreshIntervalMs={props.refreshIntervalMs}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,7 @@ import {
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { DeploymentsAccordions } from '../DeploymentsAccordions';
|
||||
import { StatefulSetsAccordions } from '../StatefulSetsAccordions';
|
||||
import { groupResponses } from '../../utils/response';
|
||||
import { IngressesAccordions } from '../IngressesAccordions';
|
||||
import { ServicesAccordions } from '../ServicesAccordions';
|
||||
@@ -142,6 +143,9 @@ export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => {
|
||||
<Grid item>
|
||||
<DeploymentsAccordions />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<StatefulSetsAccordions />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<IngressesAccordions />
|
||||
</Grid>
|
||||
|
||||
@@ -268,8 +268,11 @@ export const RolloutAccordions = ({
|
||||
<RolloutAccordion
|
||||
defaultExpanded={defaultExpanded}
|
||||
matchingHpa={getMatchingHpa(
|
||||
rollout.metadata?.name,
|
||||
'rollout',
|
||||
{
|
||||
name: rollout.metadata?.name,
|
||||
namespace: rollout.metadata?.namespace,
|
||||
kind: 'rollout',
|
||||
},
|
||||
groupedResponses.horizontalPodAutoscalers,
|
||||
)}
|
||||
ownedPods={getOwnedPodsThroughReplicaSets(
|
||||
|
||||
@@ -186,8 +186,11 @@ export const DeploymentsAccordions = ({}: DeploymentsAccordionsProps) => {
|
||||
<Grid item xs>
|
||||
<DeploymentAccordion
|
||||
matchingHpa={getMatchingHpa(
|
||||
deployment.metadata?.name,
|
||||
'deployment',
|
||||
{
|
||||
name: deployment.metadata?.name,
|
||||
namespace: deployment.metadata?.namespace,
|
||||
kind: 'deployment',
|
||||
},
|
||||
groupedResponses.horizontalPodAutoscalers,
|
||||
)}
|
||||
ownedPods={getOwnedPodsThroughReplicaSets(
|
||||
|
||||
@@ -25,10 +25,20 @@ import EmptyStateImage from '../assets/emptystate.svg';
|
||||
import { useKubernetesObjects } from '../hooks';
|
||||
import { Content, Page, Progress } from '@backstage/core-components';
|
||||
|
||||
type KubernetesContentProps = { entity: Entity; children?: React.ReactNode };
|
||||
type KubernetesContentProps = {
|
||||
entity: Entity;
|
||||
refreshIntervalMs?: number;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
|
||||
const { kubernetesObjects, error } = useKubernetesObjects(entity);
|
||||
export const KubernetesContent = ({
|
||||
entity,
|
||||
refreshIntervalMs,
|
||||
}: KubernetesContentProps) => {
|
||||
const { kubernetesObjects, error } = useKubernetesObjects(
|
||||
entity,
|
||||
refreshIntervalMs,
|
||||
);
|
||||
|
||||
const clustersWithErrors =
|
||||
kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? [];
|
||||
|
||||
+4
-4
@@ -108,10 +108,10 @@ describe('PodsTable', () => {
|
||||
expect(getByText('1/1')).toBeInTheDocument();
|
||||
expect(getByText('0')).toBeInTheDocument();
|
||||
expect(getByText('OK')).toBeInTheDocument();
|
||||
expect(getByText('requests: 99%')).toBeInTheDocument();
|
||||
expect(getByText('limits: 99%')).toBeInTheDocument();
|
||||
expect(getByText('requests: 1%')).toBeInTheDocument();
|
||||
expect(getByText('limits: 0%')).toBeInTheDocument();
|
||||
expect(getByText('requests: 99% of 50m')).toBeInTheDocument();
|
||||
expect(getByText('limits: 99% of 50m')).toBeInTheDocument();
|
||||
expect(getByText('requests: 1% of 64MiB')).toBeInTheDocument();
|
||||
expect(getByText('limits: 0% of 128MiB')).toBeInTheDocument();
|
||||
});
|
||||
it('should render placehoplder when empty metrics context', async () => {
|
||||
const podNameToClientPodStatus = new Map<string, ClientPodStatus>();
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage 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 React from 'react';
|
||||
import * as statefulsets from '../../__fixtures__/2-statefulsets.json';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { StatefulSetDrawer } from './StatefulSetDrawer';
|
||||
|
||||
describe('StatefulSetDrawer', () => {
|
||||
it('should render statefulset drawer', async () => {
|
||||
const { getByText, getAllByText } = await renderInTestApp(
|
||||
<StatefulSetDrawer
|
||||
statefulset={(statefulsets as any).statefulsets[0]}
|
||||
expanded
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getAllByText('dice-roller')).toHaveLength(3);
|
||||
expect(getByText('StatefulSet')).toBeInTheDocument();
|
||||
expect(getByText('YAML')).toBeInTheDocument();
|
||||
expect(getByText('Type: RollingUpdate')).toBeInTheDocument();
|
||||
expect(getByText('Rolling Update:')).toBeInTheDocument();
|
||||
expect(getByText('Max Surge: 25%')).toBeInTheDocument();
|
||||
expect(getByText('Max Unavailable: 25%')).toBeInTheDocument();
|
||||
expect(getByText('Pod Management Policy')).toBeInTheDocument();
|
||||
expect(getByText('Parallel')).toBeInTheDocument();
|
||||
expect(getByText('Service Name')).toBeInTheDocument();
|
||||
expect(getByText('Selector')).toBeInTheDocument();
|
||||
expect(getByText('Match Labels:')).toBeInTheDocument();
|
||||
expect(getByText('App: dice-roller')).toBeInTheDocument();
|
||||
expect(getByText('Revision History Limit')).toBeInTheDocument();
|
||||
expect(getByText('10')).toBeInTheDocument();
|
||||
expect(getByText('namespace: default')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render statefulset drawer without namespace', async () => {
|
||||
const statefulset = (statefulsets as any).statefulsets[0];
|
||||
const { queryByText } = await renderInTestApp(
|
||||
<StatefulSetDrawer
|
||||
statefulset={{
|
||||
...statefulset,
|
||||
metadata: { ...statefulset.metadata, namespace: undefined },
|
||||
}}
|
||||
expanded
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(queryByText('namespace: default')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage 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 React from 'react';
|
||||
import { V1StatefulSet } from '@kubernetes/client-node';
|
||||
import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer';
|
||||
import { renderCondition } from '../../utils/pod';
|
||||
import { Typography, Grid, Chip } from '@material-ui/core';
|
||||
|
||||
export const StatefulSetDrawer = ({
|
||||
statefulset,
|
||||
expanded,
|
||||
}: {
|
||||
statefulset: V1StatefulSet;
|
||||
expanded?: boolean;
|
||||
}) => {
|
||||
const namespace = statefulset.metadata?.namespace;
|
||||
return (
|
||||
<KubernetesDrawer
|
||||
object={statefulset}
|
||||
expanded={expanded}
|
||||
kind="StatefulSet"
|
||||
renderObject={(statefulsetObj: V1StatefulSet) => {
|
||||
const conditions = (statefulsetObj.status?.conditions ?? [])
|
||||
.map(renderCondition)
|
||||
.reduce((accum, next) => {
|
||||
accum[next[0]] = next[1];
|
||||
return accum;
|
||||
}, {} as { [key: string]: React.ReactNode });
|
||||
|
||||
return {
|
||||
updateStrategy: statefulset.spec?.updateStrategy ?? '???',
|
||||
podManagementPolicy: statefulset.spec?.podManagementPolicy ?? '???',
|
||||
serviceName: statefulset.spec?.serviceName ?? '???',
|
||||
selector: statefulset.spec?.selector ?? '???',
|
||||
revisionHistoryLimit: statefulset.spec?.revisionHistoryLimit ?? '???',
|
||||
...conditions,
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant="h5">
|
||||
{statefulset.metadata?.name ?? 'unknown object'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography color="textSecondary" variant="body1">
|
||||
Stateful Set
|
||||
</Typography>
|
||||
</Grid>
|
||||
{namespace && (
|
||||
<Grid item>
|
||||
<Chip size="small" label={`namespace: ${namespace}`} />
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</KubernetesDrawer>
|
||||
);
|
||||
};
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage 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 React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { StatefulSetsAccordions } from './StatefulSetsAccordions';
|
||||
import * as twoStatefulSetsFixture from '../../__fixtures__/2-statefulsets.json';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { kubernetesProviders } from '../../hooks/test-utils';
|
||||
|
||||
describe('StatefulSetsAccordions', () => {
|
||||
it('should render 2 statefulsets', async () => {
|
||||
const wrapper = kubernetesProviders(
|
||||
twoStatefulSetsFixture,
|
||||
new Set(['dice-roller-canary-7d64cd756c-vtbdx']),
|
||||
);
|
||||
|
||||
const { getByText, getAllByText } = render(
|
||||
wrapper(wrapInTestApp(<StatefulSetsAccordions />)),
|
||||
);
|
||||
|
||||
expect(getByText('dice-roller')).toBeInTheDocument();
|
||||
expect(getByText('10 pods')).toBeInTheDocument();
|
||||
expect(getByText('No pods with errors')).toBeInTheDocument();
|
||||
|
||||
expect(getByText('dice-roller-canary')).toBeInTheDocument();
|
||||
expect(getByText('2 pods')).toBeInTheDocument();
|
||||
expect(getByText('1 pod with errors')).toBeInTheDocument();
|
||||
|
||||
expect(getAllByText('namespace: default')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage 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 React, { useContext } from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Divider,
|
||||
Grid,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import {
|
||||
V1Pod,
|
||||
V1HorizontalPodAutoscaler,
|
||||
V1StatefulSet,
|
||||
} from '@kubernetes/client-node';
|
||||
import { PodsTable } from '../Pods';
|
||||
import { StatefulSetDrawer } from './StatefulSetDrawer';
|
||||
import { HorizontalPodAutoscalerDrawer } from '../HorizontalPodAutoscalers';
|
||||
import { getMatchingHpa, getOwnedResources } from '../../utils/owner';
|
||||
import {
|
||||
GroupedResponsesContext,
|
||||
PodNamesWithErrorsContext,
|
||||
} from '../../hooks';
|
||||
import { StatusError, StatusOK } from '@backstage/core-components';
|
||||
import { READY_COLUMNS, RESOURCE_COLUMNS } from '../Pods/PodsTable';
|
||||
|
||||
type StatefulSetsAccordionsProps = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
type StatefulSetAccordionProps = {
|
||||
statefulset: V1StatefulSet;
|
||||
ownedPods: V1Pod[];
|
||||
matchingHpa?: V1HorizontalPodAutoscaler;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
type StatefulSetSummaryProps = {
|
||||
statefulset: V1StatefulSet;
|
||||
numberOfCurrentPods: number;
|
||||
numberOfPodsWithErrors: number;
|
||||
hpa?: V1HorizontalPodAutoscaler;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const StatefulSetSummary = ({
|
||||
statefulset,
|
||||
numberOfCurrentPods,
|
||||
numberOfPodsWithErrors,
|
||||
hpa,
|
||||
}: StatefulSetSummaryProps) => {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid xs={3} item>
|
||||
<StatefulSetDrawer statefulset={statefulset} />
|
||||
</Grid>
|
||||
<Grid item xs={1}>
|
||||
<Divider style={{ height: '5em' }} orientation="vertical" />
|
||||
</Grid>
|
||||
{hpa && (
|
||||
<Grid item xs={3}>
|
||||
<HorizontalPodAutoscalerDrawer hpa={hpa}>
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
spacing={0}
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant="subtitle2">
|
||||
min replicas {hpa.spec?.minReplicas ?? '?'} / max replicas{' '}
|
||||
{hpa.spec?.maxReplicas ?? '?'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="subtitle2">
|
||||
current CPU usage:{' '}
|
||||
{hpa.status?.currentCPUUtilizationPercentage ?? '?'}%
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="subtitle2">
|
||||
target CPU usage:{' '}
|
||||
{hpa.spec?.targetCPUUtilizationPercentage ?? '?'}%
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</HorizontalPodAutoscalerDrawer>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
xs={3}
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item>
|
||||
<StatusOK>{numberOfCurrentPods} pods</StatusOK>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
{numberOfPodsWithErrors > 0 ? (
|
||||
<StatusError>
|
||||
{numberOfPodsWithErrors} pod
|
||||
{numberOfPodsWithErrors > 1 ? 's' : ''} with errors
|
||||
</StatusError>
|
||||
) : (
|
||||
<StatusOK>No pods with errors</StatusOK>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const StatefulSetAccordion = ({
|
||||
statefulset,
|
||||
ownedPods,
|
||||
matchingHpa,
|
||||
}: StatefulSetAccordionProps) => {
|
||||
const podNamesWithErrors = useContext(PodNamesWithErrorsContext);
|
||||
|
||||
const podsWithErrors = ownedPods.filter(p =>
|
||||
podNamesWithErrors.has(p.metadata?.name ?? ''),
|
||||
);
|
||||
|
||||
return (
|
||||
<Accordion TransitionProps={{ unmountOnExit: true }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<StatefulSetSummary
|
||||
statefulset={statefulset}
|
||||
numberOfCurrentPods={ownedPods.length}
|
||||
numberOfPodsWithErrors={podsWithErrors.length}
|
||||
hpa={matchingHpa}
|
||||
/>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<PodsTable
|
||||
pods={ownedPods}
|
||||
extraColumns={[READY_COLUMNS, RESOURCE_COLUMNS]}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export const StatefulSetsAccordions = ({}: StatefulSetsAccordionsProps) => {
|
||||
const groupedResponses = useContext(GroupedResponsesContext);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
{groupedResponses.statefulsets.map((statefulset, i) => (
|
||||
<Grid container item key={i} xs>
|
||||
<Grid item xs>
|
||||
<StatefulSetAccordion
|
||||
matchingHpa={getMatchingHpa(
|
||||
{
|
||||
name: statefulset.metadata?.name,
|
||||
namespace: statefulset.metadata?.namespace,
|
||||
kind: 'statefulset',
|
||||
},
|
||||
groupedResponses.horizontalPodAutoscalers,
|
||||
)}
|
||||
ownedPods={getOwnedResources(statefulset, groupedResponses.pods)}
|
||||
statefulset={statefulset}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
+1
-12
@@ -13,15 +13,4 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { KubernetesAuthProvider } from './types';
|
||||
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
|
||||
|
||||
export class AwsKubernetesAuthProvider implements KubernetesAuthProvider {
|
||||
async decorateRequestBodyForAuth(
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<KubernetesRequestBody> {
|
||||
// No-op, with aws auth, server's AWS credentials are used for access
|
||||
return requestBody;
|
||||
}
|
||||
}
|
||||
export { StatefulSetsAccordions } from './StatefulSetsAccordions';
|
||||
@@ -27,4 +27,5 @@ export const GroupedResponsesContext = React.createContext<GroupedResponses>({
|
||||
jobs: [],
|
||||
cronJobs: [],
|
||||
customResources: [],
|
||||
statefulsets: [],
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ export {
|
||||
kubernetesPlugin as plugin,
|
||||
EntityKubernetesContent,
|
||||
} from './plugin';
|
||||
export type { EntityKubernetesContentProps } from './plugin';
|
||||
export { Router, isKubernetesAvailable } from './Router';
|
||||
export * from './api';
|
||||
export * from './kubernetes-auth-provider';
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage 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 { KubernetesAuthProvider } from './types';
|
||||
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
|
||||
|
||||
export class AzureKubernetesAuthProvider implements KubernetesAuthProvider {
|
||||
async decorateRequestBodyForAuth(
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<KubernetesRequestBody> {
|
||||
// No-op, with azure auth, server's Azure credentials are used for access
|
||||
return requestBody;
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,8 @@
|
||||
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
|
||||
import { KubernetesAuthProvider, KubernetesAuthProvidersApi } from './types';
|
||||
import { GoogleKubernetesAuthProvider } from './GoogleKubernetesAuthProvider';
|
||||
import { ServiceAccountKubernetesAuthProvider } from './ServiceAccountKubernetesAuthProvider';
|
||||
import { AwsKubernetesAuthProvider } from './AwsKubernetesAuthProvider';
|
||||
import { ServerSideKubernetesAuthProvider } from './ServerSideAuthProvider';
|
||||
import { OAuthApi, OpenIdConnectApi } from '@backstage/core-plugin-api';
|
||||
import { GoogleServiceAccountAuthProvider } from './GoogleServiceAccountAuthProvider';
|
||||
import { AzureKubernetesAuthProvider } from './AzureKubernetesAuthProvider';
|
||||
import { OidcKubernetesAuthProvider } from './OidcKubernetesAuthProvider';
|
||||
|
||||
export class KubernetesAuthProviders implements KubernetesAuthProvidersApi {
|
||||
@@ -41,16 +38,23 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi {
|
||||
);
|
||||
this.kubernetesAuthProviderMap.set(
|
||||
'serviceAccount',
|
||||
new ServiceAccountKubernetesAuthProvider(),
|
||||
new ServerSideKubernetesAuthProvider(),
|
||||
);
|
||||
this.kubernetesAuthProviderMap.set(
|
||||
'googleServiceAccount',
|
||||
new GoogleServiceAccountAuthProvider(),
|
||||
new ServerSideKubernetesAuthProvider(),
|
||||
);
|
||||
this.kubernetesAuthProviderMap.set(
|
||||
'aws',
|
||||
new ServerSideKubernetesAuthProvider(),
|
||||
);
|
||||
this.kubernetesAuthProviderMap.set('aws', new AwsKubernetesAuthProvider());
|
||||
this.kubernetesAuthProviderMap.set(
|
||||
'azure',
|
||||
new AzureKubernetesAuthProvider(),
|
||||
new ServerSideKubernetesAuthProvider(),
|
||||
);
|
||||
this.kubernetesAuthProviderMap.set(
|
||||
'localKubectlProxy',
|
||||
new ServerSideKubernetesAuthProvider(),
|
||||
);
|
||||
|
||||
if (options.oidcProviders) {
|
||||
|
||||
+8
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,13 +17,18 @@
|
||||
import { KubernetesAuthProvider } from './types';
|
||||
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
|
||||
|
||||
export class GoogleServiceAccountAuthProvider
|
||||
/**
|
||||
* No-op KubernetesAuthProvider, authorization will be handled in the kubernetes-backend plugin
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class ServerSideKubernetesAuthProvider
|
||||
implements KubernetesAuthProvider
|
||||
{
|
||||
async decorateRequestBodyForAuth(
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<KubernetesRequestBody> {
|
||||
// No-op, with google service account auth, server's AWS credentials are used for access
|
||||
// No-op, auth will be taken care of on the server-side
|
||||
return requestBody;
|
||||
}
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage 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 { KubernetesAuthProvider } from './types';
|
||||
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
|
||||
|
||||
export class ServiceAccountKubernetesAuthProvider
|
||||
implements KubernetesAuthProvider
|
||||
{
|
||||
async decorateRequestBodyForAuth(
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<KubernetesRequestBody> {
|
||||
// No-op, with service account for auth, cluster config/details should already have serviceAccountToken
|
||||
return requestBody;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,5 @@
|
||||
export { kubernetesAuthProvidersApiRef } from './types';
|
||||
export type { KubernetesAuthProvidersApi } from './types';
|
||||
export { KubernetesAuthProviders } from './KubernetesAuthProviders';
|
||||
export { AwsKubernetesAuthProvider } from './AwsKubernetesAuthProvider';
|
||||
export { GoogleKubernetesAuthProvider } from './GoogleKubernetesAuthProvider';
|
||||
export { GoogleServiceAccountAuthProvider } from './GoogleServiceAccountAuthProvider';
|
||||
export { ServiceAccountKubernetesAuthProvider } from './ServiceAccountKubernetesAuthProvider';
|
||||
export { ServerSideKubernetesAuthProvider } from './ServerSideAuthProvider';
|
||||
|
||||
@@ -76,7 +76,21 @@ export const kubernetesPlugin = createPlugin({
|
||||
},
|
||||
});
|
||||
|
||||
export const EntityKubernetesContent = kubernetesPlugin.provide(
|
||||
/**
|
||||
* Props of EntityKubernetesContent
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type EntityKubernetesContentProps = {
|
||||
/**
|
||||
* Sets the refresh interval in milliseconds. The default value is 10000 (10 seconds)
|
||||
*/
|
||||
refreshIntervalMs?: number;
|
||||
};
|
||||
|
||||
export const EntityKubernetesContent: (
|
||||
props: EntityKubernetesContentProps,
|
||||
) => JSX.Element = kubernetesPlugin.provide(
|
||||
createRoutableExtension({
|
||||
name: 'EntityKubernetesContent',
|
||||
component: () => import('./Router').then(m => m.Router),
|
||||
|
||||
@@ -14,3 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import '@testing-library/jest-dom';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { TextDecoder, TextEncoder } from 'util';
|
||||
|
||||
// These are missing from jest-node, so not available on global.
|
||||
Object.defineProperty(global, 'TextEncoder', {
|
||||
value: TextEncoder,
|
||||
});
|
||||
|
||||
Object.defineProperty(global, 'TextDecoder', {
|
||||
value: TextDecoder,
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
V1Ingress,
|
||||
V1Job,
|
||||
V1CronJob,
|
||||
V1StatefulSet,
|
||||
} from '@kubernetes/client-node';
|
||||
|
||||
export interface DeploymentResources {
|
||||
@@ -41,6 +42,7 @@ export interface GroupedResponses extends DeploymentResources {
|
||||
jobs: V1Job[];
|
||||
cronJobs: V1CronJob[];
|
||||
customResources: any[];
|
||||
statefulsets: V1StatefulSet[];
|
||||
}
|
||||
|
||||
export interface ClusterLinksFormatterOptions {
|
||||
|
||||
@@ -21,6 +21,7 @@ const kindMappings: Record<string, string> = {
|
||||
ingress: 'ingress',
|
||||
service: 'service',
|
||||
horizontalpodautoscaler: 'deployment',
|
||||
statefulset: 'statefulset',
|
||||
};
|
||||
|
||||
export function standardFormatter(options: ClusterLinksFormatterOptions) {
|
||||
|
||||
@@ -54,17 +54,24 @@ export const getOwnedPodsThroughReplicaSets = (
|
||||
}, [] as V1Pod[]);
|
||||
};
|
||||
|
||||
interface ResourceRef {
|
||||
kind: string;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export const getMatchingHpa = (
|
||||
ownerName: string | undefined,
|
||||
ownerKind: string,
|
||||
owner: ResourceRef,
|
||||
hpas: V1HorizontalPodAutoscaler[],
|
||||
): V1HorizontalPodAutoscaler | undefined => {
|
||||
return hpas.find(hpa => {
|
||||
return (
|
||||
(hpa.spec?.scaleTargetRef?.kind ?? '').toLocaleLowerCase('en-US') ===
|
||||
ownerKind.toLocaleLowerCase('en-US') &&
|
||||
owner.kind.toLocaleLowerCase('en-US') &&
|
||||
(hpa.metadata?.namespace ?? '') ===
|
||||
(owner.namespace ?? 'unknown-namespace') &&
|
||||
(hpa.spec?.scaleTargetRef?.name ?? '') ===
|
||||
(ownerName ?? 'unknown-deployment')
|
||||
(owner.name ?? 'unknown-deployment')
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { currentToDeclaredResourceToPerc, podStatusToCpuUtil } from './pod';
|
||||
import {
|
||||
currentToDeclaredResourceToPerc,
|
||||
podStatusToCpuUtil,
|
||||
podStatusToMemoryUtil,
|
||||
} from './pod';
|
||||
import { SubvalueCell } from '@backstage/core-components';
|
||||
|
||||
describe('pod', () => {
|
||||
@@ -46,7 +50,30 @@ describe('pod', () => {
|
||||
},
|
||||
} as any);
|
||||
expect(result).toStrictEqual(
|
||||
<SubvalueCell subvalue="limits: 50%" value="requests: 99%" />,
|
||||
<SubvalueCell
|
||||
subvalue="limits: 50% of 100m"
|
||||
value="requests: 99% of 50m"
|
||||
/>,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('podStatusToMemoryUtil', () => {
|
||||
it('does use correct units', () => {
|
||||
const result = podStatusToMemoryUtil({
|
||||
memory: {
|
||||
// ~91.5 MiB
|
||||
currentUsage: '95948800',
|
||||
// 320 MiB
|
||||
limitTotal: '335544320',
|
||||
// 192 MiB
|
||||
requestTotal: '201326592',
|
||||
},
|
||||
} as any);
|
||||
expect(result).toStrictEqual(
|
||||
<SubvalueCell
|
||||
subvalue="limits: 28% of 320MiB"
|
||||
value="requests: 47% of 192MiB"
|
||||
/>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -130,6 +130,10 @@ export const currentToDeclaredResourceToPerc = (
|
||||
return `${(numerator * BigInt(100)) / denominator}%`;
|
||||
};
|
||||
|
||||
const formatMilicores = (value: string | number): string => {
|
||||
return `${parseFloat(value.toString()) * 1000}m`;
|
||||
};
|
||||
|
||||
export const podStatusToCpuUtil = (podStatus: ClientPodStatus): ReactNode => {
|
||||
const cpuUtil = podStatus.cpu;
|
||||
|
||||
@@ -146,15 +150,19 @@ export const podStatusToCpuUtil = (podStatus: ClientPodStatus): ReactNode => {
|
||||
value={`requests: ${currentToDeclaredResourceToPerc(
|
||||
currentUsage,
|
||||
cpuUtil.requestTotal,
|
||||
)}`}
|
||||
)} of ${formatMilicores(cpuUtil.requestTotal)}`}
|
||||
subvalue={`limits: ${currentToDeclaredResourceToPerc(
|
||||
currentUsage,
|
||||
cpuUtil.limitTotal,
|
||||
)}`}
|
||||
)} of ${formatMilicores(cpuUtil.limitTotal)}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const bytesToMiB = (value: string | number): string => {
|
||||
return `${parseFloat(value.toString()) / 1024 / 1024}MiB`;
|
||||
};
|
||||
|
||||
export const podStatusToMemoryUtil = (
|
||||
podStatus: ClientPodStatus,
|
||||
): ReactNode => {
|
||||
@@ -165,11 +173,11 @@ export const podStatusToMemoryUtil = (
|
||||
value={`requests: ${currentToDeclaredResourceToPerc(
|
||||
memUtil.currentUsage,
|
||||
memUtil.requestTotal,
|
||||
)}`}
|
||||
)} of ${bytesToMiB(memUtil.requestTotal)}`}
|
||||
subvalue={`limits: ${currentToDeclaredResourceToPerc(
|
||||
memUtil.currentUsage,
|
||||
memUtil.limitTotal,
|
||||
)}`}
|
||||
)} of ${bytesToMiB(memUtil.limitTotal)}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -54,6 +54,9 @@ export const groupResponses = (
|
||||
case 'customresources':
|
||||
prev.customResources.push(...next.resources);
|
||||
break;
|
||||
case 'statefulsets':
|
||||
prev.statefulsets.push(...next.resources);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
return prev;
|
||||
@@ -69,6 +72,7 @@ export const groupResponses = (
|
||||
jobs: [],
|
||||
cronJobs: [],
|
||||
customResources: [],
|
||||
statefulsets: [],
|
||||
} as GroupedResponses,
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user