diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index ca6d09e318..c4450c8261 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -112,6 +112,7 @@ import { EntityOwnershipCard, EntityUserProfileCard, } from '@backstage/plugin-org'; +import { EntityNomadContent, isNomadAvailable } from '@backstage/plugin-nomad'; import { EntityPagerDutyCard, isPagerDutyAvailable, @@ -501,6 +502,10 @@ const serviceEntityPage = ( + + + + {pullRequestsContent} diff --git a/plugins/nomad/src/Router.tsx b/plugins/nomad/src/Router.tsx index d65fda777a..7615d6ad99 100644 --- a/plugins/nomad/src/Router.tsx +++ b/plugins/nomad/src/Router.tsx @@ -21,16 +21,15 @@ import { MissingAnnotationEmptyState } from '@backstage/core-components'; import { Route, Routes } from 'react-router-dom'; import { GroupListForEntity } from './components/GroupList/GroupListForEntity'; +/** @public */ +export const NOMAD_NAMESPACE_ANNOTATION = 'nomad.io/namespace'; + /** @public */ export const NOMAD_GROUP_ANNOTATION = 'nomad.io/group'; -/** @public */ -export const NOMAD_TASK_ANNOTATION = 'nomad.io/task'; - /** @public */ export const isNomadAvailable = (entity: Entity) => - Boolean(entity.metadata.annotations?.[NOMAD_GROUP_ANNOTATION]) || - Boolean(entity.metadata.annotations?.[NOMAD_TASK_ANNOTATION]); + Boolean(entity.metadata.annotations?.[NOMAD_GROUP_ANNOTATION]); /** @public */ export const EmbeddedRouter = () => { diff --git a/plugins/nomad/src/api.ts b/plugins/nomad/src/api.ts index ca5113524b..e9a967e2ae 100644 --- a/plugins/nomad/src/api.ts +++ b/plugins/nomad/src/api.ts @@ -25,7 +25,7 @@ export const nomadApiRef = createApiRef({ /** @public */ export type NomadApi = { /** - * listAllocations is for listing all allocations matching some part of 'prefix'. + * listAllocations is for listing all allocations matching some part of 'filter'. * * See: https://developer.hashicorp.com/nomad/api-docs/allocations#list-allocations */ @@ -36,8 +36,8 @@ export type NomadApi = { /** @public */ export interface ListAllocationsRequest { - namespace?: string; - filter?: string; + namespace: string; + filter: string; } /** @public */ @@ -47,45 +47,71 @@ export interface ListAllocationsResponse { /** @public */ export interface Allocation { - id: string; - clientStatus: string; - createTime: number; - namespace: string; - name: string; - modifyTime: number; - nodeID: string; - taskGroup: string; + ClientStatus: string; + CreateTime: number; + DeploymentStatus: DeploymentStatus; + ID: string; + ModifyTime: number; + Name: string; + Namespace: string; + NodeID: string; + TaskGroup: string; } /** @public */ export interface DeploymentStatus { - healthy: boolean; - timestamp: string; + Healthy: boolean; + Timestamp: string; +} + +/** @public */ +export class FetchError extends Error { + get name(): string { + return this.constructor.name; + } + + static async forResponse(resp: Response): Promise { + return new FetchError( + `Request failed with status code ${ + resp.status + }.\nReason: ${await resp.text()}`, + ); + } } /** @public */ export class NomadHttpApi implements NomadApi { static fromConfig(config: Config) { return new NomadHttpApi( - config.getOptionalString('nomad.nomadAddr'), - config.getOptionalString('nomad.nomadRegion'), - config.getOptionalString('nomad.nomadNamespace'), - config.getOptionalString('nomad.nomadToken'), + config.getOptionalString('nomad.addr'), + config.getOptionalString('nomad.token'), ); } constructor( - private nomadAddr?: string, - private nomadRegion?: string, - private nomadNamespace?: string, - private nomadToken?: string, + private addr: string = 'http://127.0.0.1:4646', + private token?: string, ) {} + // TODO: pagination async listAllocations( options: ListAllocationsRequest, ): Promise { + const resp = await fetch( + `${this.addr}/v1/allocations?namespace=${encodeURIComponent( + options.namespace, + )}&filter=${encodeURIComponent(options.filter)}`, + { + method: 'GET', + headers: { + 'X-Nomad-Token': this.token || '', + }, + }, + ); + if (!resp.ok) throw await FetchError.forResponse(resp); + return Promise.resolve({ - allocations: [], + allocations: await resp.json(), }); } } diff --git a/plugins/nomad/src/components/GroupList/GroupListForEntity.tsx b/plugins/nomad/src/components/GroupList/GroupListForEntity.tsx index 101d894a81..b6323dcf87 100644 --- a/plugins/nomad/src/components/GroupList/GroupListForEntity.tsx +++ b/plugins/nomad/src/components/GroupList/GroupListForEntity.tsx @@ -14,18 +14,20 @@ * limitations under the License. */ -import { Table } from '@backstage/core-components'; +import { Progress } from '@backstage/core-components'; import React from 'react'; +import { useGroupForEntity } from '../../hooks/useGroupForEntity'; +import { GroupListTable } from './GroupListTable'; export const GroupListForEntity = () => { - return ( - - ); + const { value, loading, error } = useGroupForEntity(); + + if (loading) { + return ; + } + if (error || !value) { + return null; + } + + return ; }; diff --git a/plugins/nomad/src/components/GroupList/GroupListTable.tsx b/plugins/nomad/src/components/GroupList/GroupListTable.tsx new file mode 100644 index 0000000000..b427946805 --- /dev/null +++ b/plugins/nomad/src/components/GroupList/GroupListTable.tsx @@ -0,0 +1,47 @@ +/* + * 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 { Table, TableColumn } from '@backstage/core-components'; +import React from 'react'; +import { Allocation } from '../../api'; + +const columns: TableColumn[] = [ + { + title: 'Allocation ID', + field: 'ID', + }, + { + title: 'Name', + field: 'Name', + }, +]; + +export const GroupListTable = ({ + allocations, +}: { + allocations: Allocation[]; +}) => { + return ( +
+ ); +}; diff --git a/plugins/nomad/src/hooks/useGroupForEntity.tsx b/plugins/nomad/src/hooks/useGroupForEntity.tsx index 22a853cbb1..fac3bff598 100644 --- a/plugins/nomad/src/hooks/useGroupForEntity.tsx +++ b/plugins/nomad/src/hooks/useGroupForEntity.tsx @@ -15,12 +15,35 @@ */ import { useEntity } from '@backstage/plugin-catalog-react'; -import { NOMAD_GROUP_ANNOTATION } from '../Router'; +import { NOMAD_GROUP_ANNOTATION, NOMAD_NAMESPACE_ANNOTATION } from '../Router'; +import { errorApiRef, useApi } from '@backstage/core-plugin-api'; +import { nomadApiRef } from '../api'; +import useAsync from 'react-use/lib/useAsync'; /** * Get the entity's group and query it from the Nomad API. */ export const useGroupForEntity = () => { const { entity } = useEntity(); + + const namespace = + entity.metadata.annotations?.[NOMAD_NAMESPACE_ANNOTATION] ?? 'default'; const group = entity.metadata.annotations?.[NOMAD_GROUP_ANNOTATION] ?? ''; + + const nomadApi = useApi(nomadApiRef); + const errorApi = useApi(errorApiRef); + + const response = useAsync( + () => + nomadApi.listAllocations({ + namespace, + filter: `TaskGroup == "${group}"`, + }), + [group], + ); + + if (response.error) { + errorApi.post(response.error); + } + return response; }; diff --git a/plugins/nomad/src/index.ts b/plugins/nomad/src/index.ts index 662b52cd97..59ae5efaee 100644 --- a/plugins/nomad/src/index.ts +++ b/plugins/nomad/src/index.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { nomadPlugin, NomadPage } from './plugin'; +export { nomadPlugin, NomadPage, EntityNomadContent } from './plugin'; +export { isNomadAvailable, EmbeddedRouter } from './Router';