From d6a8c9fd1945f227a6f1b2785a1d99310800d758 Mon Sep 17 00:00:00 2001 From: Kevin McDermott Date: Mon, 7 Aug 2023 17:04:31 +0100 Subject: [PATCH] URL-encode the cluster name (#45) This changes the Weave GitOps linking to correctly encode the name of the cluster. Guidance is also provided on linking to namespaced clusters. --- plugins/backstage-plugin-flux/README.md | 40 +++++++++++++++++ .../src/hooks/useWeaveFluxDeepLink.test.tsx | 44 ++++++++++++++++--- .../src/hooks/useWeaveFluxDeepLink.ts | 8 ++-- 3 files changed, 82 insertions(+), 10 deletions(-) diff --git a/plugins/backstage-plugin-flux/README.md b/plugins/backstage-plugin-flux/README.md index a62f53c..80cf5af 100644 --- a/plugins/backstage-plugin-flux/README.md +++ b/plugins/backstage-plugin-flux/README.md @@ -233,6 +233,46 @@ gitops: baseUrl: https://wego.example.com ``` +**NOTE:** The URL generated will include the name of the cluster that is configured in Backstage's cluster location mechanism. + +```yaml +kubernetes: + serviceLocatorMethod: + type: 'multiTenant' + clusterLocatorMethods: + - type: 'config' + clusters: + - url: https://192.168.0.1:8000 + name: Default + authProvider: 'serviceAccount' + skipTLSVerify: true + skipMetricsLookup: true + serviceAccountToken: ABC123 + caData: LS0tLS1CRUdJTiBDRVJUSUZJQ0... +``` + +For example, this would generate a link to the `Default` cluster. + +If you want to link to Weave GitOps Enterprise, you will need to provide the +name and namespace of the cluster you want to query, for example: + +```yaml +kubernetes: + serviceLocatorMethod: + type: 'multiTenant' + clusterLocatorMethods: + - type: 'config' + clusters: + - url: https://192.168.0.1:8000 + name: test-ns/name + authProvider: 'serviceAccount' + skipTLSVerify: true + skipMetricsLookup: true + serviceAccountToken: ABC123 + caData: LS0tLS1CRUdJTiBDRVJUSUZJQ0... + +``` + ## Verification For the resources where we display a Verification status, if the Flux resource diff --git a/plugins/backstage-plugin-flux/src/hooks/useWeaveFluxDeepLink.test.tsx b/plugins/backstage-plugin-flux/src/hooks/useWeaveFluxDeepLink.test.tsx index c76e10c..ad7693e 100644 --- a/plugins/backstage-plugin-flux/src/hooks/useWeaveFluxDeepLink.test.tsx +++ b/plugins/backstage-plugin-flux/src/hooks/useWeaveFluxDeepLink.test.tsx @@ -4,20 +4,30 @@ import { renderHook } from '@testing-library/react-hooks'; import React, { PropsWithChildren } from 'react'; import { useWeaveFluxDeepLink } from './useWeaveFluxDeepLink'; import { GitRepository, HelmRelease, OCIRepository } from '../objects'; +import * as unverifiedGitRepository from '../__fixtures__/unverified_git_repository.json'; +import * as unverifiedOCIRepository from '../__fixtures__/unverified_oci_repository.json'; + const testHelmRelease = new HelmRelease({ + clusterName: 'Default', payload: '{"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"],"generation":5,"name":"normal","namespace":"default","resourceVersion":"1","uid":"82231842-2224-4f22-8576-5babf08d746d"}}', }); + const testGitRepository = new GitRepository({ + clusterName: 'Default', payload: - '{"apiVersion":"source.toolkit.fluxcd.io/v1","kind":"GitRepository","metadata":{"creationTimestamp":"2023-06-22T17:58:23Z","finalizers":["finalizers.fluxcd.io"],"generation":1,"name":"podinfo","namespace":"default","resourceVersion":"137468","uid":"068ec137-b2a0-4b35-90ea-4e9a8a2fe5f6"},"spec":{"interval":"1m","ref":{"branch":"master"},"timeout":"60s","url":"https://github.com/stefanprodan/podinfo"},"status":{"artifact":{"digest":"sha256:f1e2d4a8244772c47d5e10b38768acec57dc404d6409464c15d2eb8c84b28b51","lastUpdateTime":"2023-06-22T17:58:24Z","path":"gitrepository/default/podinfo/e06a5517daf5ac8c5ba74a97135499e40624885a.tar.gz","revision":"master@sha1:e06a5517daf5ac8c5ba74a97135499e40624885a","size":80053,"url":"http://source-controller.flux-system.svc.cluster.local./gitrepository/default/podinfo/e06a5517daf5ac8c5ba74a97135499e40624885a.tar.gz"},"conditions":[{"lastTransitionTime":"2023-06-23T06:58:24Z","message":"stored artifact for revision \'master@sha1:e06a5517daf5ac8c5ba74a97135499e40624885a\'","observedGeneration":1,"reason":"Succeeded","status":"True","type":"Ready"},{"lastTransitionTime":"2023-06-22T17:58:24Z","message":"stored artifact for revision \'master@sha1:e06a5517daf5ac8c5ba74a97135499e40624885a\'","observedGeneration":1,"reason":"Succeeded","status":"True","type":"ArtifactInStorage"}],"observedGeneration":1}}', + JSON.stringify(unverifiedGitRepository), }); + const testOCIRepository = new OCIRepository({ - payload: `{"apiVersion":"source.toolkit.fluxcd.io/v1beta2","kind":"OCIRepository","metadata":{"creationTimestamp":"2023-06-23T07:50:47Z","finalizers":["finalizers.fluxcd.io"],"generation":1,"name":"podinfo","namespace":"default","resourceVersion":"143955","uid":"1ec54278-ed2d-4f31-9bb0-39dc7163730e"},"spec":{"interval":"5m","provider":"generic","timeout":"60s","url":"oci://ghcr.io/stefanprodan/manifests/podinfo","verify":{"provider":"cosign"}},"status":{"artifact":{"digest":"sha256:62df151eb3714d9dfa943c7d88192d72466bffa268b25595f85530b793f77524","lastUpdateTime":"2023-06-23T07:50:53Z","metadata":{"org.opencontainers.image.created":"2023-05-03T14:30:58Z","org.opencontainers.image.revision":"6.3.6/073f1ec5aff930bd3411d33534e91cbe23302324","org.opencontainers.image.source":"https://github.com/stefanprodan/podinfo"},"path":"ocirepository/default/podinfo/sha256:2982c337af6ba98c0e9224a5d7149a19baa9cbedea09b16ae44253682050b6a4.tar.gz","revision":"latest@sha256:2982c337af6ba98c0e9224a5d7149a19baa9cbedea09b16ae44253682050b6a4","size":1071,"url":"http://source-controller.flux-system.svc.cluster.local./ocirepository/default/podinfo/sha256:2982c337af6ba98c0e9224a5d7149a19baa9cbedea09b16ae44253682050b6a4.tar.gz"},"conditions":[{"lastTransitionTime":"2023-06-23T07:50:53Z","message":"stored artifact for digest 'latest@sha256:2982c337af6ba98c0e9224a5d7149a19baa9cbedea09b16ae44253682050b6a4'","observedGeneration":1,"reason":"Succeeded","status":"True","type":"Ready"},{"lastTransitionTime":"2023-06-23T07:50:53Z","message":"stored artifact for digest 'latest@sha256:2982c337af6ba98c0e9224a5d7149a19baa9cbedea09b16ae44253682050b6a4'","observedGeneration":1,"reason":"Succeeded","status":"True","type":"ArtifactInStorage"}],"observedGeneration":1,"url":"http://source-controller.flux-system.svc.cluster.local./ocirepository/default/podinfo/latest.tar.gz"}}`, + clusterName: 'demo-cluster', + payload: + JSON.stringify(unverifiedOCIRepository), }); let gitOpsUrl: string | undefined; + const mockConfigApi = { getOptionalString: jest.fn(() => gitOpsUrl), } as Partial; @@ -47,7 +57,7 @@ describe('useWeaveFluxDeepLink', () => { }, ); expect(result.current).toBe( - 'https://example.com/helm_release/details?clusterName=&name=normal&namespace=default', + 'https://example.com/helm_release/details?clusterName=Default&name=normal&namespace=default', ); }); @@ -61,7 +71,7 @@ describe('useWeaveFluxDeepLink', () => { }, ); expect(result.current).toBe( - 'https://example.com/git_repo/details?clusterName=&name=podinfo&namespace=default', + 'https://example.com/git_repo/details?clusterName=Default&name=podinfo&namespace=backstage', ); }); @@ -75,7 +85,7 @@ describe('useWeaveFluxDeepLink', () => { }, ); expect(result.current).toBe( - 'https://example.com/oci/details?clusterName=&name=podinfo&namespace=default', + 'https://example.com/oci/details?clusterName=demo-cluster&name=testing&namespace=default', ); }); }); @@ -91,7 +101,7 @@ describe('useWeaveFluxDeepLink', () => { }, ); expect(result.current).toBe( - 'https://example.com/helm_release/details?clusterName=&name=normal&namespace=default', + 'https://example.com/helm_release/details?clusterName=Default&name=normal&namespace=default', ); }); }); @@ -107,4 +117,26 @@ describe('useWeaveFluxDeepLink', () => { expect(result.current).toBeUndefined(); }); }); + + describe('when the cluster name has a namespace name/test-ns', () => { + it('is correctly URL encoded into the request URL', async () => { + gitOpsUrl = 'https://example.com/'; + + const helmRelease = new HelmRelease({ + clusterName: 'demo-ns/test-cluster', + payload: + '{"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"],"generation":5,"name":"normal","namespace":"default","resourceVersion":"1","uid":"82231842-2224-4f22-8576-5babf08d746d"}}', + }); + + + const { result } = renderHook( + () => useWeaveFluxDeepLink(helmRelease), + { + wrapper, + }, + ); + expect(result.current).toBe( + 'https://example.com/helm_release/details?clusterName=demo-ns%2Ftest-cluster&name=normal&namespace=default', + ); }); + }); }); diff --git a/plugins/backstage-plugin-flux/src/hooks/useWeaveFluxDeepLink.ts b/plugins/backstage-plugin-flux/src/hooks/useWeaveFluxDeepLink.ts index a8cfda9..6f1b22c 100644 --- a/plugins/backstage-plugin-flux/src/hooks/useWeaveFluxDeepLink.ts +++ b/plugins/backstage-plugin-flux/src/hooks/useWeaveFluxDeepLink.ts @@ -1,4 +1,5 @@ import { configApiRef, useApi } from '@backstage/core-plugin-api'; + import { FluxObject, GitRepository, @@ -8,6 +9,7 @@ import { OCIRepository, } from '../objects'; + const typedUrl = (baseUrl: string, a: FluxObject, type: string): string => { const queryStringData = { clusterName: a.clusterName, @@ -15,11 +17,9 @@ const typedUrl = (baseUrl: string, a: FluxObject, type: string): string => { namespace: a.namespace, }; - const queryString = Object.entries(queryStringData) - .map(([key, value]) => `${key}=${value}`) - .join('&'); + const searchParams = new URLSearchParams(queryStringData); - return `${baseUrl.replace(/\/$/, '')}/${type}/details?${queryString}`; + return `${baseUrl.replace(/\/$/, '')}/${type}/details?${searchParams.toString()}`; }; export const useWeaveFluxDeepLink = (