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..1d7316b --- /dev/null +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxDeploymentsTable.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { TableColumn } from '@backstage/core-components'; +import { + idColumn, + nameAndClusterNameColumn, + statusColumn, + updatedColumn, + syncColumn, + 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(), + statusColumn(), + updatedColumn(), + syncColumn(), +]; + +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`; + } + return 'Deployments'; + }; + + let helmChart = {} as HelmChart; + let path = ''; + + const data = deployments.map(d => { + const { + clusterName, + namespace, + name, + conditions, + suspended, + sourceRef, + type, + lastAppliedRevision, + } = d; + if (d instanceof Kustomization) { + path = d.path; + return { + id: `${clusterName}/${namespace}/${name}`, + conditions, + suspended, + name, + namespace, + lastAppliedRevision, + clusterName, + sourceRef, + type, + path, + } as Kustomization & { id: string }; + } else if (d instanceof HelmRelease) { + helmChart = d.helmChart; + return { + id: `${clusterName}/${namespace}/${name}`, + conditions, + suspended, + name, + namespace, + lastAppliedRevision, + clusterName, + sourceRef, + type, + 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 new file mode 100644 index 0000000..1dbed3f --- /dev/null +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.test.tsx @@ -0,0 +1,392 @@ +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, + }, + }; +}; +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(); + + 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'), + makeTestHelmRelease('normal', 'kube-prometheus-stack', '6.3.5'), + ], + }, + ], + }, + ], + }); + } + + 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 Deployments', () => { + it('shows the details of a Deployment', async () => { + const result = await renderInTestApp( + + + + + + + , + ); + + const { getByText } = result; + + const testCases = [ + { + name: 'flux-system', + repo: 'flux-system', + reference: './clusters/my-cluster', + }, + { + name: 'default/normal', + repo: 'prometheus-community', + reference: 'kube-prometheus-stack/6.3.5', + }, + ]; + + 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.repo); + expect(tr).toHaveTextContent(testCase.reference); + } + }); + }); +}); 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..04aa0a0 --- /dev/null +++ b/plugins/backstage-plugin-flux/src/components/FluxEntityDeploymentsCard/FluxEntityDeploymentsCard.tsx @@ -0,0 +1,43 @@ +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 Deployments 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/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 4e7c9e9..0000000 --- a/plugins/backstage-plugin-flux/src/components/FluxEntityHelmReleasesCard/FluxHelmReleasesTable.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from 'react'; -import { TableColumn } from '@backstage/core-components'; -import { - idColumn, - nameAndClusterNameColumn, - sortAndFilterOptions, - statusColumn, - syncColumn, - updatedColumn, -} from '../helpers'; -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(), - 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 c45e711..6c31ca5 100644 --- a/plugins/backstage-plugin-flux/src/components/helpers.tsx +++ b/plugins/backstage-plugin-flux/src/components/helpers.tsx @@ -22,6 +22,7 @@ import { } from '../objects'; import Flex from './Flex'; import KubeStatusIndicator, { getIndicatorInfo } from './KubeStatusIndicator'; +import { helm, kubernetes } from '../images/icons'; export type Source = GitRepository | OCIRepository | HelmRepository; export type Deployment = HelmRelease | Kustomization; @@ -196,19 +197,54 @@ export const artifactColumn = () => { } as TableColumn; }; -export const repoColumn = () => { +export const referenceColumn = () => { + const formatContent = (resource: Deployment) => { + if (resource.type === 'HelmRelease') { + return `${(resource as HelmRelease)?.helmChart?.chart}/${ + resource?.lastAppliedRevision + }`; + } + return ''; + }; + return { - title: 'Repo', - field: 'repo', - render: resource => {resource?.sourceRef?.name}, + 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; }; -export const pathColumn = () => { +export const typeColumn = () => { return { - title: 'Path', - field: 'path', - render: resource => {resource?.path}, + title: '', + field: 'type', + render: resource => ( + + {resource.type === 'HelmRelease' ? helm : kubernetes} + + ), + width: '20px', + } as TableColumn; +}; + +export const repoColumn = () => { + return { + title: 'Repo', + field: 'repo', + render: resource => {resource?.sourceRef?.name}, } as TableColumn; }; diff --git a/plugins/backstage-plugin-flux/src/hooks/query.ts b/plugins/backstage-plugin-flux/src/hooks/query.ts index 209129d..6650081 100644 --- a/plugins/backstage-plugin-flux/src/hooks/query.ts +++ b/plugins/backstage-plugin-flux/src/hooks/query.ts @@ -14,10 +14,11 @@ import { gitRepositoriesGVK, helmReleaseGVK, ociRepositoriesGVK, - kustomizationsGVK, + kustomizationGVK, helmRepositoryGVK, HelmRepository, } from '../objects'; +import { Deployment } from '../components/helpers'; function toErrors( cluster: ClusterAttributes, @@ -166,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( @@ -205,3 +206,30 @@ export function useHelmRepositories(entity: Entity): Response { : kubernetesErrors, }; } + +/** + * Query for the Flux Deployments - Kustomizations and Helm Releases - associated with this Entity. + * @public + */ + +export function useFluxDeployments(entity: Entity): Response { + const { kubernetesObjects, loading, error } = useCustomResources(entity, [ + helmReleaseGVK, + kustomizationGVK, + ]); + + 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, + loading, + errors: error + ? [new Error(error), ...(kubernetesErrors || [])] + : kubernetesErrors, + }; +} 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 = ( + + + + +); diff --git a/plugins/backstage-plugin-flux/src/images/pending-action.svg b/plugins/backstage-plugin-flux/src/images/pending-action.svg deleted file mode 100644 index e095e29..0000000 --- a/plugins/backstage-plugin-flux/src/images/pending-action.svg +++ /dev/null @@ -1 +0,0 @@ - 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/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; } 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, + ), + }, + }), +);