Skip to content

Commit

Permalink
feat(lint): Support cert-manager in no-missing-secret rule
Browse files Browse the repository at this point in the history
  • Loading branch information
tommy351 committed May 20, 2024
1 parent 39be384 commit a3df843
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/four-items-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kosko/plugin-lint": minor
---

Support cert-manager's `Certificate` in `no-missing-secret` rule.
1 change: 1 addition & 0 deletions plugins/lint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"@kubernetes-models/apimachinery": "^1.2.1",
"@kubernetes-models/argo-rollouts": "^0.3.1",
"@kubernetes-models/autoscaler": "^3.2.1",
"@kubernetes-models/cert-manager": "^4.3.1",
"@kubernetes-models/gateway-api": "^0.5.0",
"@kubernetes-models/gke": "^4.3.0",
"@kubernetes-models/keda": "^0.3.1",
Expand Down
74 changes: 74 additions & 0 deletions plugins/lint/src/rules/no-missing-secret.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ServiceAccount } from "kubernetes-models/v1/ServiceAccount";
import { Ingress } from "kubernetes-models/networking.k8s.io/v1/Ingress";
import { createManifest, validateAll } from "../test-utils";
import rule from "./no-missing-secret";
import { Certificate } from "@kubernetes-models/cert-manager/cert-manager.io/v1/Certificate";

test("should pass when data is undefined", () => {
const manifest = createManifest(undefined);
Expand Down Expand Up @@ -535,3 +536,76 @@ test("should pass when a matching Kinko Asset exists", () => {
);
expect(validateAll(rule, undefined, [asset, pod])).toBeEmpty();
});

test("should pass when a matching Certificate exists", () => {
const cert = createManifest(
new Certificate({
metadata: { name: "foo", namespace: "a" },
spec: {
secretName: "foo-cert",
issuerRef: { name: "test" }
}
})
);
const pod = createManifest(
new Pod({
metadata: { name: "bar", namespace: "a" },
spec: {
containers: [],
volumes: [{ name: "abc", secret: { secretName: "foo-cert" } }]
}
})
);
expect(validateAll(rule, undefined, [cert, pod])).toBeEmpty();
});

test("should report when secret name is certificate name instead of secret name", () => {
const cert = createManifest(
new Certificate({
metadata: { name: "foo", namespace: "a" },
spec: {
secretName: "foo-cert",
issuerRef: { name: "test" }
}
})
);
const pod = createManifest(
new Pod({
metadata: { name: "bar", namespace: "a" },
spec: {
containers: [],
volumes: [{ name: "abc", secret: { secretName: "foo" } }]
}
})
);
expect(validateAll(rule, undefined, [cert, pod])).toEqual([
{ manifest: pod, message: `Secret "foo" does not exist in namespace "a".` }
]);
});

test("should report when certificate is in a different namespace", () => {
const cert = createManifest(
new Certificate({
metadata: { name: "foo", namespace: "a" },
spec: {
secretName: "foo-cert",
issuerRef: { name: "test" }
}
})
);
const pod = createManifest(
new Pod({
metadata: { name: "bar", namespace: "b" },
spec: {
containers: [],
volumes: [{ name: "abc", secret: { secretName: "foo-cert" } }]
}
})
);
expect(validateAll(rule, undefined, [cert, pod])).toEqual([
{
manifest: pod,
message: `Secret "foo-cert" does not exist in namespace "b".`
}
]);
});
8 changes: 8 additions & 0 deletions plugins/lint/src/rules/no-missing-secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ function findSecret(
const manifest = manifests.find({ ...type, ...name });
if (manifest) return manifest;
}

const cert = manifests.find({
apiGroup: "cert-manager.io",
kind: "Certificate",
namespace: name.namespace,
certSecret: name.name
});
if (cert) return cert;
}

