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");