more stuff

This commit is contained in:
Morten Olsen
2025-09-03 14:33:48 +02:00
parent 683de402ff
commit 5ee7a76443
31 changed files with 501 additions and 53 deletions

View File

@@ -9,6 +9,7 @@ class RepoService {
#istio: HelmRepo;
#authentik: HelmRepo;
#cloudflare: HelmRepo;
#argo: HelmRepo;
constructor(services: Services) {
const resourceService = services.get(ResourceService);
@@ -16,11 +17,13 @@ class RepoService {
this.#istio = resourceService.get(HelmRepo, 'istio', NAMESPACE);
this.#authentik = resourceService.get(HelmRepo, 'authentik', NAMESPACE);
this.#cloudflare = resourceService.get(HelmRepo, 'cloudflare', NAMESPACE);
this.#argo = resourceService.get(HelmRepo, 'argo', NAMESPACE);
this.#jetstack.on('changed', this.ensure);
this.#istio.on('changed', this.ensure);
this.#authentik.on('changed', this.ensure);
this.#cloudflare.on('changed', this.ensure);
this.#argo.on('changed', this.ensure);
}
public get jetstack() {
@@ -39,6 +42,10 @@ class RepoService {
return this.#cloudflare;
}
public get argo() {
return this.#argo;
}
public ensure = async () => {
await this.#jetstack.set({
url: 'https://charts.jetstack.io',
@@ -55,6 +62,10 @@ class RepoService {
await this.#cloudflare.set({
url: 'https://cloudflare.github.io/helm-charts',
});
await this.#argo.set({
url: 'https://argoproj.github.io/argo-helm',
});
};
}

View File

