Created gcp-projects plugin

This commit is contained in:
ebarrios
2020-07-14 07:16:20 +02:00
parent 240b183a01
commit 24c509b9e2
24 changed files with 970 additions and 8 deletions
+34
View File
@@ -0,0 +1,34 @@
/*
* 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';
import { Project, Operation } from './types';
export const GCPApiRef = createApiRef<GCPApi>({
id: 'plugin.gcpprojects.service',
description: 'Used by the GCP Projects plugin to make requests',
});
export type GCPApi = {
listProjects: ({ token }: { token: string }) => Promise<Project[]>;
getProject: (projectId: string, token: Promise<string>) => Promise<Project>;
createProject: (
projectName: string,
projectId: string,
owner: string,
token: string,
) => Promise<Operation>;
};
+125
View File
@@ -0,0 +1,125 @@
/*
* 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 { GCPApi } from './GCPApi';
import { Project, Operation, Status } from './types';
export class GCPClient implements GCPApi {
async listProjects({ token }: { token: string }): Promise<Project[]> {
const url = `https://content-cloudresourcemanager.googleapis.com/v1/projects`;
const response = await fetch(url, {
headers: new Headers({
Accept: '*/*',
Authorization: `Bearer ${token}`,
}),
});
if (!response.ok) {
return [
{
name: 'Error',
projectNumber: 'Response status is not OK',
projectId: 'Error',
lifecycleState: 'error',
createTime: 'Error',
},
];
}
const data = await response.json();
return data.projects;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getProject(
projectId: string,
token: Promise<string>,
): Promise<Project> {
const url = `https://content-cloudresourcemanager.googleapis.com/v1/projects/${projectId}`;
const response = await fetch(url, {
headers: new Headers({
Authorization: `Bearer ${await token}`,
}),
});
const dataBlank: Project = {
name: 'Error',
projectNumber: `Response status is${response.status}`,
projectId: 'Error',
lifecycleState: 'error',
createTime: 'Error',
};
if (!response.ok) {
return dataBlank;
}
const data = await response.json();
const newData: Project = data;
return newData;
}
async createProject(
projectName: string,
projectId: string,
token: string,
): Promise<Operation> {
const status: Status = {
code: 0,
message: '',
details: [],
};
const op: Operation = {
name: '',
metadata: '',
done: true,
error: status,
response: '',
};
const newProject: Project = {
name: projectName,
projectId: projectId,
};
const body = JSON.stringify(newProject);
const url = `https://content-cloudresourcemanager.googleapis.com/v1/projects`;
const response = await fetch(url, {
headers: new Headers({
Accept: '*/*',
Authorization: `Bearer ${token}`,
}),
body: body,
method: 'POST',
});
if (!response.ok) {
status.code = response.status;
return op;
}
const data = await response.json();
return data;
}
}
+19
View File
@@ -0,0 +1,19 @@
/*
* 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 * from './GCPApi';
export * from './GCPClient';
export * from './types';
+41
View File
@@ -0,0 +1,41 @@
/*
* 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 type Project = {
name: string;
projectNumber?: string;
projectId: string;
lifecycleState?: string;
createTime?: string;
};
export type ProjectDetails = {
details: string;
};
export type Operation = {
name: string;
metadata: string;
done: boolean;
error: Status;
response: string;
};
export type Status = {
code: number;
message: string;
details: string[];
};
@@ -0,0 +1,119 @@
/*
* 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, Button, TextField } from '@material-ui/core';
import {
InfoCard,
Header,
Page,
pageTheme,
Content,
ContentHeader,
HeaderLabel,
SupportButton,
SimpleStepper,
SimpleStepperStep,
StructuredMetadataTable,
} from '@backstage/core';
export const NewProjectPage: FC<{}> = () => {
const [projectName, setProjectName] = useState('');
const [projectId, setProjectId] = useState('');
const [done, setDone] = useState(false);
const metadata = {
ProjectName: projectName,
ProjectId: projectId,
};
return (
<Page theme={pageTheme.tool}>
<Header title="GCP - Projects" subtitle="Create new GCP Project">
<HeaderLabel label="Owner" value="Infrastructure Operations" />
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
<Content>
<ContentHeader title="Create new GCP Project at trivago">
<SupportButton>
This plugin will help you create a project in gcp.
</SupportButton>
</ContentHeader>
<Grid container spacing={3}>
<Grid item xs={12} md={4}>
<InfoCard>
<SimpleStepper>
<SimpleStepperStep title="Project Name">
<TextField
variant="outlined"
name="projectName"
label="Project Name"
helperText="The name of the new project."
inputProps={{ 'aria-label': 'Project Name' }}
onChange={e => setProjectName(e.target.value)}
value={projectName}
fullWidth
/>
</SimpleStepperStep>
<SimpleStepperStep title="Project ID">
<TextField
variant="outlined"
name="projectId"
label="projectId"
onChange={e => setProjectId(e.target.value)}
value={projectId}
fullWidth
/>
</SimpleStepperStep>
<SimpleStepperStep title="" end>
<Button
variant="contained"
color="primary"
onClick={() => setDone(true)}
>
Finish
</Button>
</SimpleStepperStep>
</SimpleStepper>
</InfoCard>
</Grid>
{done === true ? (
<Grid item xs={12} md={8}>
<InfoCard title="Project Info:">
<StructuredMetadataTable metadata={metadata} />
<br />
<br />
<br />
<Button
variant="contained"
color="primary"
href={`newProject?projectName=${encodeURIComponent(
projectName,
)},projectId=${encodeURIComponent(projectId)}`}
>
Confirm
</Button>
</InfoCard>
</Grid>
) : (
<br />
)}
</Grid>
</Content>
</Page>
);
};
@@ -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 { NewProjectPage } from './NewProjectPage';
@@ -0,0 +1,153 @@
/*
* 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 {
Button,
ButtonGroup,
LinearProgress,
makeStyles,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableRow,
Theme,
Typography,
} from '@material-ui/core';
import React from 'react';
import { useLocation } from 'react-router-dom';
import { useAsync } from 'react-use';
import { Link, useApi, googleAuthApiRef } from '@backstage/core';
import { GCPApiRef } from '../../api';
const useStyles = makeStyles<Theme>(theme => ({
root: {
maxWidth: 720,
margin: theme.spacing(2),
},
title: {
padding: theme.spacing(1, 0, 2, 0),
},
table: {
padding: theme.spacing(1),
},
}));
export const ProjectDetailsPage = () => {
const api = useApi(GCPApiRef);
const googleApi = useApi(googleAuthApiRef);
const token = googleApi.getAccessToken(
'https://www.googleapis.com/auth/cloud-platform.read-only',
);
const classes = useStyles();
const location = useLocation();
const status = useAsync(
() =>
api.getProject(
decodeURIComponent(location.search.split('projectId=')[1]),
token,
),
[location.search],
);
if (status.loading) {
return <LinearProgress />;
} else if (status.error) {
return (
<Typography variant="h6" color="error">
Failed to load build, {status.error.message}
</Typography>
);
}
const details = status.value;
return (
<div className={classes.root}>
<Typography className={classes.title} variant="h3">
<Link to="/gcp-projects">
<Typography component="span" variant="h3" color="primary">
&lt;
</Typography>
</Link>
Build Details
</Typography>
<TableContainer component={Paper} className={classes.table}>
<Table>
<TableBody>
<TableRow>
<TableCell>
<Typography noWrap>Name</Typography>
</TableCell>
<TableCell>{details?.name}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Project Number</Typography>
</TableCell>
<TableCell>{details?.projectNumber}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Project ID</Typography>
</TableCell>
<TableCell>{details?.projectId}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>State</Typography>
</TableCell>
<TableCell>
<TableCell>{details?.lifecycleState}</TableCell>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Creation Time</Typography>
</TableCell>
<TableCell>{details?.createTime}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Links</Typography>
</TableCell>
<TableCell>
<ButtonGroup
variant="text"
color="primary"
aria-label="text primary button group"
>
{details?.name && (
<Button>
<a href={details.name}>GCP</a>
</Button>
)}
{details?.name && (
<Button>
<a href={details.name}>Logs</a>
</Button>
)}
</ButtonGroup>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</div>
);
};
@@ -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 { ProjectDetailsPage } from './ProjectDetailsPage';
@@ -0,0 +1,155 @@
/*
* 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.
*/
// NEEDS WORK
import { Link, useApi, googleAuthApiRef, InfoCard } from '@backstage/core';
import {
LinearProgress,
makeStyles,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Theme,
Tooltip,
Typography,
Button,
} from '@material-ui/core';
import React from 'react';
import { useAsync } from 'react-use';
import { GCPApiRef, Project } from '../../api';
const LongText = ({ text, max }: { text: string; max: number }) => {
if (text.length < max) {
return <span>{text}</span>;
}
return (
<Tooltip title={text}>
<span>{text.slice(0, max)}...</span>
</Tooltip>
);
};
const useStyles = makeStyles<Theme>(theme => ({
root: {
padding: theme.spacing(2),
},
title: {
padding: theme.spacing(1, 0, 2, 0),
},
}));
const PageContents = () => {
const api = useApi(GCPApiRef);
const googleApi = useApi(googleAuthApiRef);
const { loading, error, value } = useAsync(async () => {
const token = await googleApi.getAccessToken(
'https://www.googleapis.com/auth/cloud-platform.read-only',
);
const projects = api.listProjects({ token });
return projects;
});
if (loading) {
return <LinearProgress />;
}
if (error) {
return (
<Typography variant="h2" color="error">
Failed to load projects, {error.message}{' '}
</Typography>
);
}
return (
<TableContainer component={Paper}>
<Table aria-label="GCP Projects table">
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Project Number</TableCell>
<TableCell>Project ID</TableCell>
<TableCell>State</TableCell>
<TableCell>Creation Time</TableCell>
</TableRow>
</TableHead>
<TableBody>
{value?.map((project: Project) => (
<TableRow key={project.projectId}>
<TableCell>
<Typography>
<LongText text={project.name} max={30} />
</Typography>
</TableCell>
<TableCell>
<Typography>
<LongText text={project.projectNumber} max={30} />
</Typography>
</TableCell>
<TableCell>
<Link
to={`project?projectId=${encodeURIComponent(
project.projectId,
)}`}
>
<Typography color="primary">
<LongText text={project.projectId} max={60} />
</Typography>
</Link>
</TableCell>
<TableCell>
<Typography>
<LongText text={project.lifecycleState} max={30} />
</Typography>
</TableCell>
<TableCell>
<Typography>
<LongText text={project.createTime} max={30} />
</Typography>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};
export const ProjectListPage = () => {
const classes = useStyles();
return (
<div className={classes.root}>
<Typography variant="h3" className={classes.title}>
GCP Projects
</Typography>
<InfoCard>
<Button variant="contained" color="primary" href="/gcp-projects/new">
Create new GCP Project
</Button>
</InfoCard>
<PageContents />
</div>
);
};
@@ -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 { ProjectListPage } from './ProjectListPage';
+18
View File
@@ -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';
+23
View File
@@ -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('gcp-projects', () => {
it('should export plugin', () => {
expect(plugin).toBeDefined();
});
});
+42
View File
@@ -0,0 +1,42 @@
/*
* 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, createRouteRef } from '@backstage/core';
import { ProjectListPage } from './components/ProjectListPage';
import { ProjectDetailsPage } from './components/ProjectDetailsPage';
import { NewProjectPage } from './components/NewProjectPage';
export const rootRouteRef = createRouteRef({
path: '/gcp-projects',
title: 'GCP Projects',
});
export const ProjectRouteRef = createRouteRef({
path: '/gcp-projects/project',
title: 'GCP Project Page',
});
export const NewProjectRouteRef = createRouteRef({
path: '/gcp-projects/new',
title: 'GCP Project Page',
});
export const plugin = createPlugin({
id: 'gcp-projects',
register({ router }) {
router.addRoute(rootRouteRef, ProjectListPage);
router.addRoute(ProjectRouteRef, ProjectDetailsPage);
router.addRoute(NewProjectRouteRef, NewProjectPage);
},
});
+19
View File
@@ -0,0 +1,19 @@
/*
* 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();