diff --git a/plugins/argo-workflows/.eslintrc.js b/plugins/argo-workflows/.eslintrc.js index e04c6ac..deb73e9 100644 --- a/plugins/argo-workflows/.eslintrc.js +++ b/plugins/argo-workflows/.eslintrc.js @@ -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"], }); diff --git a/plugins/argo-workflows/.prettierignore b/plugins/argo-workflows/.prettierignore new file mode 100644 index 0000000..f5e6c72 --- /dev/null +++ b/plugins/argo-workflows/.prettierignore @@ -0,0 +1,3 @@ +build +coverage +src/api/generated diff --git a/plugins/argo-workflows/.prettierrc.json b/plugins/argo-workflows/.prettierrc.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/plugins/argo-workflows/.prettierrc.json @@ -0,0 +1 @@ +{} diff --git a/plugins/argo-workflows/README.md b/plugins/argo-workflows/README.md index 5d9cf83..e544230 100644 --- a/plugins/argo-workflows/README.md +++ b/plugins/argo-workflows/README.md @@ -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` diff --git a/plugins/argo-workflows/config.d.ts b/plugins/argo-workflows/config.d.ts index 0d10c84..56bb6e8 100644 --- a/plugins/argo-workflows/config.d.ts +++ b/plugins/argo-workflows/config.d.ts @@ -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; + }; } diff --git a/plugins/argo-workflows/dev/index.tsx b/plugins/argo-workflows/dev/index.tsx index 712176c..6fde413 100644 --- a/plugins/argo-workflows/dev/index.tsx +++ b/plugins/argo-workflows/dev/index.tsx @@ -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: , - title: 'Root Page', - path: '/argo-workflows' + title: "Root Page", + path: "/argo-workflows", }) .render(); diff --git a/plugins/argo-workflows/package.json b/plugins/argo-workflows/package.json index 411fe12..24c0ae7 100644 --- a/plugins/argo-workflows/package.json +++ b/plugins/argo-workflows/package.json @@ -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", diff --git a/plugins/argo-workflows/src/api/ArgoWorkflows.test.tsx b/plugins/argo-workflows/src/api/ArgoWorkflows.test.tsx index 132d340..6208000 100644 --- a/plugins/argo-workflows/src/api/ArgoWorkflows.test.tsx +++ b/plugins/argo-workflows/src/api/ArgoWorkflows.test.tsx @@ -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 = { + 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 = { - 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 = { + getObjectsByEntity: jest.fn(), + getClusters: jest.fn(), + getWorkloadsByEntity: jest.fn(), + getCustomObjectsByEntity: jest.fn(), + proxy: jest.fn(), + }; - const mockKClient: jest.Mocked = { - 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" + ); + }); +}); diff --git a/plugins/argo-workflows/src/api/ArgoWorkflows.ts b/plugins/argo-workflows/src/api/ArgoWorkflows.ts index cd7c2a2..7f83073 100644 --- a/plugins/argo-workflows/src/api/ArgoWorkflows.ts +++ b/plugins/argo-workflows/src/api/ArgoWorkflows.ts @@ -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 { + 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 { + if (clusterName) { + return this.getWorkflowsFromK8s(clusterName, namespace, labels); + } + return this.getWorkflowsFromProxy(namespace, labels); + } + + async getWorkflowsFromProxy( + namespace: string | undefined, + labels: string | undefined + ): Promise { + 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 { - 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 { - if (clusterName) { - return this.getWorkflowsFromK8s(clusterName, namespace, labels) - } - return this.getWorkflowsFromProxy(namespace, labels); - } - - async getWorkflowsFromProxy(namespace: string | undefined, labels: string | undefined): Promise { - 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 { - 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 { + const clusters = await this.kubernetesApi.getClusters(); + if (clusters.length > 0) { + return Promise.resolve(clusters[0].name); } + return Promise.reject("no clusters found in configuration"); + } } diff --git a/plugins/argo-workflows/src/api/index.ts b/plugins/argo-workflows/src/api/index.ts index f7110ae..942b149 100644 --- a/plugins/argo-workflows/src/api/index.ts +++ b/plugins/argo-workflows/src/api/index.ts @@ -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({ - id: 'plugin.argoworkflows', -}) + id: "plugin.argoworkflows", +}); export interface ArgoWorkflowsApi { - discoveryApi: DiscoveryApi - kubernetesApi: KubernetesApi - getWorkflowsFromK8s(clusterName: string, namespace: string | undefined, labels: string | undefined): Promise - getWorkflows(clusterName: string | undefined, namespace: string | undefined, labels: string | undefined): Promise - getWorkflowsFromProxy(namespace: string, labels: string | undefined): Promise + discoveryApi: DiscoveryApi; + kubernetesApi: KubernetesApi; + getWorkflowsFromK8s( + clusterName: string, + namespace: string | undefined, + labels: string | undefined + ): Promise; + getWorkflows( + clusterName: string | undefined, + namespace: string | undefined, + labels: string | undefined + ): Promise; + getWorkflowsFromProxy( + namespace: string, + labels: string | undefined + ): Promise; } - - diff --git a/plugins/argo-workflows/src/components/Overview/Overview.tsx b/plugins/argo-workflows/src/components/Overview/Overview.tsx index 2a05410..5278221 100644 --- a/plugins/argo-workflows/src/components/Overview/Overview.tsx +++ b/plugins/argo-workflows/src/components/Overview/Overview.tsx @@ -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 = () => ( - -
- -
- - - - Overview of your Argo Workflows - - - - - - -
-) + +
+ +
+ + + Overview of your Argo Workflows + + + + + +
+); diff --git a/plugins/argo-workflows/src/components/Overview/index.ts b/plugins/argo-workflows/src/components/Overview/index.ts index ff77359..e7c2ca3 100644 --- a/plugins/argo-workflows/src/components/Overview/index.ts +++ b/plugins/argo-workflows/src/components/Overview/index.ts @@ -1 +1 @@ -export {OverviewComponent} from "./Overview"; +export { OverviewComponent } from "./Overview"; diff --git a/plugins/argo-workflows/src/components/WorkflowOverview/WorkflowOverview.tsx b/plugins/argo-workflows/src/components/WorkflowOverview/WorkflowOverview.tsx index d77b01a..7ca2f3e 100644 --- a/plugins/argo-workflows/src/components/WorkflowOverview/WorkflowOverview.tsx +++ b/plugins/argo-workflows/src/components/WorkflowOverview/WorkflowOverview.tsx @@ -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 ({data.name}) - } - 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 => { - return await apiClient.getWorkflows(clusterName, ns, k8sLabelSelector) + const columns: TableColumn[] = [ + { + title: "Name", + field: "name", + render: (data: any | TableData, _): any => { + if (data && argoWorkflowsBaseUrl) { + return ( + + {data.name} + + ); } - ) + 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 ; - } else if (error) { - return {error.message}; + const { value, loading, error } = useAsync( + async (): Promise => { + 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 ; + } else if (error) { + return {error.message}; + } - if (data) { - return ( - - ) - } + 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 No Workflows found with given filter. Check your entity's annotations. + if (data) { + return ( +
+ ); + } -} + return ( + + No Workflows found with given filter. Check your entity's annotations. + + ); +}; diff --git a/plugins/argo-workflows/src/index.ts b/plugins/argo-workflows/src/index.ts index 5b080f4..1678a3e 100644 --- a/plugins/argo-workflows/src/index.ts +++ b/plugins/argo-workflows/src/index.ts @@ -1 +1 @@ -export { argoWorkflowsPlugin, ArgoWorkflowsPage } from './plugin'; +export { argoWorkflowsPlugin, ArgoWorkflowsPage } from "./plugin"; diff --git a/plugins/argo-workflows/src/plugin.test.ts b/plugins/argo-workflows/src/plugin.test.ts index bcb714a..cbbe09d 100644 --- a/plugins/argo-workflows/src/plugin.test.ts +++ b/plugins/argo-workflows/src/plugin.test.ts @@ -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(); }); }); diff --git a/plugins/argo-workflows/src/plugin.ts b/plugins/argo-workflows/src/plugin.ts index ecdaa06..dea9c2a 100644 --- a/plugins/argo-workflows/src/plugin.ts +++ b/plugins/argo-workflows/src/plugin.ts @@ -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, - }), + }) ); diff --git a/plugins/argo-workflows/src/routes.ts b/plugins/argo-workflows/src/routes.ts index f971bdd..7db8711 100644 --- a/plugins/argo-workflows/src/routes.ts +++ b/plugins/argo-workflows/src/routes.ts @@ -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", }); diff --git a/plugins/argo-workflows/src/setupTests.ts b/plugins/argo-workflows/src/setupTests.ts index 48c09b5..4acb1c6 100644 --- a/plugins/argo-workflows/src/setupTests.ts +++ b/plugins/argo-workflows/src/setupTests.ts @@ -1,2 +1,2 @@ -import '@testing-library/jest-dom'; -import 'cross-fetch/polyfill'; +import "@testing-library/jest-dom"; +import "cross-fetch/polyfill"; diff --git a/plugins/argo-workflows/src/test-data/in-progress.ts b/plugins/argo-workflows/src/test-data/in-progress.ts index c3f9ade..773ac5d 100644 --- a/plugins/argo-workflows/src/test-data/in-progress.ts +++ b/plugins/argo-workflows/src/test-data/in-progress.ts @@ -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: "", + }, +};