diff --git a/plugins/argo-workflows/src/api/ArgoWorkflows.test.tsx b/plugins/argo-workflows/src/api/ArgoWorkflows.test.tsx new file mode 100644 index 0000000..132d340 --- /dev/null +++ b/plugins/argo-workflows/src/api/ArgoWorkflows.test.tsx @@ -0,0 +1,103 @@ +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'}) + + 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() + }) + + 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 53dee99..cd7c2a2 100644 --- a/plugins/argo-workflows/src/api/ArgoWorkflows.ts +++ b/plugins/argo-workflows/src/api/ArgoWorkflows.ts @@ -1,4 +1,4 @@ -import {ConfigApi, DiscoveryApi, IdentityApi} from "@backstage/core-plugin-api"; +import {ConfigApi, DiscoveryApi, FetchApi} from "@backstage/core-plugin-api"; import {KubernetesApi} from "@backstage/plugin-kubernetes"; import {IoArgoprojWorkflowV1alpha1WorkflowList} from "./generated"; import {ArgoWorkflowsApi} from "./index"; @@ -14,13 +14,13 @@ export class ArgoWorkflows implements ArgoWorkflowsApi { discoveryApi: DiscoveryApi kubernetesApi: KubernetesApi configApi: ConfigApi - identityApi: IdentityApi + fetchApi: FetchApi - constructor(discoveryApi: DiscoveryApi, kubernetesApi: KubernetesApi, configApi: ConfigApi, identityApi: IdentityApi) { + constructor(discoveryApi: DiscoveryApi, kubernetesApi: KubernetesApi, configApi: ConfigApi, fetchApi: FetchApi) { this.discoveryApi = discoveryApi this.kubernetesApi = kubernetesApi this.configApi = configApi - this.identityApi = identityApi + this.fetchApi = fetchApi } async getWorkflowsFromK8s(clusterName: string | undefined, namespace: string | undefined, labels: string | undefined): Promise { @@ -39,7 +39,7 @@ export class ArgoWorkflows implements ArgoWorkflowsApi { }) if (!resp.ok) { - return Promise.reject(`failed to fetch resources: ${resp.status}, ${resp.statusText}, ${await resp.json()}`) + return Promise.reject(`failed to fetch resources: ${resp.status}, ${resp.statusText}, ${await resp.text()}`) } // need validation return JSON.parse(await resp.text()) as IoArgoprojWorkflowV1alpha1WorkflowList @@ -64,19 +64,7 @@ export class ArgoWorkflows implements ArgoWorkflowsApi { if (labels) { query.set(API_LABEL_SELECTOR, labels) } - - const { token } = await this.identityApi.getCredentials() - - const headers = new Headers( - { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - } - ) - - const resp = await fetch(`${url}?${query.toString()}`, { - headers: headers - }) + 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()}`) diff --git a/plugins/argo-workflows/src/plugin.ts b/plugins/argo-workflows/src/plugin.ts index a58931e..ecdaa06 100644 --- a/plugins/argo-workflows/src/plugin.ts +++ b/plugins/argo-workflows/src/plugin.ts @@ -3,7 +3,7 @@ import { createApiFactory, createPlugin, createRoutableExtension, - discoveryApiRef, identityApiRef + discoveryApiRef, fetchApiRef } from '@backstage/core-plugin-api'; import { rootRouteRef } from './routes'; @@ -22,12 +22,13 @@ export const argoWorkflowsPlugin = createPlugin({ deps: { discoveryApi: discoveryApiRef, kubernetesApi: kubernetesApiRef, - identityApi: identityApiRef, - configApi: configApiRef}, + configApi: configApiRef, + fetchApi: fetchApiRef, + }, factory: ({ - discoveryApi, kubernetesApi, configApi, identityApi, + discoveryApi, kubernetesApi, configApi, fetchApi, }) => - new ArgoWorkflows(discoveryApi, kubernetesApi, configApi, identityApi) + new ArgoWorkflows(discoveryApi, kubernetesApi, configApi, fetchApi) }) ] }); diff --git a/plugins/argo-workflows/src/test-data/in-progress.ts b/plugins/argo-workflows/src/test-data/in-progress.ts new file mode 100644 index 0000000..c3f9ade --- /dev/null +++ b/plugins/argo-workflows/src/test-data/in-progress.ts @@ -0,0 +1,325 @@ + +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" + } + ] + }, + "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" + } + ] + }, + "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": {} + } + ], + "ttlStrategy": { + "secondsAfterCompletion": 28800 + }, + "workflowMetadata": { + "labels": { + "env": "dev", + "my": "label" + } + }, + "workflowTemplateRef": { + "name": "workflow-template-whalesay-template" + } + } + } + } + ], + "kind": "List", + "metadata": { + "resourceVersion": "" + } +}