From ba071655313df28cf9b73d4e02a618649092945b Mon Sep 17 00:00:00 2001 From: Tommy Chen Date: Sun, 2 Jun 2024 14:22:03 +0800 Subject: [PATCH] feat(helm): Add more options --- .changeset/funny-books-fetch.md | 5 + packages/helm/src/__fixtures__/crd/Chart.yaml | 2 + .../crd/crds/traefik.io_ingressroutes.yaml | 308 ++++++++++++++++++ .../src/__fixtures__/kube-version/Chart.yaml | 2 + .../kube-version/templates/pod.yaml | 6 + .../helm/src/__fixtures__/tests/Chart.yaml | 2 + .../templates/tests/test-connection.yaml | 12 + .../helm/src/__fixtures__/upgrade/Chart.yaml | 2 + .../__fixtures__/upgrade/templates/pod.yaml | 7 + packages/helm/src/__tests__/load.ts | 227 ++++++++++++- packages/helm/src/load.ts | 176 +++++++--- 11 files changed, 683 insertions(+), 66 deletions(-) create mode 100644 .changeset/funny-books-fetch.md create mode 100644 packages/helm/src/__fixtures__/crd/Chart.yaml create mode 100644 packages/helm/src/__fixtures__/crd/crds/traefik.io_ingressroutes.yaml create mode 100644 packages/helm/src/__fixtures__/kube-version/Chart.yaml create mode 100644 packages/helm/src/__fixtures__/kube-version/templates/pod.yaml create mode 100644 packages/helm/src/__fixtures__/tests/Chart.yaml create mode 100644 packages/helm/src/__fixtures__/tests/templates/tests/test-connection.yaml create mode 100644 packages/helm/src/__fixtures__/upgrade/Chart.yaml create mode 100644 packages/helm/src/__fixtures__/upgrade/templates/pod.yaml diff --git a/.changeset/funny-books-fetch.md b/.changeset/funny-books-fetch.md new file mode 100644 index 000000000..806924de2 --- /dev/null +++ b/.changeset/funny-books-fetch.md @@ -0,0 +1,5 @@ +--- +"@kosko/helm": minor +--- + +Add options: `insecureSkipTlsVerify`, `passCredentials`, `isUpgrade`, `kubeVersion`, `postRenderer`, `postRendererArgs`. diff --git a/packages/helm/src/__fixtures__/crd/Chart.yaml b/packages/helm/src/__fixtures__/crd/Chart.yaml new file mode 100644 index 000000000..6d034a045 --- /dev/null +++ b/packages/helm/src/__fixtures__/crd/Chart.yaml @@ -0,0 +1,2 @@ +name: crd +version: 0.0.0 diff --git a/packages/helm/src/__fixtures__/crd/crds/traefik.io_ingressroutes.yaml b/packages/helm/src/__fixtures__/crd/crds/traefik.io_ingressroutes.yaml new file mode 100644 index 000000000..1d2087d00 --- /dev/null +++ b/packages/helm/src/__fixtures__/crd/crds/traefik.io_ingressroutes.yaml @@ -0,0 +1,308 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: ingressroutes.traefik.io +spec: + group: traefik.io + names: + kind: IngressRoute + listKind: IngressRouteList + plural: ingressroutes + singular: ingressroute + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: IngressRoute is the CRD implementation of a Traefik HTTP Router. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: IngressRouteSpec defines the desired state of IngressRoute. + properties: + entryPoints: + description: |- + EntryPoints defines the list of entry point names to bind to. + Entry points have to be configured in the static configuration. + More info: https://doc.traefik.io/traefik/v3.0/routing/entrypoints/ + Default: all. + items: + type: string + type: array + routes: + description: Routes defines the list of routes. + items: + description: Route holds the HTTP route configuration. + properties: + kind: + description: |- + Kind defines the kind of the route. + Rule is the only supported kind. + enum: + - Rule + type: string + match: + description: |- + Match defines the router's rule. + More info: https://doc.traefik.io/traefik/v3.0/routing/routers/#rule + type: string + middlewares: + description: |- + Middlewares defines the list of references to Middleware resources. + More info: https://doc.traefik.io/traefik/v3.0/routing/providers/kubernetes-crd/#kind-middleware + items: + description: + MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: + Name defines the name of the referenced Middleware + resource. + type: string + namespace: + description: + Namespace defines the namespace of the referenced + Middleware resource. + type: string + required: + - name + type: object + type: array + priority: + description: |- + Priority defines the router's priority. + More info: https://doc.traefik.io/traefik/v3.0/routing/routers/#priority + type: integer + services: + description: |- + Services defines the list of Service. + It can contain any combination of TraefikService and/or reference to a Kubernetes Service. + items: + description: + Service defines an upstream HTTP service to proxy + traffic to. + properties: + kind: + description: Kind defines the kind of the Service. + enum: + - Service + - TraefikService + type: string + name: + description: |- + Name defines the name of the referenced Kubernetes Service or TraefikService. + The differentiation between the two is specified in the Kind field. + type: string + namespace: + description: + Namespace defines the namespace of the referenced + Kubernetes Service or TraefikService. + type: string + nativeLB: + description: |- + NativeLB controls, when creating the load-balancer, + whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP. + The Kubernetes Service itself does load-balance to the pods. + By default, NativeLB is false. + type: boolean + passHostHeader: + description: |- + PassHostHeader defines whether the client Host header is forwarded to the upstream Kubernetes Service. + By default, passHostHeader is true. + type: boolean + port: + anyOf: + - type: integer + - type: string + description: |- + Port defines the port of a Kubernetes Service. + This can be a reference to a named port. + x-kubernetes-int-or-string: true + responseForwarding: + description: + ResponseForwarding defines how Traefik forwards + the response from the upstream Kubernetes Service to + the client. + properties: + flushInterval: + description: |- + FlushInterval defines the interval, in milliseconds, in between flushes to the client while copying the response body. + A negative value means to flush immediately after each write to the client. + This configuration is ignored when ReverseProxy recognizes a response as a streaming response; + for such responses, writes are flushed to the client immediately. + Default: 100ms + type: string + type: object + scheme: + description: |- + Scheme defines the scheme to use for the request to the upstream Kubernetes Service. + It defaults to https when Kubernetes Service port is 443, http otherwise. + type: string + serversTransport: + description: |- + ServersTransport defines the name of ServersTransport resource to use. + It allows to configure the transport between Traefik and your servers. + Can only be used on a Kubernetes Service. + type: string + sticky: + description: |- + Sticky defines the sticky sessions configuration. + More info: https://doc.traefik.io/traefik/v3.0/routing/services/#sticky-sessions + properties: + cookie: + description: Cookie defines the sticky cookie configuration. + properties: + httpOnly: + description: + HTTPOnly defines whether the cookie + can be accessed by client-side APIs, such as + JavaScript. + type: boolean + maxAge: + description: |- + MaxAge indicates the number of seconds until the cookie expires. + When set to a negative number, the cookie expires immediately. + When set to zero, the cookie never expires. + type: integer + name: + description: Name defines the Cookie name. + type: string + sameSite: + description: |- + SameSite defines the same site policy. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite + type: string + secure: + description: + Secure defines whether the cookie + can only be transmitted over an encrypted connection + (i.e. HTTPS). + type: boolean + type: object + type: object + strategy: + description: |- + Strategy defines the load balancing strategy between the servers. + RoundRobin is the only supported value at the moment. + type: string + weight: + description: |- + Weight defines the weight and should only be specified when Name references a TraefikService object + (and to be precise, one that embeds a Weighted Round Robin). + type: integer + required: + - name + type: object + type: array + syntax: + description: |- + Syntax defines the router's rule syntax. + More info: https://doc.traefik.io/traefik/v3.0/routing/routers/#rulesyntax + type: string + required: + - kind + - match + type: object + type: array + tls: + description: |- + TLS defines the TLS configuration. + More info: https://doc.traefik.io/traefik/v3.0/routing/routers/#tls + properties: + certResolver: + description: |- + CertResolver defines the name of the certificate resolver to use. + Cert resolvers have to be configured in the static configuration. + More info: https://doc.traefik.io/traefik/v3.0/https/acme/#certificate-resolvers + type: string + domains: + description: |- + Domains defines the list of domains that will be used to issue certificates. + More info: https://doc.traefik.io/traefik/v3.0/routing/routers/#domains + items: + description: Domain holds a domain name with SANs. + properties: + main: + description: Main defines the main domain name. + type: string + sans: + description: + SANs defines the subject alternative domain + names. + items: + type: string + type: array + type: object + type: array + options: + description: |- + Options defines the reference to a TLSOption, that specifies the parameters of the TLS connection. + If not defined, the `default` TLSOption is used. + More info: https://doc.traefik.io/traefik/v3.0/https/tls/#tls-options + properties: + name: + description: |- + Name defines the name of the referenced TLSOption. + More info: https://doc.traefik.io/traefik/v3.0/routing/providers/kubernetes-crd/#kind-tlsoption + type: string + namespace: + description: |- + Namespace defines the namespace of the referenced TLSOption. + More info: https://doc.traefik.io/traefik/v3.0/routing/providers/kubernetes-crd/#kind-tlsoption + type: string + required: + - name + type: object + secretName: + description: + SecretName is the name of the referenced Kubernetes + Secret to specify the certificate details. + type: string + store: + description: |- + Store defines the reference to the TLSStore, that will be used to store certificates. + Please note that only `default` TLSStore can be used. + properties: + name: + description: |- + Name defines the name of the referenced TLSStore. + More info: https://doc.traefik.io/traefik/v3.0/routing/providers/kubernetes-crd/#kind-tlsstore + type: string + namespace: + description: |- + Namespace defines the namespace of the referenced TLSStore. + More info: https://doc.traefik.io/traefik/v3.0/routing/providers/kubernetes-crd/#kind-tlsstore + type: string + required: + - name + type: object + type: object + required: + - routes + type: object + required: + - metadata + - spec + type: object + served: true + storage: true diff --git a/packages/helm/src/__fixtures__/kube-version/Chart.yaml b/packages/helm/src/__fixtures__/kube-version/Chart.yaml new file mode 100644 index 000000000..fbf6ada45 --- /dev/null +++ b/packages/helm/src/__fixtures__/kube-version/Chart.yaml @@ -0,0 +1,2 @@ +name: kube-version +version: 0.0.0 diff --git a/packages/helm/src/__fixtures__/kube-version/templates/pod.yaml b/packages/helm/src/__fixtures__/kube-version/templates/pod.yaml new file mode 100644 index 000000000..1be7b9166 --- /dev/null +++ b/packages/helm/src/__fixtures__/kube-version/templates/pod.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Pod +metadata: + name: foo + annotations: + kube-version: "{{ .Capabilities.KubeVersion }}" diff --git a/packages/helm/src/__fixtures__/tests/Chart.yaml b/packages/helm/src/__fixtures__/tests/Chart.yaml new file mode 100644 index 000000000..18815d1bf --- /dev/null +++ b/packages/helm/src/__fixtures__/tests/Chart.yaml @@ -0,0 +1,2 @@ +name: test +version: 0.0.0 diff --git a/packages/helm/src/__fixtures__/tests/templates/tests/test-connection.yaml b/packages/helm/src/__fixtures__/tests/templates/tests/test-connection.yaml new file mode 100644 index 000000000..35afe9f98 --- /dev/null +++ b/packages/helm/src/__fixtures__/tests/templates/tests/test-connection.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: test-connection + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ["wget"] + restartPolicy: Never diff --git a/packages/helm/src/__fixtures__/upgrade/Chart.yaml b/packages/helm/src/__fixtures__/upgrade/Chart.yaml new file mode 100644 index 000000000..d5c8bc62a --- /dev/null +++ b/packages/helm/src/__fixtures__/upgrade/Chart.yaml @@ -0,0 +1,2 @@ +name: upgrade +version: 0.0.0 diff --git a/packages/helm/src/__fixtures__/upgrade/templates/pod.yaml b/packages/helm/src/__fixtures__/upgrade/templates/pod.yaml new file mode 100644 index 000000000..841249db3 --- /dev/null +++ b/packages/helm/src/__fixtures__/upgrade/templates/pod.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Pod +metadata: + name: foo + annotations: + is-install: "{{ .Release.IsInstall }}" + is-upgrade: "{{ .Release.IsUpgrade }}" diff --git a/packages/helm/src/__tests__/load.ts b/packages/helm/src/__tests__/load.ts index 7155c36f6..e6d11bd52 100644 --- a/packages/helm/src/__tests__/load.ts +++ b/packages/helm/src/__tests__/load.ts @@ -1,7 +1,10 @@ -import { ChartOptions, loadChart } from "../load"; +/// +import { loadChart } from "../load"; import { join } from "node:path"; -import { Manifest } from "@kosko/yaml"; import { spawn } from "@kosko/exec-utils"; +import tmp from "tmp-promise"; +import { readdir } from "node:fs/promises"; +import { Pod } from "kubernetes-models/v1/Pod"; jest.mock("@kosko/exec-utils", () => { const actual = jest.requireActual("@kosko/exec-utils"); @@ -70,29 +73,140 @@ test("values are specified", async () => { }); describe("includeCrds option", () => { - const baseOptions: ChartOptions = { - chart: "traefik", - repo: "https://helm.traefik.io/traefik", - version: "v10.6.1" - }; + test("should not include CRDs when includeCrds is not set", async () => { + const result = loadChart({ + chart: join(FIXTURE_DIR, "crd") + }); - function pickCrds(manifests: readonly Manifest[]): Manifest[] { - return manifests.filter((m) => m.kind === "CustomResourceDefinition"); - } + await expect(result()).resolves.toBeEmpty(); + }); - test("should not include CRDs when includeCrds is not set", async () => { - const result = loadChart(baseOptions); + test("should not include CRDs when includeCrds is false", async () => { + const result = loadChart({ + chart: join(FIXTURE_DIR, "crd"), + includeCrds: false + }); - expect(pickCrds(await result())).toHaveLength(0); + await expect(result()).resolves.toBeEmpty(); }); - test("should include CRDs when includeCrds is set", async () => { + test("should include CRDs when includeCrds is true", async () => { const result = loadChart({ - ...baseOptions, + chart: join(FIXTURE_DIR, "crd"), includeCrds: true }); - expect(pickCrds(await result())).not.toHaveLength(0); + await expect(result()).resolves.not.toBeEmpty(); + }); +}); + +describe("skipTests option", () => { + test("should include tests by default", async () => { + const result = loadChart({ + chart: join(FIXTURE_DIR, "tests") + }); + + await expect(result()).resolves.not.toBeEmpty(); + }); + + test("should include tests when skipTests is false", async () => { + const result = loadChart({ + chart: join(FIXTURE_DIR, "tests"), + skipTests: false + }); + + await expect(result()).resolves.not.toBeEmpty(); + }); + + test("should exclude tests when skipTests is true", async () => { + const result = loadChart({ + chart: join(FIXTURE_DIR, "tests"), + skipTests: true + }); + + await expect(result()).resolves.toBeEmpty(); + }); +}); + +describe("isUpgrade option", () => { + test("should not set is-upgrade by default", async () => { + const result = loadChart({ + chart: join(FIXTURE_DIR, "upgrade") + }); + const manifests = await result(); + + expect(manifests).toEqual([ + new Pod({ + metadata: { + name: "foo", + annotations: { + "is-upgrade": "false", + "is-install": "true" + } + } + }) + ]); + }); + + test("should not set is-upgrade if isUpgrade is false", async () => { + const result = loadChart({ + chart: join(FIXTURE_DIR, "upgrade"), + isUpgrade: false + }); + const manifests = await result(); + + expect(manifests).toEqual([ + new Pod({ + metadata: { + name: "foo", + annotations: { + "is-upgrade": "false", + "is-install": "true" + } + } + }) + ]); + }); + + test("should set is-upgrade if isUpgrade is true", async () => { + const result = loadChart({ + chart: join(FIXTURE_DIR, "upgrade"), + isUpgrade: true + }); + const manifests = await result(); + + expect(manifests).toEqual([ + new Pod({ + metadata: { + name: "foo", + annotations: { + "is-upgrade": "true", + "is-install": "false" + } + } + }) + ]); + }); +}); + +describe("kubeVersion is specified", () => { + test("should set kube-version when kubeVersion is specified", async () => { + const result = loadChart({ + chart: join(FIXTURE_DIR, "kube-version"), + kubeVersion: "1.22.0" + }); + const manifests = await result(); + + expect(manifests).toEqual([ + new Pod({ + metadata: { + name: "foo", + annotations: { + "kube-version": "v1.22.0" + } + } + }) + ]); }); }); @@ -117,3 +231,84 @@ test("OCI chart", async () => { await expect(result()).resolves.toMatchSnapshot(); }); + +describe("when cache is disabled", () => { + let tmpDir: tmp.DirectoryResult; + + beforeEach(async () => { + tmpDir = await tmp.dir({ unsafeCleanup: true }); + }); + + afterEach(async () => { + await tmpDir.cleanup(); + }); + + test("should not cache chart", async () => { + const result = loadChart({ + chart: "prometheus", + repo: "https://prometheus-community.github.io/helm-charts", + version: "13.6.0", + cache: { enabled: false, dir: tmpDir.path } + }); + + await expect(result()).toResolve(); + + // Cache directory should be empty + await expect(readdir(tmpDir.path)).resolves.toBeEmpty(); + }); +}); + +describe("when cache directory is specified", () => { + let tmpDir: tmp.DirectoryResult; + + beforeEach(async () => { + tmpDir = await tmp.dir({ unsafeCleanup: true }); + }); + + afterEach(async () => { + await tmpDir.cleanup(); + }); + + test("should store cache in the specified directory", async () => { + const result = loadChart({ + chart: "prometheus", + repo: "https://prometheus-community.github.io/helm-charts", + version: "13.6.0", + cache: { dir: tmpDir.path } + }); + + await expect(result()).toResolve(); + + // Cache directory should not be empty + await expect(readdir(tmpDir.path)).resolves.not.toBeEmpty(); + }); +}); + +describe("when KOSKO_HELM_CACHE_DIR is set", () => { + let tmpDir: tmp.DirectoryResult; + let origEnv: string | undefined; + + beforeEach(async () => { + tmpDir = await tmp.dir({ unsafeCleanup: true }); + origEnv = process.env.KOSKO_HELM_CACHE_DIR; + process.env.KOSKO_HELM_CACHE_DIR = tmpDir.path; + }); + + afterEach(async () => { + await tmpDir.cleanup(); + process.env.KOSKO_HELM_CACHE_DIR = origEnv; + }); + + test("should store cache in the specified directory", async () => { + const result = loadChart({ + chart: "prometheus", + repo: "https://prometheus-community.github.io/helm-charts", + version: "13.6.0" + }); + + await expect(result()).toResolve(); + + // Cache directory should not be empty + await expect(readdir(tmpDir.path)).resolves.not.toBeEmpty(); + }); +}); diff --git a/packages/helm/src/load.ts b/packages/helm/src/load.ts index 1873a3d24..ae9688941 100644 --- a/packages/helm/src/load.ts +++ b/packages/helm/src/load.ts @@ -12,11 +12,37 @@ import { getErrorCode } from "@kosko/common-utils"; import getCacheDir from "cachedir"; import { createHash } from "node:crypto"; import { join } from "node:path"; -import { Stats } from "node:fs"; +import { env } from "node:process"; const FILE_EXIST_ERROR_CODES = new Set(["EEXIST", "ENOTEMPTY"]); -const cacheDir = getCacheDir("kosko-helm"); +const defaultCacheDir = getCacheDir("kosko-helm"); + +/** + * @public + */ +export interface CacheOptions { + /** + * When cache is enabled, the chart is pulled and stored in the cache + * directory. Although Helm has its own cache, implementing our own cache + * is faster. Local charts are never cached. + * + * @defaultValue `true` + */ + enabled?: boolean; + + /** + * The path of the cache directory. You can also use `KOSKO_HELM_CACHE_DIR` + * environment variable to set the cache directory. This option always takes + * precedence over the environment variable. + * + * @defaultValue + * - Linux: `$XDG_CACHE_HOME/kosko-helm` or `~/.cache/kosko-helm` + * - macOS: `~/Library/Caches/kosko-helm` + * - Windows: `$LOCALAPPDATA/kosko-helm` or `~/AppData/Local/kosko-helm` + */ + dir?: string; +} /** * @public @@ -43,6 +69,11 @@ export interface PullOptions { */ devel?: boolean; + /** + * Skip tls certificate checks for the chart download. + */ + insecureSkipTlsVerify?: boolean; + /** * Identify HTTPS client using this SSL key file. */ @@ -55,6 +86,11 @@ export interface PullOptions { */ keyring?: string; + /** + * Pass credentials to all domains. + */ + passCredentials?: boolean; + /** * Chart repository password where to locate the requested chart. */ @@ -80,6 +116,11 @@ export interface PullOptions { * latest version is used. */ version?: string; + + /** + * Cache options. + */ + cache?: CacheOptions; } /** @@ -111,6 +152,21 @@ export interface TemplateOptions { */ generateName?: boolean; + /** + * Include CRDs in the templated output. + */ + includeCrds?: boolean; + + /** + * Set `.Release.IsUpgrade` instead of `.Release.IsInstall`. + */ + isUpgrade?: boolean; + + /** + * Kubernetes version used for `Capabilities.KubeVersion`. + */ + kubeVersion?: string; + /** * Specify template used to name the release. */ @@ -127,9 +183,18 @@ export interface TemplateOptions { noHooks?: boolean; /** - * Include CRDs in the templated output. + * The path to an executable to be used for post rendering. If it exists in + * `$PATH`, the binary will be used, otherwise it will try to look for the + * executable at the given path. */ - includeCrds?: boolean; + postRenderer?: string; + + /** + * Arguments to the post-renderer. + * + * @defaultValue `[]` + */ + postRendererArgs?: string[]; /** * Skip tests from templated output. @@ -149,11 +214,16 @@ export interface TemplateOptions { values?: unknown; } +function removeBase64Padding(str: string): string { + const index = str.indexOf("="); + return index === -1 ? str : str.substring(0, index); +} + function hashPullOptions(options: PullOptions): string { const hash = createHash("sha1"); hash.write( - JSON.stringify({ + stringify({ chart: options.chart, devel: options.devel, repo: options.repo, @@ -161,7 +231,7 @@ function hashPullOptions(options: PullOptions): string { }) ); - return hash.digest("base64url").replace(/=+$/, ""); + return removeBase64Padding(hash.digest("base64url")); } async function runHelm(args: readonly string[]) { @@ -176,21 +246,25 @@ async function runHelm(args: readonly string[]) { } } -async function maybeStat(path: string): Promise { +async function chartExists(chart: string): Promise { try { - return await stat(path); + // Check if `Chart.yaml` exists + const stats = await stat(join(chart, "Chart.yaml")); + return stats.isFile(); } catch (err) { if (getErrorCode(err) !== "ENOENT") throw err; + return false; } } -async function chartExists(chart: string): Promise { - const stats = await maybeStat(join(chart, "Chart.yaml")); - return stats?.isFile() ?? false; -} - async function isLocalChart(options: PullOptions): Promise { - return !options.repo && (await chartExists(options.chart)); + // If repo is set, it's a remote chart + if (options.repo) return false; + + // OCI charts are always remote + if (options.chart.startsWith("oci://")) return false; + + return chartExists(options.chart); } function getChartBaseName(chart: string): string { @@ -205,8 +279,10 @@ function getPullArgs(options: PullOptions): string[] { ...stringArg("ca-file", options.caFile), ...stringArg("cert-file", options.certFile), ...booleanArg("devel", options.devel), + ...booleanArg("insecure-skip-tls-verify", options.insecureSkipTlsVerify), ...stringArg("key-file", options.keyFile), ...stringArg("keyring", options.keyring), + ...booleanArg("pass-credentials", options.passCredentials), ...stringArg("password", options.password), ...stringArg("repo", options.repo), ...stringArg("username", options.username), @@ -215,17 +291,27 @@ function getPullArgs(options: PullOptions): string[] { ]; } -async function pullChart(options: PullOptions): Promise { +async function pullChart( + options: PullOptions +): Promise> { + // Skip cache if disabled + if (options.cache?.enabled === false) { + return options; + } + + // Skip cache if it's a local chart if (await isLocalChart(options)) { - return options.chart; + return options; } const hash = hashPullOptions(options); + const cacheDir = + options.cache?.dir ?? env.KOSKO_HELM_CACHE_DIR ?? defaultCacheDir; const cachePath = join(cacheDir, hash); // Return cache if exists if (await chartExists(cachePath)) { - return cachePath; + return { chart: cachePath }; } // Create a temporary directory for the chart, because when there are multiple @@ -244,7 +330,7 @@ async function pullChart(options: PullOptions): Promise { ]); // Create the cache directory - await mkdir(cacheDir, { recursive: true }); + await mkdir(defaultCacheDir, { recursive: true }); // Move the chart to the cache directory try { @@ -260,7 +346,7 @@ async function pullChart(options: PullOptions): Promise { if (!code || !FILE_EXIST_ERROR_CODES.has(code)) throw err; } - return cachePath; + return { chart: cachePath }; } finally { // Clean up the temporary directory await tmpDir.cleanup(); @@ -275,40 +361,30 @@ async function writeValues(values: unknown) { return file; } -async function renderChart({ - chart, - name, - apiVersions, - dependencyUpdate, - description, - generateName, - nameTemplate, - namespace, - noHooks, - includeCrds, - skipTests, - timeout, - values -}: Omit & { chart: string }) { +async function renderChart(options: ChartOptions) { const args: string[] = [ "template", - ...(name ? [name] : []), - chart, - ...stringArrayArg("api-versions", apiVersions), - ...booleanArg("dependency-update", dependencyUpdate), - ...stringArg("description", description), - ...booleanArg("generate-name", generateName), - ...stringArg("name-template", nameTemplate), - ...stringArg("namespace", namespace), - ...booleanArg("no-hooks", noHooks), - ...booleanArg("include-crds", includeCrds), - ...booleanArg("skip-tests", skipTests), - ...stringArg("timeout", timeout) + ...(options.name ? [options.name] : []), + ...getPullArgs(options), + ...stringArrayArg("api-versions", options.apiVersions), + ...booleanArg("dependency-update", options.dependencyUpdate), + ...stringArg("description", options.description), + ...booleanArg("generate-name", options.generateName), + ...booleanArg("include-crds", options.includeCrds), + ...booleanArg("is-upgrade", options.isUpgrade), + ...stringArg("kube-version", options.kubeVersion), + ...stringArg("name-template", options.nameTemplate), + ...stringArg("namespace", options.namespace), + ...booleanArg("no-hooks", options.noHooks), + ...stringArg("post-renderer", options.postRenderer), + ...stringArrayArg("post-renderer-args", options.postRendererArgs), + ...booleanArg("skip-tests", options.skipTests), + ...stringArg("timeout", options.timeout) ]; let valueFile: tmp.FileResult | undefined; - if (values) { - valueFile = await writeValues(values); + if (options.values) { + valueFile = await writeValues(options.values); args.push("--values", valueFile.path); } @@ -334,8 +410,8 @@ export function loadChart(options: ChartOptions): () => Promise { const { transform, ...opts } = options; return async () => { - const chart = await pullChart(opts); - const { stdout } = await renderChart({ ...opts, chart }); + const { chart, repo } = await pullChart(opts); + const { stdout } = await renderChart({ ...opts, chart, repo }); // Find the first `---` in order to skip deprecation warnings const index = stdout.indexOf("---\n");