diff --git a/dev/create-harbor-pull-secret.sh b/dev/create-harbor-pull-secret.sh new file mode 100755 index 0000000..7d3769d --- /dev/null +++ b/dev/create-harbor-pull-secret.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +set -e + +harbor_credential_path="$1" + +function get_docker_credentials() { + local harbor_credential_path="$1" + + local username="$(yq -r '.name' -oj "$harbor_credential_path")" + local secret="$(yq -r '.secret' -oj "$harbor_credential_path")" + echo "$username:$secret" +} + +credentials="$(get_docker_credentials "$harbor_credential_path")" +encoded_credentials="$(echo -n "$credentials" | base64)" + +auth_settings=$(cat < + name: harbor-pull-secret + annotations: + replicator.v1.mittwald.de/replicate-to: "" +type: kubernetes.io/dockerconfigjson +data: + .dockerconfigjson: $encoded_auth_settings +EOF diff --git a/services/databases/postgresql/cnpg-cluster.yaml b/services/databases/postgresql/cnpg-cluster.yaml index 35c62c4..7080f86 100644 --- a/services/databases/postgresql/cnpg-cluster.yaml +++ b/services/databases/postgresql/cnpg-cluster.yaml @@ -6,13 +6,6 @@ metadata: spec: instances: 3 - bootstrap: - initdb: - database: tts-db - owner: tts - secret: - name: tts-secret - managed: roles: - name: ni @@ -33,6 +26,18 @@ spec: login: true passwordSecret: name: sinf-website-2023-secret + - name: tts + ensure: present + createdb: false + login: true + passwordSecret: + name: tts-secret + - name: tts-staging + ensure: present + createdb: false + login: true + passwordSecret: + name: tts-staging-secret storage: size: 10Gi diff --git a/services/databases/postgresql/cnpg-secrets.yaml b/services/databases/postgresql/cnpg-secrets.yaml index e649291..a69ab0d 100644 --- a/services/databases/postgresql/cnpg-secrets.yaml +++ b/services/databases/postgresql/cnpg-secrets.yaml @@ -9,6 +9,15 @@ metadata: type: kubernetes.io/basic-auth --- apiVersion: v1 +stringData: + password: + username: tts-staging +kind: Secret +metadata: + name: tts-staging-secret +type: kubernetes.io/basic-auth +--- +apiVersion: v1 stringData: password: username: ni diff --git a/services/pulumi/niployments/index.ts b/services/pulumi/niployments/index.ts index bda2680..5197893 100644 --- a/services/pulumi/niployments/index.ts +++ b/services/pulumi/niployments/index.ts @@ -1,5 +1,7 @@ // ementas is an example pulumi service // import "./services/ementas/index.js"; +import "#resources/replicator/charts.js"; +import "./services/tts/index.js"; import { CommitSignal } from "./utils/pending.js"; CommitSignal.globalParent.resolve(); diff --git a/services/pulumi/niployments/services/tts/common/backend.ts b/services/pulumi/niployments/services/tts/common/backend.ts new file mode 100644 index 0000000..c941896 --- /dev/null +++ b/services/pulumi/niployments/services/tts/common/backend.ts @@ -0,0 +1,104 @@ +import * as pulumi from "@pulumi/pulumi"; +import * as k8s from "@pulumi/kubernetes"; +import { Prefixer } from "#utils/prefixer.js"; + +export class TTSBackend extends pulumi.ComponentResource { + public readonly name: pulumi.Output; + public readonly port = pulumi.output(80); + + constructor( + name: string, + args: { + namespace: pulumi.Input; + branch: pulumi.Input<"main" | "develop">; + envSecretRef: pulumi.Input; + }, + opts?: pulumi.ComponentResourceOptions, + ) { + super("niployments:tts:TTSBackend", name, opts); + + const prefixer = new Prefixer(name); + + const backendLabels = { app: "tts-backend" }; + const backendPort = 8000; + + const deployment = new k8s.apps.v1.Deployment( + prefixer.deployment(), + { + metadata: { + namespace: args.namespace, + }, + spec: { + replicas: 1, + selector: { + matchLabels: backendLabels, + }, + template: { + metadata: { + labels: backendLabels, + }, + spec: { + containers: [ + { + name: "tts-be", + image: pulumi.interpolate`registry.niaefeup.pt/niaefeup/tts-be:${args.branch}`, + imagePullPolicy: "Always", + resources: { + limits: { + memory: "128Mi", + cpu: "500m", + }, + }, + ports: [ + { + containerPort: backendPort, + }, + ], + envFrom: [ + { + secretRef: { + name: args.envSecretRef, + } + }, + ] + }, + ], + imagePullSecrets: [ + { + name: "harbor-pull-secret", + }, + ], + }, + }, + }, + }, + { parent: this }, + ); + + const service = new k8s.core.v1.Service( + prefixer.service(), + { + metadata: { + namespace: args.namespace, + }, + spec: { + ports: [ + { + port: this.port, + targetPort: backendPort, + }, + ], + selector: backendLabels, + }, + }, + { parent: this, dependsOn: [deployment] }, + ); + + this.name = service.metadata.name; + + this.registerOutputs({ + serviceName: this.name, + servicePort: this.port, + }); + } +} diff --git a/services/pulumi/niployments/services/tts/common/frontend.ts b/services/pulumi/niployments/services/tts/common/frontend.ts new file mode 100644 index 0000000..5f1c878 --- /dev/null +++ b/services/pulumi/niployments/services/tts/common/frontend.ts @@ -0,0 +1,96 @@ +import * as pulumi from "@pulumi/pulumi"; +import * as k8s from "@pulumi/kubernetes"; +import { Prefixer } from "#utils/prefixer.js"; + +export class TTSFrontend extends pulumi.ComponentResource { + public readonly name: pulumi.Output; + public readonly port = pulumi.output(80); + + constructor( + name: string, + args: { + namespace: pulumi.Input; + branch: pulumi.Input<"main" | "develop">; + }, + opts?: pulumi.ComponentResourceOptions, + ) { + super("niployments:tts:TTSFrontend", name, opts); + + const prefixer = new Prefixer(name); + + const frontendLabels = { app: "tts-frontend" }; + const frontendPort = 80; + + const deployment = new k8s.apps.v1.Deployment( + prefixer.deployment(), + { + metadata: { + namespace: args.namespace, + }, + spec: { + replicas: 1, + selector: { + matchLabels: frontendLabels, + }, + template: { + metadata: { + labels: frontendLabels, + }, + spec: { + containers: [ + { + name: "tts-fe", + image: pulumi.interpolate`registry.niaefeup.pt/niaefeup/tts-fe:${args.branch}`, + imagePullPolicy: "Always", + resources: { + limits: { + memory: "128Mi", + cpu: "500m", + }, + }, + ports: [ + { + containerPort: frontendPort, + }, + ], + }, + ], + imagePullSecrets: [ + { + name: "harbor-pull-secret", + }, + ], + }, + }, + }, + }, + { parent: this }, + ); + + const service = new k8s.core.v1.Service( + prefixer.service(), + { + metadata: { + namespace: args.namespace, + }, + spec: { + ports: [ + { + port: this.port, + targetPort: frontendPort, + }, + ], + selector: frontendLabels, + }, + }, + { parent: this, dependsOn: [deployment] }, + ); + + this.name = service.metadata.name; + + this.registerOutputs({ + serviceName: this.name, + servicePort: this.port, + }); + } +} diff --git a/services/pulumi/niployments/services/tts/index.ts b/services/pulumi/niployments/services/tts/index.ts new file mode 100644 index 0000000..d23d382 --- /dev/null +++ b/services/pulumi/niployments/services/tts/index.ts @@ -0,0 +1,2 @@ +// import "./production/index.js"; +import "./staging/index.js"; diff --git a/services/pulumi/niployments/services/tts/production/index.ts b/services/pulumi/niployments/services/tts/production/index.ts new file mode 100644 index 0000000..aaef9d1 --- /dev/null +++ b/services/pulumi/niployments/services/tts/production/index.ts @@ -0,0 +1,96 @@ +import * as pulumi from "@pulumi/pulumi"; +import * as crds from "@pulumi/crds"; +import { TTSFrontend } from "../common/frontend.js"; +import { TTSBackend } from "../common/backend.js"; +import { redirectToPrimaryAddress, stripApiPrefix } from "./middleware.js"; +import { prefixer } from "./prefixer.js"; +import { namespace } from "./namespace.js"; + +const branch = "main"; +const primaryHost = "tts.niaefeup.pt"; + +const config = new pulumi.Config("tts:production"); + +const frontend = new TTSFrontend(prefixer.create("frontend"), { + namespace: namespace.metadata.name, + branch, +}); + +const frontendService = { name: frontend.name, port: frontend.port }; + +const backend = new TTSBackend(prefixer.create("backend"), { + namespace: namespace.metadata.name, + branch, + envSecretRef: config.require("envSecretRef"), +}); + +const backendService = { name: backend.name, port: backend.port }; + +const certificateSecretName = prefixer.create("certificate-secret"); +const certificate = new crds.certmanager.v1.Certificate( + prefixer.certificate(), + { + metadata: { + namespace: namespace.metadata.name, + }, + spec: { + secretName: certificateSecretName, + issuerRef: { + name: "letsencrypt-production", + kind: "ClusterIssuer", + }, + dnsNames: [primaryHost], + }, + }, +); + +new crds.traefik.v1alpha1.IngressRoute( + prefixer.chain("primary").ingressRoute(), + { + metadata: { + namespace: namespace.metadata.name, + }, + spec: { + entryPoints: ["websecure"], + routes: [ + { + kind: "Rule", + match: `Host(\`${primaryHost}\`) && PathPrefix(\`/api\`)`, + services: [backendService], + middlewares: [stripApiPrefix], + }, + { + kind: "Rule", + match: `Host(\`${primaryHost}\`)`, + services: [frontendService], + }, + ], + tls: { + secretName: certificateSecretName, + }, + }, + }, + { dependsOn: [certificate] }, +); + +new crds.traefik.v1alpha1.IngressRoute( + prefixer.chain("secondary").ingressRoute(), + { + metadata: { + namespace: namespace.metadata.name, + }, + spec: { + entryPoints: ["websecure"], + routes: [ + { + kind: "Rule", + match: `Host(\`ni.fe.up.pt\`) && PathPrefix(\`/tts\`)`, + middlewares: [redirectToPrimaryAddress], + }, + ], + tls: { + secretName: "website-cert", + }, + }, + }, +); diff --git a/services/pulumi/niployments/services/tts/production/middleware.ts b/services/pulumi/niployments/services/tts/production/middleware.ts new file mode 100644 index 0000000..24c84f2 --- /dev/null +++ b/services/pulumi/niployments/services/tts/production/middleware.ts @@ -0,0 +1,48 @@ +import { namespace } from "./namespace.js"; +import { prefixer } from "./prefixer.js"; +import * as pulumi from "@pulumi/pulumi"; +import * as crds from "@pulumi/crds"; +import { ensureOutputIsDefined } from "#utils/pulumi.js"; + +const stripApiPrefixMiddleware = new crds.traefik.v1alpha1.Middleware( + prefixer.create("strip-api-prefix"), + { + metadata: { + namespace: namespace.metadata.name, + }, + spec: { + stripPrefix: { + prefixes: ["/api"], + forceSlash: false, + }, + }, + }, +); + +export const stripApiPrefix = { + name: pulumi.output( + ensureOutputIsDefined(stripApiPrefixMiddleware.metadata.name), + ), +}; + +const redirectToPrimaryAddressMiddleware = new crds.traefik.v1alpha1.Middleware( + prefixer.create("redirect-to-primary-address"), + { + metadata: { + namespace: namespace.metadata.name, + }, + spec: { + redirectRegex: { + regex: "^https://ni.fe.up.pt/tts/(.*)", + replacement: "https://tts.niaefeup.pt/$1", + permanent: false, + }, + }, + }, +); + +export const redirectToPrimaryAddress = { + name: pulumi.output( + ensureOutputIsDefined(redirectToPrimaryAddressMiddleware.metadata.name), + ), +}; diff --git a/services/pulumi/niployments/services/tts/production/namespace.ts b/services/pulumi/niployments/services/tts/production/namespace.ts new file mode 100644 index 0000000..14e84ba --- /dev/null +++ b/services/pulumi/niployments/services/tts/production/namespace.ts @@ -0,0 +1,8 @@ +import * as k8s from "@pulumi/kubernetes"; +import { prefixer } from "./prefixer.js"; + +export const namespace = new k8s.core.v1.Namespace(prefixer.namespace(), { + metadata: { + name: prefixer.base(), + }, +}); diff --git a/services/pulumi/niployments/services/tts/production/prefixer.ts b/services/pulumi/niployments/services/tts/production/prefixer.ts new file mode 100644 index 0000000..d9c0e3d --- /dev/null +++ b/services/pulumi/niployments/services/tts/production/prefixer.ts @@ -0,0 +1,3 @@ +import { Prefixer } from "#utils/prefixer.js"; + +export const prefixer = new Prefixer("tts"); diff --git a/services/pulumi/niployments/services/tts/staging/index.ts b/services/pulumi/niployments/services/tts/staging/index.ts new file mode 100644 index 0000000..c73ba82 --- /dev/null +++ b/services/pulumi/niployments/services/tts/staging/index.ts @@ -0,0 +1,74 @@ +import * as pulumi from "@pulumi/pulumi"; +import * as crds from "@pulumi/crds"; +import { TTSFrontend } from "../common/frontend.js"; +import { TTSBackend } from "../common/backend.js"; +import { stripApiPrefix } from "./middleware.js"; +import { prefixer } from "./prefixer.js"; +import { namespace } from "./namespace.js"; + +const branch = "develop"; +const primaryHost = "tts-staging.niaefeup.pt"; + +const config = new pulumi.Config("tts:staging"); + +const frontend = new TTSFrontend(prefixer.create("frontend"), { + namespace: namespace.metadata.name, + branch, +}); + +const frontendService = { name: frontend.name, port: frontend.port }; + +const backend = new TTSBackend(prefixer.create("backend"), { + namespace: namespace.metadata.name, + branch, + envSecretRef: config.require("envSecretRef"), +}); + +const backendService = { name: backend.name, port: backend.port }; + +const certificateSecretName = prefixer.create("certificate-secret"); +const certificate = new crds.certmanager.v1.Certificate( + prefixer.certificate(), + { + metadata: { + namespace: namespace.metadata.name, + }, + spec: { + secretName: certificateSecretName, + issuerRef: { + name: "letsencrypt-production", + kind: "ClusterIssuer", + }, + dnsNames: [primaryHost], + }, + }, +); + +new crds.traefik.v1alpha1.IngressRoute( + prefixer.ingressRoute(), + { + metadata: { + namespace: namespace.metadata.name, + }, + spec: { + entryPoints: ["websecure"], + routes: [ + { + kind: "Rule", + match: `Host(\`${primaryHost}\`) && PathPrefix(\`/api\`)`, + services: [backendService], + middlewares: [stripApiPrefix], + }, + { + kind: "Rule", + match: `Host(\`${primaryHost}\`)`, + services: [frontendService], + }, + ], + tls: { + secretName: certificateSecretName, + }, + }, + }, + { dependsOn: [certificate] }, +); diff --git a/services/pulumi/niployments/services/tts/staging/middleware.ts b/services/pulumi/niployments/services/tts/staging/middleware.ts new file mode 100644 index 0000000..65dd75d --- /dev/null +++ b/services/pulumi/niployments/services/tts/staging/middleware.ts @@ -0,0 +1,26 @@ +import { namespace } from "./namespace.js"; +import { prefixer } from "./prefixer.js"; +import * as pulumi from "@pulumi/pulumi"; +import * as crds from "@pulumi/crds"; +import { ensureOutputIsDefined } from "#utils/pulumi.js"; + +const stripApiPrefixMiddleware = new crds.traefik.v1alpha1.Middleware( + prefixer.create("strip-api-prefix"), + { + metadata: { + namespace: namespace.metadata.name, + }, + spec: { + stripPrefix: { + prefixes: ["/api"], + forceSlash: false, + }, + }, + }, +); + +export const stripApiPrefix = { + name: pulumi.output( + ensureOutputIsDefined(stripApiPrefixMiddleware.metadata.name), + ), +}; diff --git a/services/pulumi/niployments/services/tts/staging/namespace.ts b/services/pulumi/niployments/services/tts/staging/namespace.ts new file mode 100644 index 0000000..14e84ba --- /dev/null +++ b/services/pulumi/niployments/services/tts/staging/namespace.ts @@ -0,0 +1,8 @@ +import * as k8s from "@pulumi/kubernetes"; +import { prefixer } from "./prefixer.js"; + +export const namespace = new k8s.core.v1.Namespace(prefixer.namespace(), { + metadata: { + name: prefixer.base(), + }, +}); diff --git a/services/pulumi/niployments/services/tts/staging/prefixer.ts b/services/pulumi/niployments/services/tts/staging/prefixer.ts new file mode 100644 index 0000000..cee550e --- /dev/null +++ b/services/pulumi/niployments/services/tts/staging/prefixer.ts @@ -0,0 +1,3 @@ +import { Prefixer } from "#utils/prefixer.js"; + +export const prefixer = new Prefixer("tts-staging"); diff --git a/services/pulumi/niployments/utils/prefixer.ts b/services/pulumi/niployments/utils/prefixer.ts new file mode 100644 index 0000000..6ec23ac --- /dev/null +++ b/services/pulumi/niployments/utils/prefixer.ts @@ -0,0 +1,38 @@ +export class Prefixer { + constructor(private readonly prefix: T) {} + + public base() { + return this.prefix; + } + + public chain(prefix: U) { + return new Prefixer(this.create(prefix)); + } + + public create(name: U) { + return `${this.prefix}-${name}` as const; + } + + public deployment() { + return this.create("deployment"); + } + + public service() { + return this.create("service"); + } + + public ingressRoute() { + return this.create("ingress-route"); + } + + public certificate() { + return this.create("certificate"); + } + + public namespace() { + return this.create("namespace"); + } +} + + + diff --git a/services/pulumi/niployments/utils/pulumi.ts b/services/pulumi/niployments/utils/pulumi.ts index 94e1328..a5bb7b3 100644 --- a/services/pulumi/niployments/utils/pulumi.ts +++ b/services/pulumi/niployments/utils/pulumi.ts @@ -62,3 +62,17 @@ export function concat< ) .apply((val) => structuredClone(val)); // structuredClone is needed to ensure values remains readonly } + +export function ensureOutputIsDefined(output: pulumi.Output | undefined) { + if (output == undefined) { + throw new Error("output is undefined"); + } + + return output.apply(value => { + if (value == undefined) { + throw new Error("output is undefined"); + } + + return value; + }); +} \ No newline at end of file