diff --git a/cert-manager/certificate.ts b/cert-manager/certificate.ts new file mode 100644 index 0000000..ea2e34b --- /dev/null +++ b/cert-manager/certificate.ts @@ -0,0 +1,85 @@ +import { Construct } from "constructs"; +import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest"; +import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; + +export interface CertificateOptions { + provider: KubernetesProvider; + + /** Namespace to create the Certificate in */ + namespace: string; + + /** Required name of the certificate (and CRD name) */ + name: string; + + /** Secret name for storing the issued TLS cert */ + secretName: string; + + /** One or more DNS names the certificate should cover */ + dnsNames: string[]; + + /** Reference to the cert-manager issuer */ + issuerRef: { + name: string; + kind?: string; // ClusterIssuer or Issuer + }; + + /** Optional duration (default: cert-manager default) */ + duration?: string; + + /** Optional renewBefore (default: cert-manager default) */ + renewBefore?: string; +} + +class Certificate extends Construct { + public readonly manifest: Manifest; + + constructor(scope: Construct, id: string, opts: CertificateOptions) { + super(scope, id); + + const manifest: any = { + apiVersion: "cert-manager.io/v1", + kind: "Certificate", + metadata: { + name: opts.name, + namespace: opts.namespace, + }, + spec: { + secretName: opts.secretName, + dnsNames: opts.dnsNames, + issuerRef: { + name: opts.issuerRef.name, + kind: opts.issuerRef.kind ?? "ClusterIssuer", + }, + }, + }; + + if (opts.duration) { + manifest.spec.duration = opts.duration; + } + + if (opts.renewBefore) { + manifest.spec.renewBefore = opts.renewBefore; + } + + this.manifest = new Manifest(this, id, { + provider: opts.provider, + manifest, + }); + } +} + +export class CloudflareCertificate extends Certificate { + constructor( + scope: Construct, + id: string, + opts: Omit, + ) { + super(scope, id, { + ...opts, + issuerRef: { + name: "cloudflare-issuer", + kind: "ClusterIssuer", + }, + }); + } +} diff --git a/cert-manager/index.ts b/cert-manager/index.ts index 252a7cd..975a224 100644 --- a/cert-manager/index.ts +++ b/cert-manager/index.ts @@ -5,6 +5,8 @@ import { Construct } from "constructs"; import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest"; +export { CloudflareCertificate } from "./certificate"; + type CertManagerOptions = { providers: { kubernetes: KubernetesProvider; diff --git a/traefik/ingress-route.ts b/traefik/ingress-route.ts new file mode 100644 index 0000000..63de5ee --- /dev/null +++ b/traefik/ingress-route.ts @@ -0,0 +1,94 @@ +import { Construct } from "constructs"; +import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest"; +import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; + +import { CloudflareCertificate } from "../cert-manager"; + +export interface TraefikIngressRouteOptions { + provider: KubernetesProvider; + namespace: string; + + /** Hostname for this route (e.g. npm.dogar.dev) */ + host: string; + + /** Path prefix (default: "/") */ + path?: string; + + /** Backend K8s Service */ + serviceName: string; + servicePort: number; + + /** EntryPoints (default: ["websecure"]) */ + entryPoints?: string[]; + + /** TLS secret name for HTTPS termination */ + tlsSecretName?: string; + + /** Extra middlewares (traefik format: namespace/name) */ + middlewares?: string[]; + + /** Name override (otherwise auto) */ + name?: string; +} + +export class TraefikIngressRoute extends Construct { + public readonly manifest: Manifest; + + constructor(scope: Construct, id: string, opts: TraefikIngressRouteOptions) { + super(scope, id); + + const name = opts.name ?? `route-${opts.host.replace(/\./g, "-")}`; + const path = opts.path ?? "/"; + const entryPoints = opts.entryPoints ?? ["websecure"]; + + const route: any = { + match: `Host(\`${opts.host}\`) && PathPrefix(\`${path}\`)`, + kind: "Rule", + services: [ + { + name: opts.serviceName, + port: opts.servicePort, + }, + ], + }; + + if (opts.middlewares?.length) { + route.middlewares = opts.middlewares.map((mw) => { + const [namespace, name] = mw.split("/"); + return { name, namespace }; + }); + } + + const spec: any = { + entryPoints, + routes: [route], + }; + + if (opts.tlsSecretName) { + spec.tls = { + secretName: opts.tlsSecretName, + }; + + new CloudflareCertificate(this, `${name}-cert`, { + provider: opts.provider, + namespace: opts.namespace, + name: opts.host, + secretName: opts.tlsSecretName, + dnsNames: [opts.host], + }); + } + + this.manifest = new Manifest(this, name, { + provider: opts.provider, + manifest: { + apiVersion: "traefik.io/v1alpha1", + kind: "IngressRoute", + metadata: { + name, + namespace: opts.namespace, + }, + spec, + }, + }); + } +}