export default createRule({
Expand Down
27 changes: 27 additions & 0 deletions plugins/lint/src/utils/manifest-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createManifest } from "../test-utils";
import { Pod } from "kubernetes-models/v1/Pod";
import { Service } from "kubernetes-models/v1/Service";
import { Ingress } from "kubernetes-models/networking.k8s.io/v1/Ingress";
import { Certificate } from "@kubernetes-models/cert-manager/cert-manager.io/v1/Certificate";

const manifests = [
// Resource without a namespace
Expand All @@ -23,6 +24,18 @@ const manifests = [
metadata: { name: "c" }
})
),
// Certificate
createManifest(
new Certificate({
metadata: { name: "d", namespace: "foo" },
spec: {
secretName: "d-cert-secret",
issuerRef: {
name: "test"
}
}
})
),
// Empty object
createManifest({})
];
Expand Down Expand Up @@ -94,6 +107,20 @@ describe("find", () => {
});
});

describe("certSecret predicate", () => {
test("should return resource when certSecret matches", () => {
expect(store.find({ certSecret: "d-cert-secret" })).toEqual(manifests[3]);
});

test("should return undefined when the value is certificate name instead of secret name", () => {
expect(store.find({ certSecret: "d" })).toBeUndefined();
});

test("should return undefined when certSecret does not match", () => {
expect(store.find({ certSecret: "z" })).toBeUndefined();
});
});

describe("multiple predicates", () => {
test("should return resource when all predicates match", () => {
expect(store.find({ apiVersion: "v1", kind: "Pod" })).toEqual(
Expand Down
9 changes: 9 additions & 0 deletions plugins/lint/src/utils/manifest-store.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { Manifest } from "../rules/types";
import { apiVersionToGroup } from "@kosko/common-utils";
import { isCertificate } from "./manifest";

export interface Predicate {
apiVersion?: string;
apiGroup?: string;
kind?: string;
namespace?: string;
name?: string;
certSecret?: string;
}

function getIndexKeys(manifest: Manifest): string[] {
Expand Down Expand Up @@ -34,6 +36,13 @@ function getIndexKeys(manifest: Manifest): string[] {
keys.push(`name=${metadata.name}`);
}

if (
isCertificate(manifest) &&
typeof manifest.data.spec?.secretName === "string"
) {
keys.push(`certSecret=${manifest.data.spec.secretName}`);
}

return keys;
}

Expand Down
5 changes: 5 additions & 0 deletions plugins/lint/src/utils/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type { IVerticalPodAutoscaler as IVPAV1Beta1 } from "@kubernetes-models/a
import type { IVerticalPodAutoscaler as IVPAV1Beta2 } from "@kubernetes-models/autoscaler/autoscaling.k8s.io/v1beta2/VerticalPodAutoscaler";
import type { IMultidimPodAutoscaler } from "@kubernetes-models/gke/autoscaling.gke.io/v1beta1/MultidimPodAutoscaler";
import type { IScaledObject } from "@kubernetes-models/keda/keda.sh/v1alpha1/ScaledObject";
import type { ICertificate } from "@kubernetes-models/cert-manager/cert-manager.io/v1/Certificate";
import type { Manifest } from "../rules/types";
import {
type Matcher,
Expand Down Expand Up @@ -188,6 +189,10 @@ export const isScaledObject = groupKindPredicate<IScaledObject>(
"keda.sh",
"ScaledObject"
);
export const isCertificate = groupKindPredicate<ICertificate>(
"cert-manager.io",
"Certificate"
);

export function isGatewayRoute(
manifest: Manifest
Expand Down
15 changes: 15 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions website/docs/plugins/lint/rules/no-missing-secret.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Besides `Secret`, the following resources are also supported:
| `bitnami.com` | `SealedSecret` |
| `external-secrets.io` | `ExternalSecret` |
| `seals.kinko.dev` | `Asset` |
| `cert-manager.io` | `Certificate` |

## Configuration

Expand Down

0 comments on commit a3df843

Please sign in to comment.