From 6a661afc10ecb5784cea69fc75ac14c47502ac13 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Antonio=20G=C3=A1mez=2C=20PhD?=
Date: Mon, 22 Aug 2022 15:26:39 +0200
Subject: [PATCH 1/8] Fix olm test case (#5227)
* update olm version
Signed-off-by: Antonio Gamez Diaz
* make selector more specific
Signed-off-by: Antonio Gamez Diaz
* Filter by provider to ensure which operator to install
Signed-off-by: Antonio Gamez Diaz
Signed-off-by: Antonio Gamez Diaz
---
.circleci/config.yml | 2 +-
integration/tests/operators/06-operator-deployment.spec.js | 5 ++++-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 42977ac04c6..f21c8209843 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -26,7 +26,7 @@ parameters:
default: "v3.9.2"
OLM_VERSION:
type: "string"
- default: "v0.21.2"
+ default: "v0.22.0"
CHARTMUSEUM_VERSION:
type: "string"
default: "3.9.0"
diff --git a/integration/tests/operators/06-operator-deployment.spec.js b/integration/tests/operators/06-operator-deployment.spec.js
index bbe1c8c984f..6f874c6e188 100644
--- a/integration/tests/operators/06-operator-deployment.spec.js
+++ b/integration/tests/operators/06-operator-deployment.spec.js
@@ -14,11 +14,14 @@ test("Deploys an Operator", async ({ page }) => {
// Go to operators page
await page.goto(utils.getUrl("/#/c/default/ns/kubeapps/operators"));
- await page.waitForTimeout(10000);
+ await page.waitForFunction('document.querySelector("cds-progress-circle") === null');
// Select operator to deploy
await page.locator("input#search").fill("prometheus");
await page.waitForTimeout(3000);
+ // using locator with "has" instead of "hasText" to search by this exact name (and exclude others like "Red Hat, Inc.")
+ await page.locator("cds-checkbox", { has: page.locator('text="Red Hat"') }).click();
+
await page.click('a:has-text("prometheus")');
await page.click('cds-button:has-text("Deploy") >> nth=0');
await page.click('cds-button:has-text("Deploy")');
From 414ff3430df3d3178f57340bebef531f26e36765 Mon Sep 17 00:00:00 2001
From: Pepe Baena
Date: Mon, 22 Aug 2022 16:28:33 +0200
Subject: [PATCH 2/8] Update Getting started tutorial to comply documentation
style guides (#5223)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Update Getting started tutorial to comply documentation style guides
* Update prerequisites in getting started tutorial
Co-authored-by: Antonio Gámez, PhD
Co-authored-by: Antonio Gámez, PhD
Co-authored-by: Antonio Gámez, PhD
---
.../docs/latest/tutorials/getting-started.md | 29 ++++++++++---------
1 file changed, 16 insertions(+), 13 deletions(-)
diff --git a/site/content/docs/latest/tutorials/getting-started.md b/site/content/docs/latest/tutorials/getting-started.md
index 3a7ff2c9dd8..400aec3d29d 100644
--- a/site/content/docs/latest/tutorials/getting-started.md
+++ b/site/content/docs/latest/tutorials/getting-started.md
@@ -4,13 +4,17 @@ This guide will walk you through the process of deploying Kubeapps for your clus
## Prerequisites
-Kubeapps assumes a working Kubernetes cluster (v1.15+), as well as the [`helm`](https://helm.sh/docs/intro/install/) (3.1.0+) and [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/) command-line interfaces installed and configured to talk to your Kubernetes cluster. Kubeapps has been tested with Azure Kubernetes Service (AKS), Google Kubernetes Engine (GKE), `minikube` and Docker for Desktop Kubernetes. Kubeapps works on RBAC-enabled clusters and this configuration is encouraged for a more secure install.
+- Kubeapps assumes a working Kubernetes cluster (v1.21+), as well as the [`helm`](https://helm.sh/docs/intro/install/) (v3.2.0+) and [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/) command-line interfaces installed and configured to talk to your Kubernetes cluster.
-> On GKE, you must either be an "Owner" or have the "Container Engine Admin" role in order to install Kubeapps.
+- Kubeapps has been tested with Azure Kubernetes Service (AKS), Google Kubernetes Engine (GKE), kind, minikube and Docker for Desktop Kubernetes.
+
+- Kubeapps works on RBAC-enabled clusters and this configuration is encouraged for a more secure install.
+
+ > On GKE, you must either be an "Owner" or have the "Container Engine Admin" role in order to install Kubeapps.
## Step 1: Install Kubeapps
-Use the Helm chart to install the latest version of Kubeapps:
+Use the official [Bitnami Kubeapps chart](https://github.com/bitnami/charts/tree/master/bitnami/kubeapps) to install the latest version of Kubeapps:
```bash
helm repo add bitnami https://charts.bitnami.com/bitnami
@@ -95,23 +99,23 @@ Paste the token generated in the previous step to authenticate and access the Ku
**_Note:_** If you are setting up Kubeapps for other people to access, you will want to use a different service type or setup Ingress rather than using the above `kubectl port-forward`. For detailed information on installing, configuring and upgrading Kubeapps, checkout the [chart README](https://github.com/vmware-tanzu/kubeapps/blob/main/chart/kubeapps/README.md)
-## Step 4: Deploy WordPress
+## Step 4: Deploy applications: WordPress
Once you have the Kubeapps Dashboard up and running, you can start deploying applications into your cluster.
-- Use the "Deploy" button or click on the "Catalog" page in the Dashboard to select an application from the list of charts in any of the configured Helm chart repositories. This example assumes you want to deploy WordPress.
+- Use the **Deploy** button or click on the **Catalog** page in the Dashboard to select an application from the list of packages in any of the configured repositories. This example assumes you want to deploy WordPress.
- ![WordPress chart](../img/wordpress-search.png)
+ ![WordPress search](../img/wordpress-search.png)
- Click the "Deploy" button.
![WordPress chart](../img/wordpress-chart.png)
-- You will be prompted for the release name and values for the application. The form is populated by the values (YAML), which you can see in the adjacent tab.
+- You will be prompted for the release name and values for the application. The form is populated by the values (**YAML**), which you can see in the adjacent tab.
![WordPress installation](../img/wordpress-installation.png)
-- Click the "Deploy" button. The application will be deployed. You will be able to track the new Helm deployment directly from the browser. The status will be shown at the top, including the access URL and any secret included with the app. You can also look at the individual resources lower in the page. It will also show the number of ready pods. If you run your cursor over the status, you can see the workloads and number of ready and total pods within them.
+- Click the **Deploy** button. The application will be deployed. You will be able to track the new deployment directly from the browser. The status will be shown at the top, including the `access URL` and any `secret` included with the app. You can also look at the individual resources lower in the page. It will also show the number of ready pods. If you run your cursor over the **status**, you can see the workloads and number of ready and total pods within them.
![WordPress deployment](../img/wordpress-deployment.png)
@@ -121,7 +125,7 @@ To access your new WordPress site, you can run the commands in the "Notes" secti
![WordPress deployment notes](../img/wordpress-url.png)
-To get the credentials for logging into your WordPress account, refer to the "Notes" section. You can also get the WordPress password by clicking on the eye next to `wordpress-password`.
+To get the credentials for logging into your WordPress account, refer to the **Notes** section. You can also get the WordPress password by clicking on the eye next to **wordpress-password**.
![WordPress deployment notes](../img/wordpress-credentials.png)
@@ -135,8 +139,7 @@ If you want to uninstall/delete your WordPress application, you can do so by cli
Learn more about Kubeapps with the links below:
-- [Detailed installation instructions](https://github.com/vmware-tanzu/kubeapps/blob/main/chart/kubeapps/README.md)
-- [Deploying Operators](./operators.md)
-- [Kubeapps Dashboard documentation](../howto/dashboard.md)
-- [Project board](https://github.com/orgs/vmware-tanzu/projects/38/views/2)
+- [Kubeapps documentation](https://github.com/vmware-tanzu/kubeapps/tree/main/docs)
+- [Kubeapps website](https://kubeapps.dev/)
- [Roadmap](https://github.com/vmware-tanzu/kubeapps/milestones)
+- [Project board](https://github.com/orgs/vmware-tanzu/projects/38/views/2)
From 942e4d6a81ded59d03c4bfbf4f3568e0ebe0ff3f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Antonio=20G=C3=A1mez=2C=20PhD?=
Date: Mon, 22 Aug 2022 16:52:02 +0200
Subject: [PATCH 3/8] Changes to support OCI with flux in the UI (#5218)
* Rename to setSyncInterval to to avoid name collisions
Signed-off-by: Antonio Gamez Diaz
* Set flux repos as global always
Signed-off-by: Antonio Gamez Diaz
* Avoid re-selecting storage type if one already selected
Signed-off-by: Antonio Gamez Diaz
* Enable oci type selector for flux
Signed-off-by: Antonio Gamez Diaz
* Don't override scheme if non-helm plugins
Signed-off-by: Antonio Gamez Diaz
* Add test cases
Signed-off-by: Antonio Gamez Diaz
* handle "always global flux repos" in the isGlobal fn
Signed-off-by: Antonio Gamez Diaz
* fix test case
Signed-off-by: Antonio Gamez Diaz
* handle global carvel repos with `carvelGlobalNamespace`
Signed-off-by: Antonio Gamez Diaz
Signed-off-by: Antonio Gamez Diaz
---
dashboard/src/actions/repos.test.tsx | 63 ++++++++++++--
dashboard/src/actions/repos.ts | 8 +-
dashboard/src/components/Catalog/Catalog.tsx | 16 +++-
.../Config/PkgRepoList/PkgRepoForm.tsx | 84 ++++++++++---------
.../Config/PkgRepoList/PkgRepoList.tsx | 10 ++-
.../SelectRepoForm/SelectRepoForm.tsx | 17 +++-
dashboard/src/shared/utils.test.ts | 4 +-
dashboard/src/shared/utils.ts | 4 +-
8 files changed, 146 insertions(+), 60 deletions(-)
diff --git a/dashboard/src/actions/repos.test.tsx b/dashboard/src/actions/repos.test.tsx
index 49146ca1fac..e383a8667b6 100644
--- a/dashboard/src/actions/repos.test.tsx
+++ b/dashboard/src/actions/repos.test.tsx
@@ -23,7 +23,9 @@ import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import { PackageRepositoriesService } from "shared/PackageRepositoriesService";
import PackagesService from "shared/PackagesService";
-import { IPkgRepoFormData, NotFoundError, RepositoryStorageTypes } from "shared/types";
+import { initialState } from "shared/specs/mountWrapper";
+import { IPkgRepoFormData, IStoreState, NotFoundError, RepositoryStorageTypes } from "shared/types";
+import { PluginNames } from "shared/utils";
import { getType } from "typesafe-actions";
import actions from ".";
import { convertPkgRepoDetailToSummary } from "./repos";
@@ -32,7 +34,10 @@ const { repos: repoActions } = actions;
const mockStore = configureMockStore([thunk]);
let store: any;
-const plugin = { name: "my.plugin", version: "0.0.1" } as Plugin;
+const plugin = { name: PluginNames.PACKAGES_HELM, version: "0.0.1" } as Plugin;
+const fluxPlugin = { name: PluginNames.PACKAGES_FLUX, version: "v1beta1" } as Plugin;
+const carvelPlugin = { name: PluginNames.PACKAGES_KAPP, version: "v1beta1" } as Plugin;
+
const packageRepoRef = {
identifier: "repo-abc",
context: { cluster: "default", namespace: "default" },
@@ -64,19 +69,29 @@ const packageRepositoryDetail = {
const kubeappsNamespace = "kubeapps-namespace";
const globalReposNamespace = "kubeapps-repos-global";
+const carvelGlobalNamespace = "carvel-repos-global";
beforeEach(() => {
store = mockStore({
- config: { kubeappsNamespace, globalReposNamespace },
+ config: {
+ ...initialState.config,
+ kubeappsNamespace,
+ globalReposNamespace,
+ carvelGlobalNamespace,
+ },
clusters: {
+ ...initialState.clusters,
currentCluster: "default",
clusters: {
+ ...initialState.clusters.clusters,
default: {
+ ...initialState.clusters.clusters[initialState.clusters.currentCluster],
currentNamespace: kubeappsNamespace,
},
},
},
- });
+ } as Partial);
+
PackageRepositoriesService.getPackageRepositorySummaries = jest
.fn()
.mockImplementationOnce(() => {
@@ -314,7 +329,7 @@ describe("fetchRepoSummaries", () => {
},
{
type: getType(repoActions.requestRepoSummaries),
- payload: globalReposNamespace,
+ payload: "",
},
{
type: getType(repoActions.receiveRepoSummaries),
@@ -352,7 +367,7 @@ describe("fetchRepoSummaries", () => {
},
{
type: getType(repoActions.requestRepoSummaries),
- payload: globalReposNamespace,
+ payload: "",
},
{
type: getType(repoActions.receiveRepoSummaries),
@@ -489,6 +504,42 @@ describe("addRepo", () => {
const res = await store.dispatch(addRepoCMDAuth);
expect(res).toBe(true);
});
+
+ it("sets flux repos as global", async () => {
+ await store.dispatch(
+ repoActions.addRepo("my-namespace", {
+ ...pkgRepoFormData,
+ plugin: fluxPlugin as Plugin,
+ }),
+ );
+ expect(PackageRepositoriesService.addPackageRepository).toHaveBeenCalledWith(
+ "default",
+ "my-namespace",
+ {
+ ...pkgRepoFormData,
+ plugin: fluxPlugin,
+ },
+ false,
+ );
+ });
+
+ it("sets carvel repos as global if using the carvelGlobalNamespace", async () => {
+ await store.dispatch(
+ repoActions.addRepo(carvelGlobalNamespace, {
+ ...pkgRepoFormData,
+ plugin: carvelPlugin as Plugin,
+ }),
+ );
+ expect(PackageRepositoriesService.addPackageRepository).toHaveBeenCalledWith(
+ "default",
+ carvelGlobalNamespace,
+ {
+ ...pkgRepoFormData,
+ plugin: carvelPlugin,
+ },
+ false,
+ );
+ });
});
context("when authHeader and customCA are empty", () => {
diff --git a/dashboard/src/actions/repos.ts b/dashboard/src/actions/repos.ts
index d9693228274..5b82a13b92b 100644
--- a/dashboard/src/actions/repos.ts
+++ b/dashboard/src/actions/repos.ts
@@ -69,7 +69,7 @@ export const fetchRepoSummaries = (
return async (dispatch, getState) => {
const {
clusters: { currentCluster },
- config: { globalReposNamespace },
+ config: { globalReposNamespace, carvelGlobalNamespace },
} = getState();
try {
dispatch(requestRepoSummaries(namespace));
@@ -77,12 +77,14 @@ export const fetchRepoSummaries = (
cluster: currentCluster,
namespace: namespace,
});
- if (!listGlobal || namespace === globalReposNamespace) {
+ if (!listGlobal || [globalReposNamespace, carvelGlobalNamespace].includes(namespace)) {
dispatch(receiveRepoSummaries(repos.packageRepositorySummaries));
} else {
// Global repos need to be added
+ // instead of passing each global repo's namespace, we defer the decision to the backend using ns=""
+ // however, this can cause issues when using unprivileged users, see #5215
let totalRepos = repos.packageRepositorySummaries;
- dispatch(requestRepoSummaries(globalReposNamespace));
+ dispatch(requestRepoSummaries(""));
const globalRepos = await PackageRepositoriesService.getPackageRepositorySummaries({
cluster: currentCluster,
namespace: "",
diff --git a/dashboard/src/components/Catalog/Catalog.tsx b/dashboard/src/components/Catalog/Catalog.tsx
index 88450b516b3..1bea2ff24ee 100644
--- a/dashboard/src/components/Catalog/Catalog.tsx
+++ b/dashboard/src/components/Catalog/Catalog.tsx
@@ -88,7 +88,13 @@ export default function Catalog() {
},
operators,
repos: { reposSummaries: repos },
- config: { appVersion, kubeappsCluster, globalReposNamespace, featureFlags },
+ config: {
+ appVersion,
+ kubeappsCluster,
+ globalReposNamespace,
+ carvelGlobalNamespace,
+ featureFlags,
+ },
} = useSelector((state: IStoreState) => state);
const { cluster, namespace } = ReactRouter.useParams() as IRouteParams;
const location = ReactRouter.useLocation();
@@ -240,7 +246,11 @@ export default function Catalog() {
// We do not currently support package repositories on additional clusters.
const supportedCluster = cluster === kubeappsCluster;
useEffect(() => {
- if (!namespace || !supportedCluster || namespace === globalReposNamespace) {
+ if (
+ !namespace ||
+ !supportedCluster ||
+ [globalReposNamespace, carvelGlobalNamespace].includes(namespace)
+ ) {
// All Namespaces. Global namespace or other cluster, show global repos only
dispatch(actions.repos.fetchRepoSummaries(""));
return () => {};
@@ -248,7 +258,7 @@ export default function Catalog() {
// In other case, fetch global and namespace repos
dispatch(actions.repos.fetchRepoSummaries(namespace, true));
return () => {};
- }, [dispatch, supportedCluster, namespace, globalReposNamespace]);
+ }, [dispatch, supportedCluster, namespace, globalReposNamespace, carvelGlobalNamespace]);
useEffect(() => {
// Ignore operators if specified
diff --git a/dashboard/src/components/Config/PkgRepoList/PkgRepoForm.tsx b/dashboard/src/components/Config/PkgRepoList/PkgRepoForm.tsx
index 9a5c9fcafad..b400b13280f 100644
--- a/dashboard/src/components/Config/PkgRepoList/PkgRepoForm.tsx
+++ b/dashboard/src/components/Config/PkgRepoList/PkgRepoForm.tsx
@@ -150,7 +150,7 @@ export function PkgRepoForm(props: IPkgRepoFormProps) {
const [filterRegex, setFilterRegex] = useState(false);
// -- Advanced variables --
- const [interval, setInterval] = useState(initialInterval);
+ const [syncInterval, setSyncInterval] = useState(initialInterval);
const [performValidation, setPerformValidation] = useState(true);
const [customCA, setCustomCA] = useState("");
const [skipTLS, setSkipTLS] = useState(!!repo?.tlsConfig?.insecureSkipVerify);
@@ -186,7 +186,7 @@ export function PkgRepoForm(props: IPkgRepoFormProps) {
setDescription(repo.description);
setSkipTLS(!!repo.tlsConfig?.insecureSkipVerify);
setPassCredentials(!!repo.auth?.passCredentials);
- setInterval(repo.interval);
+ setSyncInterval(repo.interval);
setCustomCA(repo.tlsConfig?.certAuthority || "");
setAuthCustomHeader(repo.auth?.header || "");
setBearerToken(repo.auth?.header || "");
@@ -278,10 +278,10 @@ export function PkgRepoForm(props: IPkgRepoFormProps) {
? ociRepositories?.split(",").map(r => r.trim())
: [];
- // If the scheme is not specified, assume HTTPS. This is common for OCI registries
- // unless using the kapp plugin, which explicitly should not include https:// protocol prefix
+ // In the Helm plugin, if the scheme is not specified, assume HTTPS (also for OCI registries)
+ // Other plugins don't allow passing a scheme (eg. carvel) and others require a different one (eg. flux: oci://)
let finalURL = url;
- if (plugin?.name !== PluginNames.PACKAGES_KAPP && !url?.startsWith("http")) {
+ if (plugin?.name === PluginNames.PACKAGES_HELM && !url?.startsWith("http")) {
finalURL = `https://${url}`;
}
@@ -329,7 +329,7 @@ export function PkgRepoForm(props: IPkgRepoFormProps) {
password: !isUserManagedSecret ? secretPassword : "",
server: !isUserManagedSecret ? secretServer : "",
} as DockerCredentials,
- interval,
+ interval: syncInterval,
name,
passCredentials,
plugin,
@@ -378,7 +378,7 @@ export function PkgRepoForm(props: IPkgRepoFormProps) {
setDescription(e.target.value);
};
const handleIntervalChange = (e: React.ChangeEvent) => {
- setInterval(e.target.value);
+ setSyncInterval(e.target.value);
};
const handlePerformValidationChange = (_e: React.ChangeEvent) => {
setPerformValidation(!performValidation);
@@ -431,19 +431,27 @@ export function PkgRepoForm(props: IPkgRepoFormProps) {
setPlugin(getPluginByName(e.target.value));
// set some default values based on the selected plugin
switch (getPluginByName(e.target.value)?.name) {
- case PluginNames.PACKAGES_HELM:
- setType(RepositoryStorageTypes.PACKAGE_REPOSITORY_STORAGE_HELM);
+ case PluginNames.PACKAGES_HELM: {
+ if (!type) {
+ setType(RepositoryStorageTypes.PACKAGE_REPOSITORY_STORAGE_HELM);
+ }
// helm plugin doesn't allow interval
// eslint-disable-next-line no-implied-eval
- setInterval("");
+ setSyncInterval("");
break;
- case PluginNames.PACKAGES_FLUX:
- setType(RepositoryStorageTypes.PACKAGE_REPOSITORY_STORAGE_HELM);
- setInterval(interval || initialInterval);
+ }
+ case PluginNames.PACKAGES_FLUX: {
+ if (!type) {
+ setType(RepositoryStorageTypes.PACKAGE_REPOSITORY_STORAGE_HELM);
+ }
+ setSyncInterval(syncInterval || initialInterval);
break;
+ }
case PluginNames.PACKAGES_KAPP:
- setType(RepositoryStorageTypes.PACKAGE_REPOSITORY_STORAGE_CARVEL_IMGPKGBUNDLE);
- setInterval(interval || initialInterval);
+ if (!type) {
+ setType(RepositoryStorageTypes.PACKAGE_REPOSITORY_STORAGE_CARVEL_IMGPKGBUNDLE);
+ }
+ setSyncInterval(syncInterval || initialInterval);
break;
}
};
@@ -709,8 +717,7 @@ export function PkgRepoForm(props: IPkgRepoFormProps) {
id="kubeapps-repo-type-oci"
type="radio"
name="type"
- // TODO(agamez): workaround until Flux plugin also supports OCI artifacts
- disabled={plugin?.name === PluginNames.PACKAGES_FLUX || !!repo?.type}
+ disabled={!!repo?.type}
value={RepositoryStorageTypes.PACKAGE_REPOSITORY_STORAGE_OCI || ""}
checked={type === RepositoryStorageTypes.PACKAGE_REPOSITORY_STORAGE_OCI}
onChange={handleTypeRadioButtonChange}
@@ -1656,35 +1663,34 @@ export function PkgRepoForm(props: IPkgRepoFormProps) {
id="panel-filtering"
expanded={accordion[2]}
hidden={
- type !== RepositoryStorageTypes.PACKAGE_REPOSITORY_STORAGE_OCI &&
+ type !== RepositoryStorageTypes.PACKAGE_REPOSITORY_STORAGE_OCI ||
plugin?.name !== PluginNames.PACKAGES_HELM
}
>
toggleAccordion(2)}>Filtering
- {type === RepositoryStorageTypes.PACKAGE_REPOSITORY_STORAGE_OCI && (
-
-
- List of Repositories (required)
-
-
- Include a list of comma-separated OCI repositories that will be available in
- Kubeapps.
-
-
-
- )}
- {/* TODO(agamez): workaround until Flux plugin also supports OCI artifacts */}
{plugin?.name === PluginNames.PACKAGES_HELM && (
<>
+ {type === RepositoryStorageTypes.PACKAGE_REPOSITORY_STORAGE_OCI && (
+
+
+ List of Repositories (required)
+
+
+ Include a list of comma-separated OCI repositories that will be available
+ in Kubeapps.
+
+
+
+ )}
Filter Applications (optional)
@@ -1741,7 +1747,7 @@ export function PkgRepoForm(props: IPkgRepoFormProps) {
id="kubeapps-repo-interval"
type="text"
placeholder={initialInterval}
- value={interval || ""}
+ value={syncInterval || ""}
onChange={handleIntervalChange}
/>
diff --git a/dashboard/src/components/Config/PkgRepoList/PkgRepoList.tsx b/dashboard/src/components/Config/PkgRepoList/PkgRepoList.tsx
index 90a3c09e58a..dd9b9cfde7b 100644
--- a/dashboard/src/components/Config/PkgRepoList/PkgRepoList.tsx
+++ b/dashboard/src/components/Config/PkgRepoList/PkgRepoList.tsx
@@ -49,7 +49,11 @@ function PkgRepoList() {
// so calling several times to refetchRepos would run the code inside, even
// if the dependencies do not change.
const refetchRepos: () => void = useCallback(() => {
- if (!namespace || !supportedCluster || namespace === globalReposNamespace) {
+ if (
+ !namespace ||
+ !supportedCluster ||
+ [globalReposNamespace, carvelGlobalNamespace].includes(namespace)
+ ) {
// All Namespaces. Global namespace or other cluster, show global repos only
dispatch(actions.repos.fetchRepoSummaries(""));
return () => {};
@@ -57,7 +61,7 @@ function PkgRepoList() {
// In other case, fetch global and namespace repos
dispatch(actions.repos.fetchRepoSummaries(namespace, true));
return () => {};
- }, [dispatch, supportedCluster, namespace, globalReposNamespace]);
+ }, [dispatch, supportedCluster, namespace, globalReposNamespace, carvelGlobalNamespace]);
useEffect(() => {
refetchRepos();
@@ -232,7 +236,7 @@ function PkgRepoList() {
Repository" button to create one.
)}
- {namespace !== globalReposNamespace && (
+ {![globalReposNamespace, carvelGlobalNamespace].includes(namespace) && (
<>
Namespaced Repositories: {namespace}
diff --git a/dashboard/src/components/SelectRepoForm/SelectRepoForm.tsx b/dashboard/src/components/SelectRepoForm/SelectRepoForm.tsx
index 019d0d1a3d7..f0cb5c22dae 100644
--- a/dashboard/src/components/SelectRepoForm/SelectRepoForm.tsx
+++ b/dashboard/src/components/SelectRepoForm/SelectRepoForm.tsx
@@ -31,7 +31,7 @@ function SelectRepoForm({ cluster, namespace, app }: ISelectRepoFormProps) {
packages: {
selected: { error: packageError },
},
- config: { kubeappsNamespace, kubeappsCluster, globalReposNamespace },
+ config: { kubeappsNamespace, kubeappsCluster, globalReposNamespace, carvelGlobalNamespace },
} = useSelector((state: IStoreState) => state);
const [userRepoName, setUserRepoName] = useState(repo?.name ?? "");
@@ -41,7 +41,11 @@ function SelectRepoForm({ cluster, namespace, app }: ISelectRepoFormProps) {
// We do not currently support package repositories on additional clusters.
const supportedCluster = cluster === kubeappsCluster;
useEffect(() => {
- if (!namespace || !supportedCluster || namespace === globalReposNamespace) {
+ if (
+ !namespace ||
+ !supportedCluster ||
+ [globalReposNamespace, carvelGlobalNamespace].includes(namespace)
+ ) {
// All Namespaces. Global namespace or other cluster, show global repos only
dispatch(actions.repos.fetchRepoSummaries(""));
return () => {};
@@ -49,7 +53,14 @@ function SelectRepoForm({ cluster, namespace, app }: ISelectRepoFormProps) {
// In other case, fetch global and namespace repos
dispatch(actions.repos.fetchRepoSummaries(namespace, true));
return () => {};
- }, [dispatch, namespace, kubeappsNamespace, globalReposNamespace, supportedCluster]);
+ }, [
+ dispatch,
+ namespace,
+ kubeappsNamespace,
+ globalReposNamespace,
+ carvelGlobalNamespace,
+ supportedCluster,
+ ]);
const handleRepoNameChange = (e: React.ChangeEvent) => {
if (e.target.value) {
diff --git a/dashboard/src/shared/utils.test.ts b/dashboard/src/shared/utils.test.ts
index 294e35acd72..b2668968261 100644
--- a/dashboard/src/shared/utils.test.ts
+++ b/dashboard/src/shared/utils.test.ts
@@ -193,8 +193,8 @@ it("isGlobalNamespace", () => {
} as IConfig;
expect(isGlobalNamespace("helm-global", PluginNames.PACKAGES_HELM, kubeappsConfig)).toBe(true);
expect(isGlobalNamespace("helm-global", PluginNames.PACKAGES_KAPP, kubeappsConfig)).toBe(false);
- expect(isGlobalNamespace("helm-global", PluginNames.PACKAGES_FLUX, kubeappsConfig)).toBe(false);
+ expect(isGlobalNamespace("helm-global", PluginNames.PACKAGES_FLUX, kubeappsConfig)).toBe(true);
expect(isGlobalNamespace("carvel-global", PluginNames.PACKAGES_HELM, kubeappsConfig)).toBe(false);
expect(isGlobalNamespace("carvel-global", PluginNames.PACKAGES_KAPP, kubeappsConfig)).toBe(true);
- expect(isGlobalNamespace("carvel-global", PluginNames.PACKAGES_FLUX, kubeappsConfig)).toBe(false);
+ expect(isGlobalNamespace("carvel-global", PluginNames.PACKAGES_FLUX, kubeappsConfig)).toBe(true);
});
diff --git a/dashboard/src/shared/utils.ts b/dashboard/src/shared/utils.ts
index 2cdd311e932..3bc8c493853 100644
--- a/dashboard/src/shared/utils.ts
+++ b/dashboard/src/shared/utils.ts
@@ -228,8 +228,10 @@ export function isGlobalNamespace(namespace: string, pluginName: string, kubeapp
return namespace === kubeappsConfig.globalReposNamespace;
case PluginNames.PACKAGES_KAPP:
return namespace === kubeappsConfig.carvelGlobalNamespace;
+ // Currently, Flux doesn't namespaced repos, so it will always be global
+ case PluginNames.PACKAGES_FLUX:
+ return true;
default:
- // Currently, Flux doesn't support global namespaces
return false;
}
}
From 42b4aae1ebcdcaa4143763f7c5a4a6e5453e4e29 Mon Sep 17 00:00:00 2001
From: Greg Fichtenholtz <74032303+gfichtenholt@users.noreply.github.com>
Date: Mon, 22 Aug 2022 08:28:51 -0700
Subject: [PATCH 4/8] Flux oci support 9: fix helm repository cache out-of-sync
when remote contents changes (#5222)
* incremental
* incremental
* clean up func names
* begin GCR integration
* clean up func names
---
.../packages/v1alpha1/cache/chart_cache.go | 22 +-
.../packages/v1alpha1/cache/watcher_cache.go | 52 ++--
.../plugins/fluxv2/packages/v1alpha1/chart.go | 43 ++-
.../v1alpha1/chart_integration_test.go | 26 ++
.../packages/v1alpha1/global_vars_test.go | 26 ++
.../v1alpha1/integration_utils_test.go | 3 +-
.../fluxv2/packages/v1alpha1/oci_repo.go | 269 +++++++++++-------
.../fluxv2/packages/v1alpha1/release.go | 4 +-
.../plugins/fluxv2/packages/v1alpha1/repo.go | 22 +-
.../v1alpha1/repo_integration_test.go | 18 +-
.../fluxv2/packages/v1alpha1/repo_test.go | 22 +-
.../fluxv2/packages/v1alpha1/server.go | 2 +-
.../packages/v1alpha1/testdata/gcloud-util.sh | 48 ++++
.../packages/v1alpha1/testdata/ghcr-util.sh | 6 +-
.../v1alpha1/testdata/kind-cluster-setup.sh | 47 ++-
15 files changed, 435 insertions(+), 175 deletions(-)
create mode 100644 cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/gcloud-util.sh
diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/cache/chart_cache.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/cache/chart_cache.go
index 0082de9e934..1c08dfb8a81 100644
--- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/cache/chart_cache.go
+++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/cache/chart_cache.go
@@ -62,7 +62,7 @@ type ChartCache struct {
// significant in that it flushes the whole redis cache and re-populates the state from k8s.
// When that happens we don't really want any concurrent access to the cache until the resync()
// operation is complete. In other words, we want to:
- // - be able to have multiple concurrent readers (goroutines doing GetForOne())
+ // - be able to have multiple concurrent readers (goroutines doing Get())
// - only a single writer (goroutine doing a resync()) is allowed, and while its doing its job
// no readers are allowed
resyncCond *sync.Cond
@@ -424,11 +424,11 @@ func (c *ChartCache) syncHandler(workerName, key string) error {
}
// this is effectively a cache GET operation
-func (c *ChartCache) FetchForOne(key string) ([]byte, error) {
+func (c *ChartCache) Fetch(key string) ([]byte, error) {
c.resyncCond.L.(*sync.RWMutex).RLock()
defer c.resyncCond.L.(*sync.RWMutex).RUnlock()
- log.Infof("+FetchForOne(%s)", key)
+ log.Infof("+Fetch(%s)", key)
// read back from cache: should be either:
// - what we previously wrote OR
@@ -440,7 +440,7 @@ func (c *ChartCache) FetchForOne(key string) ([]byte, error) {
log.Infof("Redis [GET %s]: Nil", key)
return nil, nil
} else if err != nil {
- return nil, fmt.Errorf("fetchForOne() failed to get value for key [%s] from cache due to: %v", key, err)
+ return nil, fmt.Errorf("fetch() failed to get value for key [%s] from cache due to: %v", key, err)
}
log.Infof("Redis [GET %s]: %d bytes read", key, len(byteArray))
@@ -453,7 +453,7 @@ func (c *ChartCache) FetchForOne(key string) ([]byte, error) {
}
/*
- GetForOne() is like FetchForOne() but if there is a cache miss, it will then get chart data based on
+ Get() is like Fetch() but if there is a cache miss, it will then get chart data based on
the corresponding repo object, process it and then add it to the cache and return the
result.
This func should:
@@ -466,13 +466,13 @@ func (c *ChartCache) FetchForOne(key string) ([]byte, error) {
• otherwise return the bytes stored in the
chart cache for the given entry
*/
-func (c *ChartCache) GetForOne(key string, chart *models.Chart, downloadFn DownloadChartFn) ([]byte, error) {
+func (c *ChartCache) Get(key string, chart *models.Chart, downloadFn DownloadChartFn) ([]byte, error) {
// TODO (gfichtenholt) it'd be nice to get rid of all arguments except for the key, similar to that of
- // NamespacedResourceWatcherCache.GetForOne()
- log.Infof("+GetForOne(%s)", key)
+ // NamespacedResourceWatcherCache.Get()
+ log.Infof("+Get(%s)", key)
var value []byte
var err error
- if value, err = c.FetchForOne(key); err != nil {
+ if value, err = c.Fetch(key); err != nil {
return nil, err
} else if value == nil {
// cache miss
@@ -508,7 +508,7 @@ func (c *ChartCache) GetForOne(key string, chart *models.Chart, downloadFn Downl
c.queue.Add(key)
// now need to wait until this item has been processed by runWorker().
c.queue.WaitUntilForgotten(key)
- return c.FetchForOne(key)
+ return c.Fetch(key)
}
}
return value, nil
@@ -613,7 +613,7 @@ func chartCacheKeyFor(namespace, chartID, chartVersion string) (string, error) {
chartVersion), nil
}
-// FYI: The work queue is able to retry transient HTTP errors
+// FYI: The work queue is able to retry transient HTTP errors that occur while invoking downloadFn
func ChartCacheComputeValue(chartID, chartUrl, chartVersion string, downloadFn DownloadChartFn) ([]byte, error) {
chartTgz, err := downloadFn(chartID, chartUrl, chartVersion)
if err != nil {
diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/cache/watcher_cache.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/cache/watcher_cache.go
index e024e772992..e52cedf690f 100644
--- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/cache/watcher_cache.go
+++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/cache/watcher_cache.go
@@ -81,7 +81,7 @@ type NamespacedResourceWatcherCache struct {
// significant in that it flushes the whole redis cache and re-populates the state from k8s.
// When that happens we don't really want any concurrent access to the cache until the resync()
// operation is complete. In other words, we want to:
- // - be able to have multiple concurrent readers (goroutines doing GetForOne()/GetForMultiple())
+ // - be able to have multiple concurrent readers (goroutines doing Get()/GetMultiple())
// - only a single writer (goroutine doing a resync()) is allowed, and while its doing its job
// no readers are allowed
resyncCond *sync.Cond
@@ -531,7 +531,7 @@ func (c *NamespacedResourceWatcherCache) syncHandler(key string) error {
defer log.Infof("-syncHandler(%s)", key)
// Convert the namespace/name string into a distinct namespace and name
- name, err := c.fromKey(key)
+ name, err := c.NamespacedNameFromKey(key)
if err != nil {
return err
}
@@ -645,8 +645,8 @@ func (c *NamespacedResourceWatcherCache) onDelete(key string) error {
}
// this is effectively a cache GET operation
-func (c *NamespacedResourceWatcherCache) fetchForOne(key string) (interface{}, error) {
- log.InfoS("+fetchForOne", "key", key)
+func (c *NamespacedResourceWatcherCache) fetch(key string) (interface{}, error) {
+ log.InfoS("+fetch", "key", key)
// read back from cache: should be either:
// - what we previously wrote OR
// - redis.Nil if the key does not exist or has been evicted due to memory pressure/TTL expiry
@@ -657,7 +657,7 @@ func (c *NamespacedResourceWatcherCache) fetchForOne(key string) (interface{}, e
log.V(4).Infof("Redis [GET %s]: Nil", key)
return nil, nil
} else if err != nil {
- return nil, fmt.Errorf("fetchForOne() failed to get value for key [%s] from cache due to: %v", key, err)
+ return nil, fmt.Errorf("fetch() failed to get value for key [%s] from cache due to: %v", key, err)
}
log.V(4).Infof("Redis [GET %s]: %d bytes read", key, len(byteArray))
@@ -683,10 +683,10 @@ func (c *NamespacedResourceWatcherCache) fetchForOne(key string) (interface{}, e
// be relied upon to be the "source of truth". So I removed it for now as I found it
// of no use
-// parallelize the process of value retrieval because fetchForOne() calls
+// parallelize the process of value retrieval because fetch() calls
// c.config.onGet() which will de-code the data from bytes into expected struct, which
// may be computationally expensive and thus benefit from multiple threads of execution
-func (c *NamespacedResourceWatcherCache) fetchForMultiple(keys sets.String) (map[string]interface{}, error) {
+func (c *NamespacedResourceWatcherCache) fetchMultiple(keys sets.String) (map[string]interface{}, error) {
response := make(map[string]interface{})
type fetchValueJob struct {
@@ -710,7 +710,7 @@ func (c *NamespacedResourceWatcherCache) fetchForMultiple(keys sets.String) (map
// The following loop will only terminate when the request channel is
// closed (and there are no more items)
for job := range requestChan {
- result, err := c.fetchForOne(job.key)
+ result, err := c.fetch(job.key)
responseChan <- fetchValueJobResult{job, result, err}
}
wg.Done()
@@ -743,25 +743,25 @@ func (c *NamespacedResourceWatcherCache) fetchForMultiple(keys sets.String) (map
return response, errorutil.NewAggregate(errs)
}
-// the difference between 'fetchForMultiple' and 'GetForMultiple' is that 'fetch' will only
+// the difference between 'fetchMultiple' and 'GetMultiple' is that 'fetch' will only
// get the value from the cache for a given or return nil if one is missing, whereas
-// 'GetForMultiple' will first call 'fetch' but then for any cache misses it will force
+// 'GetMultiple' will first call 'fetch' but then for any cache misses it will force
// a re-computation of the value, if available, based on the input argument itemList and load
-// that result into the cache. So, 'GetForMultiple' provides a guarantee that if a key exists,
+// that result into the cache. So, 'GetMultiple' provides a guarantee that if a key exists,
// it's value will be returned,
-// whereas 'fetchForMultiple' does not guarantee that.
+// whereas 'fetchMultiple' does not guarantee that.
// The keys are expected to be in the format of the cache (the caller does that)
-func (c *NamespacedResourceWatcherCache) GetForMultiple(keys sets.String) (map[string]interface{}, error) {
+func (c *NamespacedResourceWatcherCache) GetMultiple(keys sets.String) (map[string]interface{}, error) {
c.resyncCond.L.(*sync.RWMutex).RLock()
defer c.resyncCond.L.(*sync.RWMutex).RUnlock()
- log.Infof("+GetForMultiple(%s)", keys)
+ log.Infof("+GetMultiple(%s)", keys)
// at any given moment, the redis cache may only have a subset of the entire set of existing keys.
// Some key may have been evicted due to memory pressure and LRU eviction policy.
// ref: https://redis.io/topics/lru-cache
// so, first, let's fetch the entries that are still cached at this moment
// before redis maybe forced to evict those in order to make room for new ones
- chartsUntyped, err := c.fetchForMultiple(keys)
+ chartsUntyped, err := c.fetchMultiple(keys)
if err != nil {
return nil, err
}
@@ -836,7 +836,7 @@ func (c *NamespacedResourceWatcherCache) computeValuesForKeys(keys sets.String)
// The following loop will only terminate when the request channel is
// closed (and there are no more items)
for key := range requestChan {
- // see GetForOne() for explanation of what is happening below
+ // see Get() for explanation of what is happening below
c.forceKey(key)
}
wg.Done()
@@ -876,8 +876,8 @@ func (c *NamespacedResourceWatcherCache) computeAndFetchValuesForKeys(keys sets.
// The following loop will only terminate when the request channel is
// closed (and there are no more items)
for job := range requestChan {
- // see GetForOne() for explanation of what is happening below
- value, err := c.forceAndFetchKey(job.key)
+ // see Get() for explanation of what is happening below
+ value, err := c.ForceAndFetch(job.key)
responseChan <- computeValueJobResult{job, value, err}
}
wg.Done()
@@ -937,7 +937,7 @@ func (c *NamespacedResourceWatcherCache) KeyForNamespacedName(name types.Namespa
// the opposite of keyFor()
// the goal is to keep the details of what exactly the key looks like localized to one piece of code
-func (c *NamespacedResourceWatcherCache) fromKey(key string) (*types.NamespacedName, error) {
+func (c *NamespacedResourceWatcherCache) NamespacedNameFromKey(key string) (*types.NamespacedName, error) {
parts := strings.Split(key, KeySegmentsSeparator)
if len(parts) != 3 || parts[0] != c.config.Gvr.Resource || len(parts[1]) == 0 || len(parts[2]) == 0 {
return nil, status.Errorf(codes.Internal, "invalid key [%s]", key)
@@ -945,21 +945,21 @@ func (c *NamespacedResourceWatcherCache) fromKey(key string) (*types.NamespacedN
return &types.NamespacedName{Namespace: parts[1], Name: parts[2]}, nil
}
-// GetForOne() is like fetchForOne() but if there is a cache miss, it will also check the
+// Get() is like fetch() but if there is a cache miss, it will also check the
// k8s for the corresponding object, process it and then add it to the cache and return the
// result.
-func (c *NamespacedResourceWatcherCache) GetForOne(key string) (interface{}, error) {
+func (c *NamespacedResourceWatcherCache) Get(key string) (interface{}, error) {
c.resyncCond.L.(*sync.RWMutex).RLock()
defer c.resyncCond.L.(*sync.RWMutex).RUnlock()
- log.Infof("+GetForOne(%s)", key)
+ log.Infof("+Get(%s)", key)
var value interface{}
var err error
- if value, err = c.fetchForOne(key); err != nil {
+ if value, err = c.fetch(key); err != nil {
return nil, err
} else if value == nil {
// cache miss
- return c.forceAndFetchKey(key)
+ return c.ForceAndFetch(key)
}
return value, nil
}
@@ -975,14 +975,14 @@ func (c *NamespacedResourceWatcherCache) forceKey(key string) {
c.queue.WaitUntilForgotten(key)
}
-func (c *NamespacedResourceWatcherCache) forceAndFetchKey(key string) (interface{}, error) {
+func (c *NamespacedResourceWatcherCache) ForceAndFetch(key string) (interface{}, error) {
c.forceKey(key)
// yes, there is a small time window here between after we are done with WaitUntilForgotten()
// and the following fetch, where another concurrent goroutine may force the newly added
// cache entry out, but that is an edge case and I am willing to overlook it for now
// To fix it, would somehow require WaitUntilForgotten() returning a value from a cache, so
// the whole thing would be atomic. Don't know how to do this yet
- return c.fetchForOne(key)
+ return c.fetch(key)
}
// this func is used by unit tests only
diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/chart.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/chart.go
index d84cb218416..df0f2f689d7 100644
--- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/chart.go
+++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/chart.go
@@ -67,14 +67,14 @@ func (s *Server) availableChartDetail(ctx context.Context, packageRef *corev1.Av
if chartVersion != "" {
if key, err := s.chartCache.KeyFor(repoName.Namespace, chartID, chartVersion); err != nil {
return nil, err
- } else if byteArray, err = s.chartCache.FetchForOne(key); err != nil {
+ } else if byteArray, err = s.chartCache.Fetch(key); err != nil {
return nil, err
}
}
if byteArray == nil {
// no specific chart version was provided or a cache miss, need to do a bit of work
- chartModel, err := s.getChart(ctx, repoName, chartName)
+ chartModel, err := s.getChartModel(ctx, repoName, chartName)
if err != nil {
return nil, err
} else if chartModel == nil {
@@ -104,7 +104,7 @@ func (s *Server) availableChartDetail(ctx context.Context, packageRef *corev1.Av
fn = downloadHttpChartFn(opts)
}
}
- if byteArray, err = s.chartCache.GetForOne(key, chartModel, fn); err != nil {
+ if byteArray, err = s.chartCache.Get(key, chartModel, fn); err != nil {
return nil, err
}
@@ -136,25 +136,44 @@ func (s *Server) availableChartDetail(ctx context.Context, packageRef *corev1.Av
return pkgDetail, nil
}
-func (s *Server) getChart(ctx context.Context, repo types.NamespacedName, chartName string) (*models.Chart, error) {
+func (s *Server) getChartModel(ctx context.Context, repoName types.NamespacedName, chartName string) (*models.Chart, error) {
if s.repoCache == nil {
return nil, status.Errorf(codes.FailedPrecondition, "server cache has not been properly initialized")
- } else if ok, err := s.hasAccessToNamespace(ctx, common.GetChartsGvr(), repo.Namespace); err != nil {
+ } else if ok, err := s.hasAccessToNamespace(ctx, common.GetChartsGvr(), repoName.Namespace); err != nil {
return nil, err
} else if !ok {
- return nil, status.Errorf(codes.PermissionDenied, "user has no [get] access for HelmCharts in namespace [%s]", repo.Namespace)
+ return nil, status.Errorf(codes.PermissionDenied, "user has no [get] access for HelmCharts in namespace [%s]", repoName.Namespace)
}
- key := s.repoCache.KeyForNamespacedName(repo)
- if entry, err := s.repoCache.GetForOne(key); err != nil {
+ key := s.repoCache.KeyForNamespacedName(repoName)
+ value, err := s.repoCache.Get(key)
+ if err != nil {
return nil, err
- } else if entry != nil {
- if typedEntry, ok := entry.(repoCacheEntryValue); !ok {
+ } else if value != nil {
+ if typedValue, ok := value.(repoCacheEntryValue); !ok {
return nil, status.Errorf(
codes.Internal,
- "unexpected value fetched from cache: type: [%s], value: [%v]", reflect.TypeOf(entry), entry)
+ "unexpected value fetched from cache: type: [%s], value: [%v]",
+ reflect.TypeOf(value), value)
} else {
- for _, chart := range typedEntry.Charts {
+ if typedValue.Type == "oci" {
+ // ref https://github.com/vmware-tanzu/kubeapps/issues/5007#issuecomment-1217293240
+ // helm OCI chart repos are not automatically updated when the
+ // state on remote changes. So we will force new checksum
+ // computation and update local cache if needed
+ value, err := s.repoCache.ForceAndFetch(key)
+ if err != nil {
+ return nil, err
+ }
+ typedValue, ok = value.(repoCacheEntryValue)
+ if !ok {
+ return nil, status.Errorf(
+ codes.Internal,
+ "unexpected value fetched from cache: type: [%s], value: [%v]",
+ reflect.TypeOf(value), value)
+ }
+ }
+ for _, chart := range typedValue.Charts {
if chart.Name == chartName {
return &chart, nil // found it
}
diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/chart_integration_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/chart_integration_test.go
index 0cdbdf344ef..332935af0ba 100644
--- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/chart_integration_test.go
+++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/chart_integration_test.go
@@ -603,6 +603,32 @@ func TestKindClusterAvailablePackageEndpointsForOCI(t *testing.T) {
},
}
+ /*
+ gcp_user := "oauth2accesstoken"
+ // token is very short lived
+ gcp_pwd, err := gcloudPrintAccessToken(t)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ testCases := []struct {
+ testName string
+ registryUrl string
+ secret *apiv1.Secret
+ }{
+ {
+ testName: "Testing [" + gcp_stefanprodan_podinfo_oci_registry_url + "] with basic auth secret",
+ registryUrl: gcp_stefanprodan_podinfo_oci_registry_url,
+ secret: newBasicAuthSecret(types.NamespacedName{
+ Name: "oci-repo-secret-" + randSeq(4),
+ Namespace: "default"},
+ gcp_user,
+ gcp_pwd,
+ ),
+ },
+ }
+ */
+
adminName := types.NamespacedName{
Name: "test-admin-" + randSeq(4),
Namespace: "default",
diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/global_vars_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/global_vars_test.go
index 40cb9ac44ad..9596c1f1de4 100644
--- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/global_vars_test.go
+++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/global_vars_test.go
@@ -1267,6 +1267,25 @@ var (
}
}
+ add_repo_req_28 = func(server, user, password string) *corev1.AddPackageRepositoryRequest {
+ return &corev1.AddPackageRepositoryRequest{
+ Name: "my-podinfo-10",
+ Context: &corev1.Context{Namespace: "default"},
+ Type: "oci",
+ Url: gcp_stefanprodan_podinfo_oci_registry_url,
+ Auth: &corev1.PackageRepositoryAuth{
+ Type: corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_DOCKER_CONFIG_JSON,
+ PackageRepoAuthOneOf: &corev1.PackageRepositoryAuth_DockerCreds{
+ DockerCreds: &corev1.DockerCredentials{
+ Server: server,
+ Username: user,
+ Password: password,
+ },
+ },
+ },
+ }
+ }
+
add_repo_expected_resp = &corev1.AddPackageRepositoryResponse{
PackageRepoRef: repoRef("bar", "foo"),
}
@@ -2167,6 +2186,13 @@ var (
},
}
+ expected_versions_gfichtenholt_podinfo_3 = &corev1.GetAvailablePackageVersionsResponse{
+ PackageAppVersions: []*corev1.PackageAppVersion{
+ {PkgVersion: "6.1.6"},
+ {PkgVersion: "6.1.5"},
+ },
+ }
+
create_package_simple_req = &corev1.CreateInstalledPackageRequest{
AvailablePackageRef: availableRef("podinfo/podinfo", "namespace-1"),
Name: "my-podinfo",
diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/integration_utils_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/integration_utils_test.go
index 8f3c052540b..4b1e57ed8e6 100644
--- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/integration_utils_test.go
+++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/integration_utils_test.go
@@ -87,6 +87,7 @@ const (
// gets setup by kind-cluster-setup.sh
github_stefanprodan_podinfo_oci_registry_url = "oci://ghcr.io/gfichtenholt/stefanprodan-podinfo-clone"
harbor_stefanprodan_podinfo_oci_registry_url = "oci://demo.goharbor.io/stefanprodan-podinfo-clone"
+ gcp_stefanprodan_podinfo_oci_registry_url = "oci://us-west1-docker.pkg.dev/vmware-kubeapps-ci/stefanprodan-podinfo-clone/podinfo"
// the URL of local in cluster helm registry. Gets deployed via ./kind-cluster-setup.sh
// in_cluster_oci_registry_url = "oci://registry-app-svc.default.svc.cluster.local:5000/helm-charts"
@@ -111,7 +112,7 @@ func checkEnv(t *testing.T) (fluxplugin.FluxV2PackagesServiceClient, fluxplugin.
}
if !runTests {
- t.Skipf("skipping flux plugin integration tests because environment variable %q not set to be true", envVarFluxIntegrationTests)
+ t.Skipf("skipping flux plugin integration tests because environment variable [%q] not set to be true", envVarFluxIntegrationTests)
return nil, nil, nil
} else {
if up, err := isLocalKindClusterUp(t); err != nil || !up {
diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/oci_repo.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/oci_repo.go
index 4d14b29f577..8599f79732d 100644
--- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/oci_repo.go
+++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/oci_repo.go
@@ -217,13 +217,17 @@ func newOCIChartRepository(registryURL string, registryOpts ...OCIChartRepositor
}
func (r *OCIChartRepository) listRepositoryNames() ([]string, error) {
+ log.Infof("+listRepositoryNames(stack:\n%s)", common.GetStackTrace())
+
// this needs to be done after a call to login()
- for _, lister := range builtInRepoListers {
- if ok, err := lister.IsApplicableFor(r); ok && err == nil {
- r.repositoryLister = lister
- break
- } else {
- log.Infof("Lister [%v] not applicable for registry for URL: [%s] [%v]", reflect.TypeOf(lister), r.url.String(), err)
+ if r.repositoryLister == nil {
+ for _, lister := range builtInRepoListers {
+ if ok, err := lister.IsApplicableFor(r); ok && err == nil {
+ r.repositoryLister = lister
+ break
+ } else {
+ log.Infof("Lister [%v] not applicable for registry for URL: [%s] [%v]", reflect.TypeOf(lister), r.url.String(), err)
+ }
}
}
@@ -380,7 +384,6 @@ func newRegistryClient(isLogin bool, tlsConfig *tls.Config, getterOpts []getter.
// OCI Helm repository, which defines a source, does not produce an Artifact
// ref https://fluxcd.io/docs/components/source/helmrepositories/#helm-oci-repository
-// TODO: this function is way too long. Break it up
func (s *repoEventSink) onAddOciRepo(repo sourcev1.HelmRepository) ([]byte, bool, error) {
log.Infof("+onAddOciRepo(%s)", common.PrettyPrint(repo))
defer log.Info("-onAddOciRepo")
@@ -389,14 +392,6 @@ func (s *repoEventSink) onAddOciRepo(repo sourcev1.HelmRepository) ([]byte, bool
if err != nil {
return nil, false, err
}
-
- chartRepo := &models.Repo{
- Namespace: repo.Namespace,
- Name: repo.Name,
- URL: repo.Spec.URL,
- Type: repo.Spec.Type,
- }
-
// repository names, e.g. "stefanprodan/charts/podinfo"
// asset-syncer calls them appNames
// see func (r *OCIRegistry) Charts(fetchLatestOnly bool) ([]models.Chart, error) {
@@ -407,79 +402,17 @@ func (s *repoEventSink) onAddOciRepo(repo sourcev1.HelmRepository) ([]byte, bool
return nil, false, err
}
- charts := []models.Chart{}
- for _, fullAppName := range appNames {
- appName, err := ociChartRepo.shortRepoName(fullAppName)
- if err != nil {
- return nil, false, err
- }
-
- // Encode repository names to store them in the database.
- encodedAppName := url.PathEscape(appName)
- chartID := path.Join(repo.Name, encodedAppName)
-
- log.Infof("==========>: app name: [%s], chartID: [%s]", appName, chartID)
-
- ref := fmt.Sprintf("%s/%s", ociChartRepo.url.String(), appName)
- allTags, err := ociChartRepo.getTags(ref)
- if err != nil {
- return nil, false, err
- }
-
- // to be consistent with how we support helm http repos
- // the chart fields like Desciption, home, sources come from the
- // most recent chart version
- // ref https://github.com/vmware-tanzu/kubeapps/blob/11c87926d6cd798af72875d01437d15ae8d85b9a/pkg/helm/index.go#L30
- latestChartVersion, err := ociChartRepo.pickChartVersionFrom(appName, "", allTags)
- if err != nil {
- return nil, false, status.Errorf(codes.Internal, "%v", err)
- }
- log.Infof("==========> most recent chart version: %s", latestChartVersion.Version)
-
- latestChartMetadata, err := getOCIChartMetadata(ociChartRepo, chartID, latestChartVersion)
- if err != nil {
- return nil, false, err
- }
-
- maintainers := []chart.Maintainer{}
- for _, maintainer := range latestChartMetadata.Maintainers {
- maintainers = append(maintainers, *maintainer)
- }
-
- mc := models.Chart{
- ID: chartID,
- Name: encodedAppName,
- Repo: chartRepo,
- Description: latestChartMetadata.Description,
- Home: latestChartMetadata.Home,
- Keywords: latestChartMetadata.Keywords,
- Maintainers: maintainers,
- Sources: latestChartMetadata.Sources,
- Icon: latestChartMetadata.Icon,
- Category: latestChartMetadata.Annotations["category"],
- ChartVersions: []models.ChartVersion{},
- }
+ allTags, err := ociChartRepo.getTagsForApps(appNames)
+ if err != nil {
+ return nil, false, err
+ }
- for _, tag := range allTags {
- chartVersion, err := ociChartRepo.pickChartVersionFrom(appName, tag, allTags)
- if err != nil {
- return nil, false, status.Errorf(codes.Internal, "%v", err)
- }
- log.Infof("==========>: chart version: %s", common.PrettyPrint(chartVersion))
-
- mcv := models.ChartVersion{
- Version: chartVersion.Version,
- AppVersion: chartVersion.AppVersion,
- Created: chartVersion.Created,
- Digest: chartVersion.Digest,
- URLs: chartVersion.URLs,
- }
- mc.ChartVersions = append(mc.ChartVersions, mcv)
- }
- charts = append(charts, mc)
+ charts, err := getOciChartModels(appNames, allTags, ociChartRepo, &repo)
+ if err != nil {
+ return nil, false, err
}
- checksum, err := ociChartRepo.checksum()
+ checksum, err := ociChartRepo.checksum(appNames, allTags)
if err != nil {
return nil, false, status.Errorf(codes.Internal, "%v", err)
}
@@ -487,6 +420,7 @@ func (s *repoEventSink) onAddOciRepo(repo sourcev1.HelmRepository) ([]byte, bool
cacheEntryValue := repoCacheEntryValue{
Checksum: checksum,
Charts: charts,
+ Type: "oci",
}
// use gob encoding instead of json, it peforms much better
@@ -502,12 +436,11 @@ func (s *repoEventSink) onAddOciRepo(repo sourcev1.HelmRepository) ([]byte, bool
return nil, false, err
}
}
-
return buf.Bytes(), true, nil
}
func (s *repoEventSink) onModifyOciRepo(key string, oldValue interface{}, repo sourcev1.HelmRepository) ([]byte, bool, error) {
- log.Infof("+onModifyOciRepo(%s)", common.PrettyPrint(repo))
+ log.Infof("+onModifyOciRepo(stack:\n%s,\nrepo:%s)", common.GetStackTrace(), common.PrettyPrint(repo))
defer log.Info("-onModifyOciRepo")
// We should to compare checksums on what's stored in the cache
@@ -526,29 +459,56 @@ func (s *repoEventSink) onModifyOciRepo(key string, oldValue interface{}, repo s
key, cacheEntryUntyped)
}
- ociRepo, err := s.newOCIChartRepositoryAndLogin(context.Background(), repo)
+ ociChartRepo, err := s.newOCIChartRepositoryAndLogin(context.Background(), repo)
if err != nil {
return nil, false, err
}
- newChecksum, err := ociRepo.checksum()
+ appNames, err := ociChartRepo.listRepositoryNames()
+ if err != nil {
+ return nil, false, err
+ }
+
+ allTags, err := ociChartRepo.getTagsForApps(appNames)
+ if err != nil {
+ return nil, false, err
+ }
+
+ newChecksum, err := ociChartRepo.checksum(appNames, allTags)
if err != nil {
return nil, false, err
}
if cacheEntry.Checksum != newChecksum {
- // currently this cannot happen due to https://github.com/fluxcd/source-controller/issues/839
- // skip because the content did not change
- log.Warningf("Unexpected state in onModifyOciRepo(%s)", common.PrettyPrint(repo))
- // then just move on, same as if the remote contents did not change
+ charts, err := getOciChartModels(appNames, allTags, ociChartRepo, &repo)
+ if err != nil {
+ return nil, false, err
+ }
+
+ cacheEntryValue := repoCacheEntryValue{
+ Checksum: newChecksum,
+ Charts: charts,
+ Type: "oci",
+ }
+
+ // use gob encoding instead of json, it peforms much better
+ var buf bytes.Buffer
+ enc := gob.NewEncoder(&buf)
+ if err = enc.Encode(cacheEntryValue); err != nil {
+ return nil, false, err
+ }
+
+ if s.chartCache != nil {
+ fn := downloadOCIChartFn(ociChartRepo)
+ if err = s.chartCache.SyncCharts(charts, fn); err != nil {
+ return nil, false, err
+ }
+ }
+ return buf.Bytes(), true, nil
}
return nil, false, nil
}
-//
-// misc OCI repo utilities
-//
-
// TagList represents a list of tags as specified at
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#content-discovery
type TagList struct {
@@ -559,29 +519,28 @@ type TagList struct {
// Checksum returns the sha256 of the repo by concatenating tags for
// all repositories within the registry and returning the sha256.
// Caveat: Mutated image tags won't be detected as new
-func (r *OCIChartRepository) checksum() (string, error) {
- log.Infof("+checksum()")
- defer log.Infof("-checksum()")
- appNames, err := r.listRepositoryNames()
- if err != nil {
- return "", err
- }
+func (r *OCIChartRepository) getTagsForApps(appNames []string) (map[string]TagList, error) {
tags := map[string]TagList{}
for _, fullAppName := range appNames {
appName, err := r.shortRepoName(fullAppName)
if err != nil {
- return "", err
+ return nil, err
}
ref := fmt.Sprintf("%s/%s", r.url.String(), appName)
tagz, err := r.getTags(ref)
if err != nil {
- return "", err
+ return nil, err
}
-
tags[appName] = TagList{Name: appName, Tags: tagz}
}
+ return tags, nil
+}
- content, err := json.Marshal(tags)
+func (r *OCIChartRepository) checksum(appNames []string, allTags map[string]TagList) (string, error) {
+ log.Infof("+checksum(%s)", appNames)
+ defer log.Infof("-checksum()")
+
+ content, err := json.Marshal(allTags)
if err != nil {
return "", err
}
@@ -589,6 +548,10 @@ func (r *OCIChartRepository) checksum() (string, error) {
return common.GetSha256(content)
}
+//
+// misc OCI repo utilities
+//
+
// given fullRepoName like "stefanprodan/charts/podinfo", returns "podinfo"
func (r *OCIChartRepository) shortRepoName(fullRepoName string) (string, error) {
expectedPrefix := strings.TrimLeft(r.url.Path, "/") + "/"
@@ -621,7 +584,6 @@ func (s *repoEventSink) newOCIChartRepositoryAndLogin(ctx context.Context, repo
}
func (s *repoEventSink) newOCIChartRepositoryAndLoginWithOptions(registryURL string, loginOpts []registry.LoginOption, getterOpts []getter.Option, cred *orasregistryauthv2.Credential) (*OCIChartRepository, error) {
- // TODO (gfichtenholt) DRY
u, err := url.Parse(registryURL)
if err != nil {
return nil, err
@@ -761,6 +723,95 @@ func downloadChartWithHelmGetter(tlsConfig *tls.Config, getterOptions []getter.O
return buf, err
}
+func getOciChartModels(appNames []string, allTags map[string]TagList, ociChartRepo *OCIChartRepository, repo *sourcev1.HelmRepository) ([]models.Chart, error) {
+ charts := []models.Chart{}
+ for _, fullAppName := range appNames {
+ appName, err := ociChartRepo.shortRepoName(fullAppName)
+ if err != nil {
+ return nil, err
+ }
+
+ tags, ok := allTags[appName]
+ if !ok {
+ return nil, status.Errorf(codes.Internal, "Missing tags for app [%s]", appName)
+ }
+
+ mc, err := getOciChartModel(appName, tags, ociChartRepo, repo)
+ if err != nil {
+ return nil, err
+ }
+ charts = append(charts, *mc)
+ }
+ return charts, nil
+}
+
+func getOciChartModel(appName string, tags TagList, ociChartRepo *OCIChartRepository, repo *sourcev1.HelmRepository) (*models.Chart, error) {
+ // Encode repository names to store them in the database.
+ encodedAppName := url.PathEscape(appName)
+ chartID := path.Join(repo.Name, encodedAppName)
+
+ log.Infof("==========>: app name: [%s], chartID: [%s]", appName, chartID)
+
+ // to be consistent with how we support helm http repos
+ // the chart fields like Desciption, home, sources come from the
+ // most recent chart version
+ // ref https://github.com/vmware-tanzu/kubeapps/blob/11c87926d6cd798af72875d01437d15ae8d85b9a/pkg/helm/index.go#L30
+ latestChartVersion, err := ociChartRepo.pickChartVersionFrom(appName, "", tags.Tags)
+ if err != nil {
+ return nil, status.Errorf(codes.Internal, "%v", err)
+ }
+ log.Infof("==========> most recent chart version: %s", latestChartVersion.Version)
+
+ latestChartMetadata, err := getOCIChartMetadata(ociChartRepo, chartID, latestChartVersion)
+ if err != nil {
+ return nil, err
+ }
+
+ maintainers := []chart.Maintainer{}
+ for _, maintainer := range latestChartMetadata.Maintainers {
+ maintainers = append(maintainers, *maintainer)
+ }
+
+ modelRepo := &models.Repo{
+ Namespace: repo.Namespace,
+ Name: repo.Name,
+ URL: repo.Spec.URL,
+ Type: repo.Spec.Type,
+ }
+
+ mc := models.Chart{
+ ID: chartID,
+ Name: encodedAppName,
+ Repo: modelRepo,
+ Description: latestChartMetadata.Description,
+ Home: latestChartMetadata.Home,
+ Keywords: latestChartMetadata.Keywords,
+ Maintainers: maintainers,
+ Sources: latestChartMetadata.Sources,
+ Icon: latestChartMetadata.Icon,
+ Category: latestChartMetadata.Annotations["category"],
+ ChartVersions: []models.ChartVersion{},
+ }
+
+ for _, tag := range tags.Tags {
+ chartVersion, err := ociChartRepo.pickChartVersionFrom(appName, tag, tags.Tags)
+ if err != nil {
+ return nil, status.Errorf(codes.Internal, "%v", err)
+ }
+ log.Infof("==========>: chart version: %s", common.PrettyPrint(chartVersion))
+
+ mcv := models.ChartVersion{
+ Version: chartVersion.Version,
+ AppVersion: chartVersion.AppVersion,
+ Created: chartVersion.Created,
+ Digest: chartVersion.Digest,
+ URLs: chartVersion.URLs,
+ }
+ mc.ChartVersions = append(mc.ChartVersions, mcv)
+ }
+ return &mc, nil
+}
+
func getOCIChartTarball(ociRepo *OCIChartRepository, chartID string, chartVersion *repo.ChartVersion) ([]byte, error) {
chartBuffer, err := ociRepo.registryClient.DownloadChart(chartVersion)
if err != nil {
diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release.go
index 53b16c5204a..578f9b3903e 100644
--- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release.go
+++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release.go
@@ -177,7 +177,7 @@ func (s *Server) installedPkgSummaryFromRelease(ctx context.Context, rel helmv2.
repoNamespace = name.Namespace
}
repo := types.NamespacedName{Namespace: repoNamespace, Name: repoName}
- chartFromCache, err := s.getChart(ctx, repo, chartName)
+ chartFromCache, err := s.getChartModel(ctx, repo, chartName)
if err != nil {
log.Warningf("%v", err)
} else if chartFromCache != nil && len(chartFromCache.ChartVersions) > 0 {
@@ -322,7 +322,7 @@ func (s *Server) newRelease(ctx context.Context, packageRef *corev1.AvailablePac
}
repo := types.NamespacedName{Namespace: packageRef.Context.Namespace, Name: repoName}
- chart, err := s.getChart(ctx, repo, chartName)
+ chart, err := s.getChartModel(ctx, repo, chartName)
if err != nil {
return nil, err
}
diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo.go
index 7b6fdacb3cb..6657dc5f25f 100644
--- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo.go
+++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo.go
@@ -159,11 +159,13 @@ func (s *Server) getChartsForRepos(ctx context.Context, match []string) (map[str
return nil, err
}
- chartsUntyped, err := s.repoCache.GetForMultiple(repoNames)
+ chartsUntyped, err := s.repoCache.GetMultiple(repoNames)
if err != nil {
return nil, err
}
+ log.Infof("=======> getChartsForRepos chartsUntyped: %v", chartsUntyped)
+
chartsTyped := make(map[string][]models.Chart)
for key, value := range chartsUntyped {
if value == nil {
@@ -176,6 +178,23 @@ func (s *Server) getChartsForRepos(ctx context.Context, match []string) (map[str
"unexpected value fetched from cache: type: [%s], value: [%v]",
reflect.TypeOf(value), value)
}
+ if typedValue.Type == "oci" {
+ // ref https://github.com/vmware-tanzu/kubeapps/issues/5007#issuecomment-1217293240
+ // helm OCI chart repos are not automatically updated when the
+ // state on remote changes. So we will force new checksum
+ // computation and update local cache if needed
+ value, err = s.repoCache.ForceAndFetch(key)
+ if err != nil {
+ return nil, err
+ }
+ typedValue, ok = value.(repoCacheEntryValue)
+ if !ok {
+ return nil, status.Errorf(
+ codes.Internal,
+ "unexpected value fetched from cache: type: [%s], value: [%v]",
+ reflect.TypeOf(value), value)
+ }
+ }
chartsTyped[key] = typedValue.Charts
}
}
@@ -699,6 +718,7 @@ type repoEventSink struct {
// all struct fields are capitalized so they're exported by gob encoding
type repoCacheEntryValue struct {
Checksum string
+ Type string // if missing, repo is assumed to be regular old HTTP
Charts []models.Chart
}
diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_integration_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_integration_test.go
index 049050eaf4f..948b5b10df4 100644
--- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_integration_test.go
+++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_integration_test.go
@@ -202,6 +202,11 @@ func TestKindClusterAddPackageRepository(t *testing.T) {
t.Fatalf("Environment variables GITHUB_USER and GITHUB_TOKEN need to be set to run this test")
}
+ // TODO: probably requires TLS
+ gcp_host := "us-west1-docker.pkg.dev"
+ gcp_user := ""
+ gcp_pwd := ""
+
testCases := []struct {
testName string
request *corev1.AddPackageRepositoryRequest
@@ -316,6 +321,12 @@ func TestKindClusterAddPackageRepository(t *testing.T) {
expectedResponse: add_repo_expected_resp_11,
expectedStatusCode: codes.OK,
},
+ {
+ testName: "test add OCI repo from GCP with dockerconfigjson secret (kubeapps managed)",
+ request: add_repo_req_28(gcp_host, gcp_user, gcp_pwd),
+ expectedResponse: add_repo_expected_resp_11,
+ expectedStatusCode: codes.OK,
+ },
}
adminAcctName := types.NamespacedName{
@@ -1473,11 +1484,6 @@ func TestKindClusterAddTagsToOciRepository(t *testing.T) {
t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got, opts))
}
- // just codifying the behavior described in
- // https://github.com/fluxcd/source-controller/issues/839
- // Requested feature: flux OCI helm repositories notice when tags on remote registry change
- // Should flux guys ever change their decision, this test should fail.
- // P.S. Yuck
if err = helmPushChartToMyGithubRegistry(t, "6.1.6"); err != nil {
t.Fatal(err)
}
@@ -1506,7 +1512,7 @@ func TestKindClusterAddTagsToOciRepository(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- if got, want := resp3, expected_versions_gfichtenholt_podinfo; !cmp.Equal(want, got, opts) {
+ if got, want := resp3, expected_versions_gfichtenholt_podinfo_3; !cmp.Equal(want, got, opts) {
t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got, opts))
}
})
diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_test.go
index eea018c6404..8389fbcb499 100644
--- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_test.go
+++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_test.go
@@ -2417,22 +2417,40 @@ func newRepo(name string, namespace string, spec *sourcev1.HelmRepositorySpec, s
// does a series of mock.ExpectGet(...)
func (s *Server) redisMockExpectGetFromRepoCache(mock redismock.ClientMock, filterOptions *corev1.FilterOptions, repos ...sourcev1.HelmRepository) error {
mapVals := make(map[string][]byte)
+ ociRepoKeys := sets.String{}
for _, r := range repos {
key, bytes, err := s.redisKeyValueForRepo(r)
if err != nil {
return err
}
mapVals[key] = bytes
+ if r.Spec.Type == "oci" {
+ ociRepoKeys.Insert(key)
+ }
}
if filterOptions == nil || len(filterOptions.GetRepositories()) == 0 {
for k, v := range mapVals {
- mock.ExpectGet(k).SetVal(string(v))
+ maxTries := 1
+ if ociRepoKeys.Has(k) {
+ // see comment in chart.go about caching helm OCI chart repos
+ maxTries = 3
+ }
+ for i := 0; i < maxTries; i++ {
+ mock.ExpectGet(k).SetVal(string(v))
+ }
}
} else {
for _, r := range filterOptions.GetRepositories() {
for k, v := range mapVals {
if strings.HasSuffix(k, ":"+r) {
- mock.ExpectGet(k).SetVal(string(v))
+ maxTries := 1
+ if ociRepoKeys.Has(k) {
+ // see comment in chart.go about caching helm OCI chart repos
+ maxTries = 3
+ }
+ for i := 0; i < maxTries; i++ {
+ mock.ExpectGet(k).SetVal(string(v))
+ }
}
}
}
diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server.go
index 1d84b384212..9b509fb1a40 100644
--- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server.go
+++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server.go
@@ -279,7 +279,7 @@ func (s *Server) GetAvailablePackageVersions(ctx context.Context, request *corev
log.Infof("Requesting chart [%s] in namespace [%s]", chartName, namespace)
repo := types.NamespacedName{Namespace: namespace, Name: repoName}
- chart, err := s.getChart(ctx, repo, chartName)
+ chart, err := s.getChartModel(ctx, repo, chartName)
if err != nil {
return nil, err
} else if chart != nil {
diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/gcloud-util.sh b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/gcloud-util.sh
new file mode 100644
index 00000000000..89baab6bdf7
--- /dev/null
+++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/gcloud-util.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+
+# Copyright 2022 the Kubeapps contributors.
+# SPDX-License-Identifier: Apache-2.0
+
+# This script requires gcloud CLI (gcloud) to be installed locally.
+
+function deleteArtifactRegistry()
+{
+ # sanity check
+ if [[ "$#" -lt 1 ]]; then
+ error_exit "Usage: deleteArtifactRegistry name"
+ fi
+
+ local REGISTRY_NAME=$1
+ local REGISTRY_FULL_PATH=projects/vmware-kubeapps-ci/locations/$FLUX_TEST_GCP_LOCATION/repositories/$REGISTRY_NAME
+
+ echo
+ echo -e Checking if artifact registry [${L_YELLOW}$REGISTRY_NAME${NC}] exists...
+ RESP=$(gcloud artifacts repositories list --location=$FLUX_TEST_GCP_LOCATION --format "json" | jq --arg REGISTRY_FULL_PATH "$REGISTRY_FULL_PATH" '.[] | select(.name==$REGISTRY_FULL_PATH)')
+ if [[ "$RESP" != "" ]] ; then
+ echo -e Deleting repository [${L_YELLOW}$REGISTRY_NAME${NC}]...
+ gcloud artifacts repositories delete $REGISTRY_NAME --location=$FLUX_TEST_GCP_LOCATION -q
+ fi
+}
+
+function createArtifactRegistry()
+{
+ # sanity check
+ if [[ "$#" -lt 1 ]]; then
+ error_exit "Usage: createArtifactRegistry name"
+ fi
+
+ local REGISTRY_NAME=$1
+ local REGISTRY_FULL_PATH=projects/vmware-kubeapps-ci/locations/$FLUX_TEST_GCP_LOCATION/repositories/$REGISTRY_NAME
+
+ echo
+ echo -e Checking if artifact registry [${L_YELLOW}$REGISTRY_NAME${NC}] exists...
+ RESP=$(gcloud artifacts repositories list --location=$FLUX_TEST_GCP_LOCATION --format "json" | jq --arg REGISTRY_FULL_PATH "$REGISTRY_FULL_PATH" '.[] | select(.name==$REGISTRY_FULL_PATH)')
+ if [[ "$RESP" != "" ]] ; then
+ echo -e "Artifact registry [${L_YELLOW}${$REGISTRY_NAME}${NC}] already exists in harbor..."
+ else
+ gcloud artifacts repositories create $REGISTRY_NAME --repository-format=docker --location=$FLUX_TEST_GCP_LOCATION --description="Helm repository for kubeapps flux plugin integration testing"
+ fi
+
+ # configure Docker with a credential helper to authenticate with Artifact Registry
+ gcloud auth configure-docker $FLUX_TEST_GCP_REGISTRY_DOMAIN
+}
diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/ghcr-util.sh b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/ghcr-util.sh
index 1a80cc77a32..551c85ae44a 100644
--- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/ghcr-util.sh
+++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/ghcr-util.sh
@@ -59,7 +59,7 @@ function pushChartToMyGitHubRegistry() {
-H "Accept: application/vnd.github+json" \
/user/packages/container/helm-charts%2Fpodinfo/versions | jq -rc '.[].metadata.container.tags[]')
echo
- echo Remote Repository aka Package [${L_YELLOW}$GITHUB_OCI_REGISTRY_URL/podinfo${NC}] / All Versions
+ echo -e Remote Repository aka Package [${L_YELLOW}$GITHUB_OCI_REGISTRY_URL/podinfo${NC}] / All Versions
echo ================================================================================
echo "$ALL_VERSIONS"
echo ================================================================================
@@ -174,13 +174,13 @@ function deleteChartFromMyGithubRegistry() {
# GitHub API ref https://docs.github.com/en/rest/packages#list-packages-for-the-authenticated-users-namespace
# GitHub web portal: https://github.com/gfichtenholt?tab=packages&ecosystem=container
ALL_PACKAGES=$(gh api -H "Accept: application/vnd.github+json" /user/packages?package_type=container | jq '.[].name')
- echo Remote Repository [${L_YELLOW}$GITHUB_OCI_REGISTRY_URL${NC}] / All Packages
+ echo -e Remote Repository [${L_YELLOW}$GITHUB_OCI_REGISTRY_URL${NC}] / All Packages
echo ================================================================================
echo "$ALL_PACKAGES"
echo ================================================================================
PODINFO_EXISTS=$(echo $ALL_PACKAGES | grep -sw 'helm-charts/podinfo')
if [[ "$PODINFO_EXISTS" != "" ]]; then
- echo Deleting package [${L_YELLOW}podinfo${NC}] from [${L_YELLOW}$GITHUB_OCI_REGISTRY_URL${NC}]...
+ echo -e Deleting package [${L_YELLOW}podinfo${NC}] from [${L_YELLOW}$GITHUB_OCI_REGISTRY_URL${NC}]...
# GitHub API ref https://docs.github.com/en/rest/packages#delete-a-package-for-the-authenticated-user
# GitHub web portal: https://github.com/users/gfichtenholt/packages/container/helm-charts%2Fpodinfo/settings
echo -n | gh api --method DELETE -H "Accept: application/vnd.github+json" /user/packages/container/helm-charts%2Fpodinfo --input -
diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/kind-cluster-setup.sh b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/kind-cluster-setup.sh
index c937dacebb3..b0d9b9ce1ad 100755
--- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/kind-cluster-setup.sh
+++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/kind-cluster-setup.sh
@@ -7,6 +7,8 @@
# - build an image that can be used to stand-up a pod that serves static test-data in
# local kind cluster.
# - create and seed an OCI registry on ghcr.io
+# - create and seed an OCI registry on demo.goharbor.io
+# - create and seed an OCI registry on gcr.io
# These are usedby the integration tests.
# This script needs to be run once before the running the test(s).
set -o errexit
@@ -56,6 +58,9 @@ FLUX_TEST_HARBOR_URL=https://${FLUX_TEST_HARBOR_HOST}
FLUX_TEST_HARBOR_ADMIN_USER=admin
FLUX_TEST_HARBOR_ADMIN_PWD=Harbor12345
+FLUX_TEST_GCP_LOCATION=us-west1
+FLUX_TEST_GCP_REGISTRY_DOMAIN=us-west1-docker.pkg.dev
+
function pushChartToLocalRegistryUsingHelmCLI() {
max=5
n=0
@@ -164,6 +169,7 @@ function deploy {
setupGithubStefanProdanClone
setupHarborStefanProdanClone
+ setupGcrStefanProdanClone
}
function undeploy {
@@ -231,6 +237,9 @@ function setupHarborStefanProdanClone {
createHarborProject $PROJECT_NAME
helm registry login $FLUX_TEST_HARBOR_HOST -u $FLUX_TEST_HARBOR_ADMIN_USER -p $FLUX_TEST_HARBOR_ADMIN_PWD
+ trap '{
+ helm registry logout $FLUX_TEST_HARBOR_HOST
+ }' EXIT
pushd $SCRIPTPATH/charts
trap '{
@@ -251,12 +260,46 @@ function setupHarborStefanProdanClone {
echo
}
+function setupGcrStefanProdanClone {
+ # this creates a clone of what was out on "oci://ghcr.io/stefanprodan/charts" as of Jul 28 2022
+ # to oci://demo.goharbor.io/stefanprodan-podinfo-clone
+ local REGISTRY_NAME=stefanprodan-podinfo-clone
+ deleteArtifactRegistry $REGISTRY_NAME
+ createArtifactRegistry $REGISTRY_NAME
+
+ gcloud auth print-access-token | helm registry login -u oauth2accesstoken \
+ --password-stdin $FLUX_TEST_GCP_REGISTRY_DOMAIN
+
+ trap '{
+ helm registry logout $FLUX_TEST_GCP_REGISTRY_DOMAIN
+ }' EXIT
+
+ pushd $SCRIPTPATH/charts
+ trap '{
+ popd
+ }' EXIT
+
+ SRC_URL_PREFIX=https://stefanprodan.github.io/podinfo
+ ALL_VERSIONS=("6.1.0" "6.1.1" "6.1.2" "6.1.3" "6.1.4" "6.1.5" "6.1.6" "6.1.7" "6.1.8")
+ DEST_URL=oci://$FLUX_TEST_GCP_REGISTRY_DOMAIN/vmware-kubeapps-ci/$REGISTRY_NAME/podinfo
+ for v in ${ALL_VERSIONS[@]}; do
+ curl --silent -O $SRC_URL_PREFIX/podinfo-$v.tgz
+ helm push podinfo-$v.tgz $DEST_URL
+ done
+
+ echo
+ echo Running sanity checks...
+ echo TODO
+ echo
+}
+
. ./ghcr-util.sh
. ./harbor-util.sh
+. ./gcloud-util.sh
if [ $# -lt 1 ]
then
- echo "Usage : $0 deploy|undeploy|redeploy|shell|logs|pushChartToMyGithub|deleteChartVersionFromMyGitHub|setupGithubStefanProdanClone|setupHarborStefanProdanClone"
+ echo "Usage : $0 deploy|undeploy|redeploy|shell|logs|pushChartToMyGithub|deleteChartVersionFromMyGitHub|setupGithubStefanProdanClone|setupHarborStefanProdanClone|setupGcrStefanProdanClone"
exit
fi
@@ -283,6 +326,8 @@ setupGithubStefanProdanClone) setupGithubStefanProdanClone
;;
setupHarborStefanProdanClone) setupHarborStefanProdanClone
;;
+setupGcrStefanProdanClone) setupGcrStefanProdanClone
+ ;;
*) error_exit "Invalid command: $1"
;;
esac
From 8db518faf53d1365de0177ec153301aca5369431 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Antonio=20G=C3=A1mez=2C=20PhD?=
Date: Mon, 22 Aug 2022 19:04:26 +0200
Subject: [PATCH 5/8] Add types to the state used in tests (#5217)
* Add types to the state used in tests
Signed-off-by: Antonio Gamez Diaz
* Add missing import
Signed-off-by: Antonio Gamez Diaz
Signed-off-by: Antonio Gamez Diaz
---
dashboard/src/actions/auth.test.tsx | 6 +-
.../src/actions/availablepackages.test.tsx | 4 +-
dashboard/src/actions/kube.test.tsx | 14 ++--
dashboard/src/actions/operators.test.tsx | 4 +-
dashboard/src/actions/repos.test.tsx | 5 +-
.../components/AppUpgrade/AppUpgrade.test.tsx | 17 ++--
.../DeleteButton/DeleteButton.test.tsx | 6 +-
.../RollbackButton/RollbackButton.test.tsx | 6 +-
.../AppView/AppSecrets/AppSecrets.test.tsx | 9 +-
.../AppView/AppValues/AppValues.test.tsx | 5 +-
.../src/components/AppView/AppView.test.tsx | 44 ++++++----
.../CustomAppView/CustomAppView.test.tsx | 6 +-
.../ResourceTable/ResourceTable.test.tsx | 18 ++--
.../src/components/Catalog/Catalog.test.tsx | 82 +++++++++++++------
.../Config/PkgRepoList/PkgRepoForm.test.tsx | 40 ++++-----
.../Config/PkgRepoList/PkgRepoList.test.tsx | 31 ++++---
.../DeploymentForm/DeploymentForm.test.tsx | 6 +-
.../AdvancedDeploymentForm.test.tsx | 3 +-
.../CustomFormParam.test.tsx | 4 +-
.../DeploymentFormBody/Differential.test.tsx | 3 +-
.../HeadManager/HeadManager.test.tsx | 3 +-
.../Header/ContextSelector.test.tsx | 13 +--
.../src/components/Header/Header.test.tsx | 20 +++--
dashboard/src/components/Header/Menu.test.tsx | 2 +-
.../components/LoginForm/LoginForm.test.tsx | 16 +++-
.../OperatorInstance.test.tsx | 40 +++++----
.../OperatorInstanceForm.test.tsx | 16 ++--
.../OperatorInstanceUpdateForm.test.tsx | 11 ++-
.../OperatorList/OperatorList.test.tsx | 33 +++++---
.../OperatorNew/OperatorNew.test.tsx | 22 +++--
.../OperatorSummary/OperatorSummary.test.tsx | 12 ++-
.../OperatorView/OperatorView.test.tsx | 18 ++--
.../SelectRepoForm/SelectRepoForm.test.tsx | 9 +-
.../UpgradeForm/UpgradeForm.test.tsx | 55 +++++--------
.../AccessURLTableContainer.test.tsx | 4 +-
.../ApplicationStatusContainer.test.tsx | 6 +-
.../LoginFormContainer.test.tsx | 3 +-
dashboard/src/shared/AxiosInstance.test.ts | 5 +-
38 files changed, 366 insertions(+), 235 deletions(-)
diff --git a/dashboard/src/actions/auth.test.tsx b/dashboard/src/actions/auth.test.tsx
index 495d30ce430..ce304304822 100644
--- a/dashboard/src/actions/auth.test.tsx
+++ b/dashboard/src/actions/auth.test.tsx
@@ -6,6 +6,8 @@ import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import { Auth } from "shared/Auth";
import Namespace, * as NS from "shared/Namespace";
+import { initialState } from "shared/specs/mountWrapper";
+import { IStoreState } from "shared/types";
import { getType } from "typesafe-actions";
import actions from ".";
@@ -35,12 +37,14 @@ beforeEach(() => {
store = mockStore({
auth: {
+ ...initialState.auth,
state,
},
config: {
+ ...initialState.config,
oauthLogoutURI: "/log/out",
},
- });
+ } as Partial);
});
afterEach(() => {
diff --git a/dashboard/src/actions/availablepackages.test.tsx b/dashboard/src/actions/availablepackages.test.tsx
index cfbb460e548..fe50e0ab605 100644
--- a/dashboard/src/actions/availablepackages.test.tsx
+++ b/dashboard/src/actions/availablepackages.test.tsx
@@ -14,7 +14,7 @@ import { Plugin } from "gen/kubeappsapis/core/plugins/v1alpha1/plugins";
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import PackagesService from "shared/PackagesService";
-import { FetchError, IReceivePackagesActionPayload } from "shared/types";
+import { FetchError, IReceivePackagesActionPayload, IStoreState } from "shared/types";
import { getType } from "typesafe-actions";
import actions from ".";
@@ -73,7 +73,7 @@ beforeEach(() => {
packages: {
isFetching: false,
},
- });
+ } as Partial);
});
afterEach(() => {
diff --git a/dashboard/src/actions/kube.test.tsx b/dashboard/src/actions/kube.test.tsx
index 8795f0b9b0e..b93926f3a85 100644
--- a/dashboard/src/actions/kube.test.tsx
+++ b/dashboard/src/actions/kube.test.tsx
@@ -1,17 +1,17 @@
// Copyright 2018-2022 the Kubeapps contributors.
// SPDX-License-Identifier: Apache-2.0
-import configureMockStore from "redux-mock-store";
-import thunk from "redux-thunk";
-import { Kube } from "shared/Kube";
-import { IKubeState, IResource } from "shared/types";
-import { getType } from "typesafe-actions";
-import actions from ".";
import {
InstalledPackageReference,
ResourceRef as APIResourceRef,
} from "gen/kubeappsapis/core/packages/v1alpha1/packages";
import { GetResourcesResponse } from "gen/kubeappsapis/plugins/resources/v1alpha1/resources";
+import configureMockStore from "redux-mock-store";
+import thunk from "redux-thunk";
+import { Kube } from "shared/Kube";
+import { IKubeState, IResource, IStoreState } from "shared/types";
+import { getType } from "typesafe-actions";
+import actions from ".";
const mockStore = configureMockStore([thunk]);
@@ -22,7 +22,7 @@ const makeStore = (operatorsEnabled: boolean) => {
kinds: {},
};
const config = operatorsEnabled ? { featureFlags: { operators: true } } : {};
- return mockStore({ kube: state, config: config });
+ return mockStore({ kube: state, config: config } as Partial);
};
let store: any;
diff --git a/dashboard/src/actions/operators.test.tsx b/dashboard/src/actions/operators.test.tsx
index fca532f6d48..e2fe2e46c6c 100644
--- a/dashboard/src/actions/operators.test.tsx
+++ b/dashboard/src/actions/operators.test.tsx
@@ -4,7 +4,7 @@
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import { Operators } from "shared/Operators";
-import { IResource } from "shared/types";
+import { IResource, IStoreState } from "shared/types";
import { getType } from "typesafe-actions";
import actions from ".";
@@ -14,7 +14,7 @@ const mockStore = configureMockStore([thunk]);
let store: any;
beforeEach(() => {
- store = mockStore({});
+ store = mockStore({} as Partial);
});
afterEach(jest.restoreAllMocks);
diff --git a/dashboard/src/actions/repos.test.tsx b/dashboard/src/actions/repos.test.tsx
index e383a8667b6..3771b05a470 100644
--- a/dashboard/src/actions/repos.test.tsx
+++ b/dashboard/src/actions/repos.test.tsx
@@ -226,14 +226,17 @@ describe("deleteRepo", () => {
it("dispatches requestRepoSummaries with current namespace", async () => {
const storeWithFlag: any = mockStore({
clusters: {
+ ...initialState.clusters,
currentCluster: "defaultCluster",
clusters: {
+ ...initialState.clusters.clusters,
defaultCluster: {
+ ...initialState.clusters.clusters[initialState.clusters.currentCluster],
currentNamespace,
},
},
},
- });
+ } as Partial);
await storeWithFlag.dispatch(
repoActions.deleteRepo({
context: { cluster: "default", namespace: "my-namespace" },
diff --git a/dashboard/src/components/AppUpgrade/AppUpgrade.test.tsx b/dashboard/src/components/AppUpgrade/AppUpgrade.test.tsx
index fbb7b92dd9b..45b3aa9e400 100644
--- a/dashboard/src/components/AppUpgrade/AppUpgrade.test.tsx
+++ b/dashboard/src/components/AppUpgrade/AppUpgrade.test.tsx
@@ -29,6 +29,7 @@ import {
FetchError,
IInstalledPackageState,
IPackageState,
+ IStoreState,
UpgradeError,
} from "shared/types";
import { PluginNames } from "shared/utils";
@@ -132,7 +133,7 @@ it("renders the repo selection form if not introduced", () => {
} as IInstalledPackageState,
};
const wrapper = mountWrapper(
- getStore({ ...defaultStore, ...state }),
+ getStore({ ...defaultStore, ...state } as Partial),
,
@@ -157,7 +158,7 @@ it("renders the repo selection form if not introduced when the app is loaded", (
getStore({
...defaultStore,
...state,
- }),
+ } as Partial),
,
@@ -180,7 +181,7 @@ describe("when an error exists", () => {
getStore({
...defaultStore,
...state,
- }),
+ } as Partial),
,
@@ -210,7 +211,7 @@ describe("when an error exists", () => {
getStore({
...defaultStore,
...state,
- }),
+ } as Partial),
,
@@ -238,7 +239,7 @@ describe("when an error exists", () => {
getStore({
...defaultStore,
...state,
- }),
+ } as Partial),
,
@@ -273,7 +274,7 @@ it("renders the upgrade form when the repo is available, clears state and fetche
getStore({
...defaultStore,
...state,
- }),
+ } as Partial),
,
@@ -309,7 +310,7 @@ it("renders the upgrade form with the version property", () => {
getStore({
...defaultStore,
...state,
- }),
+ } as Partial),
,
@@ -337,7 +338,7 @@ it("skips the repo selection form if the app contains upgrade info", () => {
getStore({
...defaultStore,
...state,
- }),
+ } as Partial),
,
diff --git a/dashboard/src/components/AppView/AppControls/DeleteButton/DeleteButton.test.tsx b/dashboard/src/components/AppView/AppControls/DeleteButton/DeleteButton.test.tsx
index 70c05818ea6..7acf275ad39 100644
--- a/dashboard/src/components/AppView/AppControls/DeleteButton/DeleteButton.test.tsx
+++ b/dashboard/src/components/AppView/AppControls/DeleteButton/DeleteButton.test.tsx
@@ -14,7 +14,7 @@ import { act } from "react-dom/test-utils";
import * as ReactRedux from "react-redux";
import ReactTooltip from "react-tooltip";
import { defaultStore, getStore, mountWrapper } from "shared/specs/mountWrapper";
-import { DeleteError } from "shared/types";
+import { DeleteError, IInstalledPackageState, IStoreState } from "shared/types";
import DeleteButton from "./DeleteButton";
const defaultProps = {
@@ -63,7 +63,9 @@ it("deletes an application", async () => {
});
it("renders an error", async () => {
- const store = getStore({ apps: { error: new DeleteError("Boom!") } });
+ const store = getStore({
+ apps: { error: new DeleteError("Boom!") } as Partial,
+ } as Partial);
const wrapper = mountWrapper(store, );
// Open modal
act(() => {
diff --git a/dashboard/src/components/AppView/AppControls/RollbackButton/RollbackButton.test.tsx b/dashboard/src/components/AppView/AppControls/RollbackButton/RollbackButton.test.tsx
index cd0a838cc54..96560af9e7e 100644
--- a/dashboard/src/components/AppView/AppControls/RollbackButton/RollbackButton.test.tsx
+++ b/dashboard/src/components/AppView/AppControls/RollbackButton/RollbackButton.test.tsx
@@ -15,7 +15,7 @@ import { act } from "react-dom/test-utils";
import * as ReactRedux from "react-redux";
import ReactTooltip from "react-tooltip";
import { defaultStore, getStore, mountWrapper } from "shared/specs/mountWrapper";
-import { RollbackError } from "shared/types";
+import { IInstalledPackageState, RollbackError } from "shared/types";
import RollbackButton from "./RollbackButton";
const defaultProps = {
@@ -70,7 +70,9 @@ it("rolls back an application", async () => {
});
it("renders an error", async () => {
- const store = getStore({ apps: { error: new RollbackError("Boom!") } });
+ const store = getStore({
+ apps: { error: new RollbackError("Boom!") },
+ } as Partial);
const wrapper = mountWrapper(store, );
// Open modal
act(() => {
diff --git a/dashboard/src/components/AppView/AppSecrets/AppSecrets.test.tsx b/dashboard/src/components/AppView/AppSecrets/AppSecrets.test.tsx
index be05f0d8e00..f6a499ca757 100644
--- a/dashboard/src/components/AppView/AppSecrets/AppSecrets.test.tsx
+++ b/dashboard/src/components/AppView/AppSecrets/AppSecrets.test.tsx
@@ -3,8 +3,8 @@
import { keyForResourceRef } from "shared/ResourceRef";
import { ResourceRef } from "gen/kubeappsapis/core/packages/v1alpha1/packages";
-import { defaultStore, getStore, mountWrapper } from "shared/specs/mountWrapper";
-import { ISecret } from "shared/types";
+import { defaultStore, getStore, initialState, mountWrapper } from "shared/specs/mountWrapper";
+import { ISecret, IStoreState } from "shared/types";
import SecretItemDatum from "../ResourceTable/ResourceItem/SecretItem/SecretItemDatum";
import AppSecrets from "./AppSecrets";
@@ -38,15 +38,18 @@ it("shows a message if there are no secrets", () => {
it("renders a secretItemDatum per secret", () => {
const key = keyForResourceRef(sampleResourceRef);
const state = getStore({
+ ...initialState,
kube: {
+ ...initialState.kube,
items: {
+ ...initialState.kube.items,
[key]: {
isFetching: false,
item: secret,
},
},
},
- });
+ } as Partial);
const wrapper = mountWrapper(state, );
expect(wrapper.find(SecretItemDatum)).toHaveLength(2);
});
diff --git a/dashboard/src/components/AppView/AppValues/AppValues.test.tsx b/dashboard/src/components/AppView/AppValues/AppValues.test.tsx
index 236b4fc5642..d74908e034e 100644
--- a/dashboard/src/components/AppView/AppValues/AppValues.test.tsx
+++ b/dashboard/src/components/AppView/AppValues/AppValues.test.tsx
@@ -3,7 +3,8 @@
import AceEditor from "react-ace";
import { SupportedThemes } from "shared/Config";
-import { defaultStore, getStore, mountWrapper } from "shared/specs/mountWrapper";
+import { defaultStore, getStore, initialState, mountWrapper } from "shared/specs/mountWrapper";
+import { IStoreState } from "shared/types";
import AppValues from "./AppValues";
it("includes values", () => {
@@ -26,7 +27,7 @@ it("sets light theme by default", () => {
it("changes theme", () => {
const wrapper = mountWrapper(
- getStore({ config: { theme: SupportedThemes.dark } }),
+ getStore({ ...initialState, config: { theme: SupportedThemes.dark } } as Partial),
,
);
expect(wrapper.find(AceEditor).prop("theme")).toBe("solarized_dark");
diff --git a/dashboard/src/components/AppView/AppView.test.tsx b/dashboard/src/components/AppView/AppView.test.tsx
index 18b555da91d..3b3a0a5ae70 100644
--- a/dashboard/src/components/AppView/AppView.test.tsx
+++ b/dashboard/src/components/AppView/AppView.test.tsx
@@ -28,7 +28,7 @@ import { IConfigState } from "reducers/config";
import { InstalledPackage } from "shared/InstalledPackage";
import PackagesService from "shared/PackagesService";
import { getStore, mountWrapper } from "shared/specs/mountWrapper";
-import { DeleteError, FetchError, IInstalledPackageState } from "shared/types";
+import { DeleteError, FetchError, IInstalledPackageState, IStoreState } from "shared/types";
import { PluginNames } from "shared/utils";
import { getType } from "typesafe-actions";
import AccessURLTable from "./AccessURLTable/AccessURLTable";
@@ -160,7 +160,7 @@ describe("AppView", () => {
selected: undefined,
isFetching: true,
} as IInstalledPackageState,
- }),
+ } as Partial),
@@ -181,7 +181,7 @@ describe("AppView", () => {
isFetching: false,
error: new Error("foo not found"),
} as IInstalledPackageState,
- }),
+ } as Partial),
@@ -199,7 +199,9 @@ describe("AppView", () => {
await act(async () => {
wrapper = mountWrapper(
- getStore({ apps: { error: new FetchError("boom!") } as IInstalledPackageState }),
+ getStore({
+ apps: { error: new FetchError("boom!") } as IInstalledPackageState,
+ } as Partial),
@@ -226,7 +228,7 @@ describe("AppView", () => {
},
],
} as IConfigState,
- }),
+ } as Partial),
@@ -252,7 +254,7 @@ describe("AppView", () => {
},
],
} as IConfigState,
- }),
+ } as Partial),
@@ -272,7 +274,7 @@ describe("AppView", () => {
selected: { ...installedPackage },
selectedDetails: { ...availablePackageDetail },
} as IInstalledPackageState,
- }),
+ } as Partial),
@@ -297,7 +299,7 @@ describe("AppView", () => {
} as InstalledPackageReference,
},
} as IInstalledPackageState,
- }),
+ } as Partial),
@@ -317,7 +319,7 @@ describe("AppView", () => {
wrapper = mountWrapper(
getStore({
apps: { selected: { ...installedPackage } } as IInstalledPackageState,
- }),
+ } as Partial),
@@ -351,7 +353,9 @@ describe("AppView", () => {
let wrapper: any;
await act(async () => {
wrapper = mountWrapper(
- getStore({ apps: { selected: installedPackage } as IInstalledPackageState }),
+ getStore({
+ apps: { selected: installedPackage } as IInstalledPackageState,
+ } as Partial),
@@ -383,7 +387,9 @@ describe("AppView", () => {
let wrapper: any;
await act(async () => {
wrapper = mountWrapper(
- getStore({ apps: { selected: installedPackage } as IInstalledPackageState }),
+ getStore({
+ apps: { selected: installedPackage } as IInstalledPackageState,
+ } as Partial),
@@ -425,7 +431,7 @@ describe("AppView", () => {
getStore({
...validState,
apps: { ...validState.apps, error: new Error("Boom!") } as IInstalledPackageState,
- }),
+ } as Partial),
@@ -445,7 +451,7 @@ describe("AppView", () => {
getStore({
...validState,
apps: { ...validState.apps, error: new DeleteError("Boom!") } as IInstalledPackageState,
- }),
+ } as Partial),
@@ -469,7 +475,9 @@ describe("AppView", () => {
let wrapper: any;
await act(async () => {
wrapper = mountWrapper(
- getStore({ apps: { selected: installedPackage } as IInstalledPackageState }),
+ getStore({
+ apps: { selected: installedPackage } as IInstalledPackageState,
+ } as Partial),
@@ -499,7 +507,9 @@ describe("AppView actions", () => {
resourceRefs: apiResourceRefs,
} as GetInstalledPackageResourceRefsResponse),
);
- const store = getStore({ apps: { selected: installedPackage } as IInstalledPackageState });
+ const store = getStore({
+ apps: { selected: installedPackage } as IInstalledPackageState,
+ } as Partial);
await act(async () => {
mountWrapper(
@@ -555,7 +565,9 @@ describe("AppView actions", () => {
} as GetInstalledPackageResourceRefsResponse),
);
- const store = getStore({ apps: { selected: installedPackage } as IInstalledPackageState });
+ const store = getStore({
+ apps: { selected: installedPackage } as IInstalledPackageState,
+ } as Partial);
let wrapper: any;
await act(async () => {
wrapper = mountWrapper(
diff --git a/dashboard/src/components/AppView/CustomAppView/CustomAppView.test.tsx b/dashboard/src/components/AppView/CustomAppView/CustomAppView.test.tsx
index aa1f06f08bd..ffb3a9e2081 100644
--- a/dashboard/src/components/AppView/CustomAppView/CustomAppView.test.tsx
+++ b/dashboard/src/components/AppView/CustomAppView/CustomAppView.test.tsx
@@ -6,7 +6,8 @@ import {
AvailablePackageDetail,
ResourceRef,
} from "gen/kubeappsapis/core/packages/v1alpha1/packages";
-import { getStore, mountWrapper } from "shared/specs/mountWrapper";
+import { getStore, initialState, mountWrapper } from "shared/specs/mountWrapper";
+import { IStoreState } from "shared/types";
import CustomAppView from ".";
import { CustomComponent } from "../../../RemoteComponent";
import { IAppViewResourceRefs } from "../AppView";
@@ -96,8 +97,9 @@ it("should render the remote component with the default URL", () => {
it("should render the remote component with the URL if set in the config", () => {
const wrapper = mountWrapper(
getStore({
+ ...initialState,
config: { remoteComponentsUrl: "www.thiswebsite.com" },
- }),
+ } as Partial),
,
);
expect(wrapper.find(CustomComponent).prop("url")).toBe("www.thiswebsite.com");
diff --git a/dashboard/src/components/AppView/ResourceTable/ResourceTable.test.tsx b/dashboard/src/components/AppView/ResourceTable/ResourceTable.test.tsx
index ea20d1ee825..9c73f632a2d 100644
--- a/dashboard/src/components/AppView/ResourceTable/ResourceTable.test.tsx
+++ b/dashboard/src/components/AppView/ResourceTable/ResourceTable.test.tsx
@@ -3,10 +3,10 @@
import Table from "components/js/Table";
import LoadingWrapper from "components/LoadingWrapper/LoadingWrapper";
-import { keyForResourceRef } from "shared/ResourceRef";
import { ResourceRef } from "gen/kubeappsapis/core/packages/v1alpha1/packages";
-import { getStore, mountWrapper } from "shared/specs/mountWrapper";
-import { IResource } from "shared/types";
+import { keyForResourceRef } from "shared/ResourceRef";
+import { getStore, initialState, mountWrapper } from "shared/specs/mountWrapper";
+import { IResource, IStoreState } from "shared/types";
import ResourceTable from "./ResourceTable";
const defaultProps = {
@@ -37,6 +37,7 @@ const deployment = {
it("renders a table with a resource", () => {
const state = getStore({
+ ...initialState,
kube: {
items: {
[sampleKey]: {
@@ -45,7 +46,7 @@ it("renders a table with a resource", () => {
},
},
},
- });
+ } as Partial);
const wrapper = mountWrapper(
state,
,
@@ -57,6 +58,7 @@ it("renders a table with a resource", () => {
it("renders a table with a loading resource", () => {
const state = getStore({
+ ...initialState,
kube: {
items: {
[sampleKey]: {
@@ -64,7 +66,7 @@ it("renders a table with a loading resource", () => {
},
},
},
- });
+ } as Partial);
const wrapper = mountWrapper(
state,
,
@@ -78,6 +80,7 @@ it("renders a table with a loading resource", () => {
it("renders a table with an error", () => {
const state = getStore({
+ ...initialState,
kube: {
items: {
[sampleKey]: {
@@ -86,7 +89,7 @@ it("renders a table with an error", () => {
},
},
},
- });
+ } as Partial);
const wrapper = mountWrapper(
state,
,
@@ -100,6 +103,7 @@ it("renders a table with an error", () => {
it("do not fail if the resources are already populated but the refs not yet", () => {
const state = getStore({
+ ...initialState,
kube: {
items: {
[sampleKey]: {
@@ -108,7 +112,7 @@ it("do not fail if the resources are already populated but the refs not yet", ()
},
},
},
- });
+ } as Partial);
const wrapper = mountWrapper(state, );
expect(wrapper.find(Table)).not.toExist();
});
diff --git a/dashboard/src/components/Catalog/Catalog.test.tsx b/dashboard/src/components/Catalog/Catalog.test.tsx
index 51076e63ab3..d6e84b5f869 100644
--- a/dashboard/src/components/Catalog/Catalog.test.tsx
+++ b/dashboard/src/components/Catalog/Catalog.test.tsx
@@ -8,7 +8,10 @@ import InfoCard from "components/InfoCard/InfoCard";
import Alert from "components/js/Alert";
import LoadingWrapper from "components/LoadingWrapper";
import { AvailablePackageSummary, Context } from "gen/kubeappsapis/core/packages/v1alpha1/packages";
-import { PackageRepositorySummary } from "gen/kubeappsapis/core/packages/v1alpha1/repositories";
+import {
+ PackageRepositoryDetail,
+ PackageRepositorySummary,
+} from "gen/kubeappsapis/core/packages/v1alpha1/repositories";
import { Plugin } from "gen/kubeappsapis/core/plugins/v1alpha1/plugins";
import { createMemoryHistory } from "history";
import React from "react";
@@ -91,7 +94,12 @@ const csv = {
const defaultState = {
packages: defaultPackageState,
operators: { csvs: [] } as Partial,
- repos: { reposSummaries: [] } as Partial,
+ repos: {
+ reposSummaries: [],
+ isFetching: false,
+ repoDetail: {} as PackageRepositoryDetail,
+ errors: [],
+ } as IPackageRepositoryState,
config: {
kubeappsCluster: defaultProps.cluster,
kubeappsNamespace: defaultProps.kubeappsNamespace,
@@ -182,8 +190,8 @@ it("should render a message if there are no elements in the catalog and the fetc
const wrapper = mountWrapper(
getStore({
...defaultState,
- packages: { hasFinishedFetching: true } as IPackageState,
- }),
+ packages: { hasFinishedFetching: true },
+ } as Partial),
,
);
wrapper.setProps({ searchFilter: "" });
@@ -194,7 +202,7 @@ it("should render a message if there are no elements in the catalog and the fetc
it("should render a spinner if there are no elements but it's still fetching", () => {
const wrapper = mountWrapper(
- getStore({ ...defaultState, packages: { hasFinishedFetching: false } }),
+ getStore({ ...defaultState, packages: { hasFinishedFetching: false } } as Partial),
,
);
expect(wrapper.find(LoadingWrapper)).toExist();
@@ -202,7 +210,7 @@ it("should render a spinner if there are no elements but it's still fetching", (
it("should not render a spinner if there are no elements and it finished fetching", () => {
const wrapper = mountWrapper(
- getStore({ ...defaultState, packages: { hasFinishedFetching: true } }),
+ getStore({ ...defaultState, packages: { hasFinishedFetching: true } } as Partial),
,
);
expect(wrapper.find(LoadingWrapper)).not.toExist();
@@ -210,7 +218,10 @@ it("should not render a spinner if there are no elements and it finished fetchin
it("should render a spinner if there already pending elements", () => {
const wrapper = mountWrapper(
- getStore({ ...populatedState, packages: { hasFinishedFetching: false } }),
+ getStore({
+ ...populatedState,
+ packages: { hasFinishedFetching: false },
+ } as Partial),
,
);
expect(wrapper.find(LoadingWrapper)).toExist();
@@ -218,7 +229,10 @@ it("should render a spinner if there already pending elements", () => {
it("should not render a message if only operators are selected", () => {
const wrapper = mountWrapper(
- getStore({ ...populatedState, packages: { hasFinishedFetching: true } }),
+ getStore({
+ ...populatedState,
+ packages: { hasFinishedFetching: true },
+ } as Partial),
@@ -230,7 +244,10 @@ it("should not render a message if only operators are selected", () => {
it("should not render a message if there are no more elements", () => {
const wrapper = mountWrapper(
- getStore({ ...populatedState, packages: { hasFinishedFetching: true } }),
+ getStore({
+ ...populatedState,
+ packages: { hasFinishedFetching: true },
+ } as Partial),
,
);
const message = wrapper.find(".end-page-message");
@@ -239,7 +256,10 @@ it("should not render a message if there are no more elements", () => {
it("should not render a message if there are no more elements but it's searching", () => {
const wrapper = mountWrapper(
- getStore({ ...populatedState, packages: { hasFinishedFetching: true } }),
+ getStore({
+ ...populatedState,
+ packages: { hasFinishedFetching: true },
+ } as Partial),
@@ -252,7 +272,10 @@ it("should not render a message if there are no more elements but it's searching
it("should render the scroll handler if not finished", () => {
const wrapper = mountWrapper(
- getStore({ ...populatedState, packages: { hasFinishedFetching: false } }),
+ getStore({
+ ...populatedState,
+ packages: { hasFinishedFetching: false },
+ } as Partial),
,
);
const scroll = wrapper.find(".scroll-handler");
@@ -262,7 +285,10 @@ it("should render the scroll handler if not finished", () => {
it("should not render the scroll handler if finished", () => {
const wrapper = mountWrapper(
- getStore({ ...populatedState, packages: { hasFinishedFetching: true } }),
+ getStore({
+ ...populatedState,
+ packages: { hasFinishedFetching: true },
+ } as Partial),
,
);
const scroll = wrapper.find(".scroll-handler");
@@ -703,8 +729,8 @@ describe("filters by package repository", () => {
...populatedState,
repos: {
reposSummaries: [{ name: "foo" } as PackageRepositorySummary],
- } as IPackageRepositoryState,
- }),
+ },
+ } as Partial),
@@ -733,8 +759,8 @@ describe("filters by package repository", () => {
...populatedState,
repos: {
reposSummaries: [{ name: "foo" } as PackageRepositorySummary],
- } as IPackageRepositoryState,
- }),
+ },
+ } as Partial),
@@ -761,8 +787,8 @@ describe("filters by package repository", () => {
mountWrapper(
getStore({
...populatedState,
- repos: { repos: [{ name: "foo" } as PackageRepositorySummary] },
- }),
+ repos: { ...populatedState.repos, repos: [{ name: "foo" } as PackageRepositorySummary] },
+ } as Partial),
{
mountWrapper(
getStore({
...populatedState,
- repos: { repos: [{ name: "foo" } as PackageRepositorySummary] },
- }),
+ repos: { ...populatedState.repos, repos: [{ name: "foo" } as PackageRepositorySummary] },
+ } as Partial),
@@ -831,7 +857,7 @@ describe("filters by operator provider", () => {
getStore({
...populatedState,
operators: { csvs: [csv, csv2] },
- }),
+ } as Partial),
@@ -855,7 +881,7 @@ describe("filters by operator provider", () => {
getStore({
...populatedState,
operators: { csvs: [csv, csv2] },
- }),
+ } as Partial),
@@ -879,7 +905,7 @@ describe("filters by operator provider", () => {
getStore({
...populatedState,
operators: { csvs: [csv, csv2] },
- }),
+ } as Partial),
@@ -973,7 +999,10 @@ describe("filters by category", () => {
},
} as any;
const wrapper = mountWrapper(
- getStore({ ...populatedState, operators: { csvs: [csv, csvWithCat] } }),
+ getStore({
+ ...populatedState,
+ operators: { csvs: [csv, csvWithCat] },
+ } as Partial),
@@ -994,7 +1023,10 @@ describe("filters by category", () => {
},
} as any;
const wrapper = mountWrapper(
- getStore({ ...populatedState, operators: { csvs: [csv, csvWithCat] } }),
+ getStore({
+ ...populatedState,
+ operators: { csvs: [csv, csvWithCat] },
+ } as Partial),
diff --git a/dashboard/src/components/Config/PkgRepoList/PkgRepoForm.test.tsx b/dashboard/src/components/Config/PkgRepoList/PkgRepoForm.test.tsx
index 75ef8cae0cf..6ae5592d16c 100644
--- a/dashboard/src/components/Config/PkgRepoList/PkgRepoForm.test.tsx
+++ b/dashboard/src/components/Config/PkgRepoList/PkgRepoForm.test.tsx
@@ -108,7 +108,7 @@ it("disables the submit button while loading", async () => {
getStore({
...defaultState,
repos: { ...defaultState.repos, isFetching: true } as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
@@ -155,7 +155,7 @@ it("shows an error creating a repo", async () => {
repos: {
errors: { create: new Error("boom!") },
} as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
@@ -170,7 +170,7 @@ it("shows an error deleting a repo", async () => {
repos: {
errors: { delete: new Error("boom!") },
} as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
@@ -185,7 +185,7 @@ it("shows an error fetching a repo", async () => {
repos: {
errors: { fetch: new Error("boom!") },
} as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
@@ -200,7 +200,7 @@ it("shows an error updating a repo", async () => {
repos: {
errors: { update: new Error("boom!") },
} as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
@@ -553,7 +553,7 @@ describe("when the repository info is already populated", () => {
getStore({
...defaultState,
repos: { ...defaultState.repos, repoDetail: testRepo } as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
@@ -576,7 +576,7 @@ describe("when the repository info is already populated", () => {
getStore({
...defaultState,
repos: { ...defaultState.repos, repoDetail: testRepo } as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
@@ -604,7 +604,7 @@ describe("when the repository info is already populated", () => {
getStore({
...defaultState,
repos: { ...defaultState.repos, repoDetail: testRepo } as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
@@ -633,7 +633,7 @@ describe("when the repository info is already populated", () => {
getStore({
...defaultState,
repos: { ...defaultState.repos, repoDetail: testRepo } as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
@@ -656,7 +656,7 @@ describe("when the repository info is already populated", () => {
getStore({
...defaultState,
repos: { ...defaultState.repos, repoDetail: testRepo } as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
@@ -679,7 +679,7 @@ describe("when the repository info is already populated", () => {
getStore({
...defaultState,
repos: { ...defaultState.repos, repoDetail: testRepo } as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
@@ -697,7 +697,7 @@ describe("when the repository info is already populated", () => {
getStore({
...defaultState,
repos: { ...defaultState.repos, repoDetail: testRepo } as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
@@ -716,7 +716,7 @@ describe("when the repository info is already populated", () => {
getStore({
...defaultState,
repos: { ...defaultState.repos, repoDetail: testRepo } as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
@@ -738,7 +738,7 @@ describe("when the repository info is already populated", () => {
getStore({
...defaultState,
repos: { ...defaultState.repos, repoDetail: testRepo } as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
@@ -763,7 +763,7 @@ describe("when the repository info is already populated", () => {
getStore({
...defaultState,
repos: { ...defaultState.repos, repoDetail: testRepo } as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
@@ -789,7 +789,7 @@ describe("when the repository info is already populated", () => {
getStore({
...defaultState,
repos: { ...defaultState.repos, repoDetail: testRepo } as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
@@ -819,7 +819,7 @@ describe("when the repository info is already populated", () => {
getStore({
...defaultState,
repos: { ...defaultState.repos, repoDetail: testRepo } as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
@@ -849,7 +849,7 @@ describe("when the repository info is already populated", () => {
getStore({
...defaultState,
repos: { ...defaultState.repos, repoDetail: testRepo } as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
@@ -877,7 +877,7 @@ describe("when the repository info is already populated", () => {
getStore({
...defaultState,
repos: { ...defaultState.repos, repoDetail: testRepo } as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
@@ -906,7 +906,7 @@ describe("when the repository info is already populated", () => {
getStore({
...defaultState,
repos: { ...defaultState.repos, repoDetail: testRepo } as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
});
diff --git a/dashboard/src/components/Config/PkgRepoList/PkgRepoList.test.tsx b/dashboard/src/components/Config/PkgRepoList/PkgRepoList.test.tsx
index befed81256d..d93849e3ba6 100644
--- a/dashboard/src/components/Config/PkgRepoList/PkgRepoList.test.tsx
+++ b/dashboard/src/components/Config/PkgRepoList/PkgRepoList.test.tsx
@@ -4,18 +4,19 @@
import actions from "actions";
import Alert from "components/js/Alert";
import Table from "components/js/Table";
+import TableRow from "components/js/Table/components/TableRow";
import Tooltip from "components/js/Tooltip";
import { PackageRepositorySummary } from "gen/kubeappsapis/core/packages/v1alpha1/repositories";
import { act } from "react-dom/test-utils";
import * as ReactRedux from "react-redux";
import { Link } from "react-router-dom";
+import { IPackageRepositoryState } from "reducers/repos";
import { Kube } from "shared/Kube";
import { defaultStore, getStore, initialState, mountWrapper } from "shared/specs/mountWrapper";
+import { IStoreState } from "shared/types";
import { PkgRepoControl } from "./PkgRepoControl";
import { PkgRepoDisabledControl } from "./PkgRepoDisabledControl";
import PkgRepoList from "./PkgRepoList";
-import TableRow from "components/js/Table/components/TableRow";
-import { IPackageRepositoryState } from "reducers/repos";
const {
clusters: { currentCluster, clusters },
@@ -60,7 +61,7 @@ it("fetches repos only from the globalReposNamespace", () => {
},
},
},
- }),
+ } as Partial),
,
);
expect(actions.repos.fetchRepoSummaries).toHaveBeenCalledWith("");
@@ -97,7 +98,7 @@ it("shows a warning if the cluster is not the default one", () => {
},
},
},
- }),
+ } as Partial),
,
);
expect(wrapper.find(Alert)).toIncludeText(
@@ -107,7 +108,9 @@ it("shows a warning if the cluster is not the default one", () => {
it("shows an error fetching a repo", () => {
const wrapper = mountWrapper(
- getStore({ repos: { errors: { fetch: new Error("boom!") } } as IPackageRepositoryState }),
+ getStore({
+ repos: { errors: { fetch: new Error("boom!") } } as IPackageRepositoryState,
+ } as Partial),
,
);
expect(wrapper.find(Alert)).toIncludeText("boom!");
@@ -115,7 +118,9 @@ it("shows an error fetching a repo", () => {
it("shows an error deleting a repo", () => {
const wrapper = mountWrapper(
- getStore({ repos: { errors: { delete: new Error("boom!") } } as IPackageRepositoryState }),
+ getStore({
+ repos: { errors: { delete: new Error("boom!") } } as IPackageRepositoryState,
+ } as Partial),
,
);
expect(wrapper.find(Alert)).toIncludeText("boom!");
@@ -159,7 +164,7 @@ describe("global and namespaced repositories", () => {
...initialState.clusters,
clusters: {
[currentCluster]: {
- ...initialState.clusters.clusters[currentCluster],
+ ...initialState.clusters.clusters[initialState.clusters.currentCluster],
currentNamespace: "other",
},
},
@@ -167,7 +172,7 @@ describe("global and namespaced repositories", () => {
repos: {
reposSummaries: [globalRepo],
} as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
expect(wrapper.find(Table)).toHaveLength(1);
@@ -195,7 +200,7 @@ describe("global and namespaced repositories", () => {
repos: {
reposSummaries: [globalRepo],
} as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
@@ -228,7 +233,7 @@ describe("global and namespaced repositories", () => {
repos: {
reposSummaries: [globalRepo, namespacedRepo],
} as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
// A table per repository type
@@ -241,7 +246,7 @@ describe("global and namespaced repositories", () => {
repos: {
reposSummaries: [namespacedRepo],
} as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
expect(wrapper.find(Table).find(Link).prop("to")).toEqual(
@@ -255,7 +260,7 @@ describe("global and namespaced repositories", () => {
repos: {
reposSummaries: [namespacedRepo],
} as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
act(() => {
@@ -271,7 +276,7 @@ describe("global and namespaced repositories", () => {
repos: {
reposSummaries: [namespacedRepo],
} as IPackageRepositoryState,
- }),
+ } as Partial),
,
);
act(() => {
diff --git a/dashboard/src/components/DeploymentForm/DeploymentForm.test.tsx b/dashboard/src/components/DeploymentForm/DeploymentForm.test.tsx
index 7885ff5d9f0..08105ce8333 100644
--- a/dashboard/src/components/DeploymentForm/DeploymentForm.test.tsx
+++ b/dashboard/src/components/DeploymentForm/DeploymentForm.test.tsx
@@ -76,7 +76,7 @@ it("fetches the available versions", () => {
actions.availablepackages.fetchAndSelectAvailablePackageDetail = fetchAvailablePackageVersions;
mountWrapper(
- getStore({}),
+ getStore({} as Partial),
@@ -102,7 +102,7 @@ describe("renders an error", () => {
selected: { ...defaultSelectedPkg },
},
apps: { error: new Error("wrong format!") },
- }),
+ } as Partial),
@@ -121,7 +121,7 @@ describe("renders an error", () => {
getStore({
packages: { selected: { ...defaultSelectedPkg, error: new FetchError("not found") } },
apps: { error: undefined },
- }),
+ } as Partial),
diff --git a/dashboard/src/components/DeploymentFormBody/AdvancedDeploymentForm.test.tsx b/dashboard/src/components/DeploymentFormBody/AdvancedDeploymentForm.test.tsx
index fb3d83a0b4d..a644cb64656 100644
--- a/dashboard/src/components/DeploymentFormBody/AdvancedDeploymentForm.test.tsx
+++ b/dashboard/src/components/DeploymentFormBody/AdvancedDeploymentForm.test.tsx
@@ -4,6 +4,7 @@
import AceEditor from "react-ace";
import { SupportedThemes } from "shared/Config";
import { defaultStore, getStore, mountWrapper } from "shared/specs/mountWrapper";
+import { IStoreState } from "shared/types";
import AdvancedDeploymentForm from "./AdvancedDeploymentForm";
const defaultProps = {
@@ -25,7 +26,7 @@ it("sets light theme by default", () => {
it("changes theme", () => {
const wrapper = mountWrapper(
- getStore({ config: { theme: SupportedThemes.dark } }),
+ getStore({ config: { theme: SupportedThemes.dark } } as Partial),
,
);
expect(wrapper.find(AceEditor).prop("theme")).toBe("solarized_dark");
diff --git a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/CustomFormParam.test.tsx b/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/CustomFormParam.test.tsx
index 2a5bff1d3bc..cfaa63bf111 100644
--- a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/CustomFormParam.test.tsx
+++ b/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/CustomFormParam.test.tsx
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import { getStore, mountWrapper } from "shared/specs/mountWrapper";
-import { IBasicFormParam } from "shared/types";
+import { IBasicFormParam, IStoreState } from "shared/types";
import { CustomComponent } from "../../../RemoteComponent";
import CustomFormComponentLoader from "./CustomFormParam";
@@ -71,7 +71,7 @@ it("should render the remote component with the URL if set in the config", () =>
const wrapper = mountWrapper(
getStore({
config: { remoteComponentsUrl: "www.thiswebsite.com" },
- }),
+ } as Partial),
,
);
expect(wrapper.find(CustomComponent).prop("url")).toBe("www.thiswebsite.com");
diff --git a/dashboard/src/components/DeploymentFormBody/Differential.test.tsx b/dashboard/src/components/DeploymentFormBody/Differential.test.tsx
index f01bdf48581..00c4a6721ff 100644
--- a/dashboard/src/components/DeploymentFormBody/Differential.test.tsx
+++ b/dashboard/src/components/DeploymentFormBody/Differential.test.tsx
@@ -4,6 +4,7 @@
import ReactDiffViewer from "react-diff-viewer";
import { SupportedThemes } from "shared/Config";
import { defaultStore, getStore, mountWrapper } from "shared/specs/mountWrapper";
+import { IStoreState } from "shared/types";
import Differential from "./Differential";
it("should render a diff between two strings", () => {
@@ -37,7 +38,7 @@ it("sets light theme by default", () => {
it("changes theme", () => {
const wrapper = mountWrapper(
- getStore({ config: { theme: SupportedThemes.dark } }),
+ getStore({ config: { theme: SupportedThemes.dark } } as Partial),
empty} />,
);
expect(wrapper.find(ReactDiffViewer).prop("useDarkTheme")).toBe(true);
diff --git a/dashboard/src/components/HeadManager/HeadManager.test.tsx b/dashboard/src/components/HeadManager/HeadManager.test.tsx
index 377234c20e2..13d301154c0 100644
--- a/dashboard/src/components/HeadManager/HeadManager.test.tsx
+++ b/dashboard/src/components/HeadManager/HeadManager.test.tsx
@@ -7,6 +7,7 @@ import { Helmet } from "react-helmet";
import * as ReactRedux from "react-redux";
import { SupportedThemes } from "shared/Config";
import { defaultStore, getStore, mountWrapper } from "shared/specs/mountWrapper";
+import { IStoreState } from "shared/types";
import HeadManager from "./HeadManager";
let spyOnUseDispatch: jest.SpyInstance;
@@ -40,7 +41,7 @@ it("should use the light theme by default", () => {
it("should use the dark theme", () => {
mountWrapper(
- getStore({ config: { theme: SupportedThemes.dark } }),
+ getStore({ config: { theme: SupportedThemes.dark } } as Partial),
<>>
,
diff --git a/dashboard/src/components/Header/ContextSelector.test.tsx b/dashboard/src/components/Header/ContextSelector.test.tsx
index a8f82a05f08..f28207c3613 100644
--- a/dashboard/src/components/Header/ContextSelector.test.tsx
+++ b/dashboard/src/components/Header/ContextSelector.test.tsx
@@ -13,6 +13,7 @@ import * as ReactRouter from "react-router";
import { Router } from "react-router-dom";
import { IClustersState } from "reducers/cluster";
import { defaultStore, getStore, initialState, mountWrapper } from "shared/specs/mountWrapper";
+import { IStoreState } from "shared/types";
import ContextSelector from "./ContextSelector";
let spyOnUseDispatch: jest.SpyInstance;
@@ -97,14 +98,14 @@ it("shows the current cluster", () => {
},
},
} as IClustersState;
- const wrapper = mountWrapper(getStore({ clusters }), );
+ const wrapper = mountWrapper(getStore({ clusters } as Partial), );
expect(wrapper.find("select").at(0).prop("value")).toBe("bar");
});
it("shows the current namespace", () => {
const clusters = cloneDeep(initialState.clusters);
clusters.clusters[clusters.currentCluster].currentNamespace = "other";
- const wrapper = mountWrapper(getStore({ clusters }), );
+ const wrapper = mountWrapper(getStore({ clusters } as Partial), );
expect(wrapper.find("select").at(1).prop("value")).toBe("other");
});
@@ -141,7 +142,7 @@ it("submits the form to create a new namespace with custom labels", () => {
config.createNamespaceLabels = {
"managed-by": "kubeapps",
};
- const wrapper = mountWrapper(getStore({ config }), );
+ const wrapper = mountWrapper(getStore({ config } as Partial), );
const modalButton = wrapper.find(".flat-btn").first();
act(() => {
@@ -169,7 +170,7 @@ it("shows an error creating a namespace", () => {
const clusters = cloneDeep(initialState.clusters);
clusters.clusters[clusters.currentCluster].error = { error: new Error("Boom"), action: "create" };
- const wrapper = mountWrapper(getStore({ clusters }), );
+ const wrapper = mountWrapper(getStore({ clusters } as Partial), );
const modalButton = wrapper.find(".flat-btn").first();
act(() => {
@@ -192,7 +193,7 @@ it("disables the create button if not allowed", () => {
},
},
} as IClustersState;
- const wrapper = mountWrapper(getStore({ clusters }), );
+ const wrapper = mountWrapper(getStore({ clusters } as Partial), );
expect(wrapper.find(".flat-btn").first()).toBeDisabled();
});
@@ -207,7 +208,7 @@ it("disables the change context button if namespace is not loaded yet", () => {
},
},
} as IClustersState;
- const wrapper = mountWrapper(getStore({ clusters }), );
+ const wrapper = mountWrapper(getStore({ clusters } as Partial), );
expect(wrapper.find(CdsButton).filterWhere(b => b.text() === "Change Context")).toBeDisabled();
});
diff --git a/dashboard/src/components/Header/Header.test.tsx b/dashboard/src/components/Header/Header.test.tsx
index da901fd6df7..426389d757d 100644
--- a/dashboard/src/components/Header/Header.test.tsx
+++ b/dashboard/src/components/Header/Header.test.tsx
@@ -4,7 +4,8 @@
import actions from "actions";
import * as ReactRedux from "react-redux";
import { NavLink } from "react-router-dom";
-import { getStore, mountWrapper } from "shared/specs/mountWrapper";
+import { getStore, initialState, mountWrapper } from "shared/specs/mountWrapper";
+import { IStoreState } from "shared/types";
import { app } from "shared/url";
import Header from "./Header";
@@ -25,18 +26,22 @@ afterEach(() => {
});
const defaultState = {
+ ...initialState,
clusters: {
+ ...initialState.clusters,
currentCluster: "default",
clusters: {
+ ...initialState.clusters.clusters,
default: {
+ ...initialState.clusters.clusters[initialState.clusters.currentCluster],
currentNamespace: "default",
namespaces: ["default", "other"],
},
},
},
- auth: { authenticated: true },
- config: { appVersion: "v2.0.0" },
-};
+ auth: { ...initialState.auth, authenticated: true },
+ config: { ...initialState.config, appVersion: "v2.0.0" },
+} as IStoreState;
it("fetch namespaces and the ability to create them", () => {
mountWrapper(getStore(defaultState), );
@@ -63,7 +68,7 @@ it("should skip the links if it's not authenticated", () => {
getStore({
...defaultState,
auth: { authenticated: false },
- }),
+ } as Partial),
,
);
const items = wrapper.find(".nav-link");
@@ -75,15 +80,18 @@ it("should skip the links if the namespace info is not available", () => {
getStore({
...defaultState,
clusters: {
+ ...initialState.clusters,
currentCluster: "default",
clusters: {
+ ...initialState.clusters.clusters,
default: {
+ ...initialState.clusters.clusters[initialState.clusters.currentCluster],
currentNamespace: "",
namespaces: [],
},
},
},
- }),
+ } as Partial),
,
);
const items = wrapper.find(".nav-link");
diff --git a/dashboard/src/components/Header/Menu.test.tsx b/dashboard/src/components/Header/Menu.test.tsx
index da6bbf911f2..bf311b9a3fb 100644
--- a/dashboard/src/components/Header/Menu.test.tsx
+++ b/dashboard/src/components/Header/Menu.test.tsx
@@ -94,7 +94,7 @@ describe("theme switcher toggle", () => {
it("toggle checked if dark theme is configured", () => {
const wrapper = mountWrapper(
- getStore({ config: { theme: SupportedThemes.dark } }),
+ getStore({ config: { theme: SupportedThemes.dark } } as Partial),
,
);
const toggle = wrapper.find("cds-toggle input");
diff --git a/dashboard/src/components/LoginForm/LoginForm.test.tsx b/dashboard/src/components/LoginForm/LoginForm.test.tsx
index e6daf0f8136..4ca62ecfa34 100644
--- a/dashboard/src/components/LoginForm/LoginForm.test.tsx
+++ b/dashboard/src/components/LoginForm/LoginForm.test.tsx
@@ -5,7 +5,9 @@ import LoadingWrapper from "components/LoadingWrapper";
import { Location } from "history";
import { act } from "react-dom/test-utils";
import { MemoryRouter, Redirect } from "react-router-dom";
+import { IConfigState } from "reducers/config";
import { defaultStore, getStore, mountWrapper } from "shared/specs/mountWrapper";
+import { IStoreState } from "shared/types";
import LoginForm from "./LoginForm";
import OAuthLogin from "./OauthLogin";
import TokenLogin from "./TokenLogin";
@@ -191,9 +193,12 @@ describe("oauth login form", () => {
...defaultStore,
config: {
authProxyEnabled: true,
- },
+ } as IConfigState,
};
- const wrapper = mountWrapper(getStore({ ...state }), );
+ const wrapper = mountWrapper(
+ getStore({ ...state } as Partial),
+ ,
+ );
expect(props.checkCookieAuthentication).toHaveBeenCalled();
expect(wrapper.find(OAuthLogin)).toExist();
expect(wrapper.find("a").findWhere(a => a.prop("href") === props.oauthLoginURI)).toExist();
@@ -204,7 +209,7 @@ describe("oauth login form", () => {
...defaultStore,
config: {
authProxyEnabled: true,
- },
+ } as IConfigState,
};
const props2 = {
...props,
@@ -212,7 +217,10 @@ describe("oauth login form", () => {
then: jest.fn(() => false),
}),
};
- const wrapper = mountWrapper(getStore({ ...state }), );
+ const wrapper = mountWrapper(
+ getStore({ ...state } as Partial),
+ ,
+ );
expect(wrapper.find(LoadingWrapper)).toExist();
expect(wrapper.find(OAuthLogin)).not.toExist();
});
diff --git a/dashboard/src/components/OperatorInstance/OperatorInstance.test.tsx b/dashboard/src/components/OperatorInstance/OperatorInstance.test.tsx
index 74c225527bd..154c27354f9 100644
--- a/dashboard/src/components/OperatorInstance/OperatorInstance.test.tsx
+++ b/dashboard/src/components/OperatorInstance/OperatorInstance.test.tsx
@@ -16,7 +16,7 @@ import ApplicationStatusContainer from "containers/ApplicationStatusContainer";
import { act } from "react-dom/test-utils";
import * as ReactRedux from "react-redux";
import { defaultStore, getStore, initialState, mountWrapper } from "shared/specs/mountWrapper";
-import { FetchError } from "shared/types";
+import { FetchError, IStoreState } from "shared/types";
import OperatorInstance from "./OperatorInstance";
const defaultProps = {
@@ -71,10 +71,12 @@ afterEach(() => {
it("renders a fetch error", () => {
const wrapper = mountWrapper(
getStore({
+ ...initialState,
operators: {
- errors: { resource: { fetch: new FetchError("Boom!") } },
+ ...initialState.operators,
+ errors: { ...initialState.operators.errors, resource: { fetch: new FetchError("Boom!") } },
},
- }),
+ } as Partial),
,
);
expect(wrapper.find(Alert)).toIncludeText("Boom!");
@@ -88,7 +90,7 @@ it("renders an update error", () => {
csv: defaultCSV,
errors: { resource: { update: new Error("Boom!") } },
},
- }),
+ } as Partial),
,
);
expect(wrapper.find(Alert)).toIncludeText("Boom!");
@@ -101,7 +103,7 @@ it("renders an delete error", () => {
csv: defaultCSV,
errors: { resource: { update: new Error("Boom!") } },
},
- }),
+ } as Partial),
,
);
expect(wrapper.find(Alert)).toIncludeText("Boom!");
@@ -129,7 +131,7 @@ it("retrieves CSV and resource when mounted", () => {
it("renders a loading wrapper", () => {
const wrapper = mountWrapper(
- getStore({ operators: { isFetching: true } }),
+ getStore({ operators: { isFetching: true } } as Partial),
,
);
expect(wrapper.find(LoadingWrapper)).toExist();
@@ -137,7 +139,7 @@ it("renders a loading wrapper", () => {
it("renders all the subcomponents", () => {
const wrapper = mountWrapper(
- getStore({ operators: { csv: defaultCSV, resource } }),
+ getStore({ operators: { csv: defaultCSV, resource } } as Partial),
,
);
expect(wrapper.find(ApplicationStatusContainer)).toExist();
@@ -152,7 +154,7 @@ it("skips AppNotes and AppValues if the resource doesn't have spec or status", (
const wrapper = mountWrapper(
getStore({
operators: { csv: defaultCSV, resource: { ...resource, spec: undefined, status: undefined } },
- }),
+ } as Partial),
,
);
expect(wrapper.find(AppNotes)).not.toExist();
@@ -163,7 +165,7 @@ it("deletes the resource", async () => {
const deleteResource = jest.fn().mockReturnValue(true);
actions.operators.deleteResource = deleteResource;
const wrapper = mountWrapper(
- getStore({ operators: { csv: defaultCSV, resource } }),
+ getStore({ operators: { csv: defaultCSV, resource } } as Partial),
,
);
@@ -192,9 +194,13 @@ it("deletes the resource", async () => {
it("updates the state with the CRD resources", () => {
const wrapper = mountWrapper(
getStore({
- operators: { csv: defaultCSV, resource },
- kube: { kinds: { Foo: { apiVersion: "apps/v1", plural: "foos", namespaced: true } } },
- }),
+ ...initialState,
+ operators: { ...initialState.operators, csv: defaultCSV, resource },
+ kube: {
+ ...initialState.kube,
+ kinds: { Foo: { apiVersion: "apps/v1", plural: "foos", namespaced: true } },
+ },
+ } as Partial),
,
);
expect(wrapper.find(ResourceTabs).prop("deployments")).toMatchObject([
@@ -232,9 +238,13 @@ it("updates the state with all the resources if the CRD doesn't define any", ()
} as any;
const wrapper = mountWrapper(
getStore({
- operators: { csv: csvWithoutResource, resource },
- kube: { kinds: { Foo: { apiVersion: "apps/v1", plural: "foos", namespaced: true } } },
- }),
+ ...initialState,
+ operators: { ...initialState.operators, csv: csvWithoutResource, resource },
+ kube: {
+ ...initialState.kube,
+ kinds: { Foo: { apiVersion: "apps/v1", plural: "foos", namespaced: true } },
+ },
+ } as Partial),
,
);
const resources = wrapper.find(ResourceTabs).props();
diff --git a/dashboard/src/components/OperatorInstanceForm/OperatorInstanceForm.test.tsx b/dashboard/src/components/OperatorInstanceForm/OperatorInstanceForm.test.tsx
index 7a59babe1a3..db4e309631f 100644
--- a/dashboard/src/components/OperatorInstanceForm/OperatorInstanceForm.test.tsx
+++ b/dashboard/src/components/OperatorInstanceForm/OperatorInstanceForm.test.tsx
@@ -9,7 +9,7 @@ import OperatorHeader from "components/OperatorView/OperatorHeader";
import { act } from "react-dom/test-utils";
import * as ReactRedux from "react-redux";
import { defaultStore, getStore, initialState, mountWrapper } from "shared/specs/mountWrapper";
-import { FetchError, IClusterServiceVersion } from "shared/types";
+import { FetchError, IClusterServiceVersion, IStoreState } from "shared/types";
import OperatorInstanceForm, { IOperatorInstanceFormProps } from "./OperatorInstanceForm";
const defaultProps: IOperatorInstanceFormProps = {
@@ -57,10 +57,12 @@ afterEach(() => {
it("renders a fetch error", () => {
const wrapper = mountWrapper(
getStore({
+ ...initialState,
operators: {
- errors: { csv: { fetch: new FetchError("Boom!") } },
+ ...initialState.operators,
+ errors: { ...initialState.operators.errors, csv: { fetch: new FetchError("Boom!") } },
},
- }),
+ } as Partial),
,
);
expect(wrapper.find(Alert)).toIncludeText("Boom!");
@@ -74,7 +76,7 @@ it("renders a create error", () => {
csv: defaultCSV,
errors: { resource: { create: new Error("Boom!") } },
},
- }),
+ } as Partial),
,
);
expect(wrapper.find(Alert)).toIncludeText("Boom!");
@@ -93,7 +95,7 @@ it("retrieves CSV when mounted", () => {
it("retrieves the example values and the target CRD from the given CSV", () => {
const wrapper = mountWrapper(
- getStore({ operators: { csv: defaultCSV } }),
+ getStore({ operators: { csv: defaultCSV } } as Partial),
,
);
expect(wrapper.find(OperatorInstanceFormBody).props()).toMatchObject({
@@ -107,7 +109,7 @@ it("defaults to empty defaultValues if the examples annotation is not found", ()
metadata: {},
} as IClusterServiceVersion;
const wrapper = mountWrapper(
- getStore({ operators: { csv } }),
+ getStore({ operators: { csv } } as Partial),
,
);
expect(wrapper.find(OperatorInstanceFormBody).props()).toMatchObject({
@@ -124,7 +126,7 @@ it("should submit the form", () => {
const createResource = jest.fn();
actions.operators.createResource = createResource;
const wrapper = mountWrapper(
- getStore({ operators: { csv: defaultCSV } }),
+ getStore({ operators: { csv: defaultCSV } } as Partial),
,
);
diff --git a/dashboard/src/components/OperatorInstanceUpdateForm/OperatorInstanceUpdateForm.test.tsx b/dashboard/src/components/OperatorInstanceUpdateForm/OperatorInstanceUpdateForm.test.tsx
index 3f07637e6b6..0e80f9265ec 100644
--- a/dashboard/src/components/OperatorInstanceUpdateForm/OperatorInstanceUpdateForm.test.tsx
+++ b/dashboard/src/components/OperatorInstanceUpdateForm/OperatorInstanceUpdateForm.test.tsx
@@ -7,7 +7,7 @@ import OperatorInstanceFormBody from "components/OperatorInstanceFormBody/Operat
import OperatorHeader from "components/OperatorView/OperatorHeader";
import * as ReactRedux from "react-redux";
import { defaultStore, getStore, initialState, mountWrapper } from "shared/specs/mountWrapper";
-import { FetchError } from "shared/types";
+import { FetchError, IStoreState } from "shared/types";
import OperatorInstanceUpdateForm, {
IOperatorInstanceUpgradeFormProps,
} from "./OperatorInstanceUpdateForm";
@@ -91,7 +91,7 @@ it("set default and deployed values", () => {
resource: defaultResource,
csv: defaultCSV,
},
- }),
+ } as Partial),
,
);
expect(wrapper.find(OperatorInstanceFormBody).props()).toMatchObject({
@@ -108,12 +108,15 @@ it("renders an error if the resource is not populated", () => {
it("renders only an error if the resource is not found", () => {
const wrapper = mountWrapper(
getStore({
+ ...initialState,
operators: {
+ ...initialState.operators,
errors: {
+ ...initialState.operators.errors,
fetch: new FetchError("not found"),
},
},
- }),
+ } as Partial),
,
);
expect(wrapper.find(Alert)).toIncludeText("not found");
@@ -129,7 +132,7 @@ it("should submit the form", () => {
resource: defaultResource,
csv: defaultCSV,
},
- }),
+ } as Partial),
,
);
diff --git a/dashboard/src/components/OperatorList/OperatorList.test.tsx b/dashboard/src/components/OperatorList/OperatorList.test.tsx
index 8b513948df2..7388f662996 100644
--- a/dashboard/src/components/OperatorList/OperatorList.test.tsx
+++ b/dashboard/src/components/OperatorList/OperatorList.test.tsx
@@ -8,7 +8,7 @@ import SearchFilter from "components/SearchFilter/SearchFilter";
import { act } from "react-dom/test-utils";
import * as ReactRedux from "react-redux";
import { defaultStore, getStore, initialState, mountWrapper } from "shared/specs/mountWrapper";
-import { IPackageManifest } from "shared/types";
+import { IPackageManifest, IStoreState } from "shared/types";
import InfoCard from "../InfoCard/InfoCard";
import { AUTO_PILOT, BASIC_INSTALL } from "../OperatorView/OperatorCapabilityLevel";
import OLMNotFound from "./OLMNotFound";
@@ -71,7 +71,10 @@ const sampleSubscription = {
it("renders a LoadingWrapper if fetching", () => {
const wrapper = mountWrapper(
- getStore({ operators: { isFetcing: true } }),
+ getStore({
+ ...initialState,
+ operators: { ...initialState.operators, isFetcing: true },
+ } as Partial),
,
);
expect(wrapper.find(LoadingWrapper)).toExist();
@@ -89,7 +92,7 @@ it("renders an error", () => {
const wrapper = mountWrapper(
getStore({
operators: { isOLMInstalled: true, errors: { operator: { fetch: new Error("Forbidden!") } } },
- }),
+ } as Partial),
,
);
const error = wrapper.find(Alert).filterWhere(a => a.prop("theme") === "danger");
@@ -101,7 +104,7 @@ it("request operators if the OLM is installed", () => {
const getOperators = jest.fn();
actions.operators.getOperators = getOperators;
const wrapper = mountWrapper(
- getStore({ operators: { isOLMInstalled: true } }),
+ getStore({ operators: { isOLMInstalled: true } } as Partial),
,
);
wrapper.setProps({ namespace: "other" });
@@ -110,7 +113,9 @@ it("request operators if the OLM is installed", () => {
it("render the operator list", () => {
const wrapper = mountWrapper(
- getStore({ operators: { isOLMInstalled: true, operators: [sampleOperator] } }),
+ getStore({
+ operators: { isOLMInstalled: true, operators: [sampleOperator] },
+ } as Partial),
,
);
expect(wrapper.find(OLMNotFound)).not.toExist();
@@ -125,7 +130,7 @@ it("render the operator list with installed operators", () => {
operators: [sampleOperator],
subscriptions: [sampleSubscription],
},
- }),
+ } as Partial),
,
);
expect(wrapper.find(OLMNotFound)).not.toExist();
@@ -140,7 +145,9 @@ it("render the operator list with installed operators", () => {
it("render the operator list without installed operators", () => {
const wrapper = mountWrapper(
- getStore({ operators: { isOLMInstalled: true, operators: [sampleOperator] } }),
+ getStore({
+ operators: { isOLMInstalled: true, operators: [sampleOperator] },
+ } as Partial),
,
);
expect(wrapper.find(OLMNotFound)).not.toExist();
@@ -179,7 +186,7 @@ describe("filter operators", () => {
const wrapper = mountWrapper(
getStore({
operators: { isOLMInstalled: true, operators: [sampleOperator, sampleOperator2] },
- }),
+ } as Partial),
,
);
expect(wrapper.find(InfoCard).length).toBe(2);
@@ -196,7 +203,7 @@ describe("filter operators", () => {
const wrapper = mountWrapper(
getStore({
operators: { isOLMInstalled: true, operators: [sampleOperator, sampleOperator2] },
- }),
+ } as Partial),
,
);
const operator = wrapper.find(InfoCard);
@@ -208,7 +215,7 @@ describe("filter operators", () => {
const wrapper = mountWrapper(
getStore({
operators: { isOLMInstalled: true, operators: [sampleOperator, sampleOperator2] },
- }),
+ } as Partial),
,
);
expect(wrapper.find(".label-info").text()).toBe("Provider: kubeapps,%20inc ");
@@ -218,7 +225,7 @@ describe("filter operators", () => {
const wrapper = mountWrapper(
getStore({
operators: { isOLMInstalled: true, operators: [sampleOperator, sampleOperator2] },
- }),
+ } as Partial),
,
);
expect(wrapper.find(InfoCard)).not.toExist();
@@ -229,7 +236,7 @@ describe("filter operators", () => {
const wrapper = mountWrapper(
getStore({
operators: { isOLMInstalled: true, operators: [sampleOperator, sampleOperator2] },
- }),
+ } as Partial),
,
);
const operator = wrapper.find(InfoCard);
@@ -241,7 +248,7 @@ describe("filter operators", () => {
const wrapper = mountWrapper(
getStore({
operators: { isOLMInstalled: true, operators: [sampleOperator, sampleOperator2] },
- }),
+ } as Partial),
,
);
const operator = wrapper.find(InfoCard);
diff --git a/dashboard/src/components/OperatorNew/OperatorNew.test.tsx b/dashboard/src/components/OperatorNew/OperatorNew.test.tsx
index 14632bd280a..4faa220e31d 100644
--- a/dashboard/src/components/OperatorNew/OperatorNew.test.tsx
+++ b/dashboard/src/components/OperatorNew/OperatorNew.test.tsx
@@ -6,6 +6,7 @@ import actions from "actions";
import Alert from "components/js/Alert";
import * as ReactRedux from "react-redux";
import { defaultStore, getStore, initialState, mountWrapper } from "shared/specs/mountWrapper";
+import { IStoreState } from "shared/types";
import OperatorNew from "./OperatorNew";
const defaultProps = {
@@ -79,7 +80,7 @@ it("calls getOperator when mounting the component", () => {
it("parses the default channel when receiving the operator", () => {
const wrapper = mountWrapper(
- getStore({ operators: { operator: defaultOperator } }),
+ getStore({ operators: { operator: defaultOperator } } as Partial),
,
);
const input = wrapper.find("#operator-channel-beta");
@@ -89,7 +90,9 @@ it("parses the default channel when receiving the operator", () => {
it("renders a fetch error if present", () => {
const wrapper = mountWrapper(
- getStore({ operators: { errors: { operator: { fetch: new Error("Boom") } } } }),
+ getStore({
+ operators: { errors: { operator: { fetch: new Error("Boom") } } },
+ } as Partial),
,
);
expect(wrapper.find(Alert)).toIncludeText("Boom");
@@ -97,7 +100,9 @@ it("renders a fetch error if present", () => {
it("renders a create error if present", () => {
const wrapper = mountWrapper(
- getStore({ operators: { errors: { operator: { create: new Error("Boom") } } } }),
+ getStore({
+ operators: { errors: { operator: { create: new Error("Boom") } } },
+ } as Partial),
,
);
expect(wrapper.find(Alert)).toIncludeText("Boom");
@@ -105,12 +110,17 @@ it("renders a create error if present", () => {
it("shows an error if the operator doesn't have any channel defined", () => {
const operator = {
+ ...initialState.operators.operator,
status: {
+ ...initialState.operators.operator?.status,
channels: [],
},
};
const wrapper = mountWrapper(
- getStore({ operators: { operator } }),
+ getStore({
+ ...initialState,
+ operators: { ...initialState.operators, operator },
+ } as Partial),
,
);
expect(wrapper.find(Alert)).toIncludeText(
@@ -120,7 +130,7 @@ it("shows an error if the operator doesn't have any channel defined", () => {
it("disables the submit button if the operators ns is selected", () => {
const wrapper = mountWrapper(
- getStore({ operators: { operator: defaultOperator } }),
+ getStore({ operators: { operator: defaultOperator } } as Partial),
,
);
expect(wrapper.find(CdsButton)).toBeDisabled();
@@ -132,7 +142,7 @@ it("disables the submit button if the operators ns is selected", () => {
it("deploys an operator", async () => {
const createOperator = jest.fn().mockReturnValue(true);
actions.operators.createOperator = createOperator;
- const store = getStore({ operators: { operator: defaultOperator } });
+ const store = getStore({ operators: { operator: defaultOperator } } as Partial);
const wrapper = mountWrapper(store, );
const onSubmit = wrapper.find("form").prop("onSubmit") as () => Promise;
diff --git a/dashboard/src/components/OperatorSummary/OperatorSummary.test.tsx b/dashboard/src/components/OperatorSummary/OperatorSummary.test.tsx
index a5aba83f60a..8d87a7e8405 100644
--- a/dashboard/src/components/OperatorSummary/OperatorSummary.test.tsx
+++ b/dashboard/src/components/OperatorSummary/OperatorSummary.test.tsx
@@ -5,6 +5,7 @@ import Alert from "components/js/Alert";
import LoadingWrapper from "components/LoadingWrapper/LoadingWrapper";
import { cloneDeep } from "lodash";
import { getStore, mountWrapper } from "shared/specs/mountWrapper";
+import { IStoreState } from "shared/types";
import OperatorSummary from "./OperatorSummary";
const defaultOperator = {
@@ -39,7 +40,10 @@ const defaultOperator = {
} as any;
it("shows a loading wrapper", () => {
- const wrapper = mountWrapper(getStore({ operators: { isFetching: true } }), );
+ const wrapper = mountWrapper(
+ getStore({ operators: { isFetching: true } } as Partial),
+ ,
+ );
expect(wrapper.find(LoadingWrapper)).toExist();
});
@@ -47,7 +51,7 @@ it("shows an alert if the operator doesn't have a channel", () => {
const operatorWithoutChannel = cloneDeep(defaultOperator);
operatorWithoutChannel.status.channels = [];
const wrapper = mountWrapper(
- getStore({ operators: { operator: operatorWithoutChannel } }),
+ getStore({ operators: { operator: operatorWithoutChannel } } as Partial),
,
);
expect(wrapper.find(Alert)).toExist();
@@ -57,7 +61,7 @@ it("doesn't fail with missing info", () => {
const operatorWithoutAnnotations = cloneDeep(defaultOperator);
delete operatorWithoutAnnotations.status.channels[0].currentCSVDesc.annotations;
const wrapper = mountWrapper(
- getStore({ operators: { operator: operatorWithoutAnnotations } }),
+ getStore({ operators: { operator: operatorWithoutAnnotations } } as Partial),
,
);
expect(wrapper.find(".left-menu")).toExist();
@@ -65,7 +69,7 @@ it("doesn't fail with missing info", () => {
it("shows all the operator info", () => {
const wrapper = mountWrapper(
- getStore({ operators: { operator: defaultOperator } }),
+ getStore({ operators: { operator: defaultOperator } } as Partial),
,
);
expect(wrapper.find(".left-menu-subsection")).toHaveLength(5);
diff --git a/dashboard/src/components/OperatorView/OperatorView.test.tsx b/dashboard/src/components/OperatorView/OperatorView.test.tsx
index e79bb4b576c..0c26b20ca24 100644
--- a/dashboard/src/components/OperatorView/OperatorView.test.tsx
+++ b/dashboard/src/components/OperatorView/OperatorView.test.tsx
@@ -6,6 +6,7 @@ import actions from "actions";
import Alert from "components/js/Alert";
import * as ReactRedux from "react-redux";
import { defaultStore, getStore, initialState, mountWrapper } from "shared/specs/mountWrapper";
+import { IStoreState } from "shared/types";
import OperatorDescription from "./OperatorDescription";
import OperatorView from "./OperatorView";
@@ -78,7 +79,7 @@ it("tries to get the CSV for the current operator", () => {
const getCSV = jest.fn();
actions.operators.getCSV = getCSV;
mountWrapper(
- getStore({ operators: { operator: defaultOperator } }),
+ getStore({ operators: { operator: defaultOperator } } as Partial),
,
);
@@ -91,7 +92,9 @@ it("tries to get the CSV for the current operator", () => {
it("shows an error if it exists", () => {
const wrapper = mountWrapper(
- getStore({ operators: { errors: { operator: { fetch: new Error("boom") } } } }),
+ getStore({
+ operators: { errors: { operator: { fetch: new Error("boom") } } },
+ } as Partial),
,
);
expect(wrapper.find(Alert)).toIncludeText("boom");
@@ -99,12 +102,17 @@ it("shows an error if it exists", () => {
it("shows an error if the operator doesn't have any channel defined", () => {
const operator = {
+ ...initialState.operators.operator,
status: {
+ ...initialState.operators.operator?.status,
channels: [],
},
};
const wrapper = mountWrapper(
- getStore({ operators: { operator } }),
+ getStore({
+ ...initialState,
+ operators: { ...initialState.operators, operator },
+ } as Partial),
,
);
expect(wrapper.find(Alert)).toIncludeText(
@@ -121,7 +129,7 @@ it("selects the default channel", () => {
},
};
const wrapper = mountWrapper(
- getStore({ operators: { operator } }),
+ getStore({ operators: { operator } } as Partial),
,
);
expect(wrapper.find(OperatorDescription).prop("description")).toEqual(
@@ -136,7 +144,7 @@ it("disables the Header deploy button if the subscription already exists", () =>
operator: defaultOperator,
subscriptions: [{ spec: { name: defaultOperator.metadata.name } }],
},
- }),
+ } as Partial),
,
);
wrapper.find(CdsButton).forEach(button => expect(button).toBeDisabled());
diff --git a/dashboard/src/components/SelectRepoForm/SelectRepoForm.test.tsx b/dashboard/src/components/SelectRepoForm/SelectRepoForm.test.tsx
index b90da53866c..9e9510aee5a 100644
--- a/dashboard/src/components/SelectRepoForm/SelectRepoForm.test.tsx
+++ b/dashboard/src/components/SelectRepoForm/SelectRepoForm.test.tsx
@@ -10,6 +10,7 @@ import { Plugin } from "gen/kubeappsapis/core/plugins/v1alpha1/plugins";
import * as ReactRedux from "react-redux";
import { IPackageRepositoryState } from "reducers/repos";
import { defaultStore, getStore, initialState, mountWrapper } from "shared/specs/mountWrapper";
+import { IStoreState } from "shared/types";
import SelectRepoForm from "./SelectRepoForm";
const defaultContext = {
@@ -63,7 +64,7 @@ it("should fetch repositories", () => {
it("should render a loading page if fetching", () => {
expect(
mountWrapper(
- getStore({ repos: { isFetching: true } }),
+ getStore({ repos: { isFetching: true } } as Partial),
,
).find("LoadingWrapper"),
).toExist();
@@ -71,7 +72,7 @@ it("should render a loading page if fetching", () => {
it("render an error if failed to request repos", () => {
const wrapper = mountWrapper(
- getStore({ repos: { errors: { fetch: new Error("boom") } } }),
+ getStore({ repos: { errors: { fetch: new Error("boom") } } } as Partial),
,
);
expect(wrapper.find(Alert)).toIncludeText("boom");
@@ -97,7 +98,9 @@ it("should select a repo", () => {
const props = { ...defaultContext, app: installedPackageDetail };
const wrapper = mountWrapper(
- getStore({ repos: { reposSummaries: [repo] } as IPackageRepositoryState }),
+ getStore({
+ repos: { reposSummaries: [repo] } as IPackageRepositoryState,
+ } as Partial),
,
);
wrapper.find("select").simulate("change", { target: { value: "default/bitnami" } });
diff --git a/dashboard/src/components/UpgradeForm/UpgradeForm.test.tsx b/dashboard/src/components/UpgradeForm/UpgradeForm.test.tsx
index 24ad744702f..6a1c9c764a3 100644
--- a/dashboard/src/components/UpgradeForm/UpgradeForm.test.tsx
+++ b/dashboard/src/components/UpgradeForm/UpgradeForm.test.tsx
@@ -2,12 +2,11 @@
// SPDX-License-Identifier: Apache-2.0
import actions from "actions";
-import { cloneDeep } from "lodash";
import DeploymentFormBody from "components/DeploymentFormBody/DeploymentFormBody";
import Alert from "components/js/Alert";
import LoadingWrapper from "components/LoadingWrapper/LoadingWrapper";
-import PackageVersionSelector from "components/PackageHeader/PackageVersionSelector";
import PackageHeader from "components/PackageHeader/PackageHeader";
+import PackageVersionSelector from "components/PackageHeader/PackageVersionSelector";
import {
AvailablePackageDetail,
AvailablePackageReference,
@@ -20,6 +19,7 @@ import {
VersionReference,
} from "gen/kubeappsapis/core/packages/v1alpha1/packages";
import { Plugin } from "gen/kubeappsapis/core/plugins/v1alpha1/plugins";
+import { cloneDeep } from "lodash";
import { act } from "react-dom/test-utils";
import * as ReactRedux from "react-redux";
import { MemoryRouter, Route } from "react-router-dom";
@@ -30,6 +30,7 @@ import {
FetchError,
IInstalledPackageState,
IPackageState,
+ IStoreState,
} from "shared/types";
import * as url from "shared/url";
import UpgradeForm from "./UpgradeForm";
@@ -121,11 +122,11 @@ describe("it behaves like a loading component", () => {
...defaultStore,
apps: {
isFetching: true,
- } as IPackageState,
+ } as IInstalledPackageState,
};
expect(
mountWrapper(
- getStore({ ...state }),
+ getStore({ ...state } as Partial),
,
@@ -144,7 +145,7 @@ describe("it behaves like a loading component", () => {
};
expect(
mountWrapper(
- getStore({ ...state }),
+ getStore({ ...state } as Partial),
,
@@ -164,7 +165,7 @@ describe("it behaves like a loading component", () => {
};
expect(
mountWrapper(
- getStore({ ...state }),
+ getStore({ ...state } as Partial),
,
@@ -186,7 +187,7 @@ describe("it behaves like a loading component", () => {
expect(
mountWrapper(
- getStore({ ...state }),
+ getStore({ ...state } as Partial),
,
@@ -210,7 +211,7 @@ it("fetches the available versions", () => {
} as IInstalledPackageState,
};
mountWrapper(
- getStore({ ...state }),
+ getStore({ ...state } as Partial),
,
@@ -240,7 +241,7 @@ it("hides the PackageVersionSelector in the PackageHeader", () => {
} as IPackageState,
};
const wrapper = mountWrapper(
- getStore({ ...state }),
+ getStore({ ...state } as Partial),
,
@@ -266,7 +267,7 @@ it("does not fetch the current package version if there is already one in the st
} as IPackageState,
};
mountWrapper(
- getStore({ ...state }),
+ getStore({ ...state } as Partial),
,
@@ -300,7 +301,7 @@ describe("renders an error", () => {
};
const wrapper = mountWrapper(
- getStore({ ...state }),
+ getStore({ ...state } as Partial),