Merge pull request #18010 from jjtimmons/jjtimmons/add-nomad-plugin
Add Nomad plugin
This commit is contained in:
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,50 @@
|
||||
# @backstage/plugin-nomad-backend
|
||||
|
||||
A backend for [Nomad](https://www.nomadproject.io/), this plugin exposes a service with routes that are used by the `@backstage/plugin-nomad` plugin to query Job and Group information from a Nomad API.
|
||||
|
||||
## Set Up
|
||||
|
||||
1. Install the plugin using:
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
yarn add --cwd packages/backend @backstage/plugin-nomad-backend
|
||||
```
|
||||
|
||||
2. Create a `nomad.ts` file inside `packages/backend/src/plugins/`:
|
||||
|
||||
```typescript
|
||||
import { createRouter } from '@backstage/plugin-nomad-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin(
|
||||
props: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
return await createRouter(props);
|
||||
}
|
||||
```
|
||||
|
||||
3. Modify your `packages/backend/src/index.ts` to include:
|
||||
|
||||
```diff
|
||||
...
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import app from './plugins/app';
|
||||
+import nomad from './plugins/nomad';
|
||||
...
|
||||
|
||||
async function main() {
|
||||
...
|
||||
|
||||
const authEnv = useHotMemoize(module, () => createEnv('auth'));
|
||||
+ const nomadEnv = useHotMemoize(module, () => createEnv('nomad'));
|
||||
...
|
||||
|
||||
const apiRouter = Router();
|
||||
apiRouter.use('/catalog', await catalog(catalogEnv));
|
||||
+ apiRouter.use('/nomad', await nomad(nomadEnv));
|
||||
```
|
||||
|
||||
Note: for this backend to work, the `nomad` configuration described in the README of `@backstage/plugin-nomad` must be implemented.
|
||||
@@ -0,0 +1,22 @@
|
||||
## API Report File for "@backstage/plugin-nomad-backend"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { Config } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
// @public (undocumented)
|
||||
export function createRouter(options: RouterOptions): Promise<express.Router>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface RouterOptions {
|
||||
// (undocumented)
|
||||
config: Config;
|
||||
// (undocumented)
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "@backstage/plugin-nomad-backend",
|
||||
"version": "0.0.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "backend-plugin"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@types/express": "*",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^4.1.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"winston": "^3.2.1",
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"msw": "^1.0.0",
|
||||
"supertest": "^6.2.4"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2023 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.
|
||||
*/
|
||||
export * from './service/router';
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2023 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 { getRootLogger } from '@backstage/backend-common';
|
||||
import yn from 'yn';
|
||||
import { startStandaloneServer } from './service/standaloneServer';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
const config = new ConfigReader({});
|
||||
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007;
|
||||
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
|
||||
const logger = getRootLogger();
|
||||
|
||||
startStandaloneServer({ config, port, enableCors, logger }).catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
logger.info('CTRL+C pressed; exiting.');
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2023 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 { getVoidLogger } from '@backstage/backend-common';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
|
||||
import { createRouter } from './router';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
describe('createRouter', () => {
|
||||
let app: express.Express;
|
||||
|
||||
beforeAll(async () => {
|
||||
const router = await createRouter({
|
||||
config: new ConfigReader({
|
||||
nomad: {
|
||||
addr: 'http://localhost.com',
|
||||
token: 'asdf',
|
||||
},
|
||||
}),
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
app = express().use(router);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('GET /health', () => {
|
||||
it('returns ok', async () => {
|
||||
const response = await request(app).get('/health');
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({ status: 'ok' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2023 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 fetch from 'node-fetch';
|
||||
import { errorHandler, requestLoggingHandler } from '@backstage/backend-common';
|
||||
import { Config } from '@backstage/config';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
/** @public */
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const { config, logger } = options;
|
||||
|
||||
// Get Nomad addr and token from config
|
||||
const addr = config.getString('nomad.addr');
|
||||
const token = config.getOptionalString('nomad.token');
|
||||
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
|
||||
router.get('/health', (_, resp) => {
|
||||
resp.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
router.get('/v1/allocations', async (req, res) => {
|
||||
// Check namespace argument
|
||||
const namespace = (req.query.namespace as string) ?? '';
|
||||
if (!namespace || namespace === '') {
|
||||
throw new InputError(`Missing "namespace" query parameter`);
|
||||
}
|
||||
|
||||
// Check filter argument
|
||||
const filter = (req.query.filter as string) ?? '';
|
||||
logger.debug(`request headers: namespace=${namespace} filter=${filter}`);
|
||||
|
||||
// Issue the request
|
||||
const endpoint = `${addr}/v1/allocations?namespace=${encodeURIComponent(
|
||||
namespace,
|
||||
)}&filter=${encodeURIComponent(filter)}`;
|
||||
const allocationsResp = await fetch(endpoint, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-Nomad-Token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
// Check response
|
||||
if (allocationsResp.status !== 200) {
|
||||
const body = await allocationsResp.text();
|
||||
logger.error(`failed to call /v1/allocations endpoint: ${body}`);
|
||||
res.status(allocationsResp.status).json({ message: body });
|
||||
return;
|
||||
}
|
||||
|
||||
// Deserialize and return
|
||||
const allocationsBody = await allocationsResp.json();
|
||||
logger.debug(`/v1/allocations response: ${allocationsBody}`);
|
||||
res.json(allocationsBody);
|
||||
});
|
||||
|
||||
router.get('/v1/job/:job_id/versions', async (req, resp) => {
|
||||
// Check namespace argument
|
||||
const namespace = (req.query.namespace as string) ?? '';
|
||||
if (!namespace || namespace === '') {
|
||||
throw new InputError(`Missing "namespace" query parameter`);
|
||||
}
|
||||
|
||||
// Get job ID
|
||||
const jobID = (req.params.job_id as string) ?? '';
|
||||
|
||||
logger.info(`token: ${token}`);
|
||||
|
||||
// Issue the request
|
||||
const apiResp = await fetch(
|
||||
`${addr}/v1/job/${jobID}/versions?namespace=${encodeURIComponent(
|
||||
namespace,
|
||||
)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-Nomad-Token': token || '',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Check response
|
||||
if (apiResp.status !== 200) {
|
||||
const body = await apiResp.text();
|
||||
logger.error(`failed to call /v1/job/:job_id/versions endpoint: ${body}`);
|
||||
resp.status(apiResp.status).json({ message: body });
|
||||
return;
|
||||
}
|
||||
|
||||
// Deserialize and return
|
||||
const versionsBody = await apiResp.json();
|
||||
logger.debug(`/v1/job/:job_id/versions response: ${versionsBody}`);
|
||||
resp.json(versionsBody);
|
||||
});
|
||||
|
||||
router.use(requestLoggingHandler());
|
||||
router.use(errorHandler());
|
||||
return router;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2023 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 { createServiceBuilder } from '@backstage/backend-common';
|
||||
import { Server } from 'http';
|
||||
import { Logger } from 'winston';
|
||||
import { createRouter } from './router';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
export interface ServerOptions {
|
||||
config: Config;
|
||||
enableCors: boolean;
|
||||
logger: Logger;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export async function startStandaloneServer(
|
||||
options: ServerOptions,
|
||||
): Promise<Server> {
|
||||
const logger = options.logger.child({ service: 'nomad' });
|
||||
logger.debug('Starting application server...');
|
||||
const router = await createRouter({
|
||||
logger,
|
||||
config: options.config,
|
||||
});
|
||||
|
||||
let service = createServiceBuilder(module)
|
||||
.setPort(options.port)
|
||||
.addRouter('/nomad', router);
|
||||
if (options.enableCors) {
|
||||
service = service.enableCors({ origin: 'http://localhost:3000' });
|
||||
}
|
||||
|
||||
return await service.start().catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.hot?.accept();
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2023 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.
|
||||
*/
|
||||
export {};
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,167 @@
|
||||
# @backstage/plugin-nomad
|
||||
|
||||
This is a frontend plugin is for viewing [Nomad](https://www.nomadproject.io/) [job versions](https://developer.hashicorp.com/nomad/docs/concepts/architecture#job) and [task group allocations](https://developer.hashicorp.com/nomad/docs/concepts/architecture#allocation).
|
||||
|
||||
This plugin has a corresponding backend plugin required to call the Nomad cluster's API: ` @backstage/plugin-nomad-backend`.
|
||||
|
||||
## Introduction
|
||||
|
||||
### [Nomad](https://www.nomadproject.io/)
|
||||
|
||||
> A simple and flexible scheduler and orchestrator to deploy and manage containers and non-containerized applications across on-prem and clouds at scale.
|
||||
|
||||
### Features
|
||||
|
||||
This plugin provides two components:
|
||||
|
||||
- a table to view recent [job versions](https://developer.hashicorp.com/nomad/docs/commands/job/history)
|
||||
- a table to view [allocations for a job and/or group](https://developer.hashicorp.com/nomad/tutorials/manage-jobs/jobs-inspect)
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Requirements
|
||||
|
||||
You will need to have the backend Nomad plugin, `@backstage/plugin-nomad-backend`, installed and running. See its README for set up instructions.
|
||||
|
||||
You will need a running Nomad cluster with an API address that is reachable from the `@backstage/plugin-nomad/backend` plugin [running in the back end](https://backstage.io/docs/overview/architecture-overview/#third-party-backed-plugins). You can follow [this tutorial](https://developer.hashicorp.com/nomad/tutorials/enterprise/production-deployment-guide-vm-with-consul) to learn how to deploy one.
|
||||
|
||||
If your Nomad cluster has ACLs enabled, you will need a `token` with at least the [`list-jobs`capability](https://developer.hashicorp.com/nomad/tutorials/access-control/access-control-policies#namespace-rules). You can check [this tutorial](https://developer.hashicorp.com/nomad/tutorials/access-control/access-control-create-policy) for more info or the minimal [example below](#example-policy).
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
yarn add --cwd packages/app @backstage/plugin-nomad
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Add configuration to your [`app-config.yaml`](https://github.com/backstage/backstage/blob/master/app-config.yaml). For example:
|
||||
|
||||
```yaml
|
||||
nomad:
|
||||
addr: 'http://localhost:4646'
|
||||
token: '93e034ad-e504-42f9-129d-5d81be9f13d3'
|
||||
```
|
||||
|
||||
The `token` can be excluded if [ACLs are not enabled](https://developer.hashicorp.com/nomad/api-docs#authentication).
|
||||
|
||||
### Annotate Components
|
||||
|
||||
Several annotations are available for Components that make use of this plugin:
|
||||
|
||||
```yaml
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
annotations:
|
||||
nomad.io/namespace: default
|
||||
nomad.io/job-id: redis
|
||||
nomad.io/group: 'redis|prometheus-collector'
|
||||
```
|
||||
|
||||
- `nomad.io/job-id` annotation's value is matched exactly and corresponds to `JobID` in the Nomad API. It is required for the Job Versions Card but optional for the Allocations Table
|
||||
- `nomad.io/group` annotation's value is used as a regex pattern against `TaskGroup` using [Nomad's server-side filtering](https://developer.hashicorp.com/nomad/api-docs#filtering). It is optional for the [Allocations Table](#allocations-table)
|
||||
- `nomad.io/namespace` is the Namespace of the Job and Allocations of the Component. If omitted, it defaults to `default`
|
||||
|
||||
### Job Versions Card
|
||||
|
||||
The snippet below adds a card to the overview tab on the EntityPage. It shows versions of a Nomad job associated with a Component in the Catalog.
|
||||
|
||||
```typescript
|
||||
// In packages/app/src/components/catalog/EntityPage.tsx
|
||||
|
||||
import { EntityNomadJobVersionListCard, isNomadJobIDAvailable } from '@backstage/plugin-nomad';
|
||||
|
||||
const overviewContent = (
|
||||
...
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={isNomadJobIDAvailable}>
|
||||
<Grid item md={6} xs={12}>
|
||||
<EntityNomadJobVersionListCard />
|
||||
</Grid>
|
||||
</EntitySwitch.Case>
|
||||
</EntitySwitch>
|
||||
);
|
||||
```
|
||||
|
||||
<div>
|
||||
<img src="./img/job-versions-component.png" width="350em">
|
||||
</div>
|
||||
|
||||
#### Requirements
|
||||
|
||||
- the `nomad.io/job-id` annotation must be set
|
||||
|
||||
### Allocations Table
|
||||
|
||||
The snippet below adds a `/nomad` tab to the EntityPage that displays all allocations associated the `nomad.io/job-id` and/or `nomad.io/group` of Component's annotations.
|
||||
|
||||
```typescript
|
||||
// In packages/app/src/components/catalog/EntityPage.tsx
|
||||
|
||||
import { EntityNomadAllocationListTable, isNomadAllocationsAvailable } from '@backstage/plugin-nomad';
|
||||
|
||||
const serviceEntityPage = (
|
||||
...
|
||||
<EntityLayout.Route
|
||||
if={isNomadAllocationsAvailable}
|
||||
path="/nomad"
|
||||
title="Nomad"
|
||||
>
|
||||
<EntityNomadAllocationListTable />
|
||||
</EntityLayout.Route>
|
||||
)
|
||||
```
|
||||
|
||||
<div>
|
||||
<img src="./img/allocations-component.png" width="800em">
|
||||
</div>
|
||||
|
||||
#### Requirements
|
||||
|
||||
- `nomad.io/job-id` and/or `nomad.io/group` annotations must be set
|
||||
|
||||
## ACL Policy Example
|
||||
|
||||
Because this plugin uses API endpoints that require the `list-jobs` capability, the token you provide to the plugin's [`nomad` configuration](#configuration) needs at least that.
|
||||
|
||||
To create such a token you can create a policy like below. This policy applies to all namespaces:
|
||||
|
||||
```hcl
|
||||
# backstage.policy.hcl
|
||||
namespace "*" {
|
||||
policy = "read"
|
||||
}
|
||||
|
||||
node {
|
||||
policy = "read"
|
||||
}
|
||||
```
|
||||
|
||||
And create a policy for it:
|
||||
|
||||
```bash
|
||||
nomad acl policy apply backstage backstage.policy.hcl
|
||||
```
|
||||
|
||||
Then create a client token for it:
|
||||
|
||||
```bash
|
||||
nomad acl token create -name=backstage -policy=backstage
|
||||
Accessor ID = 5e9fe97b-76c5-8803-21b8-083308dc6c11
|
||||
Secret ID = 93e034ad-e504-42f9-129d-5d81be9f13d3
|
||||
Name = backstage
|
||||
Type = client
|
||||
Global = false
|
||||
Create Time = 2023-06-05 00:45:20.51905 +0000 UTC
|
||||
Expiry Time = <none>
|
||||
Create Index = 54
|
||||
Modify Index = 54
|
||||
Policies = [backstage]
|
||||
|
||||
Roles
|
||||
<none>
|
||||
```
|
||||
|
||||
In the example above, the `Secret ID` is the `token` to use in the [configuration](#configuration).
|
||||
@@ -0,0 +1,35 @@
|
||||
## API Report File for "@backstage/plugin-nomad"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
/// <reference types="react" />
|
||||
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
// @public
|
||||
export const EntityNomadAllocationListTable: () => JSX.Element;
|
||||
|
||||
// @public
|
||||
export const EntityNomadJobVersionListCard: () => JSX.Element | null;
|
||||
|
||||
// @public (undocumented)
|
||||
export const isNomadAllocationsAvailable: (entity: Entity) => boolean;
|
||||
|
||||
// @public (undocumented)
|
||||
export const isNomadJobIDAvailable: (entity: Entity) => boolean;
|
||||
|
||||
// @public (undocumented)
|
||||
export const nomadPlugin: BackstagePlugin<
|
||||
{
|
||||
root: RouteRef<undefined>;
|
||||
entityContent: RouteRef<undefined>;
|
||||
},
|
||||
{},
|
||||
{}
|
||||
>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2023 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 { createDevApp } from '@backstage/dev-utils';
|
||||
import { nomadPlugin } from '../src/plugin';
|
||||
|
||||
createDevApp().registerPlugin(nomadPlugin).render();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"name": "@backstage/plugin-nomad",
|
||||
"version": "0.0.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "frontend-plugin"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/core-components": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/plugin-catalog-react": "workspace:^",
|
||||
"@backstage/theme": "workspace:^",
|
||||
"@material-ui/core": "^4.9.13",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||
"luxon": "^3.3.0",
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17.0.0",
|
||||
"react-router-dom": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/core-app-api": "workspace:^",
|
||||
"@backstage/dev-utils": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^12.1.3",
|
||||
"@testing-library/user-event": "^14.0.0",
|
||||
"@types/node": "*",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"msw": "^1.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"configSchema": {
|
||||
"$schema": "https://backstage.io/schema/config-v1",
|
||||
"title": "@backstage/plugin-nomad",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"nomad": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"addr": {
|
||||
"type": "string",
|
||||
"description": "The address of the Nomad API. See: https://developer.hashicorp.com/nomad/api-docs#addressing-and-ports",
|
||||
"visibility": "frontend"
|
||||
},
|
||||
"token": {
|
||||
"type": "string",
|
||||
"description": "The token to call the Nomad API with. See: https://developer.hashicorp.com/nomad/api-docs#authentication",
|
||||
"visibility": "secret"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"addr"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 { Entity } from '@backstage/catalog-model';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { MissingAnnotationEmptyState } from '@backstage/core-components';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { EntityNomadAllocationListTable } from './components/EntityNomadAllocationListTable/EntityNomadAllocationListTable';
|
||||
|
||||
/** @public */
|
||||
export const NOMAD_NAMESPACE_ANNOTATION = 'nomad.io/namespace';
|
||||
|
||||
/** @public */
|
||||
export const NOMAD_JOB_ID_ANNOTATION = 'nomad.io/job-id';
|
||||
|
||||
/** @public */
|
||||
export const NOMAD_GROUP_ANNOTATION = 'nomad.io/group';
|
||||
|
||||
/** @public */
|
||||
export const isNomadJobIDAvailable = (entity: Entity) =>
|
||||
Boolean(entity.metadata.annotations?.[NOMAD_JOB_ID_ANNOTATION]);
|
||||
|
||||
/** @public */
|
||||
export const isNomadAllocationsAvailable = (entity: Entity) =>
|
||||
Boolean(entity.metadata.annotations?.[NOMAD_JOB_ID_ANNOTATION]) ||
|
||||
Boolean(entity.metadata.annotations?.[NOMAD_GROUP_ANNOTATION]);
|
||||
|
||||
export const EmbeddedRouter = () => {
|
||||
const { entity } = useEntity();
|
||||
|
||||
if (!isNomadAllocationsAvailable(entity)) {
|
||||
return (
|
||||
<MissingAnnotationEmptyState
|
||||
annotation={[NOMAD_JOB_ID_ANNOTATION, NOMAD_GROUP_ANNOTATION]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<EntityNomadAllocationListTable />} />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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 {
|
||||
DiscoveryApi,
|
||||
FetchApi,
|
||||
createApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
/** @public */
|
||||
export const nomadApiRef = createApiRef<NomadApi>({
|
||||
id: 'plugin.nomad.service',
|
||||
});
|
||||
|
||||
/** @public */
|
||||
export type NomadApi = {
|
||||
/**
|
||||
* listAllocations is for listing all allocations matching some part of 'filter'.
|
||||
*
|
||||
* See: https://developer.hashicorp.com/nomad/api-docs/allocations#list-allocations
|
||||
*/
|
||||
listAllocations: (
|
||||
options: ListAllocationsRequest,
|
||||
) => Promise<ListAllocationsResponse>;
|
||||
|
||||
/**
|
||||
* listJobVersions is for listing all deployments matching some part of 'filter'.
|
||||
*
|
||||
* See: https://developer.hashicorp.com/nomad/api-docs/deployments#list-deployments
|
||||
*/
|
||||
listJobVersions: (
|
||||
options: ListJobVersionsRequest,
|
||||
) => Promise<ListJobVersionsResponse>;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export interface ListAllocationsRequest {
|
||||
namespace: string;
|
||||
filter: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ListAllocationsResponse {
|
||||
allocations: Allocation[];
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface Allocation {
|
||||
ClientStatus: string;
|
||||
CreateTime: number;
|
||||
DeploymentStatus: DeploymentStatus;
|
||||
ID: string;
|
||||
JobID: string;
|
||||
JobVersion: string;
|
||||
ModifyTime: number;
|
||||
Name: string;
|
||||
Namespace: string;
|
||||
NodeID: string;
|
||||
TaskGroup: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface DeploymentStatus {
|
||||
Healthy: boolean;
|
||||
Timestamp: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ListJobVersionsRequest {
|
||||
namespace: string;
|
||||
jobID: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ListJobVersionsResponse {
|
||||
versions: Version[];
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface Version {
|
||||
ID: string;
|
||||
SubmitTime: number;
|
||||
Stable: boolean;
|
||||
Version: number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
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()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export class NomadHttpApi implements NomadApi {
|
||||
static create(discoveryApi: DiscoveryApi, fetchApi: FetchApi) {
|
||||
return new NomadHttpApi(discoveryApi, fetchApi);
|
||||
}
|
||||
|
||||
constructor(private discoveryApi: DiscoveryApi, private fetchApi: FetchApi) {}
|
||||
|
||||
// TODO: pagination
|
||||
async listAllocations(
|
||||
options: ListAllocationsRequest,
|
||||
): Promise<ListAllocationsResponse> {
|
||||
const apiUrl = await this.discoveryApi.getBaseUrl('nomad');
|
||||
|
||||
const resp = await this.fetchApi.fetch(
|
||||
`${apiUrl}/v1/allocations?namespace=${encodeURIComponent(
|
||||
options.namespace,
|
||||
)}&filter=${encodeURIComponent(options.filter)}`,
|
||||
);
|
||||
if (!resp.ok) throw await FetchError.forResponse(resp);
|
||||
|
||||
return {
|
||||
allocations: await resp.json(),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: pagination
|
||||
async listJobVersions(
|
||||
options: ListJobVersionsRequest,
|
||||
): Promise<ListJobVersionsResponse> {
|
||||
const apiUrl = await this.discoveryApi.getBaseUrl('nomad');
|
||||
|
||||
const resp = await this.fetchApi.fetch(
|
||||
`${apiUrl}/v1/job/${
|
||||
options.jobID
|
||||
}/versions?namespace=${encodeURIComponent(options.namespace)}`,
|
||||
);
|
||||
if (!resp.ok) throw await FetchError.forResponse(resp);
|
||||
|
||||
const respJson = await resp.json();
|
||||
|
||||
return {
|
||||
versions: respJson.Versions,
|
||||
};
|
||||
}
|
||||
}
|
||||
+201
@@ -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 { DateTime } from 'luxon';
|
||||
import {
|
||||
Link,
|
||||
MissingAnnotationEmptyState,
|
||||
ResponseErrorPanel,
|
||||
StatusError,
|
||||
StatusOK,
|
||||
StatusPending,
|
||||
StatusRunning,
|
||||
Table,
|
||||
TableColumn,
|
||||
} from '@backstage/core-components';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import React, { useState } from 'react';
|
||||
import { Allocation, nomadApiRef } from '../../api';
|
||||
import { configApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
NOMAD_GROUP_ANNOTATION,
|
||||
NOMAD_JOB_ID_ANNOTATION,
|
||||
NOMAD_NAMESPACE_ANNOTATION,
|
||||
isNomadAllocationsAvailable,
|
||||
} from '../../Router';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
type rowType = Allocation & { nomadAddr: string };
|
||||
|
||||
const columns: TableColumn<rowType>[] = [
|
||||
{
|
||||
title: 'ID',
|
||||
field: 'ID',
|
||||
render: row => (
|
||||
<Link to={`${row.nomadAddr}/ui/allocations/${row.ID}`} underline="always">
|
||||
{row.ID.split('-')[0]}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Task Group',
|
||||
field: 'TaskGroup',
|
||||
render: row => (
|
||||
<Link
|
||||
to={`${row.nomadAddr}/ui/jobs/${row.JobID}/${row.TaskGroup}`}
|
||||
underline="always"
|
||||
>
|
||||
{row.TaskGroup}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Created',
|
||||
field: 'CreateTime',
|
||||
render: row =>
|
||||
DateTime.fromMillis(row.CreateTime / 1000000).toLocaleString(
|
||||
DateTime.DATETIME_MED_WITH_SECONDS,
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
field: 'ClientStatus',
|
||||
render: row =>
|
||||
({
|
||||
pending: <StatusPending>pending</StatusPending>,
|
||||
running: <StatusOK>running</StatusOK>,
|
||||
failed: <StatusError>failed</StatusError>,
|
||||
complete: <StatusRunning>complete</StatusRunning>,
|
||||
}[row.ClientStatus] || <text>{row.ClientStatus}</text>),
|
||||
},
|
||||
{
|
||||
title: 'Version',
|
||||
field: 'JobVersion',
|
||||
render: row => row.JobVersion,
|
||||
},
|
||||
{
|
||||
title: 'Client',
|
||||
field: 'NodeID',
|
||||
render: row => (
|
||||
<Link to={`${row.nomadAddr}/ui/clients/${row.NodeID}`} underline="always">
|
||||
{row.ID.split('-')[0]}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* EntityNomadAllocationListTable is roughly based off Nomad's Allocations tab's view.
|
||||
*/
|
||||
export const EntityNomadAllocationListTable = () => {
|
||||
// Wait on entity
|
||||
const { entity } = useEntity();
|
||||
|
||||
// Get ref to the backend API
|
||||
const [init, setInit] = useState(true);
|
||||
const configApi = useApi(configApiRef);
|
||||
const nomadApi = useApi(nomadApiRef);
|
||||
const nomadAddr = configApi.getString('nomad.addr');
|
||||
|
||||
// Store results of calling API
|
||||
const [allocations, setAllocations] = useState<rowType[]>([]);
|
||||
const [err, setErr] = useState<Error>();
|
||||
|
||||
// Check that attributes are available
|
||||
if (!isNomadAllocationsAvailable(entity)) {
|
||||
<MissingAnnotationEmptyState
|
||||
annotation={[NOMAD_JOB_ID_ANNOTATION, NOMAD_GROUP_ANNOTATION]}
|
||||
/>;
|
||||
}
|
||||
|
||||
// Get plugin attributes
|
||||
const namespace =
|
||||
entity.metadata.annotations?.[NOMAD_NAMESPACE_ANNOTATION] ?? 'default';
|
||||
const job = entity.metadata.annotations?.[NOMAD_JOB_ID_ANNOTATION] ?? '';
|
||||
const group = entity.metadata.annotations?.[NOMAD_GROUP_ANNOTATION] ?? '';
|
||||
|
||||
// Make filter from attributes
|
||||
const filter: string[] = [];
|
||||
if (job) {
|
||||
filter.push(`(JobID == "${job}")`);
|
||||
}
|
||||
if (group) {
|
||||
filter.push(`(TaskGroup matches "${group}")`);
|
||||
}
|
||||
|
||||
// Create a query to update allocations
|
||||
const query = async () => {
|
||||
try {
|
||||
// Make call to nomad-backend
|
||||
const resp = await nomadApi.listAllocations({
|
||||
namespace,
|
||||
filter: filter.join(' and '),
|
||||
});
|
||||
|
||||
// Sort results
|
||||
const results = resp.allocations
|
||||
.sort((a, b) => a.CreateTime - b.CreateTime)
|
||||
.sort(({ ClientStatus: a }, { ClientStatus: b }) => {
|
||||
if (a === 'running' || b !== 'running') {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
setAllocations(results.map(row => ({ ...row, id: row.ID, nomadAddr })));
|
||||
setErr(undefined);
|
||||
} catch (e) {
|
||||
setAllocations([]);
|
||||
setErr(e);
|
||||
}
|
||||
};
|
||||
|
||||
// Start querying for allocations every 5s
|
||||
useAsync(async () => {
|
||||
if (init) {
|
||||
setInit(false);
|
||||
query();
|
||||
}
|
||||
|
||||
const interval = setTimeout(() => {
|
||||
query();
|
||||
}, 5_000);
|
||||
|
||||
return () => clearTimeout(interval);
|
||||
}, [allocations, entity]);
|
||||
|
||||
// Store a ref to a potential error
|
||||
if (err) {
|
||||
return <ResponseErrorPanel error={err} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table<rowType>
|
||||
title="Allocations"
|
||||
options={{
|
||||
search: true,
|
||||
padding: 'dense',
|
||||
sorting: true,
|
||||
draggable: false,
|
||||
paging: false,
|
||||
debounceInterval: 500,
|
||||
filterCellStyle: { padding: '0 16px 0 20px' },
|
||||
}}
|
||||
columns={columns}
|
||||
data={allocations}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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 { DateTime } from 'luxon';
|
||||
import {
|
||||
InfoCard,
|
||||
MissingAnnotationEmptyState,
|
||||
ResponseErrorPanel,
|
||||
Table,
|
||||
TableColumn,
|
||||
} from '@backstage/core-components';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Version, nomadApiRef } from '../../api';
|
||||
import { configApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
NOMAD_JOB_ID_ANNOTATION,
|
||||
NOMAD_NAMESPACE_ANNOTATION,
|
||||
isNomadJobIDAvailable,
|
||||
} from '../../Router';
|
||||
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
|
||||
import { Chip } from '@material-ui/core';
|
||||
|
||||
type rowType = Version & { nomadAddr: string };
|
||||
|
||||
const columns: TableColumn<rowType>[] = [
|
||||
{
|
||||
title: 'Version',
|
||||
field: 'Version',
|
||||
render: row => row.Version,
|
||||
},
|
||||
{
|
||||
title: 'Stable',
|
||||
field: 'Stable',
|
||||
render: row => <Chip label={`${row.Stable}`} />,
|
||||
},
|
||||
{
|
||||
title: 'Submitted',
|
||||
field: 'SubmitTime',
|
||||
render: row =>
|
||||
DateTime.fromMillis(row.SubmitTime / 1000000).toLocaleString(
|
||||
DateTime.DATETIME_MED_WITH_SECONDS,
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* EntityNomadJobVersionListCard is roughly based on the Nomad UI's versions tab.
|
||||
*/
|
||||
export const EntityNomadJobVersionListCard = () => {
|
||||
// Wait on entity
|
||||
const { entity } = useEntity();
|
||||
|
||||
// Get ref to the backend API
|
||||
const configApi = useApi(configApiRef);
|
||||
const nomadApi = useApi(nomadApiRef);
|
||||
const nomadAddr = configApi.getString('nomad.addr');
|
||||
|
||||
// Store results of calling API
|
||||
const [init, setInit] = useState(true);
|
||||
const [versions, setVersions] = useState<rowType[]>([]);
|
||||
const [err, setErr] = useState<Error>();
|
||||
|
||||
// Get plugin attributes
|
||||
const namespace =
|
||||
entity.metadata.annotations?.[NOMAD_NAMESPACE_ANNOTATION] ?? 'default';
|
||||
const jobID = entity.metadata.annotations?.[NOMAD_JOB_ID_ANNOTATION] ?? '';
|
||||
|
||||
// Start querying for allocations every 10s
|
||||
useEffect(() => {
|
||||
// Create a query to update allocations
|
||||
const query = async () => {
|
||||
try {
|
||||
// Make call to nomad-backend
|
||||
const resp = await nomadApi.listJobVersions({ namespace, jobID });
|
||||
|
||||
setVersions(
|
||||
resp.versions.map(row => ({ ...row, id: row.ID, nomadAddr })),
|
||||
);
|
||||
setErr(undefined);
|
||||
} catch (e) {
|
||||
setVersions([]);
|
||||
setErr(e);
|
||||
}
|
||||
};
|
||||
|
||||
if (init) {
|
||||
setInit(false);
|
||||
query();
|
||||
}
|
||||
|
||||
const interval = setTimeout(() => {
|
||||
query();
|
||||
}, 10_000);
|
||||
|
||||
return () => clearTimeout(interval);
|
||||
}, [init, jobID, namespace, nomadAddr, nomadApi]);
|
||||
|
||||
// Store a ref to a potential error
|
||||
if (err) {
|
||||
return <ResponseErrorPanel error={err} />;
|
||||
}
|
||||
|
||||
if (!entity) return null;
|
||||
|
||||
// Check that job ID is set
|
||||
if (!isNomadJobIDAvailable(entity)) {
|
||||
return (
|
||||
<InfoCard title="Job Versions">
|
||||
<MissingAnnotationEmptyState annotation={NOMAD_JOB_ID_ANNOTATION} />
|
||||
</InfoCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Table<rowType>
|
||||
title="Job versions"
|
||||
actions={[
|
||||
{
|
||||
icon: () => <OpenInNewIcon />,
|
||||
tooltip: 'Open in Nomad UI',
|
||||
isFreeAction: true,
|
||||
onClick: () => window.open(`${nomadAddr}/ui/jobs/${jobID}/versions`),
|
||||
},
|
||||
]}
|
||||
options={{
|
||||
search: false,
|
||||
padding: 'dense',
|
||||
sorting: true,
|
||||
draggable: false,
|
||||
paging: false,
|
||||
debounceInterval: 500,
|
||||
filterCellStyle: { padding: '0 16px 0 20px' },
|
||||
}}
|
||||
columns={columns}
|
||||
data={versions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2023 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 { EntityNomadJobVersionListCard } from './EntityNomadJobVersionListCard/EntityNomadJobVersionListCard';
|
||||
import { EntityNomadAllocationListTable } from './EntityNomadAllocationListTable/EntityNomadAllocationListTable';
|
||||
|
||||
export { EntityNomadJobVersionListCard, EntityNomadAllocationListTable };
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2023 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.
|
||||
*/
|
||||
export {
|
||||
nomadPlugin,
|
||||
EntityNomadJobVersionListCard,
|
||||
EntityNomadAllocationListTable,
|
||||
} from './plugin';
|
||||
export { isNomadAllocationsAvailable, isNomadJobIDAvailable } from './Router';
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2023 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 {
|
||||
nomadPlugin,
|
||||
entityContentRouteRef,
|
||||
EntityNomadContent,
|
||||
} from './plugin';
|
||||
|
||||
describe('nomad', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(nomadPlugin).toBeDefined();
|
||||
});
|
||||
|
||||
it('should export Nomad entity ref', () => {
|
||||
expect(entityContentRouteRef).toBeDefined();
|
||||
});
|
||||
|
||||
it('should export Nomad entity', () => {
|
||||
expect(EntityNomadContent).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2023 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 {
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
createApiFactory,
|
||||
createPlugin,
|
||||
createRouteRef,
|
||||
createRoutableExtension,
|
||||
createComponentExtension,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
import { rootRouteRef } from './routes';
|
||||
import { NomadHttpApi, nomadApiRef } from './api';
|
||||
|
||||
export const entityContentRouteRef = createRouteRef({
|
||||
id: 'nomad:entity-content',
|
||||
});
|
||||
|
||||
/** @public */
|
||||
export const nomadPlugin = createPlugin({
|
||||
id: 'nomad',
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: nomadApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },
|
||||
factory: ({ discoveryApi, fetchApi }) =>
|
||||
NomadHttpApi.create(discoveryApi, fetchApi),
|
||||
}),
|
||||
],
|
||||
routes: {
|
||||
root: rootRouteRef,
|
||||
entityContent: entityContentRouteRef,
|
||||
},
|
||||
});
|
||||
|
||||
/** @public */
|
||||
export const EntityNomadContent = nomadPlugin.provide(
|
||||
createRoutableExtension({
|
||||
name: 'EntityNomadContent',
|
||||
component: () => import('./Router').then(m => m.EmbeddedRouter),
|
||||
mountPoint: entityContentRouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Card used to show the list of Nomad job versions.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const EntityNomadJobVersionListCard = nomadPlugin.provide(
|
||||
createComponentExtension({
|
||||
name: 'EntityNomadJobVersionListCard',
|
||||
component: {
|
||||
lazy: () =>
|
||||
import('./components').then(m => m.EntityNomadJobVersionListCard),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Table used to show the list of Nomad allocations for a job and/or task-group.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const EntityNomadAllocationListTable = nomadPlugin.provide(
|
||||
createComponentExtension({
|
||||
name: 'EntityNomadAllocationListTable',
|
||||
component: {
|
||||
lazy: () =>
|
||||
import('./components').then(m => m.EntityNomadAllocationListTable),
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2023 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 { createRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
id: 'nomad',
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2023 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 '@testing-library/jest-dom';
|
||||
import 'cross-fetch/polyfill';
|
||||
Reference in New Issue
Block a user