add new plugin to create GitOps-managed Kubernetes clusters with ready-to-use profiles
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { createApiRef } from '@backstage/core-api';
|
||||
|
||||
export interface CloneFromTemplateRequest {
|
||||
templateRepository: string;
|
||||
secrets: {
|
||||
awsAccessKeyId: string;
|
||||
awsSecretAccessKey: string;
|
||||
};
|
||||
targetOrg: string;
|
||||
targetRepo: string;
|
||||
gitHubUser: string;
|
||||
gitHubToken: string;
|
||||
}
|
||||
|
||||
export interface ApplyProfileRequest {
|
||||
targetOrg: string;
|
||||
targetRepo: string;
|
||||
gitHubUser: string;
|
||||
gitHubToken: string;
|
||||
profiles: string[];
|
||||
}
|
||||
|
||||
export interface ChangeClusterStateRequest {
|
||||
targetOrg: string;
|
||||
targetRepo: string;
|
||||
gitHubUser: string;
|
||||
gitHubToken: string;
|
||||
clusterState: 'present' | 'absent'; // /api/cluster/state
|
||||
}
|
||||
|
||||
export interface PollLogRequest {
|
||||
targetOrg: string;
|
||||
targetRepo: string;
|
||||
gitHubUser: string;
|
||||
gitHubToken: string;
|
||||
}
|
||||
|
||||
export interface Status {
|
||||
status: string; // queued, in_progress, or completed
|
||||
message: string;
|
||||
conclusion: string; // success, failure, neutral, cancelled, skipped, timed_out, or action_required
|
||||
}
|
||||
|
||||
export interface StatusResponse {
|
||||
result: Status[];
|
||||
link: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface ClusterStatus {
|
||||
name: string;
|
||||
link: string;
|
||||
status: string;
|
||||
conclusion: string;
|
||||
runStatus: Status[];
|
||||
}
|
||||
|
||||
export interface ListClusterStatusesResponse {
|
||||
result: ClusterStatus[];
|
||||
}
|
||||
|
||||
export interface ListClusterRequest {
|
||||
gitHubUser: string;
|
||||
gitHubToken: string;
|
||||
}
|
||||
|
||||
export class FetchError extends Error {
|
||||
get name(): string {
|
||||
return this.constructor.name;
|
||||
}
|
||||
|
||||
static async forResponse(resp: Response): Promise<FetchError> {
|
||||
return new FetchError(
|
||||
`Request failed with status code ${
|
||||
resp.status
|
||||
}.\nReason: ${await resp.text()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type GitOpsApi = {
|
||||
url: string;
|
||||
fetchLog(req: PollLogRequest): Promise<StatusResponse>;
|
||||
changeClusterState(req: ChangeClusterStateRequest): Promise<any>;
|
||||
cloneClusterFromTemplate(req: CloneFromTemplateRequest): Promise<any>;
|
||||
applyProfiles(req: ApplyProfileRequest): Promise<any>;
|
||||
listClusters(req: ListClusterRequest): Promise<ListClusterStatusesResponse>;
|
||||
};
|
||||
|
||||
export const gitOpsApiRef = createApiRef<GitOpsApi>({
|
||||
id: 'plugin.gitops.service',
|
||||
description: 'Used by the GitOps profiles plugin to make requests',
|
||||
});
|
||||
|
||||
export class GitOpsRestApi implements GitOpsApi {
|
||||
constructor(public url: string = '') {}
|
||||
|
||||
private async fetch<T = any>(path: string, init?: RequestInit): Promise<T> {
|
||||
const resp = await fetch(`${this.url}${path}`, init);
|
||||
if (!resp.ok) throw await FetchError.forResponse(resp);
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
async fetchLog(req: PollLogRequest): Promise<StatusResponse> {
|
||||
return await this.fetch<StatusResponse>(`/api/cluster/run-status`, {
|
||||
method: 'post',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
}
|
||||
|
||||
async changeClusterState(req: ChangeClusterStateRequest): Promise<any> {
|
||||
return await this.fetch<any>('/api/cluster/state', {
|
||||
method: 'post',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
}
|
||||
|
||||
async cloneClusterFromTemplate(req: CloneFromTemplateRequest): Promise<any> {
|
||||
return await this.fetch<any>('/api/cluster/clone-from-template', {
|
||||
method: 'post',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
}
|
||||
|
||||
async applyProfiles(req: ApplyProfileRequest): Promise<any> {
|
||||
return await this.fetch<any>('/api/cluster/profiles', {
|
||||
method: 'post',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
}
|
||||
|
||||
async listClusters(
|
||||
req: ListClusterRequest,
|
||||
): Promise<ListClusterStatusesResponse> {
|
||||
return await this.fetch<ListClusterStatusesResponse>('/api/clusters', {
|
||||
method: 'post',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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, { FC, useEffect, useState } from 'react';
|
||||
import {
|
||||
Content,
|
||||
ContentHeader,
|
||||
Header,
|
||||
SupportButton,
|
||||
Page,
|
||||
pageTheme,
|
||||
Table,
|
||||
Progress,
|
||||
HeaderLabel,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import ClusterTable from '../ClusterTable/ClusterTable';
|
||||
import { Button, Link, Typography } from '@material-ui/core';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useAsync, useLocalStorage } from 'react-use';
|
||||
import { gitOpsApiRef, ListClusterStatusesResponse, Status } from '../../api';
|
||||
import { transformRunStatus } from '../ProfileCatalog';
|
||||
|
||||
const ClusterPage: FC<{}> = () => {
|
||||
const params = useParams<{ owner: string; repo: string }>();
|
||||
|
||||
const [loginInfo] = useLocalStorage<{
|
||||
token: string;
|
||||
username: string;
|
||||
name: string;
|
||||
}>('githubLoginDetails');
|
||||
const [pollingLog, setPollingLog] = useState(true);
|
||||
const [runStatus, setRunStatus] = useState<Status[]>([]);
|
||||
const [runLink, setRunLink] = useState<string>('');
|
||||
const [showProgress, setShowProgress] = useState(true);
|
||||
|
||||
const api = useApi(gitOpsApiRef);
|
||||
|
||||
const columns = [
|
||||
{ field: 'status', title: 'Status' },
|
||||
{ field: 'message', title: 'Message' },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (pollingLog) {
|
||||
const interval = setInterval(async () => {
|
||||
const resp = await api.fetchLog({
|
||||
gitHubToken: loginInfo.token,
|
||||
gitHubUser: loginInfo.username,
|
||||
targetOrg: params.owner,
|
||||
targetRepo: params.repo,
|
||||
});
|
||||
|
||||
setRunStatus(resp.result);
|
||||
setRunLink(resp.link);
|
||||
if (resp.status === 'completed') {
|
||||
setPollingLog(false);
|
||||
setShowProgress(false);
|
||||
}
|
||||
}, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
return () => {};
|
||||
}, [pollingLog]);
|
||||
|
||||
if (params.owner === undefined || params.repo === undefined) {
|
||||
const { loading, error, value } = useAsync<ListClusterStatusesResponse>(
|
||||
() => {
|
||||
setPollingLog(false);
|
||||
return api.listClusters({
|
||||
gitHubToken: loginInfo.token,
|
||||
gitHubUser: loginInfo.username,
|
||||
});
|
||||
},
|
||||
);
|
||||
let content: JSX.Element;
|
||||
if (loading) {
|
||||
content = (
|
||||
<Content>
|
||||
<Progress />
|
||||
</Content>
|
||||
);
|
||||
} else if (error) {
|
||||
content = (
|
||||
<Content>
|
||||
<Typography variant="h4" color="error">
|
||||
Failed to load cluster, {String(error)}
|
||||
</Typography>
|
||||
</Content>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<Content>
|
||||
<ContentHeader title="Clusters">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
href="/gitops-cluster-create"
|
||||
>
|
||||
Create GitOps-managed Cluster
|
||||
</Button>
|
||||
<SupportButton>All clusters</SupportButton>
|
||||
</ContentHeader>
|
||||
<ClusterTable components={value!.result} />
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page theme={pageTheme.home}>
|
||||
<Header
|
||||
title="GitOps-managed Clusters"
|
||||
subtitle="Keep track of your software"
|
||||
>
|
||||
<HeaderLabel label="Welcome" value={loginInfo.name} />
|
||||
</Header>
|
||||
{content}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Page theme={pageTheme.home}>
|
||||
<Header title={`Cluster ${params.owner}/${params.repo}`}>
|
||||
<HeaderLabel label="Welcome" value={loginInfo.name} />
|
||||
</Header>
|
||||
<Content>
|
||||
<Progress hidden={!showProgress} />
|
||||
<Table
|
||||
options={{ search: false, paging: false, toolbar: false }}
|
||||
data={transformRunStatus(runStatus)}
|
||||
columns={columns}
|
||||
/>
|
||||
<Link
|
||||
hidden={runLink === ''}
|
||||
rel="noopener noreferrer"
|
||||
href={`${runLink}?check_suite_focus=true`}
|
||||
target="_blank"
|
||||
>
|
||||
Details
|
||||
</Link>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClusterPage;
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { default } from './ClusterPage';
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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, { FC } from 'react';
|
||||
import { Table, TableColumn } from '@backstage/core';
|
||||
import { Link } from '@material-ui/core';
|
||||
import { ClusterStatus } from '../../api';
|
||||
import { transformStatus } from '../ProfileCatalog/ProfileCatalog';
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{
|
||||
title: 'Cluster Name',
|
||||
field: 'name',
|
||||
highlight: true,
|
||||
render: (componentData: any) => (
|
||||
<Link href={`/gitops-cluster/${componentData.name}`}>
|
||||
{componentData.name}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
field: 'status',
|
||||
render: (componentData: any) => (
|
||||
<>
|
||||
{transformStatus({
|
||||
status: componentData.status,
|
||||
conclusion: componentData.conclusion,
|
||||
message: componentData.status,
|
||||
})}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Conclusion',
|
||||
field: 'Conclusion',
|
||||
render: (componentData: any) => (
|
||||
<>
|
||||
{transformStatus({
|
||||
status: componentData.status,
|
||||
conclusion: componentData.conclusion,
|
||||
message: componentData.conclusion,
|
||||
})}
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
type ClusterTableProps = {
|
||||
components: ClusterStatus[];
|
||||
};
|
||||
const ClusterTable: FC<ClusterTableProps> = ({ components }) => {
|
||||
return (
|
||||
<Table columns={columns} options={{ paging: false }} data={components} />
|
||||
);
|
||||
};
|
||||
export default ClusterTable;
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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, { FC } from 'react';
|
||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardHeader from '@material-ui/core/CardHeader';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import CardActions from '@material-ui/core/CardActions';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { red } from '@material-ui/core/colors';
|
||||
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank';
|
||||
import CheckBoxIcon from '@material-ui/icons/CheckBox';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
maxWidth: 345,
|
||||
},
|
||||
media: {
|
||||
height: 0,
|
||||
paddingTop: '56.25%', // 16:9
|
||||
},
|
||||
expand: {
|
||||
transform: 'rotate(0deg)',
|
||||
marginLeft: 'auto',
|
||||
transition: theme.transitions.create('transform', {
|
||||
duration: theme.transitions.duration.shortest,
|
||||
}),
|
||||
},
|
||||
expandOpen: {
|
||||
transform: 'rotate(180deg)',
|
||||
},
|
||||
avatar: {
|
||||
fontSize: '1.0rem',
|
||||
backgroundColor: red[500],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
interface Props {
|
||||
platformName: string;
|
||||
title: string;
|
||||
repository: string;
|
||||
description: string;
|
||||
index: number;
|
||||
onClick: (i: number, repo: string) => void;
|
||||
activeIndex: number;
|
||||
}
|
||||
|
||||
const ClusterTemplateCard: FC<Props> = props => {
|
||||
const classes = useStyles();
|
||||
|
||||
const handleSelect = () => {
|
||||
props.onClick(props.index, props.repository);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={classes.root}>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar aria-label="recipe" className={classes.avatar}>
|
||||
{props.platformName}
|
||||
</Avatar>
|
||||
}
|
||||
action={<IconButton aria-label="settings" />}
|
||||
title={props.title}
|
||||
subheader={props.repository}
|
||||
/>
|
||||
<CardContent>
|
||||
<Typography variant="body2" color="textSecondary" component="p">
|
||||
{props.description}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions disableSpacing>
|
||||
<IconButton aria-label="select" onClick={handleSelect}>
|
||||
{props.activeIndex === props.index ? (
|
||||
<CheckBoxIcon color="primary" />
|
||||
) : (
|
||||
<CheckBoxOutlineBlankIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClusterTemplateCard;
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { default } from './ClusterTemplateCard';
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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, { FC } from 'react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import ClusterTemplateCard from '../ClusterTemplateCard';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
|
||||
interface Props {
|
||||
template: {
|
||||
platformName: string;
|
||||
title: string;
|
||||
repository: string;
|
||||
description: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
const ClusterTemplateCardList: FC<Props> = props => {
|
||||
const [activeIndex, setActiveIndex] = React.useState(-1);
|
||||
const [templateRepo, setTemplateRepo] = useLocalStorage<string>(
|
||||
'gitops-template-repo',
|
||||
);
|
||||
|
||||
const handleClicked = (index: number, repository: string) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(templateRepo);
|
||||
|
||||
setActiveIndex(index);
|
||||
setTemplateRepo(repository);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container xl={12} spacing={4}>
|
||||
{props.template.map((value, index) => (
|
||||
<Grid item xl={2} key={index}>
|
||||
<ClusterTemplateCard
|
||||
activeIndex={activeIndex}
|
||||
onClick={handleClicked}
|
||||
index={index}
|
||||
key={index}
|
||||
platformName={value.platformName}
|
||||
title={value.title}
|
||||
repository={value.repository}
|
||||
description={value.description}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClusterTemplateCardList;
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { default } from './ClusterTemplateCardList';
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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, { FC, useState } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
createStyles,
|
||||
IconButton,
|
||||
Theme,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import { green } from '@material-ui/core/colors';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank';
|
||||
import CheckBoxIcon from '@material-ui/icons/CheckBox';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
maxWidth: 345,
|
||||
},
|
||||
media: {
|
||||
height: 0,
|
||||
paddingTop: '56.25%', // 16:9
|
||||
},
|
||||
expand: {
|
||||
transform: 'rotate(0deg)',
|
||||
marginLeft: 'auto',
|
||||
transition: theme.transitions.create('transform', {
|
||||
duration: theme.transitions.duration.shortest,
|
||||
}),
|
||||
},
|
||||
expandOpen: {
|
||||
transform: 'rotate(180deg)',
|
||||
},
|
||||
avatar: {
|
||||
backgroundColor: green[500],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
interface Props {
|
||||
shortName: string;
|
||||
title: string;
|
||||
repository: string;
|
||||
description: string;
|
||||
index: number;
|
||||
onClick: (i: number, repository: string) => void;
|
||||
selections: Set<number>;
|
||||
}
|
||||
|
||||
const ProfileCard: FC<Props> = props => {
|
||||
const [selection, setSelection] = useState(false);
|
||||
|
||||
const handleSelect = () => {
|
||||
props.onClick(props.index, props.repository);
|
||||
setSelection(props.selections.has(props.index));
|
||||
};
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Card className={classes.root}>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar aria-label="recipe" className={classes.avatar}>
|
||||
{props.shortName}
|
||||
</Avatar>
|
||||
}
|
||||
action={<IconButton aria-label="settings" />}
|
||||
title={props.title}
|
||||
subheader={props.repository.replace('https://github.com/', '')}
|
||||
/>
|
||||
<CardContent>
|
||||
<Typography variant="body2" color="textSecondary" component="p">
|
||||
{props.description}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions disableSpacing>
|
||||
<IconButton aria-label="select" onClick={handleSelect}>
|
||||
{selection ? (
|
||||
<CheckBoxIcon color="primary" />
|
||||
) : (
|
||||
<CheckBoxOutlineBlankIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileCard;
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { default } from './ProfileCard';
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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, { FC, useState } from 'react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import ProfileCard from '../ProfileCard';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
|
||||
interface Props {
|
||||
profileTemplates: {
|
||||
shortName: string;
|
||||
title: string;
|
||||
repository: string;
|
||||
description: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
const ProfileCardList: FC<Props> = props => {
|
||||
const [selections, setSelections] = useState<Set<number>>(new Set<number>());
|
||||
const [profiles, setProfiles] = useState<Set<string>>(new Set<string>());
|
||||
const [gitopsProfiles, setGitopsProfiles] = useLocalStorage<string[]>(
|
||||
'gitops-profiles',
|
||||
);
|
||||
|
||||
const handleClicked = (index: number, repository: string) => {
|
||||
if (selections.has(index)) {
|
||||
selections.delete(index);
|
||||
profiles.delete(repository);
|
||||
} else {
|
||||
selections.add(index);
|
||||
profiles.add(repository);
|
||||
}
|
||||
|
||||
setSelections(selections);
|
||||
setProfiles(profiles);
|
||||
|
||||
setGitopsProfiles(Array.from(profiles));
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(profiles);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(gitopsProfiles);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(gitopsProfiles);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container xl={12} spacing={4}>
|
||||
{props.profileTemplates.map((value, index) => (
|
||||
<Grid item xl={2} key={index}>
|
||||
<ProfileCard
|
||||
shortName={value.shortName}
|
||||
selections={selections}
|
||||
onClick={handleClicked}
|
||||
key={index}
|
||||
index={index}
|
||||
title={value.title}
|
||||
repository={value.repository}
|
||||
description={value.description}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileCardList;
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { default } from './ProfileCardList';
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 mockFetch from 'jest-fetch-mock';
|
||||
import ProfileCatalog from './ProfileCatalog';
|
||||
import { ThemeProvider } from '@material-ui/core';
|
||||
import { lightTheme } from '@backstage/theme';
|
||||
|
||||
describe('ProfileCatalog', () => {
|
||||
it('should render', () => {
|
||||
mockFetch.mockResponse(() => new Promise(() => {}));
|
||||
const rendered = render(
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<ProfileCatalog />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
expect(
|
||||
rendered.getByText('Create GitOps-managed Cluster'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,338 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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, { FC, useEffect, useState } from 'react';
|
||||
import {
|
||||
Header,
|
||||
Page,
|
||||
pageTheme,
|
||||
Content,
|
||||
ContentHeader,
|
||||
HeaderLabel,
|
||||
SupportButton,
|
||||
SimpleStepper,
|
||||
SimpleStepperStep,
|
||||
InfoCard,
|
||||
Progress,
|
||||
Table,
|
||||
StatusWarning,
|
||||
StatusOK,
|
||||
StatusRunning,
|
||||
StatusError,
|
||||
StatusPending,
|
||||
StatusAborted,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import { TextField, List, ListItem, Link } from '@material-ui/core';
|
||||
|
||||
import ClusterTemplateCardList from '../ClusterTemplateCardList';
|
||||
import ProfileCardList from '../ProfileCardList';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { gitOpsApiRef, Status } from '../../api';
|
||||
|
||||
// OK = (completed, success)
|
||||
// Error = (?,failure)
|
||||
// Aborted = (?,cancelled)
|
||||
// Error = (?,timed_out)
|
||||
// Warning = (?, skipped)
|
||||
// Running = (queued, ?)
|
||||
// Running = (in_progress,?)
|
||||
export const transformStatus = (value: Status): JSX.Element => {
|
||||
let status: JSX.Element = <StatusRunning>Unknown</StatusRunning>;
|
||||
if (value.status === 'completed' && value.conclusion === 'success') {
|
||||
status = <StatusOK>Success</StatusOK>;
|
||||
} else if (value.conclusion === 'failure') {
|
||||
status = <StatusError>Failure</StatusError>;
|
||||
} else if (value.conclusion === 'cancelled') {
|
||||
status = <StatusAborted>Cancelled</StatusAborted>;
|
||||
} else if (value.conclusion === 'timed_out') {
|
||||
status = <StatusError>Timed out</StatusError>;
|
||||
} else if (value.conclusion === 'skipped') {
|
||||
status = <StatusWarning>Skipped</StatusWarning>;
|
||||
} else if (value.status === 'queued') {
|
||||
status = <StatusPending>Queued</StatusPending>;
|
||||
} else if (value.status === 'in_progress') {
|
||||
status = <StatusRunning>In Progress</StatusRunning>;
|
||||
}
|
||||
return status;
|
||||
};
|
||||
|
||||
export const transformRunStatus = (x: Status[]) => {
|
||||
return x.map(value => {
|
||||
return {
|
||||
status: transformStatus(value),
|
||||
message: value.message,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const ProfileCatalog: FC<{}> = () => {
|
||||
// TODO: get data from REST API
|
||||
const [clusterTemplates] = React.useState([
|
||||
{
|
||||
platformName: '15m',
|
||||
title: 'EKS 2 workers',
|
||||
repository: 'chanwit/eks-cluster-template',
|
||||
description: 'EKS with Kubernetes 1.16 / 2 nodes of m5.xlarge (15 mins)',
|
||||
},
|
||||
{
|
||||
platformName: '15m',
|
||||
title: 'EKS 1 worker',
|
||||
repository: 'chanwit/template-2',
|
||||
description: 'EKS with Kubernetes 1.16 / 1 node of m5.xlarge (15 mins)',
|
||||
},
|
||||
]);
|
||||
|
||||
const [profileTemplates] = React.useState([
|
||||
{
|
||||
shortName: 'ml',
|
||||
title: 'MLOps',
|
||||
repository: 'https://github.com/weaveworks/mlops-profile',
|
||||
description: 'Kubeflow-based Machine Learning pipeline',
|
||||
},
|
||||
{
|
||||
shortName: 'ai',
|
||||
title: 'COVID ML',
|
||||
repository: 'https://github.com/weaveworks/covid-ml-profile',
|
||||
description: 'Fk-covid Application profile',
|
||||
},
|
||||
]);
|
||||
|
||||
const [loginInfo] = useLocalStorage('githubLoginDetails', {
|
||||
name: 'Guest',
|
||||
username: '',
|
||||
token: '',
|
||||
});
|
||||
const [templateRepo] = useLocalStorage<string>('gitops-template-repo');
|
||||
const [gitopsProfiles] = useLocalStorage<string[]>('gitops-profiles');
|
||||
|
||||
const [showProgress, setShowProgress] = useState(false);
|
||||
const [pollingLog, setPollingLog] = useState(false);
|
||||
const [gitHubOrg, setGitHubOrg] = useState(loginInfo.username);
|
||||
const [gitHubRepo, setGitHubRepo] = useState('new-cluster');
|
||||
const [awsAccessKeyId, setAwsAccessKeyId] = useState(String);
|
||||
const [awsSecretAccessKey, setAwsSecretAccessKey] = useState(String);
|
||||
const [runStatus, setRunStatus] = useState<Status[]>([]);
|
||||
const [runLink, setRunLink] = useState<string>('');
|
||||
|
||||
const api = useApi(gitOpsApiRef);
|
||||
|
||||
useEffect(() => {
|
||||
if (pollingLog) {
|
||||
const interval = setInterval(async () => {
|
||||
const resp = await api.fetchLog({
|
||||
gitHubToken: loginInfo.token,
|
||||
gitHubUser: loginInfo.username,
|
||||
targetOrg: gitHubOrg,
|
||||
targetRepo: gitHubRepo,
|
||||
});
|
||||
|
||||
setRunStatus(resp.result);
|
||||
setRunLink(resp.link);
|
||||
if (resp.status === 'completed') {
|
||||
setPollingLog(false);
|
||||
setShowProgress(false);
|
||||
}
|
||||
}, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
return () => {};
|
||||
}, [pollingLog]);
|
||||
|
||||
const showFailureMessage = (msg: string) => {
|
||||
setRunStatus(
|
||||
runStatus.concat([
|
||||
{
|
||||
status: 'completed',
|
||||
message: msg,
|
||||
conclusion: 'failure',
|
||||
},
|
||||
]),
|
||||
);
|
||||
};
|
||||
|
||||
const showSuccessMessage = (msg: string) => {
|
||||
setRunStatus(
|
||||
runStatus.concat([
|
||||
{
|
||||
status: 'completed',
|
||||
message: msg,
|
||||
conclusion: 'success',
|
||||
},
|
||||
]),
|
||||
);
|
||||
};
|
||||
|
||||
const doCreateCluster = async () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(gitopsProfiles);
|
||||
|
||||
setShowProgress(true);
|
||||
setRunStatus([]);
|
||||
|
||||
const cloneResponse = await api.cloneClusterFromTemplate({
|
||||
templateRepository: templateRepo,
|
||||
gitHubToken: loginInfo.token,
|
||||
gitHubUser: loginInfo.username,
|
||||
targetOrg: gitHubOrg,
|
||||
targetRepo: gitHubRepo,
|
||||
secrets: {
|
||||
awsAccessKeyId: awsAccessKeyId,
|
||||
awsSecretAccessKey: awsSecretAccessKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (cloneResponse.error === undefined) {
|
||||
showSuccessMessage('Forked new cluster repo');
|
||||
} else {
|
||||
setShowProgress(false);
|
||||
showFailureMessage(cloneResponse.error);
|
||||
}
|
||||
|
||||
const applyProfileResp = await api.applyProfiles({
|
||||
gitHubToken: loginInfo.token,
|
||||
gitHubUser: loginInfo.username,
|
||||
targetOrg: gitHubOrg,
|
||||
targetRepo: gitHubRepo,
|
||||
profiles: gitopsProfiles,
|
||||
});
|
||||
|
||||
if (applyProfileResp.error === undefined) {
|
||||
showSuccessMessage('Applied profiles to the repo');
|
||||
} else {
|
||||
setShowProgress(false);
|
||||
showFailureMessage(applyProfileResp.error);
|
||||
}
|
||||
|
||||
const clusterStateResp = await api.changeClusterState({
|
||||
gitHubToken: loginInfo.token,
|
||||
gitHubUser: loginInfo.username,
|
||||
targetOrg: gitHubOrg,
|
||||
targetRepo: gitHubRepo,
|
||||
clusterState: 'present',
|
||||
});
|
||||
|
||||
if (clusterStateResp.error === undefined) {
|
||||
// cluster creation start, so start pulling log
|
||||
setPollingLog(true);
|
||||
showSuccessMessage('Changed desired cluster state to present');
|
||||
} else {
|
||||
setPollingLog(false);
|
||||
setShowProgress(false);
|
||||
showFailureMessage(clusterStateResp.error);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ field: 'status', title: 'Status' },
|
||||
{ field: 'message', title: 'Message' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
title="Create GitOps-managed Cluster"
|
||||
subtitle="Kubernetes cluster with ready-to-use profiles"
|
||||
>
|
||||
<HeaderLabel label="Welcome" value={loginInfo.name} />
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentHeader title="Create Cluster">
|
||||
<SupportButton>A description of your plugin goes here.</SupportButton>
|
||||
</ContentHeader>
|
||||
<SimpleStepper>
|
||||
<SimpleStepperStep title="Choose Cluster Template">
|
||||
<ClusterTemplateCardList template={clusterTemplates} />
|
||||
</SimpleStepperStep>
|
||||
<SimpleStepperStep title="Select GitOps Profile">
|
||||
<ProfileCardList profileTemplates={profileTemplates} />
|
||||
</SimpleStepperStep>
|
||||
<SimpleStepperStep
|
||||
title="Create Cluster"
|
||||
actions={{ nextText: 'Create', onNext: () => doCreateCluster() }}
|
||||
>
|
||||
<InfoCard>
|
||||
<List>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="github-org-tf"
|
||||
label="GitHub Organization"
|
||||
defaultValue={gitHubOrg}
|
||||
required
|
||||
onChange={e => {
|
||||
setGitHubOrg(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="github-repo-tf"
|
||||
label="New Repository"
|
||||
defaultValue={gitHubRepo}
|
||||
required
|
||||
onChange={e => {
|
||||
setGitHubRepo(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="aws-access-key-id-tf"
|
||||
label="Access Key ID"
|
||||
required
|
||||
type="password"
|
||||
onChange={e => {
|
||||
setAwsAccessKeyId(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="aws-secret-access-key-tf"
|
||||
label="Secret Access Key"
|
||||
required
|
||||
type="password"
|
||||
onChange={e => {
|
||||
setAwsSecretAccessKey(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</InfoCard>
|
||||
</SimpleStepperStep>
|
||||
</SimpleStepper>
|
||||
<div>
|
||||
<Progress hidden={!showProgress} />
|
||||
<Table
|
||||
options={{ search: false, paging: false, toolbar: false }}
|
||||
data={transformRunStatus(runStatus)}
|
||||
columns={columns}
|
||||
/>
|
||||
<Link
|
||||
hidden={runLink === ''}
|
||||
rel="noopener noreferrer"
|
||||
href={`${runLink}?check_suite_focus=true`}
|
||||
target="_blank"
|
||||
>
|
||||
Details
|
||||
</Link>
|
||||
</div>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileCatalog;
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { default, transformRunStatus } from './ProfileCatalog';
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { plugin } from './plugin';
|
||||
export * from './api';
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { plugin } from './plugin';
|
||||
|
||||
describe('gitops-profiles', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(plugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { createPlugin } from '@backstage/core';
|
||||
import ProfileCatalog from './components/ProfileCatalog';
|
||||
import ClusterPage from './components/ClusterPage';
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'gitops-profiles',
|
||||
register({ router }) {
|
||||
router.registerRoute('/gitops-clusters', ClusterPage);
|
||||
router.registerRoute('/gitops-cluster/:owner/:repo', ClusterPage);
|
||||
router.registerRoute('/gitops-cluster-create', ProfileCatalog);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 '@testing-library/jest-dom';
|
||||
require('jest-fetch-mock').enableMocks();
|
||||
Reference in New Issue
Block a user