diff --git a/dashboard/src/actions/repos.test.tsx b/dashboard/src/actions/repos.test.tsx index 03b1242abd5..010e8ef8268 100644 --- a/dashboard/src/actions/repos.test.tsx +++ b/dashboard/src/actions/repos.test.tsx @@ -46,7 +46,6 @@ beforeEach(() => { AppRepository.create = jest.fn().mockImplementationOnce(() => { return { appRepository: { metadata: { name: "repo-abc" } } }; }); - Secret.create = jest.fn(); Secret.list = jest.fn().mockReturnValue({ items: [], }); @@ -327,11 +326,6 @@ describe("installRepo", () => { ); }); - it("does not create the K8s secret as API includes this", async () => { - await store.dispatch(installRepoCMDAuth); - expect(Secret.create).not.toHaveBeenCalled(); - }); - it("returns true", async () => { const res = await store.dispatch(installRepoCMDAuth); expect(res).toBe(true); @@ -362,11 +356,6 @@ describe("installRepo", () => { ); }); - it("does not create the K8s secret as API includes this", async () => { - await store.dispatch(installRepoCMDAuth); - expect(Secret.create).not.toHaveBeenCalled(); - }); - it("returns true", async () => { const res = await store.dispatch(installRepoCMDAuth); expect(res).toBe(true); diff --git a/dashboard/src/actions/repos.ts b/dashboard/src/actions/repos.ts index 73fd87dc951..e7241fbc53f 100644 --- a/dashboard/src/actions/repos.ts +++ b/dashboard/src/actions/repos.ts @@ -160,7 +160,7 @@ export const fetchRepoSecrets = ( // TODO(andresmgot): Create an endpoint for returning credentials related to an AppRepository // to avoid listing secrets // /~https://github.com/kubeapps/kubeapps/issues/1686 - const secrets = await Secret.list(namespace); + const secrets = await Secret.list("default", namespace); const repoSecrets = secrets.items?.filter(s => s.metadata.ownerReferences?.some(ownerRef => ownerRef.kind === "AppRepository"), ); @@ -173,7 +173,7 @@ export const fetchRepoSecret = ( name: string, ): ThunkAction, IStoreState, null, AppReposAction> => { return async dispatch => { - const secret = await Secret.get(name, namespace); + const secret = await Secret.get("default", name, namespace); dispatch(receiveReposSecret(secret)); }; }; @@ -347,7 +347,7 @@ export function fetchImagePullSecrets( // TODO(andresmgot): Create an endpoint for returning just the list of secret names // to avoid listing all the secrets with protected information // /~https://github.com/kubeapps/kubeapps/issues/1686 - const secrets = await Secret.list(namespace); + const secrets = await Secret.list("default", namespace); const imgPullSecrets = secrets.items?.filter( s => s.type === "kubernetes.io/dockerconfigjson", ); @@ -368,7 +368,15 @@ export function createDockerRegistrySecret( ): ThunkAction, IStoreState, null, AppReposAction> { return async dispatch => { try { - const secret = await Secret.createPullSecret(name, user, password, email, server, namespace); + const secret = await Secret.createPullSecret( + "default", + name, + user, + password, + email, + server, + namespace, + ); dispatch(createImagePullSecret(secret)); return true; } catch (e) { diff --git a/dashboard/src/shared/Namespace.ts b/dashboard/src/shared/Namespace.ts index b3d9ada476c..44ea1762fa2 100644 --- a/dashboard/src/shared/Namespace.ts +++ b/dashboard/src/shared/Namespace.ts @@ -15,24 +15,19 @@ export default class Namespace { } public static async create(cluster: string, name: string) { - const { data } = await axiosWithAuth.post( - `api/clusters/${cluster}/api/v1/namespaces/`, - { - apiVersion: "v1", - kind: "Namespace", - metadata: { - name, - }, + const { data } = await axiosWithAuth.post(url.api.k8s.namespaces(cluster), { + apiVersion: "v1", + kind: "Namespace", + metadata: { + name, }, - ); + }); return data; } public static async get(cluster: string, name: string) { try { - const { data } = await axiosWithAuth.get( - `api/clusters/${cluster}/api/v1/namespaces/${name}`, - ); + const { data } = await axiosWithAuth.get(url.api.k8s.namespace(cluster, name)); return data; } catch (err) { switch (err.constructor) { diff --git a/dashboard/src/shared/Operators.ts b/dashboard/src/shared/Operators.ts index ecc593320c4..750f5b09c84 100644 --- a/dashboard/src/shared/Operators.ts +++ b/dashboard/src/shared/Operators.ts @@ -11,20 +11,20 @@ import { export class Operators { public static async isOLMInstalled(namespace: string) { - const { status } = await axiosWithAuth.get(urls.api.operators.operators(namespace)); + const { status } = await axiosWithAuth.get(urls.api.k8s.operators.operators(namespace)); return status === 200; } public static async getOperators(namespace: string) { const { data } = await axiosWithAuth.get>( - urls.api.operators.operators(namespace), + urls.api.k8s.operators.operators(namespace), ); return data.items; } public static async getOperator(namespace: string, name: string) { const { data } = await axiosWithAuth.get( - urls.api.operators.operator(namespace, name), + urls.api.k8s.operators.operator(namespace, name), ); return data; } @@ -33,14 +33,14 @@ export class Operators { // Global operators are installed in the "operators" namespace const reqNamespace = namespace === "_all" ? "operators" : namespace; const { data } = await axiosWithAuth.get>( - urls.api.operators.clusterServiceVersions(reqNamespace), + urls.api.k8s.operators.clusterServiceVersions(reqNamespace), ); return data.items; } public static async getCSV(namespace: string, name: string) { const { data } = await axiosWithAuth.get( - urls.api.operators.clusterServiceVersion(namespace, name), + urls.api.k8s.operators.clusterServiceVersion(namespace, name), ); return data; } @@ -52,7 +52,7 @@ export class Operators { body: object, ) { const { data } = await axiosWithAuth.post( - urls.api.operators.resources(namespace, apiVersion, resource), + urls.api.k8s.operators.resources(namespace, apiVersion, resource), body, ); return data; @@ -60,7 +60,7 @@ export class Operators { public static async listResources(namespace: string, apiVersion: string, resource: string) { const { data } = await axiosWithAuth.get>( - urls.api.operators.resources(namespace, apiVersion, resource), + urls.api.k8s.operators.resources(namespace, apiVersion, resource), ); return data; } @@ -72,7 +72,7 @@ export class Operators { name: string, ) { const { data } = await axiosWithAuth.get( - urls.api.operators.resource(namespace, apiVersion, crd, name), + urls.api.k8s.operators.resource(namespace, apiVersion, crd, name), ); return data; } @@ -84,7 +84,7 @@ export class Operators { name: string, ) { const { data } = await axiosWithAuth.delete( - urls.api.operators.resource(namespace, apiVersion, plural, name), + urls.api.k8s.operators.resource(namespace, apiVersion, plural, name), ); return data; } @@ -97,7 +97,7 @@ export class Operators { body: object, ) { const { data } = await axiosWithAuth.put( - urls.api.operators.resource(namespace, apiVersion, resource, name), + urls.api.k8s.operators.resource(namespace, apiVersion, resource, name), body, ); return data; @@ -114,7 +114,7 @@ export class Operators { await this.createOperatorGroupIfNotExists(namespace); // Now create the subscription const { data: result } = await axiosWithAuth.post( - urls.api.operators.subscription(namespace, name), + urls.api.k8s.operators.subscription(namespace, name), { apiVersion: "operators.coreos.com/v1alpha1", kind: "Subscription", @@ -149,14 +149,14 @@ export class Operators { return; } const { data } = await axiosWithAuth.get>( - urls.api.operators.operatorGroups(namespace), + urls.api.k8s.operators.operatorGroups(namespace), ); if (data.items.length > 0) { // An operatorgroup already exists, do nothing return; } const { data: result } = await axiosWithAuth.post>( - urls.api.operators.operatorGroups(namespace), + urls.api.k8s.operators.operatorGroups(namespace), { apiVersion: "operators.coreos.com/v1", kind: "OperatorGroup", diff --git a/dashboard/src/shared/Secret.test.ts b/dashboard/src/shared/Secret.test.ts index f2d8d9ede81..cd766f4b1f3 100644 --- a/dashboard/src/shared/Secret.test.ts +++ b/dashboard/src/shared/Secret.test.ts @@ -1,40 +1,9 @@ import { axiosWithAuth } from "./AxiosInstance"; import Secret from "./Secret"; -it("creates a secret", async () => { - axiosWithAuth.post = jest.fn().mockReturnValue({ data: "ok" }); - const secrets = { - foo: "bar", - }; - const owner = { - foo: "bar", - } as any; - const name = "secret"; - const namespace = "default"; - expect(await Secret.create(name, secrets, owner, namespace)).toEqual("ok"); - expect(axiosWithAuth.post).toHaveBeenCalledWith( - "api/clusters/default/api/v1/namespaces/default/secrets", - { - apiVersion: "v1", - data: secrets, - kind: "Secret", - metadata: { name: "secret", ownerReferences: [owner] }, - type: "Opaque", - }, - ); -}); - -it("deletes a secret", async () => { - axiosWithAuth.delete = jest.fn(); - await Secret.delete("foo", "bar"); - expect(axiosWithAuth.delete).toHaveBeenCalledWith( - "api/clusters/default/api/v1/namespaces/bar/secrets/foo", - ); -}); - it("gets a secret", async () => { axiosWithAuth.get = jest.fn().mockReturnValue({ data: "ok" }); - await Secret.get("foo", "bar"); + await Secret.get("default", "foo", "bar"); expect(axiosWithAuth.get).toHaveBeenCalledWith( "api/clusters/default/api/v1/namespaces/bar/secrets/foo", ); @@ -42,7 +11,7 @@ it("gets a secret", async () => { it("lists secrets", async () => { axiosWithAuth.get = jest.fn().mockReturnValue({ data: "ok" }); - await Secret.list("foo"); + await Secret.list("default", "foo"); expect(axiosWithAuth.get).toHaveBeenCalledWith( "api/clusters/default/api/v1/namespaces/foo/secrets", ); @@ -56,7 +25,9 @@ it("creates a pull secret", async () => { const email = "foo@bar.com"; const server = "docker.io"; const namespace = "default"; - expect(await Secret.createPullSecret(name, user, password, email, server, namespace)).toBe("ok"); + expect( + await Secret.createPullSecret("default", name, user, password, email, server, namespace), + ).toBe("ok"); expect(axiosWithAuth.post).toHaveBeenCalledWith( "api/clusters/default/api/v1/namespaces/default/secrets", { diff --git a/dashboard/src/shared/Secret.ts b/dashboard/src/shared/Secret.ts index 95dd10b798d..00daa5b7c20 100644 --- a/dashboard/src/shared/Secret.ts +++ b/dashboard/src/shared/Secret.ts @@ -1,46 +1,27 @@ import { axiosWithAuth } from "./AxiosInstance"; -import { APIBase } from "./Kube"; -import { IK8sList, IOwnerReference, ISecret } from "./types"; +import { IK8sList, ISecret } from "./types"; +import * as url from "./url"; export default class Secret { - public static async create( - name: string, - secrets: { [s: string]: string }, - owner: IOwnerReference | undefined, - namespace: string, - ) { - const url = Secret.getLink(namespace); - const { data } = await axiosWithAuth.post(url, { - apiVersion: "v1", - data: secrets, - kind: "Secret", - metadata: { - name, - ownerReferences: [owner], - }, - type: "Opaque", - }); - return data; - } - public static async delete(name: string, namespace: string) { - const url = this.getLink(namespace, name); - return axiosWithAuth.delete(url); + const u = url.api.k8s.secret("default", namespace, name); + return axiosWithAuth.delete(u); } - public static async get(name: string, namespace: string) { - const url = this.getLink(namespace, name); - const { data } = await axiosWithAuth.get(url); + public static async get(cluster: string, name: string, namespace: string) { + const u = url.api.k8s.secret(cluster, namespace, name); + const { data } = await axiosWithAuth.get(u); return data; } - public static async list(namespace: string) { - const url = Secret.getLink(namespace); - const { data } = await axiosWithAuth.get>(url); + public static async list(cluster: string, namespace: string) { + const u = url.api.k8s.secrets(cluster, namespace); + const { data } = await axiosWithAuth.get>(u); return data; } public static async createPullSecret( + cluster: string, name: string, user: string, password: string, @@ -48,7 +29,7 @@ export default class Secret { server: string, namespace: string, ) { - const url = Secret.getLink(namespace); + const u = url.api.k8s.secrets(cluster, namespace); const dockercfg = { auths: { [server]: { @@ -59,7 +40,7 @@ export default class Secret { }, }, }; - const { data } = await axiosWithAuth.post(url, { + const { data } = await axiosWithAuth.post(u, { apiVersion: "v1", stringData: { ".dockerconfigjson": JSON.stringify(dockercfg), @@ -72,8 +53,4 @@ export default class Secret { }); return data; } - - private static getLink(namespace: string, name?: string): string { - return `${APIBase}/api/v1/namespaces/${namespace}/secrets${name ? `/${name}` : ""}`; - } } diff --git a/dashboard/src/shared/ServiceBinding.ts b/dashboard/src/shared/ServiceBinding.ts index 766da2c611a..5a032b02b77 100644 --- a/dashboard/src/shared/ServiceBinding.ts +++ b/dashboard/src/shared/ServiceBinding.ts @@ -1,6 +1,7 @@ import { axiosWithAuth } from "./AxiosInstance"; import { APIBase } from "./Kube"; import { ICondition, ServiceCatalog } from "./ServiceCatalog"; +import * as url from "./url"; interface IK8sApiSecretResponse { kind: string; @@ -54,8 +55,8 @@ export class ServiceBinding { namespace: string, parameters: {}, ) { - const url = ServiceBinding.getLink(namespace); - const { data } = await axiosWithAuth.post(url, { + const u = ServiceBinding.getLink(namespace); + const { data } = await axiosWithAuth.post(u, { metadata: { name: bindingName, }, @@ -70,13 +71,13 @@ export class ServiceBinding { } public static async delete(name: string, namespace: string) { - const url = this.getLink(namespace, name); - return axiosWithAuth.delete(url); + const u = this.getLink(namespace, name); + return axiosWithAuth.delete(u); } public static async get(namespace: string, name: string) { - const url = this.getLink(namespace, name); - const { data } = await axiosWithAuth.get(url); + const u = this.getLink(namespace, name); + const { data } = await axiosWithAuth.get(u); return data; } @@ -88,7 +89,7 @@ export class ServiceBinding { const { secretName } = binding.spec; const ns = binding.metadata.namespace; return axiosWithAuth - .get(this.secretEndpoint(ns) + secretName) + .get(url.api.k8s.secret("default", ns, secretName)) .then(response => { return { binding, secret: response.data }; }) @@ -105,8 +106,4 @@ export class ServiceBinding { namespace ? `/namespaces/${namespace}` : "" }/servicebindings${name ? `/${name}` : ""}`; } - - private static secretEndpoint(namespace: string): string { - return `${APIBase}/api/v1/namespaces/${namespace}/secrets/`; - } } diff --git a/dashboard/src/shared/ServiceCatalog.ts b/dashboard/src/shared/ServiceCatalog.ts index ec02bc02154..b44bc27ddfc 100644 --- a/dashboard/src/shared/ServiceCatalog.ts +++ b/dashboard/src/shared/ServiceCatalog.ts @@ -26,7 +26,7 @@ export class ServiceCatalog { public static async syncBroker(broker: IServiceBroker) { const { data } = await axiosWithAuth.patch( - urls.api.clusterservicebrokers.sync(broker), + urls.api.k8s.clusterservicebrokers.sync(broker), { spec: { relistRequests: broker.spec.relistRequests + 1, diff --git a/dashboard/src/shared/url.ts b/dashboard/src/shared/url.ts index 3ed0ec60c12..39951bf6221 100644 --- a/dashboard/src/shared/url.ts +++ b/dashboard/src/shared/url.ts @@ -1,4 +1,3 @@ -import { APIBase } from "./Kube"; import { IServiceBroker } from "./ServiceCatalog"; import { IChartVersion, IRepo } from "./types"; @@ -81,12 +80,6 @@ export const kubeops = { }; export const api = { - apprepostories: { - base: `${APIBase}/apis/kubeapps.com/v1alpha1`, - create: (namespace: string) => - `${api.apprepostories.base}/namespaces/${namespace}/apprepositories`, - }, - charts: { base: "api/assetsvc/v1", get: (namespace: string, id: string) => `${api.charts.base}/ns/${namespace}/charts/${id}`, @@ -109,36 +102,60 @@ export const api = { listVersions: (namespace: string, id: string) => `${api.charts.get(namespace, id)}/versions`, }, - serviceinstances: { - base: `${APIBase}/apis/servicecatalog.k8s.io/v1beta1`, - create: (namespace: string) => - `${api.serviceinstances.base}/namespaces/${namespace}/serviceinstances`, - }, - - clusterservicebrokers: { - base: `${APIBase}/apis/servicecatalog.k8s.io/v1beta1`, - sync: (broker: IServiceBroker) => - `${api.clusterservicebrokers.base}/clusterservicebrokers/${broker.metadata.name}`, + // URLs which are accessing the k8s API server directly are grouped together + // so we can clearly differentiate and possibly begin to remove. + // Note that this list is not yet exhaustive (search for APIBase to find other call-sites which + // access the k8s api server directly). + k8s: { + base: (cluster: string) => `api/clusters/${cluster}`, + namespaces: (cluster: string) => `${api.k8s.base(cluster)}/api/v1/namespaces`, + namespace: (cluster: string, namespace: string) => + `${api.k8s.namespaces(cluster)}/${namespace}`, + // clusterservicebrokers and operators operate on the default cluster only, currently. + clusterservicebrokers: { + sync: (broker: IServiceBroker) => + `${api.k8s.base("default")}/apis/servicecatalog.k8s.io/v1beta1/clusterservicebrokers/${ + broker.metadata.name + }`, + }, + operators: { + operators: (namespace: string) => + `${api.k8s.base("default")}/apis/packages.operators.coreos.com/v1/${withNS( + namespace, + )}packagemanifests`, + operator: (namespace: string, name: string) => + `${api.k8s.base( + "default", + )}/apis/packages.operators.coreos.com/v1/namespaces/${namespace}/packagemanifests/${name}`, + clusterServiceVersions: (namespace: string) => + `${api.k8s.base("default")}/apis/operators.coreos.com/v1alpha1/${withNS( + namespace, + )}clusterserviceversions`, + clusterServiceVersion: (namespace: string, name: string) => + `${api.k8s.base( + "default", + )}/apis/operators.coreos.com/v1alpha1/namespaces/${namespace}/clusterserviceversions/${name}`, + resources: (namespace: string, apiVersion: string, resource: string) => + `${api.k8s.base("default")}/apis/${apiVersion}/${withNS(namespace)}${resource}`, + resource: (namespace: string, apiVersion: string, resource: string, name: string) => + `${api.k8s.base("default")}/apis/${apiVersion}/namespaces/${namespace}/${resource}/${name}`, + operatorGroups: (namespace: string) => + `${api.k8s.base( + "default", + )}/apis/operators.coreos.com/v1/namespaces/${namespace}/operatorgroups`, + subscription: (namespace: string, name: string) => + `${api.k8s.base( + "default", + )}/apis/operators.coreos.com/v1alpha1/namespaces/${namespace}/subscriptions/${name}`, + }, + secrets: (cluster: string, namespace: string) => + `${api.k8s.namespace(cluster, namespace)}/secrets`, + secret: (cluster: string, namespace: string, name: string) => + `${api.k8s.secrets(cluster, namespace)}/${name}`, }, operators: { - operators: (namespace: string) => - `${APIBase}/apis/packages.operators.coreos.com/v1/${withNS(namespace)}packagemanifests`, - operator: (namespace: string, name: string) => - `${APIBase}/apis/packages.operators.coreos.com/v1/namespaces/${namespace}/packagemanifests/${name}`, - clusterServiceVersions: (namespace: string) => - `${APIBase}/apis/operators.coreos.com/v1alpha1/${withNS(namespace)}clusterserviceversions`, - clusterServiceVersion: (namespace: string, name: string) => - `${APIBase}/apis/operators.coreos.com/v1alpha1/namespaces/${namespace}/clusterserviceversions/${name}`, operatorIcon: (namespace: string, name: string) => `api/v1/namespaces/${namespace}/operator/${name}/logo`, - resources: (namespace: string, apiVersion: string, resource: string) => - `${APIBase}/apis/${apiVersion}/${withNS(namespace)}${resource}`, - resource: (namespace: string, apiVersion: string, resource: string, name: string) => - `${APIBase}/apis/${apiVersion}/namespaces/${namespace}/${resource}/${name}`, - operatorGroups: (namespace: string) => - `${APIBase}/apis/operators.coreos.com/v1/namespaces/${namespace}/operatorgroups`, - subscription: (namespace: string, name: string) => - `${APIBase}/apis/operators.coreos.com/v1alpha1/namespaces/${namespace}/subscriptions/${name}`, }, };