feat: improve API keys and build workflows
This commit is contained in:
+70
-42
@@ -4,6 +4,27 @@ import type { ComposeSpecification, Service } from "../schema/docker.d";
|
||||
import { LABELS } from "../const";
|
||||
import { deleteResource, applyResource } from "./apply";
|
||||
|
||||
export class DatabaseReconciliationError extends Error {
|
||||
constructor(phase: string, error: unknown, claim?: PostgresClaim) {
|
||||
const context = claim
|
||||
? ` for database ${claim.database} (service ${claim.service}, role ${claim.username})`
|
||||
: "";
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
// Provider errors can contain credentials or entire request bodies. Only
|
||||
// expose a short, recognisable operational reason, never a raw response.
|
||||
const knownReason = /^(forbidden|not found|conflict|permission denied|connection refused|timed out|timeout|unauthorized|unprocessable entity|service unavailable)\b/i.exec(reason);
|
||||
const status = error && typeof error === "object" && "code" in error &&
|
||||
typeof error.code === "number" && error.code >= 400 && error.code < 600
|
||||
? ` (HTTP ${error.code})`
|
||||
: "";
|
||||
const safeReason = `${knownReason ? knownReason[1] : "Kubernetes request failed"}${status}`;
|
||||
super(`Database reconciliation failed during ${phase}${context}: ${safeReason}`, {
|
||||
cause: error,
|
||||
});
|
||||
this.name = "DatabaseReconciliationError";
|
||||
}
|
||||
}
|
||||
|
||||
export const DATABASE_NAMESPACE = "database";
|
||||
export const DATABASE_CLUSTER = "postgres";
|
||||
export const DATABASE_HOST = `c.${DATABASE_NAMESPACE}.svc.cluster.local`;
|
||||
@@ -189,33 +210,37 @@ async function readObject<T>(
|
||||
async function ensureRoleSecret(
|
||||
claim: PostgresClaim,
|
||||
): Promise<RoleCredentials> {
|
||||
const existing = await readObject<V1Secret>({
|
||||
apiVersion: "v1",
|
||||
kind: "Secret",
|
||||
metadata: {
|
||||
name: claim.secretName,
|
||||
namespace: DATABASE_NAMESPACE,
|
||||
},
|
||||
});
|
||||
try {
|
||||
const existing = await readObject<V1Secret>({
|
||||
apiVersion: "v1",
|
||||
kind: "Secret",
|
||||
metadata: {
|
||||
name: claim.secretName,
|
||||
namespace: DATABASE_NAMESPACE,
|
||||
},
|
||||
});
|
||||
|
||||
const username = claim.username;
|
||||
const password = decodeSecretValue(existing?.data?.password) ?? randomUUID();
|
||||
const username = claim.username;
|
||||
const password = decodeSecretValue(existing?.data?.password) ?? randomUUID();
|
||||
|
||||
await applyResource({
|
||||
apiVersion: "v1",
|
||||
kind: "Secret",
|
||||
metadata: {
|
||||
name: claim.secretName,
|
||||
namespace: DATABASE_NAMESPACE,
|
||||
},
|
||||
type: existing?.type ?? "Opaque",
|
||||
stringData: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
} satisfies V1Secret);
|
||||
await applyResource({
|
||||
apiVersion: "v1",
|
||||
kind: "Secret",
|
||||
metadata: {
|
||||
name: claim.secretName,
|
||||
namespace: DATABASE_NAMESPACE,
|
||||
},
|
||||
type: existing?.type ?? "Opaque",
|
||||
stringData: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
} satisfies V1Secret);
|
||||
|
||||
return { username, password };
|
||||
return { username, password };
|
||||
} catch (error) {
|
||||
throw new DatabaseReconciliationError("role secret setup", error, claim);
|
||||
}
|
||||
}
|
||||
|
||||
function toManagedRole(claim: PostgresClaim): ManagedRole {
|
||||
@@ -247,20 +272,27 @@ async function reconcileManagedRoles(
|
||||
): Promise<void> {
|
||||
if (claims.length === 0) return;
|
||||
|
||||
const cluster = await readObject<
|
||||
KubernetesObject & { spec?: { managed?: { roles?: ManagedRole[] } } }
|
||||
>({
|
||||
apiVersion: "postgresql.cnpg.io/v1",
|
||||
kind: "Cluster",
|
||||
metadata: {
|
||||
name: DATABASE_CLUSTER,
|
||||
namespace: DATABASE_NAMESPACE,
|
||||
},
|
||||
});
|
||||
let cluster: KubernetesObject & { spec?: { managed?: { roles?: ManagedRole[] } } } | undefined;
|
||||
try {
|
||||
cluster = await readObject<
|
||||
KubernetesObject & { spec?: { managed?: { roles?: ManagedRole[] } } }
|
||||
>({
|
||||
apiVersion: "postgresql.cnpg.io/v1",
|
||||
kind: "Cluster",
|
||||
metadata: {
|
||||
name: DATABASE_CLUSTER,
|
||||
namespace: DATABASE_NAMESPACE,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseReconciliationError("cluster lookup", error, claims[0]);
|
||||
}
|
||||
|
||||
if (!cluster) {
|
||||
throw new Error(
|
||||
`CNPG cluster ${DATABASE_CLUSTER} was not found in namespace ${DATABASE_NAMESPACE}.`,
|
||||
throw new DatabaseReconciliationError(
|
||||
`CNPG cluster ${DATABASE_NAMESPACE}/${DATABASE_CLUSTER} lookup`,
|
||||
new Error("Not found"),
|
||||
claims[0],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -287,9 +319,7 @@ async function reconcileManagedRoles(
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to reconcile managed roles on ${DATABASE_NAMESPACE}/${DATABASE_CLUSTER}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
throw new DatabaseReconciliationError("managed role update", error, claims[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,9 +359,7 @@ async function reconcileDatabases(
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to reconcile database ${claim.database} owned by ${claim.username}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
throw new DatabaseReconciliationError("database apply", error, claim);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user