add formater and fix formating

This commit is contained in:
Manabu Mccloskey 2023-06-27 16:59:30 -07:00
parent b500dbd903
commit a0ba50d39f
19 changed files with 772 additions and 714 deletions

View file

@ -1,3 +1,4 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
ignorePatterns: ["src/api/generated/models/**.ts"]
module.exports = require("@backstage/cli/config/eslint-factory")(__dirname, {
ignorePatterns: ["src/api/generated/models/**.ts"],
extends: ["prettier"],
});

View file

@ -0,0 +1,3 @@
build
coverage
src/api/generated

View file

@ -0,0 +1 @@
{}

View file

@ -6,10 +6,10 @@ This plugin displays your Argo Workflows in Backstage
## Getting started
### Configuration
Entities must be annotated with Kubernetes annotations. For example:
```yaml
apiVersion: backstage.io/v1alpha1
kind: Component
@ -22,69 +22,69 @@ metadata:
```
Configure your Argo Workflows' instance base URL
```yaml
argoWorkflows:
baseUrl: https://my-argo-workflows.url
baseUrl: https://my-argo-workflows.url
```
### Authentication
This plugin supports two methods of authentication.
This plugin supports two methods of authentication.
#### Through Argo API with Service Account Token
This method uses a service account token to retrieve information from Argo API through the configured proxy endpoint.
This method uses a service account token to retrieve information from Argo API through the configured proxy endpoint.
1. Create a service account and associated permissions. For this plugin to work, you need list, get, and watch verbs.
for example create a file called `sa.yaml` with the following contents:
1. Create a service account and associated permissions. For this plugin to work, you need list, get, and watch verbs.
for example create a file called `sa.yaml` with the following contents:
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: backstage-argo-workflows-plugin
namespace: argo
---
# This is a long-lived token intended to be used by the backstage proxy.
apiVersion: v1
kind: Secret
metadata:
name: backstage-argo-workflows-plugin-token
annotations:
kubernetes.io/service-account.name: backstage-argo-workflows-plugin
namespace: argo
type: kubernetes.io/service-account-token
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: backstage-argo-workflows-plugin
rules:
- apiGroups: ["argoproj.io"]
resources: ["workflows"]
verbs: [ "get", "watch", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: backstage-argo-workflows-plugin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: backstage-argo-workflows-plugin
subjects:
- kind: ServiceAccount
name: backstage-argo-workflows-plugin
namespace: argo
```
2. Apply them to your cluster
---
# This is a long-lived token intended to be used by the backstage proxy.
apiVersion: v1
kind: Secret
metadata:
name: backstage-argo-workflows-plugin-token
annotations:
kubernetes.io/service-account.name: backstage-argo-workflows-plugin
namespace: argo
type: kubernetes.io/service-account-token
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: backstage-argo-workflows-plugin
rules:
- apiGroups: ["argoproj.io"]
resources: ["workflows"]
verbs: [ "get", "watch", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: backstage-argo-workflows-plugin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: backstage-argo-workflows-plugin
subjects:
- kind: ServiceAccount
name: backstage-argo-workflows-plugin
namespace: argo
```
2. Apply them to your cluster
```bash
kubectl apply -f sa.yaml
```
3. Configure [Backstage Proxy](https://backstage.io/docs/plugins/proxying). In this example we are instructing Backstage to get the token value from environment variable called `ARGO_WORKFLOWS_AUTH_TOKEN`
3. Configure [Backstage Proxy](https://backstage.io/docs/plugins/proxying). In this example we are instructing Backstage to get the token value from environment variable called `ARGO_WORKFLOWS_AUTH_TOKEN`
```yaml
proxy:
'/argo-workflows/api':
"/argo-workflows/api":
target: https://argo.a1.mccloman.people.aws.dev
changeOrigin: true
secure: true
@ -92,32 +92,33 @@ for example create a file called `sa.yaml` with the following contents:
Authorization:
$env: ARGO_WORKFLOWS_AUTH_TOKEN
```
4. Grab the token value and make it available as an environment variable for your backstage backend.
4. Grab the token value and make it available as an environment variable for your backstage backend.
```bash
ARGO_TOKEN="Bearer $(kubectl get secret -n argo backstage-argo-workflows-plugin-token -o=jsonpath='{.data.token}' | base64 --decode)"
```
If this is running in Kubernetes see this [documentation](https://kubernetes.io/docs/tasks/inject-data-application/distribute-credentials-secure/#define-container-environment-variables-using-secret-data).
See [this documentation](https://argoproj.github.io/argo-workflows/access-token/) for more information on getting your token.
#### Using configured Kubernetes API
The plugin can use configured Kubernetes clusters to fetch resources instead of going through the Argo Workflows API
The entity must be annotated correctly for it to work.
For example, for a Kubernetes cluster given in your `ap-config.yaml`
```yaml
kubernetes:
serviceLocatorMethod:
type: 'multiTenant'
type: "multiTenant"
clusterLocatorMethods:
- type: 'config'
clusters:
- url: https://abcd.gr7.us-west-2.eks.amazonaws.com:443
name: my-cluster-1
authProvider: 'serviceAccount'
serviceAccountToken: eyJh
caData: LS0t
- type: "config"
clusters:
- url: https://abcd.gr7.us-west-2.eks.amazonaws.com:443
name: my-cluster-1
authProvider: "serviceAccount"
serviceAccountToken: eyJh
caData: LS0t
```
For this configuration, the `argo-workflows/cluster-name` annotaton value must be `my-cluster-1`

View file

@ -1,10 +1,10 @@
export interface Config {
/** Optional configurations for the Argo Workflows plugin */
argoWorkflows: {
/**
* The base url of the Argo Workflows installation.
* @visibility frontend
*/
baseUrl: string;
};
/** Optional configurations for the Argo Workflows plugin */
argoWorkflows: {
/**
* The base url of the Argo Workflows installation.
* @visibility frontend
*/
baseUrl: string;
};
}

View file

@ -1,12 +1,12 @@
import React from 'react';
import { createDevApp } from '@backstage/dev-utils';
import { argoWorkflowsPlugin, ArgoWorkflowsPage } from '../src/plugin';
import React from "react";
import { createDevApp } from "@backstage/dev-utils";
import { argoWorkflowsPlugin, ArgoWorkflowsPage } from "../src/plugin";
createDevApp()
.registerPlugin(argoWorkflowsPlugin)
.addPage({
element: <ArgoWorkflowsPage />,
title: 'Root Page',
path: '/argo-workflows'
title: "Root Page",
path: "/argo-workflows",
})
.render();

View file

@ -46,7 +46,9 @@
"@testing-library/user-event": "^14.0.0",
"@types/node": "*",
"cross-fetch": "^3.1.5",
"msw": "^1.0.0"
"eslint-config-prettier": "8.8.0",
"msw": "^1.0.0",
"prettier": "2.8.8"
},
"files": [
"dist",

View file

@ -1,103 +1,110 @@
import {DiscoveryApi} from "@backstage/core-plugin-api";
import {ArgoWorkflows} from "./ArgoWorkflows";
import {KubernetesApi} from "@backstage/plugin-kubernetes";
import {MockConfigApi, MockFetchApi} from "@backstage/test-utils";
import {FrontendHostDiscovery} from "@backstage/core-app-api";
import {UserIdentity} from "@backstage/core-components";
import {inProgress} from "../test-data/in-progress";
import { DiscoveryApi } from "@backstage/core-plugin-api";
import { ArgoWorkflows } from "./ArgoWorkflows";
import { KubernetesApi } from "@backstage/plugin-kubernetes";
import { MockConfigApi, MockFetchApi } from "@backstage/test-utils";
import { FrontendHostDiscovery } from "@backstage/core-app-api";
import { UserIdentity } from "@backstage/core-components";
import { inProgress } from "../test-data/in-progress";
describe("ArgoWorkflowsClient", () => {
const mockDiscoveryApi: jest.Mocked<DiscoveryApi> = {
getBaseUrl: jest.fn().mockImplementation((id) => {
return Promise.resolve(`https://backstage.io/${id}`);
}),
};
const mockConfigApi = new MockConfigApi({
app: { baseUrl: "https://backstage.io" },
});
const noopFetchApi = new MockFetchApi({ baseImplementation: "none" });
describe('ArgoWorkflowsClient', () => {
const mockDiscoveryApi: jest.Mocked<DiscoveryApi> = {
getBaseUrl: jest.fn().mockImplementation((id) => {
return Promise.resolve(`https://backstage.io/${id}`)
})
}
const mockConfigApi = new MockConfigApi({
app: { baseUrl: 'https://backstage.io'}
})
const noopFetchApi = new MockFetchApi({baseImplementation: 'none'})
const mockKClient: jest.Mocked<KubernetesApi> = {
getObjectsByEntity: jest.fn(),
getClusters: jest.fn(),
getWorkloadsByEntity: jest.fn(),
getCustomObjectsByEntity: jest.fn(),
proxy: jest.fn(),
};
const mockKClient: jest.Mocked<KubernetesApi> = {
getObjectsByEntity: jest.fn(),
getClusters: jest.fn(),
getWorkloadsByEntity: jest.fn(),
getCustomObjectsByEntity: jest.fn(),
proxy: jest.fn()
}
beforeAll(() => {
jest
.spyOn(FrontendHostDiscovery.prototype, "getBaseUrl")
.mockImplementation((id) => {
return Promise.resolve(`https://backstage.io/${id}`);
});
jest
.spyOn(UserIdentity.prototype, "getCredentials")
.mockImplementation(() => {
return Promise.resolve({ token: "abc" });
});
});
afterEach(() => {
jest.clearAllMocks();
});
beforeAll( () => {
jest.spyOn(FrontendHostDiscovery.prototype, 'getBaseUrl')
.mockImplementation((id) => {
return Promise.resolve(`https://backstage.io/${id}`)
})
jest.spyOn(UserIdentity.prototype, 'getCredentials')
.mockImplementation( () => {
return Promise.resolve({token: 'abc'})
})
})
afterEach(() => {
jest.clearAllMocks()
})
it('can fetch from k8s', async () => {
mockKClient.proxy.mockResolvedValue(
{
status: 200,
ok: true,
text: async () => (JSON.stringify(inProgress))
} as Response
)
const a = new ArgoWorkflows(mockDiscoveryApi, mockKClient, mockConfigApi, noopFetchApi)
const spy = jest.spyOn(mockKClient, "proxy")
const resp = await a.getWorkflowsFromK8s("abc", "default", "my=env")
expect(resp.items.length).toBe(1)
expect(spy).toHaveBeenCalledWith({
clusterName: 'abc',
path: "/apis/argoproj.io/v1alpha1/namespaces/default/workflows?timeoutSeconds=30&labelSelector=my%3Denv",
})
})
it('can fetch from default k8s cluster', async () => {
mockKClient.proxy.mockResolvedValue(
{
status: 200,
ok: true,
text: async () => (JSON.stringify(inProgress))
} as Response
)
mockKClient.getClusters.mockResolvedValue(
[
{
name: 'cluster-1',
authProvider: 'provider-1'
}
]
)
const a = new ArgoWorkflows(mockDiscoveryApi, mockKClient, mockConfigApi, noopFetchApi)
const spy = jest.spyOn(a, "getCluster")
const resp = await a.getWorkflowsFromK8s(undefined, "default", "my=env")
expect(resp.items.length).toBe(1)
expect(spy).toHaveBeenCalled()
})
it('non ok status returned', async () => {
mockKClient.proxy.mockResolvedValue(
{
status: 500,
ok: false,
statusText: "something went wrong",
text: async () => ("oh no")
} as Response
)
const a = new ArgoWorkflows(mockDiscoveryApi, mockKClient, mockConfigApi, noopFetchApi)
await expect(a.getWorkflowsFromK8s("abc", "default", 'not used'))
.rejects.toEqual("failed to fetch resources: 500, something went wrong, oh no")
})
})
it("can fetch from k8s", async () => {
mockKClient.proxy.mockResolvedValue({
status: 200,
ok: true,
text: async () => JSON.stringify(inProgress),
} as Response);
const a = new ArgoWorkflows(
mockDiscoveryApi,
mockKClient,
mockConfigApi,
noopFetchApi
);
const spy = jest.spyOn(mockKClient, "proxy");
const resp = await a.getWorkflowsFromK8s("abc", "default", "my=env");
expect(resp.items.length).toBe(1);
expect(spy).toHaveBeenCalledWith({
clusterName: "abc",
path: "/apis/argoproj.io/v1alpha1/namespaces/default/workflows?timeoutSeconds=30&labelSelector=my%3Denv",
});
});
it("can fetch from default k8s cluster", async () => {
mockKClient.proxy.mockResolvedValue({
status: 200,
ok: true,
text: async () => JSON.stringify(inProgress),
} as Response);
mockKClient.getClusters.mockResolvedValue([
{
name: "cluster-1",
authProvider: "provider-1",
},
]);
const a = new ArgoWorkflows(
mockDiscoveryApi,
mockKClient,
mockConfigApi,
noopFetchApi
);
const spy = jest.spyOn(a, "getCluster");
const resp = await a.getWorkflowsFromK8s(undefined, "default", "my=env");
expect(resp.items.length).toBe(1);
expect(spy).toHaveBeenCalled();
});
it("non ok status returned", async () => {
mockKClient.proxy.mockResolvedValue({
status: 500,
ok: false,
statusText: "something went wrong",
text: async () => "oh no",
} as Response);
const a = new ArgoWorkflows(
mockDiscoveryApi,
mockKClient,
mockConfigApi,
noopFetchApi
);
await expect(
a.getWorkflowsFromK8s("abc", "default", "not used")
).rejects.toEqual(
"failed to fetch resources: 500, something went wrong, oh no"
);
});
});

View file

@ -1,85 +1,111 @@
import {ConfigApi, DiscoveryApi, FetchApi} from "@backstage/core-plugin-api";
import {KubernetesApi} from "@backstage/plugin-kubernetes";
import {IoArgoprojWorkflowV1alpha1WorkflowList} from "./generated";
import {ArgoWorkflowsApi} from "./index";
import { ConfigApi, DiscoveryApi, FetchApi } from "@backstage/core-plugin-api";
import { KubernetesApi } from "@backstage/plugin-kubernetes";
import { IoArgoprojWorkflowV1alpha1WorkflowList } from "./generated";
import { ArgoWorkflowsApi } from "./index";
const API_VERSION = 'argoproj.io/v1alpha1'
const WORKFLOW_PLURAL = 'workflows'
const DEFAULT_WORKFLOW_PROXY = '/argo-workflows/api'
const API_LABEL_SELECTOR = 'listOptions.labelSelector'
const API_TIMEOUT = "listOptions.timeoutSeconds"
const K8s_API_TIMEOUT = "timeoutSeconds"
const API_VERSION = "argoproj.io/v1alpha1";
const WORKFLOW_PLURAL = "workflows";
const DEFAULT_WORKFLOW_PROXY = "/argo-workflows/api";
const API_LABEL_SELECTOR = "listOptions.labelSelector";
const API_TIMEOUT = "listOptions.timeoutSeconds";
const K8s_API_TIMEOUT = "timeoutSeconds";
export class ArgoWorkflows implements ArgoWorkflowsApi {
discoveryApi: DiscoveryApi
kubernetesApi: KubernetesApi
configApi: ConfigApi
discoveryApi: DiscoveryApi;
kubernetesApi: KubernetesApi;
configApi: ConfigApi;
fetchApi: FetchApi;
constructor(
discoveryApi: DiscoveryApi,
kubernetesApi: KubernetesApi,
configApi: ConfigApi,
fetchApi: FetchApi
) {
this.discoveryApi = discoveryApi;
this.kubernetesApi = kubernetesApi;
this.configApi = configApi;
this.fetchApi = fetchApi;
}
constructor(discoveryApi: DiscoveryApi, kubernetesApi: KubernetesApi, configApi: ConfigApi, fetchApi: FetchApi) {
this.discoveryApi = discoveryApi
this.kubernetesApi = kubernetesApi
this.configApi = configApi
this.fetchApi = fetchApi
async getWorkflowsFromK8s(
clusterName: string | undefined,
namespace: string | undefined,
labels: string | undefined
): Promise<IoArgoprojWorkflowV1alpha1WorkflowList> {
const ns = namespace !== undefined ? namespace : "default";
const path = `/apis/${API_VERSION}/namespaces/${ns}/${WORKFLOW_PLURAL}`;
const query = new URLSearchParams({
[K8s_API_TIMEOUT]: "30",
});
if (labels) {
query.set("labelSelector", labels);
}
// need limits and pagination
const resp = await this.kubernetesApi.proxy({
clusterName:
clusterName !== undefined ? clusterName : await this.getCluster(),
path: `${path}?${query.toString()}`,
});
if (!resp.ok) {
return Promise.reject(
`failed to fetch resources: ${resp.status}, ${
resp.statusText
}, ${await resp.text()}`
);
}
// need validation
return JSON.parse(
await resp.text()
) as IoArgoprojWorkflowV1alpha1WorkflowList;
}
getWorkflows(
clusterName: string | undefined,
namespace: string,
labels: string | undefined
): Promise<IoArgoprojWorkflowV1alpha1WorkflowList> {
if (clusterName) {
return this.getWorkflowsFromK8s(clusterName, namespace, labels);
}
return this.getWorkflowsFromProxy(namespace, labels);
}
async getWorkflowsFromProxy(
namespace: string | undefined,
labels: string | undefined
): Promise<IoArgoprojWorkflowV1alpha1WorkflowList> {
const proxyUrl = await this.discoveryApi.getBaseUrl("proxy");
const ns = namespace !== undefined ? namespace : "default";
const url = `${proxyUrl}${DEFAULT_WORKFLOW_PROXY}/api/v1/workflows/${ns}`;
const query = new URLSearchParams({ [API_TIMEOUT]: "30" });
if (labels) {
query.set(API_LABEL_SELECTOR, labels);
}
const resp = await this.fetchApi.fetch(`${url}?${query.toString()}`, {});
if (!resp.ok) {
return Promise.reject(
`failed to fetch resources: ${resp.status}, ${
resp.statusText
}, ${await resp.json()}`
);
}
async getWorkflowsFromK8s(clusterName: string | undefined, namespace: string | undefined, labels: string | undefined): Promise<IoArgoprojWorkflowV1alpha1WorkflowList> {
const ns = namespace !== undefined ? namespace : 'default'
const path = `/apis/${API_VERSION}/namespaces/${ns}/${WORKFLOW_PLURAL}`
const query = new URLSearchParams({
[K8s_API_TIMEOUT]: "30"
})
if (labels) {
query.set('labelSelector', labels)
}
// need limits and pagination
const resp = await this.kubernetesApi.proxy({
clusterName: clusterName !== undefined ? clusterName: await this.getCluster(),
path: `${path}?${query.toString()}`
})
// need validation
return Promise.resolve(
JSON.parse(await resp.text()) as IoArgoprojWorkflowV1alpha1WorkflowList
);
}
if (!resp.ok) {
return Promise.reject(`failed to fetch resources: ${resp.status}, ${resp.statusText}, ${await resp.text()}`)
}
// need validation
return JSON.parse(await resp.text()) as IoArgoprojWorkflowV1alpha1WorkflowList
}
getWorkflows(clusterName: string | undefined, namespace: string, labels: string | undefined): Promise<IoArgoprojWorkflowV1alpha1WorkflowList> {
if (clusterName) {
return this.getWorkflowsFromK8s(clusterName, namespace, labels)
}
return this.getWorkflowsFromProxy(namespace, labels);
}
async getWorkflowsFromProxy(namespace: string | undefined, labels: string | undefined): Promise<IoArgoprojWorkflowV1alpha1WorkflowList> {
const proxyUrl = await this.discoveryApi.getBaseUrl('proxy')
const ns = namespace !== undefined ? namespace : 'default'
const url = `${proxyUrl}${DEFAULT_WORKFLOW_PROXY}/api/v1/workflows/${ns}`
const query = new URLSearchParams(
{[API_TIMEOUT]: "30"}
)
if (labels) {
query.set(API_LABEL_SELECTOR, labels)
}
const resp = await this.fetchApi.fetch(`${url}?${query.toString()}`, {})
if (!resp.ok) {
return Promise.reject(`failed to fetch resources: ${resp.status}, ${resp.statusText}, ${await resp.json()}`)
}
// need validation
return Promise.resolve(JSON.parse(await resp.text()) as IoArgoprojWorkflowV1alpha1WorkflowList);
}
async getCluster(): Promise<string> {
const clusters = await this.kubernetesApi.getClusters()
if (clusters.length > 0) {
return Promise.resolve(clusters[0].name)
}
return Promise.reject("no clusters found in configuration")
async getCluster(): Promise<string> {
const clusters = await this.kubernetesApi.getClusters();
if (clusters.length > 0) {
return Promise.resolve(clusters[0].name);
}
return Promise.reject("no clusters found in configuration");
}
}

View file

@ -1,22 +1,28 @@
import {
createApiRef,
DiscoveryApi,
} from '@backstage/core-plugin-api';
import { createApiRef, DiscoveryApi } from "@backstage/core-plugin-api";
import { KubernetesApi } from "@backstage/plugin-kubernetes";
import { IoArgoprojWorkflowV1alpha1WorkflowList } from "./generated/";
export {ArgoWorkflows} from "./ArgoWorkflows";
export { ArgoWorkflows } from "./ArgoWorkflows";
export const argoWorkflowsApiRef = createApiRef<ArgoWorkflowsApi>({
id: 'plugin.argoworkflows',
})
id: "plugin.argoworkflows",
});
export interface ArgoWorkflowsApi {
discoveryApi: DiscoveryApi
kubernetesApi: KubernetesApi
getWorkflowsFromK8s(clusterName: string, namespace: string | undefined, labels: string | undefined): Promise<IoArgoprojWorkflowV1alpha1WorkflowList>
getWorkflows(clusterName: string | undefined, namespace: string | undefined, labels: string | undefined): Promise<IoArgoprojWorkflowV1alpha1WorkflowList>
getWorkflowsFromProxy(namespace: string, labels: string | undefined): Promise<IoArgoprojWorkflowV1alpha1WorkflowList>
discoveryApi: DiscoveryApi;
kubernetesApi: KubernetesApi;
getWorkflowsFromK8s(
clusterName: string,
namespace: string | undefined,
labels: string | undefined
): Promise<IoArgoprojWorkflowV1alpha1WorkflowList>;
getWorkflows(
clusterName: string | undefined,
namespace: string | undefined,
labels: string | undefined
): Promise<IoArgoprojWorkflowV1alpha1WorkflowList>;
getWorkflowsFromProxy(
namespace: string,
labels: string | undefined
): Promise<IoArgoprojWorkflowV1alpha1WorkflowList>;
}

View file

@ -1,24 +1,27 @@
import React from 'react';
import {Header, HeaderLabel, Page, Content, ContentHeader, SupportButton} from "@backstage/core-components";
import {Grid} from "@material-ui/core";
import {WorkflowOverviewComponent} from "../WorkflowOverview/WorkflowOverview";
import React from "react";
import {
Header,
HeaderLabel,
Page,
Content,
ContentHeader,
SupportButton,
} from "@backstage/core-components";
import { Grid } from "@material-ui/core";
import { WorkflowOverviewComponent } from "../WorkflowOverview/WorkflowOverview";
export const OverviewComponent = () => (
<Page themeId="tool">
<Header title="Argo Workflows">
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
<Content>
<ContentHeader title="Overview">
<SupportButton>
Overview of your Argo Workflows
</SupportButton>
</ContentHeader>
<Grid item>
<WorkflowOverviewComponent />
</Grid>
</Content>
</Page>
)
<Page themeId="tool">
<Header title="Argo Workflows">
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
<Content>
<ContentHeader title="Overview">
<SupportButton>Overview of your Argo Workflows</SupportButton>
</ContentHeader>
<Grid item>
<WorkflowOverviewComponent />
</Grid>
</Content>
</Page>
);

View file

@ -1 +1 @@
export {OverviewComponent} from "./Overview";
export { OverviewComponent } from "./Overview";

View file

@ -1,99 +1,127 @@
import {configApiRef, useApi} from "@backstage/core-plugin-api";
import {argoWorkflowsApiRef} from "../../api";
import { configApiRef, useApi } from "@backstage/core-plugin-api";
import { argoWorkflowsApiRef } from "../../api";
import useAsync from "react-use/lib/useAsync";
import {Link, Progress, Table, TableColumn} from '@backstage/core-components'
import { Link, Progress, Table, TableColumn } from "@backstage/core-components";
import React from "react";
import Alert from "@material-ui/lab/Alert";
import { useEntity } from '@backstage/plugin-catalog-react';
import {IoArgoprojWorkflowV1alpha1WorkflowList} from "../../api/generated";
import { useEntity } from "@backstage/plugin-catalog-react";
import { IoArgoprojWorkflowV1alpha1WorkflowList } from "../../api/generated";
type TableData = {
name: string
namespace: string
phase?: string
progress?: string
startedAt?: string
finishedAt?: string
}
name: string;
namespace: string;
phase?: string;
progress?: string;
startedAt?: string;
finishedAt?: string;
};
export const WorkflowOverviewComponent = () => {
const {entity} = useEntity()
const apiClient = useApi(argoWorkflowsApiRef)
const configApi = useApi(configApiRef)
let argoWorkflowsBaseUrl = configApi.getOptionalString("argoWorkflows.baseUrl")
if (argoWorkflowsBaseUrl && argoWorkflowsBaseUrl.endsWith("/")) {
argoWorkflowsBaseUrl = argoWorkflowsBaseUrl.substring(0, argoWorkflowsBaseUrl.length - 1)
}
const { entity } = useEntity();
const apiClient = useApi(argoWorkflowsApiRef);
const configApi = useApi(configApiRef);
let argoWorkflowsBaseUrl = configApi.getOptionalString(
"argoWorkflows.baseUrl"
);
if (argoWorkflowsBaseUrl && argoWorkflowsBaseUrl.endsWith("/")) {
argoWorkflowsBaseUrl = argoWorkflowsBaseUrl.substring(
0,
argoWorkflowsBaseUrl.length - 1
);
}
const ln = entity.metadata.annotations?.["backstage.io/kubernetes-namespace"];
const ns = ln !== undefined ? ln : "default";
const clusterName =
entity.metadata.annotations?.["argo-workflows/cluster-name"];
const k8sLabelSelector =
entity.metadata.annotations?.["backstage.io/kubernetes-label-selector"];
const ln = entity.metadata.annotations?.['backstage.io/kubernetes-namespace']
const ns = ln !== undefined ? ln : 'default'
const clusterName = entity.metadata.annotations?.['argo-workflows/cluster-name']
const k8sLabelSelector = entity.metadata.annotations?.['backstage.io/kubernetes-label-selector']
const columns: TableColumn[] = [
{title: "Name", field: "name", render: (data: any | TableData, _): any => {
if (data && argoWorkflowsBaseUrl) {
return (<Link to={`${argoWorkflowsBaseUrl}/workflows/${data.namespace}/${data.name}`}>{data.name}</Link>)
}
return data.name
}},
{title: "Phase", field: "phase", cellStyle: (data, _) => {
if (data === "Succeeded") {
return {
color: '#6CD75F',
}
}
if (data === "Error" || data === "Failed") {
return {
color: '#DC3D5A'
}
}
return {}
}},
{title: "Progress", field: "progress"},
{title: "StartTime", field: "startedAt", type: "datetime", defaultSort: 'desc'},
{title: "EndTime", field: "finishedAt", type: "datetime"},
{title: "Namespace", field: "namespace", type: "string"}
]
const {value, loading, error} = useAsync(
async (): Promise<IoArgoprojWorkflowV1alpha1WorkflowList> => {
return await apiClient.getWorkflows(clusterName, ns, k8sLabelSelector)
const columns: TableColumn[] = [
{
title: "Name",
field: "name",
render: (data: any | TableData, _): any => {
if (data && argoWorkflowsBaseUrl) {
return (
<Link
to={`${argoWorkflowsBaseUrl}/workflows/${data.namespace}/${data.name}`}
>
{data.name}
</Link>
);
}
)
return data.name;
},
},
{
title: "Phase",
field: "phase",
cellStyle: (data, _) => {
if (data === "Succeeded") {
return {
color: "#6CD75F",
};
}
if (data === "Error" || data === "Failed") {
return {
color: "#DC3D5A",
};
}
return {};
},
},
{ title: "Progress", field: "progress" },
{
title: "StartTime",
field: "startedAt",
type: "datetime",
defaultSort: "desc",
},
{ title: "EndTime", field: "finishedAt", type: "datetime" },
{ title: "Namespace", field: "namespace", type: "string" },
];
if (loading) {
return <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
const { value, loading, error } = useAsync(
async (): Promise<IoArgoprojWorkflowV1alpha1WorkflowList> => {
return await apiClient.getWorkflows(clusterName, ns, k8sLabelSelector);
}
);
const data = value?.items?.map( val => {
return {
name: val.metadata.name,
namespace: val.metadata.namespace,
phase: val.status?.phase,
progress: val.status?.progress,
startedAt: val.status?.startedAt,
finishedAt: val.status?.finishedAt
} as TableData
})
if (loading) {
return <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
if (data) {
return (
<Table options={{
paging: true,
search: true,
sorting: true,
}}
columns={columns}
data={data.sort()}
/>
)
}
const data = value?.items?.map((val) => {
return {
name: val.metadata.name,
namespace: val.metadata.namespace,
phase: val.status?.phase,
progress: val.status?.progress,
startedAt: val.status?.startedAt,
finishedAt: val.status?.finishedAt,
} as TableData;
});
return <Alert severity="warning">No Workflows found with given filter. Check your entity's annotations.</Alert>
if (data) {
return (
<Table
options={{
paging: true,
search: true,
sorting: true,
}}
columns={columns}
data={data.sort()}
/>
);
}
}
return (
<Alert severity="warning">
No Workflows found with given filter. Check your entity's annotations.
</Alert>
);
};

View file

@ -1 +1 @@
export { argoWorkflowsPlugin, ArgoWorkflowsPage } from './plugin';
export { argoWorkflowsPlugin, ArgoWorkflowsPage } from "./plugin";

View file

@ -1,7 +1,7 @@
import { argoWorkflowsPlugin } from './plugin';
import { argoWorkflowsPlugin } from "./plugin";
describe('argo-workflows', () => {
it('should export plugin', () => {
describe("argo-workflows", () => {
it("should export plugin", () => {
expect(argoWorkflowsPlugin).toBeDefined();
});
});

View file

@ -1,43 +1,41 @@
import {
configApiRef,
createApiFactory,
createPlugin,
createRoutableExtension,
discoveryApiRef, fetchApiRef
} from '@backstage/core-plugin-api';
import { rootRouteRef } from './routes';
import {ArgoWorkflows, argoWorkflowsApiRef} from "./api";
import {kubernetesApiRef} from "@backstage/plugin-kubernetes";
configApiRef,
createApiFactory,
createPlugin,
createRoutableExtension,
discoveryApiRef,
fetchApiRef,
} from "@backstage/core-plugin-api";
import { rootRouteRef } from "./routes";
import { ArgoWorkflows, argoWorkflowsApiRef } from "./api";
import { kubernetesApiRef } from "@backstage/plugin-kubernetes";
export const argoWorkflowsPlugin = createPlugin({
id: 'argo-workflows',
id: "argo-workflows",
routes: {
root: rootRouteRef,
},
apis: [
createApiFactory({
api: argoWorkflowsApiRef,
deps: {
discoveryApi: discoveryApiRef,
kubernetesApi: kubernetesApiRef,
configApi: configApiRef,
fetchApi: fetchApiRef,
},
factory: ({
discoveryApi, kubernetesApi, configApi, fetchApi,
}) =>
new ArgoWorkflows(discoveryApi, kubernetesApi, configApi, fetchApi)
})
]
createApiFactory({
api: argoWorkflowsApiRef,
deps: {
discoveryApi: discoveryApiRef,
kubernetesApi: kubernetesApiRef,
configApi: configApiRef,
fetchApi: fetchApiRef,
},
factory: ({ discoveryApi, kubernetesApi, configApi, fetchApi }) =>
new ArgoWorkflows(discoveryApi, kubernetesApi, configApi, fetchApi),
}),
],
});
export const ArgoWorkflowsPage = argoWorkflowsPlugin.provide(
createRoutableExtension({
name: 'ArgoWorkflowsPage',
name: "ArgoWorkflowsPage",
component: () =>
import('./components/Overview').then(m => m.OverviewComponent),
import("./components/Overview").then((m) => m.OverviewComponent),
mountPoint: rootRouteRef,
}),
})
);

View file

@ -1,5 +1,5 @@
import { createRouteRef } from '@backstage/core-plugin-api';
import { createRouteRef } from "@backstage/core-plugin-api";
export const rootRouteRef = createRouteRef({
id: 'argo-workflows',
id: "argo-workflows",
});

View file

@ -1,2 +1,2 @@
import '@testing-library/jest-dom';
import 'cross-fetch/polyfill';
import "@testing-library/jest-dom";
import "cross-fetch/polyfill";

View file

@ -1,325 +1,307 @@
export const inProgress= {
"apiVersion": "v1",
"items": [
{
"apiVersion": "argoproj.io/v1alpha1",
"kind": "Workflow",
"metadata": {
"annotations": {
"workflows.argoproj.io/pod-name-format": "v2"
},
"creationTimestamp": "2023-06-27T21:41:33Z",
"generateName": "test-workflow-",
"generation": 3,
"labels": {
"backstage.io/kubernetes-id": "backstage",
"env": "dev",
"my": "label",
"workflows.argoproj.io/phase": "Running"
},
"name": "test-workflow-f49nr",
"namespace": "default",
"resourceVersion": "44977391",
"uid": "188b33ab-c877-4e04-901c-32babece9573"
export const inProgress = {
apiVersion: "v1",
items: [
{
apiVersion: "argoproj.io/v1alpha1",
kind: "Workflow",
metadata: {
annotations: {
"workflows.argoproj.io/pod-name-format": "v2",
},
creationTimestamp: "2023-06-27T21:41:33Z",
generateName: "test-workflow-",
generation: 3,
labels: {
"backstage.io/kubernetes-id": "backstage",
env: "dev",
my: "label",
"workflows.argoproj.io/phase": "Running",
},
name: "test-workflow-f49nr",
namespace: "default",
resourceVersion: "44977391",
uid: "188b33ab-c877-4e04-901c-32babece9573",
},
spec: {
arguments: {
parameters: [
{
name: "message",
value: "from workflow",
},
"spec": {
"arguments": {
"parameters": [
{
"name": "message",
"value": "from workflow"
}
]
],
},
workflowTemplateRef: {
name: "workflow-template-whalesay-template",
},
},
status: {
artifactGCStatus: {
notSpecified: true,
},
artifactRepositoryRef: {
artifactRepository: {},
default: true,
},
conditions: [
{
status: "True",
type: "PodRunning",
},
],
finishedAt: null,
nodes: {
"test-workflow-f49nr": {
children: ["test-workflow-f49nr-1432144569"],
displayName: "test-workflow-f49nr",
finishedAt: null,
id: "test-workflow-f49nr",
inputs: {
parameters: [
{
name: "message",
value: "from workflow",
},
"workflowTemplateRef": {
"name": "workflow-template-whalesay-template"
}
],
},
"status": {
"artifactGCStatus": {
"notSpecified": true
name: "test-workflow-f49nr",
phase: "Running",
progress: "1/2",
startedAt: "2023-06-27T21:41:33Z",
templateName: "whalesay-template",
templateScope: "local/",
type: "Steps",
},
"test-workflow-f49nr-1432144569": {
boundaryID: "test-workflow-f49nr",
children: [
"test-workflow-f49nr-1588075630",
"test-workflow-f49nr-2771663768",
],
displayName: "[0]",
finishedAt: null,
id: "test-workflow-f49nr-1432144569",
name: "test-workflow-f49nr[0]",
phase: "Running",
progress: "1/2",
startedAt: "2023-06-27T21:41:33Z",
templateScope: "local/",
type: "StepGroup",
},
"test-workflow-f49nr-1588075630": {
boundaryID: "test-workflow-f49nr",
displayName: "whalesay3",
finishedAt: "2023-06-27T21:41:37Z",
hostNodeName: "ip-192-168-10-135.us-west-2.compute.internal",
id: "test-workflow-f49nr-1588075630",
inputs: {
parameters: [
{
name: "message",
value: "from workflow",
},
"artifactRepositoryRef": {
"artifactRepository": {},
"default": true
],
},
name: "test-workflow-f49nr[0].whalesay3",
outputs: {
exitCode: "0",
},
phase: "Succeeded",
progress: "1/1",
resourcesDuration: {
cpu: 4,
memory: 4,
},
startedAt: "2023-06-27T21:41:33Z",
templateName: "whalesay-template-3",
templateScope: "local/",
type: "Pod",
},
"test-workflow-f49nr-2771663768": {
boundaryID: "test-workflow-f49nr",
displayName: "sleep",
finishedAt: null,
hostNodeName: "ip-192-168-5-156.us-west-2.compute.internal",
id: "test-workflow-f49nr-2771663768",
name: "test-workflow-f49nr[0].sleep",
phase: "Running",
progress: "0/1",
startedAt: "2023-06-27T21:41:33Z",
templateName: "sleep",
templateScope: "local/",
type: "Pod",
},
},
phase: "Running",
progress: "1/2",
resourcesDuration: {
cpu: 4,
memory: 4,
},
startedAt: "2023-06-27T21:41:33Z",
storedTemplates: {
"namespaced/workflow-template-whalesay-template/sleep": {
container: {
args: ["600"],
command: ["sleep"],
image: "docker/whalesay",
name: "",
resources: {},
},
inputs: {},
metadata: {},
name: "sleep",
outputs: {},
},
"namespaced/workflow-template-whalesay-template/whalesay-template": {
inputs: {
parameters: [
{
name: "message",
},
"conditions": [
{
"status": "True",
"type": "PodRunning"
}
],
"finishedAt": null,
"nodes": {
"test-workflow-f49nr": {
"children": [
"test-workflow-f49nr-1432144569"
],
"displayName": "test-workflow-f49nr",
"finishedAt": null,
"id": "test-workflow-f49nr",
"inputs": {
"parameters": [
{
"name": "message",
"value": "from workflow"
}
]
},
"name": "test-workflow-f49nr",
"phase": "Running",
"progress": "1/2",
"startedAt": "2023-06-27T21:41:33Z",
"templateName": "whalesay-template",
"templateScope": "local/",
"type": "Steps"
},
"test-workflow-f49nr-1432144569": {
"boundaryID": "test-workflow-f49nr",
"children": [
"test-workflow-f49nr-1588075630",
"test-workflow-f49nr-2771663768"
],
"displayName": "[0]",
"finishedAt": null,
"id": "test-workflow-f49nr-1432144569",
"name": "test-workflow-f49nr[0]",
"phase": "Running",
"progress": "1/2",
"startedAt": "2023-06-27T21:41:33Z",
"templateScope": "local/",
"type": "StepGroup"
},
"test-workflow-f49nr-1588075630": {
"boundaryID": "test-workflow-f49nr",
"displayName": "whalesay3",
"finishedAt": "2023-06-27T21:41:37Z",
"hostNodeName": "ip-192-168-10-135.us-west-2.compute.internal",
"id": "test-workflow-f49nr-1588075630",
"inputs": {
"parameters": [
{
"name": "message",
"value": "from workflow"
}
]
},
"name": "test-workflow-f49nr[0].whalesay3",
"outputs": {
"exitCode": "0"
},
"phase": "Succeeded",
"progress": "1/1",
"resourcesDuration": {
"cpu": 4,
"memory": 4
},
"startedAt": "2023-06-27T21:41:33Z",
"templateName": "whalesay-template-3",
"templateScope": "local/",
"type": "Pod"
},
"test-workflow-f49nr-2771663768": {
"boundaryID": "test-workflow-f49nr",
"displayName": "sleep",
"finishedAt": null,
"hostNodeName": "ip-192-168-5-156.us-west-2.compute.internal",
"id": "test-workflow-f49nr-2771663768",
"name": "test-workflow-f49nr[0].sleep",
"phase": "Running",
"progress": "0/1",
"startedAt": "2023-06-27T21:41:33Z",
"templateName": "sleep",
"templateScope": "local/",
"type": "Pod"
}
},
"phase": "Running",
"progress": "1/2",
"resourcesDuration": {
"cpu": 4,
"memory": 4
},
"startedAt": "2023-06-27T21:41:33Z",
"storedTemplates": {
"namespaced/workflow-template-whalesay-template/sleep": {
"container": {
"args": [
"600"
],
"command": [
"sleep"
],
"image": "docker/whalesay",
"name": "",
"resources": {}
},
"inputs": {},
"metadata": {},
"name": "sleep",
"outputs": {}
},
"namespaced/workflow-template-whalesay-template/whalesay-template": {
"inputs": {
"parameters": [
{
"name": "message"
}
]
},
"metadata": {},
"name": "whalesay-template",
"outputs": {},
"steps": [
[
{
"arguments": {
"parameters": [
{
"name": "message",
"value": "{{inputs.parameters.message}}"
}
]
},
"name": "whalesay3",
"template": "whalesay-template-3"
},
{
"arguments": {},
"name": "sleep",
"template": "sleep"
}
]
]
},
"namespaced/workflow-template-whalesay-template/whalesay-template-3": {
"container": {
"args": [
"{{inputs.parameters.message}}"
],
"command": [
"cowsay"
],
"image": "docker/whalesay",
"name": "",
"resources": {}
},
"inputs": {
"parameters": [
{
"name": "message"
}
]
},
"metadata": {},
"name": "whalesay-template-3",
"outputs": {}
}
},
"storedWorkflowTemplateSpec": {
"arguments": {
"parameters": [
{
"name": "message",
"value": "from workflow"
}
]
},
"entrypoint": "whalesay-template",
"templates": [
{
"inputs": {
"parameters": [
{
"name": "message"
}
]
},
"metadata": {},
"name": "whalesay-template",
"outputs": {},
"steps": [
[
{
"arguments": {
"parameters": [
{
"name": "message",
"value": "{{inputs.parameters.message}}"
}
]
},
"name": "whalesay3",
"template": "whalesay-template-3"
},
{
"arguments": {},
"name": "sleep",
"template": "sleep"
}
]
]
},
{
"container": {
"args": [
"600"
],
"command": [
"sleep"
],
"image": "docker/whalesay",
"name": "",
"resources": {}
},
"inputs": {},
"metadata": {},
"name": "sleep",
"outputs": {}
},
{
"container": {
"args": [
"{{inputs.parameters.message}}"
],
"command": [
"cowsay"
],
"image": "docker/whalesay",
"name": "",
"resources": {}
},
"inputs": {
"parameters": [
{
"name": "message"
}
]
},
"metadata": {},
"name": "whalesay-template-3",
"outputs": {}
}
],
},
metadata: {},
name: "whalesay-template",
outputs: {},
steps: [
[
{
arguments: {
parameters: [
{
name: "message",
value: "{{inputs.parameters.message}}",
},
],
"ttlStrategy": {
"secondsAfterCompletion": 28800
},
name: "whalesay3",
template: "whalesay-template-3",
},
{
arguments: {},
name: "sleep",
template: "sleep",
},
],
],
},
"namespaced/workflow-template-whalesay-template/whalesay-template-3":
{
container: {
args: ["{{inputs.parameters.message}}"],
command: ["cowsay"],
image: "docker/whalesay",
name: "",
resources: {},
},
inputs: {
parameters: [
{
name: "message",
},
],
},
metadata: {},
name: "whalesay-template-3",
outputs: {},
},
},
storedWorkflowTemplateSpec: {
arguments: {
parameters: [
{
name: "message",
value: "from workflow",
},
],
},
entrypoint: "whalesay-template",
templates: [
{
inputs: {
parameters: [
{
name: "message",
},
],
},
metadata: {},
name: "whalesay-template",
outputs: {},
steps: [
[
{
arguments: {
parameters: [
{
name: "message",
value: "{{inputs.parameters.message}}",
},
],
},
"workflowMetadata": {
"labels": {
"env": "dev",
"my": "label"
}
},
"workflowTemplateRef": {
"name": "workflow-template-whalesay-template"
}
}
}
}
],
"kind": "List",
"metadata": {
"resourceVersion": ""
}
}
name: "whalesay3",
template: "whalesay-template-3",
},
{
arguments: {},
name: "sleep",
template: "sleep",
},
],
],
},
{
container: {
args: ["600"],
command: ["sleep"],
image: "docker/whalesay",
name: "",
resources: {},
},
inputs: {},
metadata: {},
name: "sleep",
outputs: {},
},
{
container: {
args: ["{{inputs.parameters.message}}"],
command: ["cowsay"],
image: "docker/whalesay",
name: "",
resources: {},
},
inputs: {
parameters: [
{
name: "message",
},
],
},
metadata: {},
name: "whalesay-template-3",
outputs: {},
},
],
ttlStrategy: {
secondsAfterCompletion: 28800,
},
workflowMetadata: {
labels: {
env: "dev",
my: "label",
},
},
workflowTemplateRef: {
name: "workflow-template-whalesay-template",
},
},
},
},
],
kind: "List",
metadata: {
resourceVersion: "",
},
};