@@ -15,9 +15,9 @@ import { generateRandomHexPass } from '#utils/secrets.ts';
import { Service } from '#resources/core/service/service.ts';
import { HelmRelease } from '#resources/flux/helm-release/helm-release.ts';
import { RepoService } from '#bootstrap/repos/repos.ts';
import { VirtualService } from '#resources/istio/virtual-service/virtual-service.ts';
import { DestinationRule } from '#resources/istio/destination-rule/destination-rule.ts';
import { NotReadyError } from '#utils/errors.ts';
import { ExternalHttpService } from '../external-http-service.ts/external-http-service.ts';
const specSchema = z.object({
environment: z.string(),
@@ -44,7 +44,7 @@ class AuthentikServer extends CustomResource<typeof specSchema> {
#initSecret: Secret<InitSecretData>;
#service: Service;
#helmRelease: HelmRelease;
#virtualService: VirtualService;
#externalHttpService: ExternalHttpService;
#destinationRule: DestinationRule;
constructor(options: CustomResourceOptions<typeof specSchema>) {
@@ -68,11 +68,10 @@ class AuthentikServer extends CustomResource<typeof specSchema> {
this.#helmRelease = resourceService.get(HelmRelease, this.name, this.namespace);
this.#helmRelease.on('changed', this.queueReconcile);
this.#virtualService = resourceService.get(VirtualService, this.name, this.namespace);
this.#virtualService.on('changed', this.queueReconcile);
this.#destinationRule = resourceService.get(DestinationRule, this.name, this.namespace);
this.#destinationRule.on('changed', this.queueReconcile);
this.#externalHttpService = resourceService.get(ExternalHttpService, this.name, this.namespace);
}
public get service() {
@@ -254,28 +253,19 @@ class AuthentikServer extends CustomResource<typeof specSchema> {
},
});
const gateway = this.#environment.current.gateway;
await this.#virtualService.set({
await this.#externalHttpService.ensure({
metadata: {
ownerReferences: [this.ref],
},
spec: {
gateways: [`${gateway.namespace}/${gateway.name}`, 'mesh'],
hosts: [domain],
http: [
{
route: [
{
destination: {
host: this.#service.hostname,
port: {
number: 80,
},
},
},
],
environment: this.spec.environment,
subdomain: this.spec.subdomain || 'authentik',
destination: {
host: this.#service.hostname,
port: {
number: 80,
},
],
},
},
});
};

View File

@@ -12,6 +12,7 @@ import { RepoService } from '#bootstrap/repos/repos.ts';
import { Secret } from '#resources/core/secret/secret.ts';
import { NotReadyError } from '#utils/errors.ts';
import { NamespaceService } from '#bootstrap/namespaces/namespaces.ts';
import { CloudflareService } from '#services/cloudflare/cloudflare.ts';
const specSchema = z.object({});
@@ -29,6 +30,7 @@ class CloudflareTunnel extends CustomResource<typeof specSchema> {
#helmRelease: HelmRelease;
#secret: Secret<SecretData>;
#cloudflareService;
constructor(options: CustomResourceOptions<typeof specSchema>) {
super(options);
@@ -40,6 +42,9 @@ class CloudflareTunnel extends CustomResource<typeof specSchema> {
this.#helmRelease = resourceService.get(HelmRelease, this.name, namespace);
this.#secret = resourceService.get(Secret<SecretData>, 'cloudflare', namespace);
this.#secret.on('changed', this.queueReconcile);
this.#cloudflareService = this.services.get(CloudflareService);
this.#cloudflareService.on('changed', this.queueReconcile);
}
#handleResourceChanged = (resource: Resource<ExpectedAny>) => {
@@ -60,6 +65,18 @@ class CloudflareTunnel extends CustomResource<typeof specSchema> {
hostname: rule?.hostname,
service: `http://${rule?.destination.host}:${rule?.destination.port.number}`,
}));
if (this.#cloudflareService.ready) {
for (const route of ingress) {
if (!route.hostname) {
continue;
}
try {
await this.#cloudflareService.ensureTunnel(route.hostname);
} catch (err) {
console.error(err);
}
}
}
await this.#helmRelease.ensure({
metadata: {
ownerReferences: [this.ref],

View File

@@ -14,6 +14,8 @@ import { Gateway } from '#resources/istio/gateway/gateway.ts';
import { NotReadyError } from '#utils/errors.ts';
import { NamespaceService } from '#bootstrap/namespaces/namespaces.ts';
import { CloudflareService } from '#services/cloudflare/cloudflare.ts';
import { HelmRelease } from '#resources/flux/helm-release/helm-release.ts';
import { RepoService } from '#bootstrap/repos/repos.ts';
const specSchema = z.object({
domain: z.string(),
@@ -37,6 +39,8 @@ class Environment extends CustomResource<typeof specSchema> {
#redisServer: RedisServer;
#authentikServer: AuthentikServer;
#cloudflareService: CloudflareService;
#argoRelease: HelmRelease;
#argoNamespace: Namespace;
constructor(options: CustomResourceOptions<typeof specSchema>) {
super(options);
@@ -67,6 +71,11 @@ class Environment extends CustomResource<typeof specSchema> {
this.#authentikServer = resourceService.get(AuthentikServer, `${this.name}-authentik`, homelabNamespace);
this.#authentikServer.on('changed', this.queueReconcile);
this.#argoNamespace = resourceService.get(Namespace, `${this.name}-argo`);
this.#argoRelease = resourceService.get(HelmRelease, `${this.name}-argo`, homelabNamespace);
this.#argoRelease.on('changed', this.queueReconcile);
}
public get certificate() {
@@ -99,27 +108,6 @@ class Environment extends CustomResource<typeof specSchema> {
throw new NotReadyError('InvalidSpec');
}
if (this.#cloudflareService.ready && spec.networkIp) {
const client = this.#cloudflareService.client;
const zones = await client.zones.list({
name: spec.domain,
});
const [zone] = zones.result;
if (!zone) {
throw new NotReadyError('NoZoneFound');
}
const existingRecords = await client.dns.records.list({
zone_id: zone.id,
name: {
exact: `*.${spec.domain}`,
},
});
console.log('Cloudflare records', existingRecords);
// zones.result[0].
}
await this.#namespace.ensure({
metadata: {
labels: {
@@ -208,6 +196,29 @@ class Environment extends CustomResource<typeof specSchema> {
],
},
});
await this.#argoNamespace.ensure({});
const repoService = this.services.get(RepoService);
await this.#argoRelease.ensure({
spec: {
targetNamespace: this.#argoNamespace.name,
interval: '1h',
values: {},
chart: {
spec: {
chart: 'argo-cd',
version: '3.9.0',
sourceRef: {
apiVersion: 'source.toolkit.fluxcd.io/v1',
kind: 'HelmRepository',
name: repoService.argo.name,
namespace: repoService.argo.namespace,
},
},
},
},
});
};
}

View File

@@ -19,7 +19,8 @@ const specSchema = z.object({
clientType: z.enum(ClientTypeEnum).optional(),
redirectUris: z.array(
z.object({
url: z.string(),
subdomain: z.string(),
path: z.string(),
matchingMode: z.enum(['strict', 'regex']),
}),
),
@@ -98,8 +99,14 @@ class OIDCClient extends CustomResource<typeof specSchema> {
token: authentikSecret.token,
});
const redirectUris = this.spec.redirectUris.map((uri) => ({
matchingMode: uri.matchingMode,
url: new URL(uri.path, `https://${uri.subdomain}.${this.#environment.current?.spec?.domain}`).toString(),
}));
await authentikServer.upsertClient({
...this.spec,
redirectUris,
name: this.name,
secret: secret.clientSecret,
});

View File

@@ -52,6 +52,55 @@ class CloudflareService extends EventEmitter<CloudflareServiceEvents> {
return client;
}
public ensureTunnel = async (route: string) => {
const secret = this.#secret.value;
if (!secret) {
return;
}
const client = this.client;
const domainParts = route.split('.');
const cname = `${secret.tunnelId}.cfargotunnel.com`;
const tld = domainParts.pop();
const root = domainParts.pop();
const zoneName = `${root}.${tld}`;
const name = domainParts.join('.');
const zones = await client.zones.list({
name: zoneName,
});
const [zone] = zones.result;
if (!zone) {
return;
}
const records = await client.dns.records.list({
zone_id: zone.id,
name: {
exact: route,
},
type: 'CNAME',
});
const [record] = records.result;
if (record) {
await client.dns.records.edit(record.id, {
zone_id: zone.id,
type: 'CNAME',
content: cname,
name: name,
ttl: 1,
proxied: true,
});
} else {
await client.dns.records.create({
zone_id: zone.id,
type: 'CNAME',
content: cname,
name: name,
ttl: 1,
proxied: true,
});
}
};
}
export { CloudflareService };