mirror of
https://github.com/morten-olsen/homelab-operator.git
synced 2026-02-08 01:36:28 +01:00
more stuff
This commit is contained in:
@@ -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',
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user