From aed4ea056a049edfe42cbe9428364a51306a96c7 Mon Sep 17 00:00:00 2001 From: AlinaGoaga Date: Wed, 5 Jul 2023 17:06:50 +0200 Subject: [PATCH 01/14] Implement a deployments card - WIP --- plugins/backstage-plugin-flux/dev/index.tsx | 40 ++ .../FluxDeploymentsTable.tsx | 71 ++++ .../FluxEntityDeploymentsCard.test.tsx | 344 ++++++++++++++++++ .../FluxEntityDeploymentsCard.tsx | 42 +++ .../FluxEntityDeploymentsCard/index.ts | 1 + .../backstage-plugin-flux/src/hooks/query.ts | 27 ++ plugins/backstage-plugin-flux/src/index.ts | 1 + plugins/backstage-plugin-flux/src/plugin.ts | 16 + 8 files changed, 542 insertions(+) create mode 100644 plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx create mode 100644 plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx create mode 100644 plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.tsx create mode 100644 plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/index.ts diff --git a/plugins/backstage-plugin-flux/dev/index.tsx b/plugins/backstage-plugin-flux/dev/index.tsx index bbff895..24fc508 100644 --- a/plugins/backstage-plugin-flux/dev/index.tsx +++ b/plugins/backstage-plugin-flux/dev/index.tsx @@ -25,6 +25,7 @@ import { FluxEntityOCIRepositoriesCard, FluxEntityHelmRepositoriesCard, FluxEntityKustomizationsCard, + FluxEntityDeploymentsCard, } from '../src/plugin'; import { newTestHelmRelease, @@ -397,5 +398,44 @@ createDevApp() ), }) + .addPage({ + title: 'Deployments', + path: '/deployments', + element: ( + + + + + + + + ), + }) .registerPlugin(weaveworksFluxPlugin) .render(); diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx new file mode 100644 index 0000000..8ccd719 --- /dev/null +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { TableColumn } from '@backstage/core-components'; +import { + idColumn, + nameAndClusterNameColumn, + pathColumn, + repoColumn, + statusColumn, + updatedColumn, + syncColumn, +} from '../helpers'; +import { HelmRelease, Kustomization } from '../../objects'; +import { FluxEntityTable } from '../FluxEntityTable'; +import { T } from '../../hooks/query'; + +export const defaultColumns: TableColumn[] = [ + idColumn(), + nameAndClusterNameColumn(), + pathColumn(), + repoColumn(), + statusColumn(), + updatedColumn(), + syncColumn(), +]; + +type Props = { + deployments: T[]; + isLoading: boolean; + columns: TableColumn[]; +}; + +export const FluxDeploymentsTable = ({ + deployments, + isLoading, + columns, +}: Props) => { + console.log(deployments); + // const data = kustomizations.map(k => { + // const { + // clusterName, + // namespace, + // name, + // sourceRef, + // path, + // conditions, + // suspended, + // type, + // } = k; + // return { + // id: `${clusterName}/${namespace}/${name}`, + // conditions, + // suspended, + // name, + // namespace, + // clusterName, + // sourceRef, + // path, + // type, + // } as Kustomization & { id: string }; + // }); + + return ( + + ); +}; diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx new file mode 100644 index 0000000..1c04700 --- /dev/null +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx @@ -0,0 +1,344 @@ +import React from 'react'; +import { Entity } from '@backstage/catalog-model'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { configApiRef } from '@backstage/core-plugin-api'; +import { ConfigReader } from '@backstage/core-app-api'; +import { + KubernetesApi, + kubernetesApiRef, + KubernetesAuthProvidersApi, + kubernetesAuthProvidersApiRef, +} from '@backstage/plugin-kubernetes'; +import { + CustomObjectsByEntityRequest, + KubernetesRequestBody, + ObjectsByEntityResponse, +} from '@backstage/plugin-kubernetes-common'; +import { FluxEntityDeploymentsCard } from './FluxEntityDeploymentsCard'; + +const makeTestKustomization = (name: string, path: string) => { + return { + apiVersion: 'kustomize.toolkit.fluxcd.io/v1', + kind: 'Kustomization', + metadata: { + annotations: { + 'reconcile.fluxcd.io/requestedAt': '2023-07-03T17:18:03.990333+01:00', + }, + creationTimestamp: '2023-06-29T08:06:59Z', + finalizers: ['finalizers.fluxcd.io'], + generation: 1, + labels: { + 'kustomize.toolkit.fluxcd.io/name': 'flux-system', + 'kustomize.toolkit.fluxcd.io/namespace': 'flux-system', + }, + name, + namespace: 'flux-system', + resourceVersion: '1181625', + uid: 'ab33ae5b-a282-40b1-9fdc-d87f05401628', + }, + spec: { + force: false, + interval: '10m0s', + path, + prune: true, + sourceRef: { + kind: 'GitRepository', + name: 'flux-system', + }, + }, + status: { + conditions: [ + { + lastTransitionTime: '2023-07-03T16:18:04Z', + message: + 'Applied revision: main@sha1:c933408394a3af8fa7208af8c9abf7fe430f99d4', + observedGeneration: 1, + reason: 'ReconciliationSucceeded', + status: 'True', + type: 'Ready', + }, + ], + inventory: { + entries: [ + { + id: '_alerts.notification.toolkit.fluxcd.io_apiextensions.k8s.io_CustomResourceDefinition', + v: 'v1', + }, + { + id: '_buckets.source.toolkit.fluxcd.io_apiextensions.k8s.io_CustomResourceDefinition', + v: 'v1', + }, + { + id: '_gitrepositories.source.toolkit.fluxcd.io_apiextensions.k8s.io_CustomResourceDefinition', + v: 'v1', + }, + { + id: '_helmcharts.source.toolkit.fluxcd.io_apiextensions.k8s.io_CustomResourceDefinition', + v: 'v1', + }, + { + id: '_helmreleases.helm.toolkit.fluxcd.io_apiextensions.k8s.io_CustomResourceDefinition', + v: 'v1', + }, + { + id: '_helmrepositories.source.toolkit.fluxcd.io_apiextensions.k8s.io_CustomResourceDefinition', + v: 'v1', + }, + { + id: '_kustomizations.kustomize.toolkit.fluxcd.io_apiextensions.k8s.io_CustomResourceDefinition', + v: 'v1', + }, + { + id: '_ocirepositories.source.toolkit.fluxcd.io_apiextensions.k8s.io_CustomResourceDefinition', + v: 'v1', + }, + { + id: '_providers.notification.toolkit.fluxcd.io_apiextensions.k8s.io_CustomResourceDefinition', + v: 'v1', + }, + { + id: '_receivers.notification.toolkit.fluxcd.io_apiextensions.k8s.io_CustomResourceDefinition', + v: 'v1', + }, + { + id: '_flux-system__Namespace', + v: 'v1', + }, + { + id: 'flux-system_critical-pods-flux-system__ResourceQuota', + v: 'v1', + }, + { + id: 'flux-system_helm-controller__ServiceAccount', + v: 'v1', + }, + { + id: 'flux-system_kustomize-controller__ServiceAccount', + v: 'v1', + }, + { + id: 'flux-system_notification-controller__ServiceAccount', + v: 'v1', + }, + { + id: 'flux-system_source-controller__ServiceAccount', + v: 'v1', + }, + { + id: '_crd-controller-flux-system_rbac.authorization.k8s.io_ClusterRole', + v: 'v1', + }, + { + id: '_flux-edit-flux-system_rbac.authorization.k8s.io_ClusterRole', + v: 'v1', + }, + { + id: '_flux-view-flux-system_rbac.authorization.k8s.io_ClusterRole', + v: 'v1', + }, + { + id: '_cluster-reconciler-flux-system_rbac.authorization.k8s.io_ClusterRoleBinding', + v: 'v1', + }, + { + id: '_crd-controller-flux-system_rbac.authorization.k8s.io_ClusterRoleBinding', + v: 'v1', + }, + { + id: 'flux-system_notification-controller__Service', + v: 'v1', + }, + { + id: 'flux-system_source-controller__Service', + v: 'v1', + }, + { + id: 'flux-system_webhook-receiver__Service', + v: 'v1', + }, + { + id: 'flux-system_helm-controller_apps_Deployment', + v: 'v1', + }, + { + id: 'flux-system_kustomize-controller_apps_Deployment', + v: 'v1', + }, + { + id: 'flux-system_notification-controller_apps_Deployment', + v: 'v1', + }, + { + id: 'flux-system_source-controller_apps_Deployment', + v: 'v1', + }, + { + id: 'flux-system_flux-system_kustomize.toolkit.fluxcd.io_Kustomization', + v: 'v1', + }, + { + id: 'flux-system_allow-egress_networking.k8s.io_NetworkPolicy', + v: 'v1', + }, + { + id: 'flux-system_allow-scraping_networking.k8s.io_NetworkPolicy', + v: 'v1', + }, + { + id: 'flux-system_allow-webhooks_networking.k8s.io_NetworkPolicy', + v: 'v1', + }, + { + id: 'default_podinfo_source.toolkit.fluxcd.io_GitRepository', + v: 'v1', + }, + { + id: 'default_podinfo-shard1_source.toolkit.fluxcd.io_GitRepository', + v: 'v1', + }, + { + id: 'default_podinfo-shard2_source.toolkit.fluxcd.io_GitRepository', + v: 'v1', + }, + { + id: 'flux-system_flux-system_source.toolkit.fluxcd.io_GitRepository', + v: 'v1', + }, + { + id: 'flux-system_source-controller-shardset_templates.weave.works_FluxShardSet', + v: 'v1alpha1', + }, + ], + }, + lastAppliedRevision: 'main@sha1:c933408394a3af8fa7208af8c9abf7fe430f99d4', + lastAttemptedRevision: + 'main@sha1:c933408394a3af8fa7208af8c9abf7fe430f99d4', + lastHandledReconcileAt: '2023-07-03T17:18:03.990333+01:00', + observedGeneration: 1, + }, + }; +}; + +class StubKubernetesClient implements KubernetesApi { + getObjectsByEntity = jest.fn(); + + async getClusters(): Promise<{ name: string; authProvider: string }[]> { + return [{ name: 'mock-cluster', authProvider: 'serviceAccount' }]; + } + + getWorkloadsByEntity = jest.fn(); + + getCustomObjectsByEntity( + _: CustomObjectsByEntityRequest, + ): Promise { + return Promise.resolve({ + items: [ + { + cluster: { + name: 'demo-cluster', + }, + podMetrics: [], + errors: [], + resources: [ + { + type: 'customresources', + resources: [ + makeTestKustomization('flux-system', './clusters/my-cluster'), + ], + }, + ], + }, + ], + }); + } + + proxy = jest.fn(); +} + +class StubKubernetesAuthProvidersApi implements KubernetesAuthProvidersApi { + decorateRequestBodyForAuth( + _: string, + requestBody: KubernetesRequestBody, + ): Promise { + return Promise.resolve(requestBody); + } + getCredentials(_: string): Promise<{ + token?: string; + }> { + return Promise.resolve({ token: 'mock-token' }); + } +} + +const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'my-name', + annotations: { + 'backstage.io/kubernetes-id': 'testing-service', + }, + }, +}; + +describe('', () => { + let Wrapper: React.ComponentType>; + + beforeEach(() => { + Wrapper = ({ children }: { children?: React.ReactNode }) => ( +
{children}
+ ); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('listing Kustomizations', () => { + it('shows the details of an Kustomization', async () => { + const result = await renderInTestApp( + + + + + + + , + ); + + const { getByText } = result; + + const testCases = [ + { + name: 'flux-system', + path: './clusters/my-cluster', + repo: 'flux-system', + }, + ]; + + for (const testCase of testCases) { + const cell = getByText(testCase.name); + expect(cell).toBeInTheDocument(); + + const tr = cell.closest('tr'); + expect(tr).toBeInTheDocument(); + expect(tr).toHaveTextContent(testCase.path); + expect(tr).toHaveTextContent(testCase.repo); + } + }); + }); +}); diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.tsx new file mode 100644 index 0000000..0dbab39 --- /dev/null +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { WeaveGitOpsContext } from '../WeaveGitOpsContext'; +import { useFluxDeployments } from '../../hooks'; +import { FluxDeploymentsTable, defaultColumns } from './FluxDeploymentsTable'; + +const DeploymentsPanel = () => { + const { entity } = useEntity(); + const { data, loading, errors } = useFluxDeployments(entity); + + if (errors) { + return ( +
+ Errors: +
    + {errors.map(err => ( +
  • {err.message}
  • + ))} +
+
+ ); + } + + return ( + + ); +}; + +/** + * Render the Kustomizations associated with the current Entity. + * + * @public + */ +export const FluxEntityDeploymentsCard = () => ( + + + +); diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/index.ts b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/index.ts new file mode 100644 index 0000000..4b9b497 --- /dev/null +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/index.ts @@ -0,0 +1 @@ +export { FluxEntityDeploymentsCard } from './FluxEntityDeploymentsCard'; diff --git a/plugins/backstage-plugin-flux/src/hooks/query.ts b/plugins/backstage-plugin-flux/src/hooks/query.ts index 209129d..1757cb3 100644 --- a/plugins/backstage-plugin-flux/src/hooks/query.ts +++ b/plugins/backstage-plugin-flux/src/hooks/query.ts @@ -205,3 +205,30 @@ export function useHelmRepositories(entity: Entity): Response { : kubernetesErrors, }; } + +/** + * Query for the Flux Deployments - Kustomizations and Helm Releases - associated with this Entity. + * @public + */ + +export interface T extends FluxObject {} + +export function useFluxDeployments(entity: Entity): Response { + const { kubernetesObjects, loading, error } = useCustomResources(entity, [ + helmReleaseGVK, + kustomizationsGVK, + ]); + + const { data, kubernetesErrors } = toResponse( + item => new FluxObject(item), + kubernetesObjects, + ); + + return { + data, + loading, + errors: error + ? [new Error(error), ...(kubernetesErrors || [])] + : kubernetesErrors, + }; +} diff --git a/plugins/backstage-plugin-flux/src/index.ts b/plugins/backstage-plugin-flux/src/index.ts index 417a4a0..61fe493 100644 --- a/plugins/backstage-plugin-flux/src/index.ts +++ b/plugins/backstage-plugin-flux/src/index.ts @@ -5,4 +5,5 @@ export { FluxEntityOCIRepositoriesCard, FluxEntityKustomizationsCard, FluxEntityHelmRepositoriesCard, + FluxEntityDeploymentsCard, } from './plugin'; diff --git a/plugins/backstage-plugin-flux/src/plugin.ts b/plugins/backstage-plugin-flux/src/plugin.ts index 083f87a..1136bd6 100644 --- a/plugins/backstage-plugin-flux/src/plugin.ts +++ b/plugins/backstage-plugin-flux/src/plugin.ts @@ -95,3 +95,19 @@ export const FluxEntityKustomizationsCard = weaveworksFluxPlugin.provide( }, }), ); + +/** + * Card used to show the state of Flux Kustomizations for an Entity. + * @public + */ +export const FluxEntityDeploymentsCard = weaveworksFluxPlugin.provide( + createComponentExtension({ + name: 'FluxEntityDeploymentsCard', + component: { + lazy: () => + import('./components/FluxEntityDeploymentsCard').then( + m => m.FluxEntityDeploymentsCard, + ), + }, + }), +); From 0ce0f2c9abf9877401d4ff9d4f5abe6978f3cb47 Mon Sep 17 00:00:00 2001 From: AlinaGoaga Date: Wed, 5 Jul 2023 17:54:28 +0200 Subject: [PATCH 02/14] Implement a deployments card - WIP2 --- .../FluxDeploymentsTable.tsx | 89 ++++++++++++------- .../backstage-plugin-flux/src/hooks/query.ts | 12 +-- 2 files changed, 63 insertions(+), 38 deletions(-) diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx index 8ccd719..a611244 100644 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx @@ -3,30 +3,26 @@ import { TableColumn } from '@backstage/core-components'; import { idColumn, nameAndClusterNameColumn, - pathColumn, - repoColumn, statusColumn, updatedColumn, syncColumn, + Deployment, } from '../helpers'; import { HelmRelease, Kustomization } from '../../objects'; import { FluxEntityTable } from '../FluxEntityTable'; -import { T } from '../../hooks/query'; -export const defaultColumns: TableColumn[] = [ +export const defaultColumns: TableColumn[] = [ idColumn(), nameAndClusterNameColumn(), - pathColumn(), - repoColumn(), statusColumn(), updatedColumn(), syncColumn(), ]; type Props = { - deployments: T[]; + deployments: Deployment[]; isLoading: boolean; - columns: TableColumn[]; + columns: TableColumn[]; }; export const FluxDeploymentsTable = ({ @@ -35,36 +31,63 @@ export const FluxDeploymentsTable = ({ columns, }: Props) => { console.log(deployments); - // const data = kustomizations.map(k => { - // const { - // clusterName, - // namespace, - // name, - // sourceRef, - // path, - // conditions, - // suspended, - // type, - // } = k; - // return { - // id: `${clusterName}/${namespace}/${name}`, - // conditions, - // suspended, - // name, - // namespace, - // clusterName, - // sourceRef, - // path, - // type, - // } as Kustomization & { id: string }; - // }); + + const data = deployments.map(d => { + // TODO: Simplify the the below, extract common fields and add custom + if (d instanceof Kustomization) { + const { + clusterName, + namespace, + name, + sourceRef, + path, + conditions, + suspended, + type, + } = d; + return { + id: `${clusterName}/${namespace}/${name}`, + conditions, + suspended, + name, + namespace, + clusterName, + sourceRef, + path, + type, + } as Kustomization & { id: string }; + } else if (d instanceof HelmRelease) { + const { + clusterName, + namespace, + name, + helmChart, + conditions, + suspended, + sourceRef, + type, + lastAppliedRevision, + } = d; + return { + id: `${clusterName}/${namespace}/${name}`, + conditions, + suspended, + name, + namespace, + helmChart, + lastAppliedRevision, + clusterName, + sourceRef, + type, + } as HelmRelease & { id: string }; + } + }); return ( ); diff --git a/plugins/backstage-plugin-flux/src/hooks/query.ts b/plugins/backstage-plugin-flux/src/hooks/query.ts index 1757cb3..d3e70e2 100644 --- a/plugins/backstage-plugin-flux/src/hooks/query.ts +++ b/plugins/backstage-plugin-flux/src/hooks/query.ts @@ -18,6 +18,7 @@ import { helmRepositoryGVK, HelmRepository, } from '../objects'; +import { Deployment } from '../components/helpers'; function toErrors( cluster: ClusterAttributes, @@ -211,16 +212,17 @@ export function useHelmRepositories(entity: Entity): Response { * @public */ -export interface T extends FluxObject {} - -export function useFluxDeployments(entity: Entity): Response { +export function useFluxDeployments(entity: Entity): Response { const { kubernetesObjects, loading, error } = useCustomResources(entity, [ helmReleaseGVK, kustomizationsGVK, ]); - const { data, kubernetesErrors } = toResponse( - item => new FluxObject(item), + const { data, kubernetesErrors } = toResponse( + item => + item instanceof Kustomization + ? new Kustomization(item) + : new HelmRelease(item), kubernetesObjects, ); From 47da1e4eb336a4efde9992bcb8b16083c3ecd40c Mon Sep 17 00:00:00 2001 From: AlinaGoaga Date: Thu, 6 Jul 2023 17:25:27 +0200 Subject: [PATCH 03/14] Adjust hooks, helpers and table to work wiht both Deployments types --- plugins/backstage-plugin-flux/package.json | 3 +- .../FluxDeploymentsTable.tsx | 59 ++++++++++--------- .../FluxEntityDeploymentsCard.test.tsx | 1 + .../FluxHelmReleasesTable.tsx | 12 +--- .../src/components/helpers.tsx | 36 ++++++++++- .../backstage-plugin-flux/src/hooks/query.ts | 19 +++--- plugins/backstage-plugin-flux/src/objects.ts | 4 +- 7 files changed, 79 insertions(+), 55 deletions(-) diff --git a/plugins/backstage-plugin-flux/package.json b/plugins/backstage-plugin-flux/package.json index 222d0b1..64400a0 100644 --- a/plugins/backstage-plugin-flux/package.json +++ b/plugins/backstage-plugin-flux/package.json @@ -38,8 +38,7 @@ "react-query": "^3.39.3", "react-use": "^17.2.4", "styled-components": "^5.3.0", - "use-deep-compare": "^1.1.0", - "yaml": "^2.3.1" + "use-deep-compare": "^1.1.0" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0" diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx index a611244..b5f9ac0 100644 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx @@ -7,13 +7,21 @@ import { updatedColumn, syncColumn, Deployment, + typeColumn, + pathColumn, + chartColumn, + repoColumn, } from '../helpers'; -import { HelmRelease, Kustomization } from '../../objects'; +import { HelmChart, HelmRelease, Kustomization } from '../../objects'; import { FluxEntityTable } from '../FluxEntityTable'; export const defaultColumns: TableColumn[] = [ idColumn(), nameAndClusterNameColumn(), + typeColumn(), + pathColumn(), + repoColumn(), + chartColumn(), statusColumn(), updatedColumn(), syncColumn(), @@ -30,55 +38,48 @@ export const FluxDeploymentsTable = ({ isLoading, columns, }: Props) => { - console.log(deployments); + let helmChart = {} as HelmChart; + let path = ''; + let repo = ''; const data = deployments.map(d => { - // TODO: Simplify the the below, extract common fields and add custom + const { + clusterName, + namespace, + name, + conditions, + suspended, + sourceRef, + type, + lastAppliedRevision, + } = d; if (d instanceof Kustomization) { - const { - clusterName, - namespace, - name, - sourceRef, - path, - conditions, - suspended, - type, - } = d; + path = d.path; return { id: `${clusterName}/${namespace}/${name}`, conditions, suspended, name, namespace, + lastAppliedRevision, clusterName, sourceRef, - path, type, + path, } as Kustomization & { id: string }; } else if (d instanceof HelmRelease) { - const { - clusterName, - namespace, - name, - helmChart, - conditions, - suspended, - sourceRef, - type, - lastAppliedRevision, - } = d; + helmChart = d.helmChart; return { id: `${clusterName}/${namespace}/${name}`, conditions, suspended, name, namespace, - helmChart, lastAppliedRevision, clusterName, sourceRef, type, + helmChart, } as HelmRelease & { id: string }; } }); @@ -87,7 +88,11 @@ export const FluxDeploymentsTable = ({ ); diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx index 1c04700..e8380ca 100644 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx @@ -17,6 +17,7 @@ import { } from '@backstage/plugin-kubernetes-common'; import { FluxEntityDeploymentsCard } from './FluxEntityDeploymentsCard'; +// add a helm release also const makeTestKustomization = (name: string, path: string) => { return { apiVersion: 'kustomize.toolkit.fluxcd.io/v1', diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityHelmReleasesCard/FluxHelmReleasesTable.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityHelmReleasesCard/FluxHelmReleasesTable.tsx index 4e7c9e9..ca69a95 100644 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityHelmReleasesCard/FluxHelmReleasesTable.tsx +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityHelmReleasesCard/FluxHelmReleasesTable.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { TableColumn } from '@backstage/core-components'; import { + chartColumn, idColumn, nameAndClusterNameColumn, - sortAndFilterOptions, statusColumn, syncColumn, updatedColumn, @@ -11,16 +11,6 @@ import { import { HelmRelease } from '../../objects'; import { FluxEntityTable } from '../FluxEntityTable'; -function chartColumn() { - const formatContent = (hr: HelmRelease) => - `${hr.helmChart.chart}/${hr.lastAppliedRevision}`; - return { - title: 'Chart', - render: (hr: HelmRelease) => formatContent(hr), - ...sortAndFilterOptions(hr => formatContent(hr)), - } as TableColumn; -} - export const defaultColumns: TableColumn[] = [ idColumn(), nameAndClusterNameColumn(), diff --git a/plugins/backstage-plugin-flux/src/components/helpers.tsx b/plugins/backstage-plugin-flux/src/components/helpers.tsx index c45e711..2a61bc9 100644 --- a/plugins/backstage-plugin-flux/src/components/helpers.tsx +++ b/plugins/backstage-plugin-flux/src/components/helpers.tsx @@ -157,6 +157,14 @@ export const nameAndClusterNameColumn = () => { } as TableColumn; }; +export const typeColumn = () => { + return { + title: 'Type', + field: 'type', + render: resource => {resource?.type}, + } as TableColumn; +}; + export const verifiedColumn = () => { return { title: ( @@ -196,7 +204,23 @@ export const artifactColumn = () => { } as TableColumn; }; -export const repoColumn = () => { +export const chartColumn = () => { + const formatContent = (resource: Deployment) => { + if (resource.type === 'HelmRelease') { + return `${(resource as HelmRelease)?.helmChart?.chart}/${ + resource?.lastAppliedRevision + }`; + } else return ''; + }; + + return { + title: 'Chart', + render: (resource: Deployment) => formatContent(resource), + ...sortAndFilterOptions(resource => formatContent(resource)), + } as TableColumn; +}; + +export const repoColumn = () => { return { title: 'Repo', field: 'repo', @@ -204,11 +228,17 @@ export const repoColumn = () => { } as TableColumn; }; -export const pathColumn = () => { +export const pathColumn = () => { return { title: 'Path', field: 'path', - render: resource => {resource?.path}, + render: resource => ( + + {resource.type === 'Kustomization' + ? (resource as Kustomization)?.path + : ''} + + ), } as TableColumn; }; diff --git a/plugins/backstage-plugin-flux/src/hooks/query.ts b/plugins/backstage-plugin-flux/src/hooks/query.ts index d3e70e2..6650081 100644 --- a/plugins/backstage-plugin-flux/src/hooks/query.ts +++ b/plugins/backstage-plugin-flux/src/hooks/query.ts @@ -14,7 +14,7 @@ import { gitRepositoriesGVK, helmReleaseGVK, ociRepositoriesGVK, - kustomizationsGVK, + kustomizationGVK, helmRepositoryGVK, HelmRepository, } from '../objects'; @@ -167,7 +167,7 @@ export function useOCIRepositories(entity: Entity): Response { */ export function useKustomizations(entity: Entity): Response { const { kubernetesObjects, loading, error } = useCustomResources(entity, [ - kustomizationsGVK, + kustomizationGVK, ]); const { data, kubernetesErrors } = toResponse( @@ -215,16 +215,15 @@ export function useHelmRepositories(entity: Entity): Response { export function useFluxDeployments(entity: Entity): Response { const { kubernetesObjects, loading, error } = useCustomResources(entity, [ helmReleaseGVK, - kustomizationsGVK, + kustomizationGVK, ]); - const { data, kubernetesErrors } = toResponse( - item => - item instanceof Kustomization - ? new Kustomization(item) - : new HelmRelease(item), - kubernetesObjects, - ); + const { data, kubernetesErrors } = toResponse(item => { + const { kind } = JSON.parse(item.payload as string); + return kind === 'Kustomization' + ? new Kustomization(item) + : new HelmRelease(item); + }, kubernetesObjects); return { data, diff --git a/plugins/backstage-plugin-flux/src/objects.ts b/plugins/backstage-plugin-flux/src/objects.ts index 2aacbfa..78a61a0 100644 --- a/plugins/backstage-plugin-flux/src/objects.ts +++ b/plugins/backstage-plugin-flux/src/objects.ts @@ -413,7 +413,7 @@ export const helmRepositoryGVK: CustomResourceMatcher = { plural: 'helmrepositories', }; -export const kustomizationsGVK: CustomResourceMatcher = { +export const kustomizationGVK: CustomResourceMatcher = { apiVersion: 'v1beta2', group: 'kustomize.toolkit.fluxcd.io', plural: 'kustomizations', @@ -432,7 +432,7 @@ export function gvkFromKind( case 'OCIRepository': return ociRepositoriesGVK; case 'Kustomization': - return kustomizationsGVK; + return kustomizationGVK; default: break; } From 3dfaaa22a485bea84594b4bbf93a424bfab440fe Mon Sep 17 00:00:00 2001 From: AlinaGoaga Date: Thu, 6 Jul 2023 18:15:25 +0200 Subject: [PATCH 04/14] Tests --- .../FluxEntityDeploymentsCard.test.tsx | 67 +++++++++++++++++-- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx index e8380ca..4eac1eb 100644 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx @@ -17,7 +17,6 @@ import { } from '@backstage/plugin-kubernetes-common'; import { FluxEntityDeploymentsCard } from './FluxEntityDeploymentsCard'; -// add a helm release also const makeTestKustomization = (name: string, path: string) => { return { apiVersion: 'kustomize.toolkit.fluxcd.io/v1', @@ -220,6 +219,48 @@ const makeTestKustomization = (name: string, path: string) => { }, }; }; +const makeTestHelmRelease = (name: string, chart: string, version: string) => { + return { + apiVersion: 'helm.toolkit.fluxcd.io/v2beta1', + kind: 'HelmRelease', + metadata: { + annotations: { + 'metadata.weave.works/test': 'value', + }, + creationTimestamp: '2023-05-25T14:14:46Z', + finalizers: ['finalizers.fluxcd.io'], + name: name, + namespace: 'default', + }, + spec: { + interval: '5m', + chart: { + spec: { + chart, + version: '45.x', + sourceRef: { + kind: 'HelmRepository', + name: 'prometheus-community', + namespace: 'default', + }, + interval: '60m', + }, + }, + }, + status: { + lastAppliedRevision: version, + conditions: [ + { + lastTransitionTime: '2023-05-25T15:03:33Z', + message: 'pulled "test" chart with version "1.0.0"', + reason: 'ChartPullSucceeded', + status: 'True', + type: 'Ready', + }, + ], + }, + }; +}; class StubKubernetesClient implements KubernetesApi { getObjectsByEntity = jest.fn(); @@ -246,6 +287,7 @@ class StubKubernetesClient implements KubernetesApi { type: 'customresources', resources: [ makeTestKustomization('flux-system', './clusters/my-cluster'), + makeTestHelmRelease('redis', 'redis', '1.2.3'), ], }, ], @@ -295,7 +337,7 @@ describe('', () => { jest.resetAllMocks(); }); - describe('listing Kustomizations', () => { + describe('listing Deployments', () => { it('shows the details of an Kustomization', async () => { const result = await renderInTestApp( @@ -328,17 +370,30 @@ describe('', () => { name: 'flux-system', path: './clusters/my-cluster', repo: 'flux-system', + type: 'Kustomization', + }, + { + name: 'default/normal', + version: 'kube-prometheus-stack/6.3.5', + type: 'HelmRelease', }, ]; - for (const testCase of testCases) { - const cell = getByText(testCase.name); + for (let i = 0; i < testCases.length; i++) { + const cell = getByText(testCases[i].name); expect(cell).toBeInTheDocument(); const tr = cell.closest('tr'); expect(tr).toBeInTheDocument(); - expect(tr).toHaveTextContent(testCase.path); - expect(tr).toHaveTextContent(testCase.repo); + + if (i === 0) { + expect(tr).toHaveTextContent(testCases?.[i].path as string); + } + if (i === 1) { + expect(tr).toHaveTextContent(testCases?.[i].version as string); + } + expect(tr).toHaveTextContent(testCases?.[i].type as string); + expect(tr).toHaveTextContent(testCases[i].repo as string); } }); }); From 46f447484d87a53a1768380a092d16545536443a Mon Sep 17 00:00:00 2001 From: AlinaGoaga Date: Thu, 6 Jul 2023 18:19:03 +0200 Subject: [PATCH 05/14] Cleanup --- plugins/backstage-plugin-flux/package.json | 3 ++- .../FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/backstage-plugin-flux/package.json b/plugins/backstage-plugin-flux/package.json index 64400a0..222d0b1 100644 --- a/plugins/backstage-plugin-flux/package.json +++ b/plugins/backstage-plugin-flux/package.json @@ -38,7 +38,8 @@ "react-query": "^3.39.3", "react-use": "^17.2.4", "styled-components": "^5.3.0", - "use-deep-compare": "^1.1.0" + "use-deep-compare": "^1.1.0", + "yaml": "^2.3.1" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0" diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx index b5f9ac0..66e7d32 100644 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx @@ -40,7 +40,6 @@ export const FluxDeploymentsTable = ({ }: Props) => { let helmChart = {} as HelmChart; let path = ''; - let repo = ''; const data = deployments.map(d => { const { @@ -89,9 +88,10 @@ export const FluxDeploymentsTable = ({ columns={columns} title="Deployments" data={ - data as - | (HelmRelease & { id: string })[] - | (Kustomization & { id: string })[] + data as ( + | (HelmRelease & { id: string }) + | (Kustomization & { id: string }) + )[] } isLoading={isLoading} /> From 5794842a7ed6cae72afcd903db9d989bdd40857b Mon Sep 17 00:00:00 2001 From: AlinaGoaga Date: Thu, 6 Jul 2023 18:40:32 +0200 Subject: [PATCH 06/14] Split test assertions to avoid calling expect conditionally --- .../FluxDeploymentsTable.tsx | 1 + .../FluxEntityDeploymentsCard.test.tsx | 31 ++++++++++--------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx index 66e7d32..3ca47ad 100644 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx @@ -81,6 +81,7 @@ export const FluxDeploymentsTable = ({ helmChart, } as HelmRelease & { id: string }; } + return null; }); return ( diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx index 4eac1eb..a0ecff0 100644 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx @@ -379,22 +379,23 @@ describe('', () => { }, ]; - for (let i = 0; i < testCases.length; i++) { - const cell = getByText(testCases[i].name); - expect(cell).toBeInTheDocument(); + // kustomization + const kcell = getByText(testCases[0].name); + expect(kcell).toBeInTheDocument(); + const ktr = kcell.closest('tr'); + expect(ktr).toBeInTheDocument(); + expect(ktr).toHaveTextContent(testCases[0].path as string); + expect(ktr).toHaveTextContent(testCases[0].type as string); + expect(ktr).toHaveTextContent(testCases[0].repo as string); - const tr = cell.closest('tr'); - expect(tr).toBeInTheDocument(); - - if (i === 0) { - expect(tr).toHaveTextContent(testCases?.[i].path as string); - } - if (i === 1) { - expect(tr).toHaveTextContent(testCases?.[i].version as string); - } - expect(tr).toHaveTextContent(testCases?.[i].type as string); - expect(tr).toHaveTextContent(testCases[i].repo as string); - } + //helmrelease + const hrcell = getByText(testCases[1].name); + expect(hrcell).toBeInTheDocument(); + const hrtr = hrcell.closest('tr'); + expect(hrtr).toBeInTheDocument(); + expect(hrtr).toHaveTextContent(testCases[1].version as string); + expect(hrtr).toHaveTextContent(testCases[1].type as string); + expect(hrtr).toHaveTextContent(testCases[1].repo as string); }); }); }); From bf65c46b3400bf3000e8909d00c2cf8f5fea9cc2 Mon Sep 17 00:00:00 2001 From: AlinaGoaga Date: Thu, 6 Jul 2023 18:49:20 +0200 Subject: [PATCH 07/14] Fix warnings --- .../FluxEntityDeploymentsCard.test.tsx | 2 +- plugins/backstage-plugin-flux/src/components/helpers.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx index a0ecff0..f4f6f26 100644 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx @@ -388,7 +388,7 @@ describe('', () => { expect(ktr).toHaveTextContent(testCases[0].type as string); expect(ktr).toHaveTextContent(testCases[0].repo as string); - //helmrelease + // helmrelease const hrcell = getByText(testCases[1].name); expect(hrcell).toBeInTheDocument(); const hrtr = hrcell.closest('tr'); diff --git a/plugins/backstage-plugin-flux/src/components/helpers.tsx b/plugins/backstage-plugin-flux/src/components/helpers.tsx index 2a61bc9..e86fa65 100644 --- a/plugins/backstage-plugin-flux/src/components/helpers.tsx +++ b/plugins/backstage-plugin-flux/src/components/helpers.tsx @@ -210,7 +210,8 @@ export const chartColumn = () => { return `${(resource as HelmRelease)?.helmChart?.chart}/${ resource?.lastAppliedRevision }`; - } else return ''; + } + return ''; }; return { From 9317c17bdec3326a92f4cc2ee71e87f892ce1266 Mon Sep 17 00:00:00 2001 From: AlinaGoaga Date: Fri, 7 Jul 2023 15:29:43 +0200 Subject: [PATCH 08/14] Implement PR feedback --- .../FluxDeploymentsTable.tsx | 8 +-- .../src/components/helpers.tsx | 67 ++++++++++++------ .../backstage-plugin-flux/src/hooks/query.ts | 9 +-- .../backstage-plugin-flux/src/images/helm.png | Bin 0 -> 14439 bytes .../src/images/kustomize.png | Bin 0 -> 5414 bytes .../src/images/pending-action.svg | 1 - 6 files changed, 53 insertions(+), 32 deletions(-) create mode 100644 plugins/backstage-plugin-flux/src/images/helm.png create mode 100644 plugins/backstage-plugin-flux/src/images/kustomize.png delete mode 100644 plugins/backstage-plugin-flux/src/images/pending-action.svg diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx index 3ca47ad..2fd1443 100644 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx @@ -7,10 +7,8 @@ import { updatedColumn, syncColumn, Deployment, - typeColumn, - pathColumn, - chartColumn, repoColumn, + referenceColumn, } from '../helpers'; import { HelmChart, HelmRelease, Kustomization } from '../../objects'; import { FluxEntityTable } from '../FluxEntityTable'; @@ -18,10 +16,8 @@ import { FluxEntityTable } from '../FluxEntityTable'; export const defaultColumns: TableColumn[] = [ idColumn(), nameAndClusterNameColumn(), - typeColumn(), - pathColumn(), repoColumn(), - chartColumn(), + referenceColumn(), statusColumn(), updatedColumn(), syncColumn(), diff --git a/plugins/backstage-plugin-flux/src/components/helpers.tsx b/plugins/backstage-plugin-flux/src/components/helpers.tsx index e86fa65..1fd95ef 100644 --- a/plugins/backstage-plugin-flux/src/components/helpers.tsx +++ b/plugins/backstage-plugin-flux/src/components/helpers.tsx @@ -22,6 +22,8 @@ import { } from '../objects'; import Flex from './Flex'; import KubeStatusIndicator, { getIndicatorInfo } from './KubeStatusIndicator'; +import kustomize from '../images/kustomize.png'; +import helm from '../images/helm.png'; export type Source = GitRepository | OCIRepository | HelmRepository; export type Deployment = HelmRelease | Kustomization; @@ -157,14 +159,6 @@ export const nameAndClusterNameColumn = () => { } as TableColumn; }; -export const typeColumn = () => { - return { - title: 'Type', - field: 'type', - render: resource => {resource?.type}, - } as TableColumn; -}; - export const verifiedColumn = () => { return { title: ( @@ -204,7 +198,7 @@ export const artifactColumn = () => { } as TableColumn; }; -export const chartColumn = () => { +export const referenceColumn = () => { const formatContent = (resource: Deployment) => { if (resource.type === 'HelmRelease') { return `${(resource as HelmRelease)?.helmChart?.chart}/${ @@ -215,9 +209,22 @@ export const chartColumn = () => { }; return { - title: 'Chart', - render: (resource: Deployment) => formatContent(resource), - ...sortAndFilterOptions(resource => formatContent(resource)), + title: 'Reference', + render: (resource: Deployment) => + resource.type === 'HelmRelease' ? ( + formatContent(resource) + ) : ( + + {resource.type === 'Kustomization' + ? (resource as Kustomization)?.path + : ''} + + ), + ...sortAndFilterOptions(resource => + resource.type === 'HelmRelease' + ? formatContent(resource) + : (resource as Kustomization)?.path, + ), } as TableColumn; }; @@ -225,21 +232,39 @@ export const repoColumn = () => { return { title: 'Repo', field: 'repo', - render: resource => {resource?.sourceRef?.name}, + render: resource => ( + +
+ + repo + {resource?.sourceRef?.name} + +
+
+ ), } as TableColumn; }; -export const pathColumn = () => { +export const pathColumn = () => { return { title: 'Path', field: 'path', - render: resource => ( - - {resource.type === 'Kustomization' - ? (resource as Kustomization)?.path - : ''} - - ), + render: resource => {resource?.path}, + } as TableColumn; +}; + +export const chartColumn = () => { + const formatContent = (resource: HelmRelease) => + `${resource.helmChart.chart}/${resource.lastAppliedRevision}`; + return { + title: 'Chart', + render: (resource: HelmRelease) => formatContent(resource), + ...sortAndFilterOptions(resource => formatContent(resource)), } as TableColumn; }; diff --git a/plugins/backstage-plugin-flux/src/hooks/query.ts b/plugins/backstage-plugin-flux/src/hooks/query.ts index 6650081..bd21e92 100644 --- a/plugins/backstage-plugin-flux/src/hooks/query.ts +++ b/plugins/backstage-plugin-flux/src/hooks/query.ts @@ -213,10 +213,11 @@ export function useHelmRepositories(entity: Entity): Response { */ export function useFluxDeployments(entity: Entity): Response { - const { kubernetesObjects, loading, error } = useCustomResources(entity, [ - helmReleaseGVK, - kustomizationGVK, - ]); + const { kubernetesObjects, loading, error } = useCustomResources( + entity, + [helmReleaseGVK, kustomizationGVK], + 60000000, + ); const { data, kubernetesErrors } = toResponse(item => { const { kind } = JSON.parse(item.payload as string); diff --git a/plugins/backstage-plugin-flux/src/images/helm.png b/plugins/backstage-plugin-flux/src/images/helm.png new file mode 100644 index 0000000000000000000000000000000000000000..e9d4be0be6bfd934e3875c00385847eedd35115c GIT binary patch literal 14439 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 From 9c74f77c89cd93004c4f400c25434acc65867796 Mon Sep 17 00:00:00 2001 From: AlinaGoaga Date: Fri, 7 Jul 2023 15:33:33 +0200 Subject: [PATCH 09/14] Update tests --- .../FluxEntityDeploymentsCard.test.tsx | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx index f4f6f26..2c875b5 100644 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx @@ -368,34 +368,25 @@ describe('', () => { const testCases = [ { name: 'flux-system', - path: './clusters/my-cluster', repo: 'flux-system', - type: 'Kustomization', + reference: './clusters/my-cluster', }, { name: 'default/normal', - version: 'kube-prometheus-stack/6.3.5', - type: 'HelmRelease', + repo: 'prometheus-community', + reference: 'kube-prometheus-stack/6.3.5', }, ]; - // kustomization - const kcell = getByText(testCases[0].name); - expect(kcell).toBeInTheDocument(); - const ktr = kcell.closest('tr'); - expect(ktr).toBeInTheDocument(); - expect(ktr).toHaveTextContent(testCases[0].path as string); - expect(ktr).toHaveTextContent(testCases[0].type as string); - expect(ktr).toHaveTextContent(testCases[0].repo as string); + for (const testCase of testCases) { + const cell = getByText(testCase.name); + expect(cell).toBeInTheDocument(); - // helmrelease - const hrcell = getByText(testCases[1].name); - expect(hrcell).toBeInTheDocument(); - const hrtr = hrcell.closest('tr'); - expect(hrtr).toBeInTheDocument(); - expect(hrtr).toHaveTextContent(testCases[1].version as string); - expect(hrtr).toHaveTextContent(testCases[1].type as string); - expect(hrtr).toHaveTextContent(testCases[1].repo as string); + const tr = cell.closest('tr'); + expect(tr).toBeInTheDocument(); + expect(tr).toHaveTextContent(testCase.repo); + expect(tr).toHaveTextContent(testCase.reference); + } }); }); }); From a8a413f288f212a608b86372559b518cedd3aca3 Mon Sep 17 00:00:00 2001 From: AlinaGoaga Date: Fri, 7 Jul 2023 19:26:26 +0200 Subject: [PATCH 10/14] Update test and add new icons for k8s and helm --- .../FluxEntityDeploymentsCard.test.tsx | 4 +- .../src/components/helpers.tsx | 65 +++++++++++++++--- .../backstage-plugin-flux/src/images/helm.png | Bin 14439 -> 0 bytes .../src/images/kustomize.png | Bin 5414 -> 0 bytes 4 files changed, 59 insertions(+), 10 deletions(-) delete mode 100644 plugins/backstage-plugin-flux/src/images/helm.png delete mode 100644 plugins/backstage-plugin-flux/src/images/kustomize.png diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx index 2c875b5..c4949a6 100644 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx @@ -338,7 +338,7 @@ describe('', () => { }); describe('listing Deployments', () => { - it('shows the details of an Kustomization', async () => { + it('shows the details of a Deployment', async () => { const result = await renderInTestApp( ', () => { reference: './clusters/my-cluster', }, { - name: 'default/normal', + name: 'normal', repo: 'prometheus-community', reference: 'kube-prometheus-stack/6.3.5', }, diff --git a/plugins/backstage-plugin-flux/src/components/helpers.tsx b/plugins/backstage-plugin-flux/src/components/helpers.tsx index 1fd95ef..e88ae3a 100644 --- a/plugins/backstage-plugin-flux/src/components/helpers.tsx +++ b/plugins/backstage-plugin-flux/src/components/helpers.tsx @@ -22,8 +22,60 @@ import { } from '../objects'; import Flex from './Flex'; import KubeStatusIndicator, { getIndicatorInfo } from './KubeStatusIndicator'; -import kustomize from '../images/kustomize.png'; -import helm from '../images/helm.png'; + +const helm = ( + + + + + + + + +); +const kubernetes = ( + + + + +); export type Source = GitRepository | OCIRepository | HelmRepository; export type Deployment = HelmRelease | Kustomization; @@ -236,12 +288,9 @@ export const repoColumn = () => {
- repo +
+ {resource.type === 'HelmRelease' ? helm : kubernetes} +
{resource?.sourceRef?.name}
diff --git a/plugins/backstage-plugin-flux/src/images/helm.png b/plugins/backstage-plugin-flux/src/images/helm.png deleted file mode 100644 index e9d4be0be6bfd934e3875c00385847eedd35115c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14439 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 Date: Mon, 10 Jul 2023 10:52:40 +0200 Subject: [PATCH 11/14] Update test --- .../FluxEntityDeploymentsCard.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx index c4949a6..1dbed3f 100644 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx @@ -287,7 +287,7 @@ class StubKubernetesClient implements KubernetesApi { type: 'customresources', resources: [ makeTestKustomization('flux-system', './clusters/my-cluster'), - makeTestHelmRelease('redis', 'redis', '1.2.3'), + makeTestHelmRelease('normal', 'kube-prometheus-stack', '6.3.5'), ], }, ], @@ -372,7 +372,7 @@ describe('', () => { reference: './clusters/my-cluster', }, { - name: 'normal', + name: 'default/normal', repo: 'prometheus-community', reference: 'kube-prometheus-stack/6.3.5', }, From 834efd9874d292592e85f816ed736c0e244faecb Mon Sep 17 00:00:00 2001 From: AlinaGoaga Date: Mon, 10 Jul 2023 12:43:43 +0200 Subject: [PATCH 12/14] Remove interval from hook --- plugins/backstage-plugin-flux/src/hooks/query.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/backstage-plugin-flux/src/hooks/query.ts b/plugins/backstage-plugin-flux/src/hooks/query.ts index bd21e92..6650081 100644 --- a/plugins/backstage-plugin-flux/src/hooks/query.ts +++ b/plugins/backstage-plugin-flux/src/hooks/query.ts @@ -213,11 +213,10 @@ export function useHelmRepositories(entity: Entity): Response { */ export function useFluxDeployments(entity: Entity): Response { - const { kubernetesObjects, loading, error } = useCustomResources( - entity, - [helmReleaseGVK, kustomizationGVK], - 60000000, - ); + const { kubernetesObjects, loading, error } = useCustomResources(entity, [ + helmReleaseGVK, + kustomizationGVK, + ]); const { data, kubernetesErrors } = toResponse(item => { const { kind } = JSON.parse(item.payload as string); From 0af1ef53bb0ed6b15d62468fee5200d3a5c64b05 Mon Sep 17 00:00:00 2001 From: AlinaGoaga Date: Tue, 11 Jul 2023 14:17:33 +0200 Subject: [PATCH 13/14] Implement PR feedback --- .../FluxDeploymentsTable.tsx | 14 ++- .../FluxEntityDeploymentsCard.tsx | 3 +- .../FluxEntityHelmReleasesCard.tsx | 10 ++- .../FluxHelmReleasesTable.tsx | 72 --------------- .../FluxEntityKustomizationsCard.tsx | 9 +- .../FluxKustomizationsTable.tsx | 68 -------------- .../src/components/helpers.tsx | 89 +++---------------- .../src/images/icons.tsx | 56 ++++++++++++ 8 files changed, 93 insertions(+), 228 deletions(-) delete mode 100644 plugins/backstage-plugin-flux/src/components/FluxEntityHelmReleasesCard/FluxHelmReleasesTable.tsx delete mode 100644 plugins/backstage-plugin-flux/src/components/FluxEntityKustomizationsCard/FluxKustomizationsTable.tsx create mode 100644 plugins/backstage-plugin-flux/src/images/icons.tsx diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx index 2fd1443..a9c357e 100644 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx @@ -9,12 +9,14 @@ import { Deployment, repoColumn, referenceColumn, + typeColumn, } from '../helpers'; import { HelmChart, HelmRelease, Kustomization } from '../../objects'; import { FluxEntityTable } from '../FluxEntityTable'; export const defaultColumns: TableColumn[] = [ idColumn(), + typeColumn(), nameAndClusterNameColumn(), repoColumn(), referenceColumn(), @@ -27,13 +29,23 @@ type Props = { deployments: Deployment[]; isLoading: boolean; columns: TableColumn[]; + kinds: string[]; }; export const FluxDeploymentsTable = ({ + kinds, deployments, isLoading, columns, }: Props) => { + const getTitle = () => { + if (kinds.length === 1) { + return `${kinds[0]}s`; + } else { + return 'Deployments'; + } + }; + let helmChart = {} as HelmChart; let path = ''; @@ -83,7 +95,7 @@ export const FluxDeploymentsTable = ({ return ( { return ( { }; /** - * Render the Kustomizations associated with the current Entity. + * Render the Deployments associated with the current Entity. * * @public */ diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityHelmReleasesCard/FluxEntityHelmReleasesCard.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityHelmReleasesCard/FluxEntityHelmReleasesCard.tsx index e0a3330..0e93337 100644 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityHelmReleasesCard/FluxEntityHelmReleasesCard.tsx +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityHelmReleasesCard/FluxEntityHelmReleasesCard.tsx @@ -1,8 +1,11 @@ import React from 'react'; import { useEntity } from '@backstage/plugin-catalog-react'; import { useHelmReleases } from '../../hooks/query'; -import { FluxHelmReleasesTable, defaultColumns } from './FluxHelmReleasesTable'; import { WeaveGitOpsContext } from '../WeaveGitOpsContext'; +import { + FluxDeploymentsTable, + defaultColumns, +} from '../FluxEntityDeploymentsCard/FluxDeploymentsTable'; const HelmReleasePanel = () => { const { entity } = useEntity(); @@ -23,8 +26,9 @@ const HelmReleasePanel = () => { } return ( - diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityHelmReleasesCard/FluxHelmReleasesTable.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityHelmReleasesCard/FluxHelmReleasesTable.tsx deleted file mode 100644 index ca69a95..0000000 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityHelmReleasesCard/FluxHelmReleasesTable.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import { TableColumn } from '@backstage/core-components'; -import { - chartColumn, - idColumn, - nameAndClusterNameColumn, - statusColumn, - syncColumn, - updatedColumn, -} from '../helpers'; -import { HelmRelease } from '../../objects'; -import { FluxEntityTable } from '../FluxEntityTable'; - -export const defaultColumns: TableColumn[] = [ - idColumn(), - nameAndClusterNameColumn(), - chartColumn(), - statusColumn(), - updatedColumn(), - syncColumn(), -]; - -type Props = { - helmReleases: HelmRelease[]; - isLoading: boolean; - columns: TableColumn[]; -}; - -/** - * @public - */ -export const FluxHelmReleasesTable = ({ - helmReleases, - isLoading, - columns, -}: Props) => { - // TODO: Simplify this to store the ID and HelmRelease - const data = helmReleases.map(hr => { - const { - clusterName, - namespace, - name, - helmChart, - conditions, - suspended, - sourceRef, - type, - lastAppliedRevision, - } = hr; - return { - id: `${clusterName}/${namespace}/${name}`, - conditions, - suspended, - name, - namespace, - helmChart, - lastAppliedRevision, - clusterName, - sourceRef, - type, - } as HelmRelease & { id: string }; - }); - - return ( - - ); -}; diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityKustomizationsCard/FluxEntityKustomizationsCard.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityKustomizationsCard/FluxEntityKustomizationsCard.tsx index 415cfb0..5d6e22a 100644 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityKustomizationsCard/FluxEntityKustomizationsCard.tsx +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityKustomizationsCard/FluxEntityKustomizationsCard.tsx @@ -3,9 +3,9 @@ import { useEntity } from '@backstage/plugin-catalog-react'; import { WeaveGitOpsContext } from '../WeaveGitOpsContext'; import { useKustomizations } from '../../hooks'; import { - FluxKustomizationsTable, + FluxDeploymentsTable, defaultColumns, -} from './FluxKustomizationsTable'; +} from '../FluxEntityDeploymentsCard/FluxDeploymentsTable'; const KustomizationPanel = () => { const { entity } = useEntity(); @@ -25,8 +25,9 @@ const KustomizationPanel = () => { } return ( - diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityKustomizationsCard/FluxKustomizationsTable.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityKustomizationsCard/FluxKustomizationsTable.tsx deleted file mode 100644 index e3ca8ec..0000000 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityKustomizationsCard/FluxKustomizationsTable.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import { TableColumn } from '@backstage/core-components'; -import { - idColumn, - nameAndClusterNameColumn, - pathColumn, - repoColumn, - statusColumn, - updatedColumn, - syncColumn, -} from '../helpers'; -import { Kustomization } from '../../objects'; -import { FluxEntityTable } from '../FluxEntityTable'; - -export const defaultColumns: TableColumn[] = [ - idColumn(), - nameAndClusterNameColumn(), - pathColumn(), - repoColumn(), - statusColumn(), - updatedColumn(), - syncColumn(), -]; - -type Props = { - kustomizations: Kustomization[]; - isLoading: boolean; - columns: TableColumn[]; -}; - -export const FluxKustomizationsTable = ({ - kustomizations, - isLoading, - columns, -}: Props) => { - const data = kustomizations.map(k => { - const { - clusterName, - namespace, - name, - sourceRef, - path, - conditions, - suspended, - type, - } = k; - return { - id: `${clusterName}/${namespace}/${name}`, - conditions, - suspended, - name, - namespace, - clusterName, - sourceRef, - path, - type, - } as Kustomization & { id: string }; - }); - - return ( - - ); -}; diff --git a/plugins/backstage-plugin-flux/src/components/helpers.tsx b/plugins/backstage-plugin-flux/src/components/helpers.tsx index e88ae3a..6c31ca5 100644 --- a/plugins/backstage-plugin-flux/src/components/helpers.tsx +++ b/plugins/backstage-plugin-flux/src/components/helpers.tsx @@ -22,60 +22,7 @@ import { } from '../objects'; import Flex from './Flex'; import KubeStatusIndicator, { getIndicatorInfo } from './KubeStatusIndicator'; - -const helm = ( - - - - - - - - -); -const kubernetes = ( - - - - -); +import { helm, kubernetes } from '../images/icons'; export type Source = GitRepository | OCIRepository | HelmRepository; export type Deployment = HelmRelease | Kustomization; @@ -280,40 +227,24 @@ export const referenceColumn = () => { } as TableColumn; }; -export const repoColumn = () => { +export const typeColumn = () => { return { - title: 'Repo', - field: 'repo', + title: '', + field: 'type', render: resource => ( -
- -
- {resource.type === 'HelmRelease' ? helm : kubernetes} -
- {resource?.sourceRef?.name} -
-
+
{resource.type === 'HelmRelease' ? helm : kubernetes}
), + width: '20px', } as TableColumn; }; -export const pathColumn = () => { - return { - title: 'Path', - field: 'path', - render: resource => {resource?.path}, - } as TableColumn; -}; - -export const chartColumn = () => { - const formatContent = (resource: HelmRelease) => - `${resource.helmChart.chart}/${resource.lastAppliedRevision}`; +export const repoColumn = () => { return { - title: 'Chart', - render: (resource: HelmRelease) => formatContent(resource), - ...sortAndFilterOptions(resource => formatContent(resource)), + title: 'Repo', + field: 'repo', + render: resource => {resource?.sourceRef?.name}, } as TableColumn; }; diff --git a/plugins/backstage-plugin-flux/src/images/icons.tsx b/plugins/backstage-plugin-flux/src/images/icons.tsx new file mode 100644 index 0000000..0ac4b38 --- /dev/null +++ b/plugins/backstage-plugin-flux/src/images/icons.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +export const helm = ( + + + + + + + + +); + +export const kubernetes = ( + + + + +); From f66c9768ec5031c8ac48595fc7bc91db0b8f7c4d Mon Sep 17 00:00:00 2001 From: AlinaGoaga Date: Tue, 11 Jul 2023 15:09:38 +0200 Subject: [PATCH 14/14] Solve test warning --- .../FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx index a9c357e..1d7316b 100644 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx @@ -41,9 +41,8 @@ export const FluxDeploymentsTable = ({ const getTitle = () => { if (kinds.length === 1) { return `${kinds[0]}s`; - } else { - return 'Deployments'; } + return 'Deployments'; }; let helmChart = {} as HelmChart;