|
|
|
@@ -64,7 +64,7 @@ interface PacTarget {
|
|
|
|
|
enabled: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface PacConfig {
|
|
|
|
|
export interface PacConfig {
|
|
|
|
|
version: number;
|
|
|
|
|
kind: "platform-infra-pipelines-as-code";
|
|
|
|
|
metadata: {
|
|
|
|
@@ -139,6 +139,20 @@ interface PacConfig {
|
|
|
|
|
};
|
|
|
|
|
repositories: PacRepository[];
|
|
|
|
|
consumers: PacConsumer[];
|
|
|
|
|
validationWarnings: PacConfigValidationWarning[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface PacConfigValidationWarning {
|
|
|
|
|
readonly code:
|
|
|
|
|
| "pac-unselected-consumer-parse-invalid"
|
|
|
|
|
| "pac-unselected-consumer-invalid"
|
|
|
|
|
| "pac-unselected-repository-parse-invalid"
|
|
|
|
|
| "pac-unselected-repository-invalid";
|
|
|
|
|
readonly object: { readonly kind: "consumer" | "repository"; readonly id: string };
|
|
|
|
|
readonly consumer: string | null;
|
|
|
|
|
readonly configPath: string;
|
|
|
|
|
readonly blocking: false;
|
|
|
|
|
readonly evidence: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface PacRepository {
|
|
|
|
@@ -185,6 +199,7 @@ interface PacConsumer {
|
|
|
|
|
gitOps: { repoUrl: string; targetRevision: string };
|
|
|
|
|
} | null;
|
|
|
|
|
repositoryRef: string;
|
|
|
|
|
params: Record<string, string>;
|
|
|
|
|
closeoutGitOpsMirrorFlush: boolean;
|
|
|
|
|
closeoutGitOpsMirrorLane: "v02" | "v03" | null;
|
|
|
|
|
sourceArtifact: PacSourceArtifactSpec | null;
|
|
|
|
@@ -294,7 +309,7 @@ export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConf
|
|
|
|
|
const structuredError = args.includes("--json") || args.includes("--full");
|
|
|
|
|
try {
|
|
|
|
|
const options = parsePacSourceArtifactOptions(args.slice(1));
|
|
|
|
|
const pac = readPacConfig();
|
|
|
|
|
const pac = readPacConfig({ consumerId: options.consumerId });
|
|
|
|
|
const target = resolveTarget(pac, options.targetId);
|
|
|
|
|
const consumer = resolveConsumer(pac, options.consumerId);
|
|
|
|
|
if (consumer.node.toLowerCase() !== target.id.toLowerCase()) throw new Error(`Pipelines-as-Code consumer ${consumer.id} belongs to ${consumer.node}, not target ${target.id}`);
|
|
|
|
@@ -325,10 +340,13 @@ export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConf
|
|
|
|
|
cloneUrl: repository.cloneUrl,
|
|
|
|
|
owner: repository.owner,
|
|
|
|
|
repo: repository.repo,
|
|
|
|
|
params: repository.params,
|
|
|
|
|
params: consumerParams(repository, consumer),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
const result = await runPacSourceArtifact(binding, options, async ({ desiredSpec, desiredWrapper, provenance, sourceCommit }) => await observeSourceArtifactRuntime(config, target, consumer, desiredSpec, desiredWrapper, provenance, sourceCommit));
|
|
|
|
|
const result = {
|
|
|
|
|
...await runPacSourceArtifact(binding, options, async ({ desiredSpec, desiredWrapper, provenance, sourceCommit }) => await observeSourceArtifactRuntime(config, target, consumer, desiredSpec, desiredWrapper, provenance, sourceCommit)),
|
|
|
|
|
warnings: pac.validationWarnings,
|
|
|
|
|
};
|
|
|
|
|
return options.json || options.full ? result : renderPacSourceArtifactResult(result);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const safeError = pacSourceArtifactSafeError(error);
|
|
|
|
@@ -424,8 +442,15 @@ function help(scope: string | null): Record<string, unknown> {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readPacConfig(): PacConfig {
|
|
|
|
|
export function readPacConfig(selection: { readonly consumerId?: string | null; readonly selectDefault?: boolean } = {}): PacConfig {
|
|
|
|
|
const root = materializeYamlComposition(readYamlRecord<Record<string, unknown>>(configFile, "platform-infra-pipelines-as-code"), { label: configLabel }).value;
|
|
|
|
|
return parsePacConfigDocument(root, selection);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function parsePacConfigDocument(
|
|
|
|
|
root: Record<string, unknown>,
|
|
|
|
|
selection: { readonly consumerId?: string | null; readonly selectDefault?: boolean } = {},
|
|
|
|
|
): PacConfig {
|
|
|
|
|
const release = y.objectField(root, "release", "");
|
|
|
|
|
const deliveryProvenance = y.objectField(root, "deliveryProvenance", "");
|
|
|
|
|
const provenanceController = y.objectField(deliveryProvenance, "controller", "deliveryProvenance");
|
|
|
|
@@ -441,13 +466,19 @@ function readPacConfig(): PacConfig {
|
|
|
|
|
const exporterFailure = y.objectField(observability, "exporterFailure", "observability");
|
|
|
|
|
const admin = y.objectField(gitea, "admin", "gitea");
|
|
|
|
|
const webhook = y.objectField(gitea, "webhook", "gitea");
|
|
|
|
|
const repositories = root.repositories === undefined
|
|
|
|
|
? [parseRepository(y.objectField(root, "repository", ""), "repository")]
|
|
|
|
|
: y.arrayOfRecords(root.repositories, "repositories").map((item, index) => parseRepository(item, `repositories[${index}]`));
|
|
|
|
|
const consumers = root.consumers === undefined
|
|
|
|
|
? [parseConsumer(y.objectField(root, "consumer", ""), "consumer", repositories[0]?.id ?? "default")]
|
|
|
|
|
: y.arrayOfRecords(root.consumers, "consumers").map((item, index) => parseConsumer(item, `consumers[${index}]`, repositories[0]?.id ?? "default"));
|
|
|
|
|
const defaults = y.objectField(root, "defaults", "");
|
|
|
|
|
const repositoryEntries = pacConfigObjectEntries(root.repositories, root.repository, "repositories", "repository");
|
|
|
|
|
const consumerEntries = pacConfigObjectEntries(root.consumers, root.consumer, "consumers", "consumer");
|
|
|
|
|
const defaultRepositoryRef = rawPacObjectId(repositoryEntries[0]?.value, repositoryEntries[0]?.path ?? "repository", "repository");
|
|
|
|
|
const defaultConsumerId = typeof defaults.consumerId === "string" && defaults.consumerId.length > 0
|
|
|
|
|
? defaults.consumerId
|
|
|
|
|
: rawPacObjectId(consumerEntries[0]?.value, consumerEntries[0]?.path ?? "consumer", "consumer");
|
|
|
|
|
const selectedConsumerIds = selectedPacConsumerIds(defaultConsumerId, selection);
|
|
|
|
|
assertSelectedRawObjectsUnique(consumerEntries, selectedConsumerIds, "consumer");
|
|
|
|
|
const selectedRepositoryRefs = selectedPacRepositoryRefs(consumerEntries, selectedConsumerIds, defaultRepositoryRef);
|
|
|
|
|
assertSelectedRawObjectsUnique(repositoryEntries, selectedRepositoryRefs, "repository");
|
|
|
|
|
const parsedRepositories = parsePacRepositories(repositoryEntries, selectedRepositoryRefs);
|
|
|
|
|
const parsedConsumers = parsePacConsumers(consumerEntries, selectedConsumerIds, defaultRepositoryRef);
|
|
|
|
|
const parsed: PacConfig = {
|
|
|
|
|
version: y.integerField(root, "version", ""),
|
|
|
|
|
kind: "platform-infra-pipelines-as-code",
|
|
|
|
@@ -459,7 +490,7 @@ function readPacConfig(): PacConfig {
|
|
|
|
|
},
|
|
|
|
|
defaults: {
|
|
|
|
|
targetId: y.stringField(defaults, "targetId", "defaults"),
|
|
|
|
|
consumerId: typeof defaults.consumerId === "string" && defaults.consumerId.length > 0 ? defaults.consumerId : consumers[0]?.id ?? "default",
|
|
|
|
|
consumerId: defaultConsumerId,
|
|
|
|
|
},
|
|
|
|
|
display: {
|
|
|
|
|
timeZone: y.stringField(display, "timeZone", "display"),
|
|
|
|
@@ -525,13 +556,140 @@ function readPacConfig(): PacConfig {
|
|
|
|
|
branch: y.stringField(webhook, "branch", "gitea.webhook"),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
repositories,
|
|
|
|
|
consumers,
|
|
|
|
|
repositories: parsedRepositories.values,
|
|
|
|
|
consumers: parsedConsumers.values,
|
|
|
|
|
validationWarnings: [...parsedRepositories.warnings, ...parsedConsumers.warnings],
|
|
|
|
|
};
|
|
|
|
|
validateConfig(parsed);
|
|
|
|
|
parsed.validationWarnings = validateConfig(parsed, selectedConsumerIds, parsed.validationWarnings);
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectedPacConsumerIds(
|
|
|
|
|
defaultConsumerId: string,
|
|
|
|
|
selection: { readonly consumerId?: string | null; readonly selectDefault?: boolean },
|
|
|
|
|
): ReadonlySet<string> {
|
|
|
|
|
if (typeof selection.consumerId === "string" && selection.consumerId.length > 0) {
|
|
|
|
|
return new Set([selection.consumerId.toLowerCase()]);
|
|
|
|
|
}
|
|
|
|
|
return selection.selectDefault === true ? new Set([defaultConsumerId.toLowerCase()]) : new Set();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface PacConfigObjectEntry {
|
|
|
|
|
readonly value: unknown;
|
|
|
|
|
readonly path: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pacConfigObjectEntries(plural: unknown, singular: unknown, pluralLabel: string, singularLabel: string): PacConfigObjectEntry[] {
|
|
|
|
|
if (plural === undefined) {
|
|
|
|
|
if (singular === undefined) throw new Error(`${configLabel}.${singularLabel} must be an object`);
|
|
|
|
|
return [{ value: singular, path: singularLabel }];
|
|
|
|
|
}
|
|
|
|
|
if (!Array.isArray(plural)) throw new Error(`${configLabel}.${pluralLabel} must be an array`);
|
|
|
|
|
return plural.map((value, index) => ({ value, path: `${pluralLabel}[${index}]` }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function rawPacObjectId(value: unknown, path: string, kind: "consumer" | "repository"): string {
|
|
|
|
|
if (!isRecord(value)) return path;
|
|
|
|
|
if (typeof value.id === "string" && value.id.length > 0) return value.id;
|
|
|
|
|
if (kind === "repository" && typeof value.name === "string" && value.name.length > 0) return value.name;
|
|
|
|
|
if (kind === "consumer" && typeof value.node === "string" && value.node.length > 0 && typeof value.lane === "string" && value.lane.length > 0) {
|
|
|
|
|
return `${value.node}-${value.lane}`;
|
|
|
|
|
}
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function rawPacObjectRefs(entry: PacConfigObjectEntry, kind: "consumer" | "repository"): string[] {
|
|
|
|
|
if (!isRecord(entry.value)) return [entry.path.toLowerCase()];
|
|
|
|
|
const refs = [rawPacObjectId(entry.value, entry.path, kind)];
|
|
|
|
|
if (kind === "repository" && typeof entry.value.name === "string" && entry.value.name.length > 0) refs.push(entry.value.name);
|
|
|
|
|
return [...new Set(refs.map((value) => value.toLowerCase()))];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function assertSelectedRawObjectsUnique(
|
|
|
|
|
entries: readonly PacConfigObjectEntry[],
|
|
|
|
|
selectedRefs: ReadonlySet<string>,
|
|
|
|
|
kind: "consumer" | "repository",
|
|
|
|
|
): void {
|
|
|
|
|
for (const selectedRef of selectedRefs) {
|
|
|
|
|
const matches = entries.filter((entry) => rawPacObjectRefs(entry, kind).includes(selectedRef));
|
|
|
|
|
if (matches.length !== 1) {
|
|
|
|
|
throw new Error(`selected Pipelines-as-Code ${kind} ${selectedRef} must match exactly one YAML object; matched ${matches.length}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectedPacRepositoryRefs(
|
|
|
|
|
consumerEntries: readonly PacConfigObjectEntry[],
|
|
|
|
|
selectedConsumerIds: ReadonlySet<string>,
|
|
|
|
|
defaultRepositoryRef: string,
|
|
|
|
|
): ReadonlySet<string> {
|
|
|
|
|
const refs = new Set<string>();
|
|
|
|
|
for (const entry of consumerEntries) {
|
|
|
|
|
if (!rawPacObjectRefs(entry, "consumer").some((value) => selectedConsumerIds.has(value))) continue;
|
|
|
|
|
const repositoryRef = isRecord(entry.value) && typeof entry.value.repositoryRef === "string" && entry.value.repositoryRef.length > 0
|
|
|
|
|
? entry.value.repositoryRef
|
|
|
|
|
: defaultRepositoryRef;
|
|
|
|
|
refs.add(repositoryRef.toLowerCase());
|
|
|
|
|
}
|
|
|
|
|
return refs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parsePacRepositories(
|
|
|
|
|
entries: readonly PacConfigObjectEntry[],
|
|
|
|
|
selectedRepositoryRefs: ReadonlySet<string>,
|
|
|
|
|
): { readonly values: PacRepository[]; readonly warnings: PacConfigValidationWarning[] } {
|
|
|
|
|
const values: PacRepository[] = [];
|
|
|
|
|
const warnings: PacConfigValidationWarning[] = [];
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
const id = rawPacObjectId(entry.value, entry.path, "repository");
|
|
|
|
|
try {
|
|
|
|
|
if (!isRecord(entry.value)) throw new Error(`${configLabel}.${entry.path} must be an object`);
|
|
|
|
|
values.push(parseRepository(entry.value, entry.path));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (rawPacObjectRefs(entry, "repository").some((value) => selectedRepositoryRefs.has(value))) throw error;
|
|
|
|
|
warnings.push(pacConfigWarning("pac-unselected-repository-parse-invalid", "repository", id, entry.path, error));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return { values, warnings };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parsePacConsumers(
|
|
|
|
|
entries: readonly PacConfigObjectEntry[],
|
|
|
|
|
selectedConsumerIds: ReadonlySet<string>,
|
|
|
|
|
defaultRepositoryRef: string,
|
|
|
|
|
): { readonly values: PacConsumer[]; readonly warnings: PacConfigValidationWarning[] } {
|
|
|
|
|
const values: PacConsumer[] = [];
|
|
|
|
|
const warnings: PacConfigValidationWarning[] = [];
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
const id = rawPacObjectId(entry.value, entry.path, "consumer");
|
|
|
|
|
try {
|
|
|
|
|
if (!isRecord(entry.value)) throw new Error(`${configLabel}.${entry.path} must be an object`);
|
|
|
|
|
values.push(parseConsumer(entry.value, entry.path, defaultRepositoryRef));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (selectedConsumerIds.has(id.toLowerCase())) throw error;
|
|
|
|
|
warnings.push(pacConfigWarning("pac-unselected-consumer-parse-invalid", "consumer", id, entry.path, error));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return { values, warnings };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pacConfigWarning(
|
|
|
|
|
code: PacConfigValidationWarning["code"],
|
|
|
|
|
kind: PacConfigValidationWarning["object"]["kind"],
|
|
|
|
|
id: string,
|
|
|
|
|
path: string,
|
|
|
|
|
error: unknown,
|
|
|
|
|
): PacConfigValidationWarning {
|
|
|
|
|
return {
|
|
|
|
|
code,
|
|
|
|
|
object: { kind, id },
|
|
|
|
|
consumer: kind === "consumer" ? id : null,
|
|
|
|
|
configPath: `${configLabel}#${path}`,
|
|
|
|
|
blocking: false,
|
|
|
|
|
evidence: pacValueEvidence(error instanceof Error ? error.message : String(error)),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseRepository(repository: Record<string, unknown>, path: string): PacRepository {
|
|
|
|
|
const name = y.kubernetesNameField(repository, "name", path);
|
|
|
|
|
return {
|
|
|
|
@@ -582,6 +740,7 @@ function parseConsumer(consumer: Record<string, unknown>, path: string, defaultR
|
|
|
|
|
},
|
|
|
|
|
deliveryProvenance,
|
|
|
|
|
repositoryRef: typeof consumer.repositoryRef === "string" && consumer.repositoryRef.length > 0 ? consumer.repositoryRef : defaultRepositoryRef,
|
|
|
|
|
params: consumer.params === undefined ? {} : stringRecord(y.objectField(consumer, "params", path), `${path}.params`),
|
|
|
|
|
closeoutGitOpsMirrorFlush: y.booleanField(consumer, "closeoutGitOpsMirrorFlush", path),
|
|
|
|
|
closeoutGitOpsMirrorLane: consumer.closeoutGitOpsMirrorLane === undefined ? null : y.enumField(consumer, "closeoutGitOpsMirrorLane", path, ["v02", "v03"] as const),
|
|
|
|
|
sourceArtifact: consumer.sourceArtifact === undefined ? null : parseSourceArtifact(y.objectField(consumer, "sourceArtifact", path), `${path}.sourceArtifact`),
|
|
|
|
@@ -819,27 +978,67 @@ function parseTarget(record: Record<string, unknown>, index: number): PacTarget
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateConfig(config: PacConfig): void {
|
|
|
|
|
export function validatePacConfig(config: PacConfig, selectedConsumerIds: ReadonlySet<string>): PacConfigValidationWarning[] {
|
|
|
|
|
return validateConfig(config, selectedConsumerIds, []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateConfig(
|
|
|
|
|
config: PacConfig,
|
|
|
|
|
selectedConsumerIds: ReadonlySet<string>,
|
|
|
|
|
initialWarnings: readonly PacConfigValidationWarning[],
|
|
|
|
|
): PacConfigValidationWarning[] {
|
|
|
|
|
if (config.version !== 1) throw new Error(`${configLabel}.version must be 1`);
|
|
|
|
|
resolveTarget(config, config.defaults.targetId);
|
|
|
|
|
if (config.release.waitTimeoutSeconds > 55) throw new Error(`${configLabel}.release.waitTimeoutSeconds must fit the 60s trans budget`);
|
|
|
|
|
if (config.closeout.gitOpsMirrorFlush.waitTimeoutSeconds > 55) throw new Error(`${configLabel}.closeout.gitOpsMirrorFlush.waitTimeoutSeconds must fit the 60s trans budget`);
|
|
|
|
|
resolveConsumer(config, config.defaults.consumerId);
|
|
|
|
|
if (selectedConsumerIds.has(config.defaults.consumerId.toLowerCase())) resolveConsumer(config, config.defaults.consumerId);
|
|
|
|
|
const warnings: PacConfigValidationWarning[] = [...initialWarnings];
|
|
|
|
|
const selectedRepositoryRefs = new Set(config.consumers
|
|
|
|
|
.filter((consumer) => selectedConsumerIds.has(consumer.id.toLowerCase()))
|
|
|
|
|
.map((consumer) => consumer.repositoryRef.toLowerCase()));
|
|
|
|
|
const ids = new Set<string>();
|
|
|
|
|
for (const repository of config.repositories) {
|
|
|
|
|
if (ids.has(repository.id)) throw new Error(`${configLabel}.repositories id must be unique: ${repository.id}`);
|
|
|
|
|
ids.add(repository.id);
|
|
|
|
|
if (repository.providerType !== "gitea") throw new Error(`${configLabel}.repositories.${repository.id}.providerType must be gitea`);
|
|
|
|
|
if (isInternalRepositoryMatchUrl(repository.url)) throw new Error(`${configLabel}.repositories.${repository.id}.url must be the public Gitea URL used in webhook payloads; keep internal service URLs in cloneUrl/params.git_read_url`);
|
|
|
|
|
if (new URL(repository.cloneUrl).origin !== new URL(config.gitea.internalBaseUrl).origin) throw new Error(`${configLabel}.repositories.${repository.id}.cloneUrl must use the configured internal Gitea base URL`);
|
|
|
|
|
try {
|
|
|
|
|
if (ids.has(repository.id)) throw new Error(`${configLabel}.repositories id must be unique: ${repository.id}`);
|
|
|
|
|
if (repository.providerType !== "gitea") throw new Error(`${configLabel}.repositories.${repository.id}.providerType must be gitea`);
|
|
|
|
|
if (isInternalRepositoryMatchUrl(repository.url)) throw new Error(`${configLabel}.repositories.${repository.id}.url must be the public Gitea URL used in webhook payloads; keep internal service URLs in cloneUrl/params.git_read_url`);
|
|
|
|
|
if (new URL(repository.cloneUrl).origin !== new URL(config.gitea.internalBaseUrl).origin) throw new Error(`${configLabel}.repositories.${repository.id}.cloneUrl must use the configured internal Gitea base URL`);
|
|
|
|
|
ids.add(repository.id);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (selectedRepositoryRefs.has(repository.id.toLowerCase()) || selectedRepositoryRefs.has(repository.name.toLowerCase())) throw error;
|
|
|
|
|
warnings.push(pacConfigWarning("pac-unselected-repository-invalid", "repository", repository.id, `repositories.${repository.id}`, error));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const consumerIds = new Set<string>();
|
|
|
|
|
const markerValues = new Set<string>();
|
|
|
|
|
const reservedServiceAccounts = new Set<string>();
|
|
|
|
|
for (const consumer of config.consumers) {
|
|
|
|
|
if (consumerIds.has(consumer.id)) throw new Error(`${configLabel}.consumers id must be unique: ${consumer.id}`);
|
|
|
|
|
consumerIds.add(consumer.id);
|
|
|
|
|
try {
|
|
|
|
|
if (consumerIds.has(consumer.id)) throw new Error(`${configLabel}.consumers id must be unique: ${consumer.id}`);
|
|
|
|
|
validateConsumerConfig(config, consumer, markerValues, reservedServiceAccounts);
|
|
|
|
|
consumerIds.add(consumer.id);
|
|
|
|
|
if (consumer.deliveryProvenance !== null) {
|
|
|
|
|
markerValues.add(consumer.deliveryProvenance.markerValue);
|
|
|
|
|
reservedServiceAccounts.add(`${consumer.node.toLowerCase()}\u0000${consumer.deliveryProvenance.executionServiceAccountName}`);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (selectedConsumerIds.has(consumer.id.toLowerCase())) throw error;
|
|
|
|
|
warnings.push(pacConfigWarning("pac-unselected-consumer-invalid", "consumer", consumer.id, `consumers.${consumer.id}`, error));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!config.gitea.webhook.events.includes("push")) throw new Error(`${configLabel}.gitea.webhook.events must include push`);
|
|
|
|
|
validateTimeZone(config.display.timeZone, `${configLabel}.display.timeZone`);
|
|
|
|
|
return warnings;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateConsumerConfig(
|
|
|
|
|
config: PacConfig,
|
|
|
|
|
consumer: PacConsumer,
|
|
|
|
|
markerValues: Set<string>,
|
|
|
|
|
reservedServiceAccounts: Set<string>,
|
|
|
|
|
): void {
|
|
|
|
|
const repository = resolveRepository(config, consumer.repositoryRef);
|
|
|
|
|
const params = consumerParams(repository, consumer);
|
|
|
|
|
if (consumer.id.startsWith("sentinel-")) {
|
|
|
|
|
if (repository.params.otel_traces_endpoint !== config.observability.tracesEndpoint) {
|
|
|
|
|
throw new Error(`${configLabel}.repositories.${repository.id}.params.otel_traces_endpoint must match observability.tracesEndpoint`);
|
|
|
|
@@ -855,7 +1054,6 @@ function validateConfig(config: PacConfig): void {
|
|
|
|
|
if (consumer.deliveryProvenance !== null) {
|
|
|
|
|
if (consumer.sourceArtifact === null) throw new Error(`${configLabel}.consumers.${consumer.id}.deliveryProvenance requires sourceArtifact ownership`);
|
|
|
|
|
if (markerValues.has(consumer.deliveryProvenance.markerValue)) throw new Error(`${configLabel}.consumers deliveryProvenance.markerValue must be unique: ${consumer.deliveryProvenance.markerValue}`);
|
|
|
|
|
markerValues.add(consumer.deliveryProvenance.markerValue);
|
|
|
|
|
if (consumer.deliveryProvenance.executionServiceAccountName === "default") {
|
|
|
|
|
throw new Error(`${configLabel}.consumers.${consumer.id}.deliveryProvenance.executionServiceAccountName must not reserve the shared default ServiceAccount`);
|
|
|
|
|
}
|
|
|
|
@@ -863,9 +1061,8 @@ function validateConfig(config: PacConfig): void {
|
|
|
|
|
if (reservedServiceAccounts.has(serviceAccountKey)) {
|
|
|
|
|
throw new Error(`${configLabel}.consumers deliveryProvenance.executionServiceAccountName must be unique per target: ${consumer.deliveryProvenance.executionServiceAccountName}`);
|
|
|
|
|
}
|
|
|
|
|
reservedServiceAccounts.add(serviceAccountKey);
|
|
|
|
|
if (repository.params.service_account !== consumer.deliveryProvenance.executionServiceAccountName) {
|
|
|
|
|
throw new Error(`${configLabel}.consumers.${consumer.id}.deliveryProvenance.executionServiceAccountName must match repository params.service_account`);
|
|
|
|
|
if (params.service_account !== consumer.deliveryProvenance.executionServiceAccountName) {
|
|
|
|
|
throw new Error(`${configLabel}.consumers.${consumer.id}.deliveryProvenance.executionServiceAccountName must match effective params.service_account`);
|
|
|
|
|
}
|
|
|
|
|
if (consumer.argoBootstrap !== null
|
|
|
|
|
&& (consumer.argoBootstrap.repoUrl !== consumer.deliveryProvenance.gitOps.repoUrl
|
|
|
|
@@ -885,13 +1082,10 @@ function validateConfig(config: PacConfig): void {
|
|
|
|
|
throw new Error(`${configLabel}.consumers.${consumer.id}.argoBootstrap.repositoryCredential is required for the private selfmedia repository`);
|
|
|
|
|
}
|
|
|
|
|
if (consumer.closeoutGitOpsMirrorFlush) throw new Error(`${configLabel}.consumers.${consumer.id}.closeoutGitOpsMirrorFlush must be false for Gitea writeback GitOps`);
|
|
|
|
|
if (repository.params.gitops_secret_name !== repository.secretName) throw new Error(`${configLabel}.repositories.${repository.id}.params.gitops_secret_name must match secretName`);
|
|
|
|
|
if (repository.params.gitops_username !== consumer.argoBootstrap.repositoryCredential.username) throw new Error(`${configLabel}.consumers.${consumer.id}.Argo and Tekton Gitea usernames must match`);
|
|
|
|
|
if (repository.params.gitops_read_url !== consumer.argoBootstrap.repoUrl) throw new Error(`${configLabel}.consumers.${consumer.id}.Argo repoUrl must match params.gitops_read_url`);
|
|
|
|
|
if (params.gitops_secret_name !== repository.secretName) throw new Error(`${configLabel}.consumers.${consumer.id}.effective params.gitops_secret_name must match repository secretName`);
|
|
|
|
|
if (params.gitops_username !== consumer.argoBootstrap.repositoryCredential.username) throw new Error(`${configLabel}.consumers.${consumer.id}.Argo and Tekton Gitea usernames must match`);
|
|
|
|
|
if (params.gitops_read_url !== consumer.argoBootstrap.repoUrl) throw new Error(`${configLabel}.consumers.${consumer.id}.Argo repoUrl must match effective params.gitops_read_url`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!config.gitea.webhook.events.includes("push")) throw new Error(`${configLabel}.gitea.webhook.events must include push`);
|
|
|
|
|
validateTimeZone(config.display.timeZone, `${configLabel}.display.timeZone`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function numberInRange(value: unknown, minimum: number, maximum: number, path: string): number {
|
|
|
|
@@ -922,19 +1116,42 @@ function resolveTarget(config: PacConfig, targetId: string | null): PacTarget {
|
|
|
|
|
|
|
|
|
|
function resolveConsumer(config: PacConfig, consumerId: string | null): PacConsumer {
|
|
|
|
|
const id = consumerId ?? config.defaults.consumerId;
|
|
|
|
|
const consumer = config.consumers.find((item) => item.id.toLowerCase() === id.toLowerCase());
|
|
|
|
|
if (consumer === undefined) throw new Error(`unknown Pipelines-as-Code consumer ${id}; known consumers: ${config.consumers.map((item) => item.id).join(", ")}`);
|
|
|
|
|
return consumer;
|
|
|
|
|
const matches = config.consumers.filter((item) => item.id.toLowerCase() === id.toLowerCase());
|
|
|
|
|
if (matches.length !== 1) throw new Error(`Pipelines-as-Code consumer ${id} must resolve exactly once; matched ${matches.length}; known consumers: ${config.consumers.map((item) => item.id).join(", ")}`);
|
|
|
|
|
return matches[0] as PacConsumer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveRepository(config: PacConfig, repositoryRef: string): PacRepository {
|
|
|
|
|
const repository = config.repositories.find((item) => item.id === repositoryRef || item.name === repositoryRef);
|
|
|
|
|
if (repository === undefined) throw new Error(`unknown Pipelines-as-Code repository ${repositoryRef}; known repositories: ${config.repositories.map((item) => item.id).join(", ")}`);
|
|
|
|
|
return repository;
|
|
|
|
|
const matches = config.repositories.filter((item) => item.id === repositoryRef || item.name === repositoryRef);
|
|
|
|
|
if (matches.length !== 1) throw new Error(`Pipelines-as-Code repository ${repositoryRef} must resolve exactly once; matched ${matches.length}; known repositories: ${config.repositories.map((item) => item.id).join(", ")}`);
|
|
|
|
|
return matches[0] as PacRepository;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function consumerParams(repository: PacRepository, consumer: PacConsumer): Record<string, string> {
|
|
|
|
|
return { ...repository.params, ...consumer.params };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function validPacConsumers(config: PacConfig): PacConsumer[] {
|
|
|
|
|
const invalidConsumers = new Set(config.validationWarnings
|
|
|
|
|
.filter((warning) => warning.object.kind === "consumer")
|
|
|
|
|
.map((warning) => warning.object.id.toLowerCase()));
|
|
|
|
|
const invalidRepositories = new Set(config.validationWarnings
|
|
|
|
|
.filter((warning) => warning.object.kind === "repository")
|
|
|
|
|
.map((warning) => warning.object.id.toLowerCase()));
|
|
|
|
|
for (const repository of config.repositories) {
|
|
|
|
|
if (invalidRepositories.has(repository.id.toLowerCase()) || invalidRepositories.has(repository.name.toLowerCase())) {
|
|
|
|
|
invalidRepositories.add(repository.id.toLowerCase());
|
|
|
|
|
invalidRepositories.add(repository.name.toLowerCase());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return config.consumers.filter((consumer) => (
|
|
|
|
|
!invalidConsumers.has(consumer.id.toLowerCase())
|
|
|
|
|
&& !invalidRepositories.has(consumer.repositoryRef.toLowerCase())
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function plan(options: CommonOptions): Record<string, unknown> {
|
|
|
|
|
const pac = readPacConfig();
|
|
|
|
|
const pac = readPacConfig({ consumerId: options.consumerId, selectDefault: true });
|
|
|
|
|
const target = resolveTarget(pac, options.targetId);
|
|
|
|
|
const consumer = resolveConsumer(pac, options.consumerId);
|
|
|
|
|
const repository = resolveRepository(pac, consumer.repositoryRef);
|
|
|
|
@@ -950,12 +1167,13 @@ function plan(options: CommonOptions): Record<string, unknown> {
|
|
|
|
|
consumer: consumerObservationSummary(consumer),
|
|
|
|
|
secrets,
|
|
|
|
|
policy: policyChecks(repository),
|
|
|
|
|
warnings: pac.validationWarnings,
|
|
|
|
|
next: nextCommands(target.id, consumer.id, pac.defaults.consumerId),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Record<string, unknown>> {
|
|
|
|
|
const pac = readPacConfig();
|
|
|
|
|
const pac = readPacConfig({ consumerId: options.consumerId, selectDefault: true });
|
|
|
|
|
const target = resolveTarget(pac, options.targetId);
|
|
|
|
|
const consumer = resolveConsumer(pac, options.consumerId);
|
|
|
|
|
const repository = resolveRepository(pac, consumer.repositoryRef);
|
|
|
|
@@ -975,12 +1193,13 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
|
|
|
|
consumer: consumerObservationSummary(consumer),
|
|
|
|
|
secrets: secretSummaries(pac),
|
|
|
|
|
remote: parsed ?? compactCapture(result, { full: true }),
|
|
|
|
|
warnings: pac.validationWarnings,
|
|
|
|
|
next: nextCommands(target.id, consumer.id, pac.defaults.consumerId),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function bootstrap(config: UniDeskConfig, options: ApplyOptions): Promise<Record<string, unknown>> {
|
|
|
|
|
const pac = readPacConfig();
|
|
|
|
|
const pac = readPacConfig({ consumerId: options.consumerId, selectDefault: true });
|
|
|
|
|
const target = resolveTarget(pac, options.targetId);
|
|
|
|
|
const consumer = resolveConsumer(pac, options.consumerId);
|
|
|
|
|
if (consumer.node.toLowerCase() !== target.id.toLowerCase()) {
|
|
|
|
@@ -991,14 +1210,14 @@ async function bootstrap(config: UniDeskConfig, options: ApplyOptions): Promise<
|
|
|
|
|
try {
|
|
|
|
|
mirror = resolvePacBootstrapMirrorRepository(readGiteaConfig(), target.id, {
|
|
|
|
|
...repository,
|
|
|
|
|
sourceBranch: repository.params.source_branch,
|
|
|
|
|
sourceBranch: consumerParams(repository, consumer).source_branch,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return pacBootstrapYamlMatchFailure(target.id, consumer.id, error);
|
|
|
|
|
return { ...pacBootstrapYamlMatchFailure(target.id, consumer.id, error), warnings: pac.validationWarnings };
|
|
|
|
|
}
|
|
|
|
|
const githubPreflight = record(await runGiteaMirrorBootstrapPreflight(config, target.id, mirror.key));
|
|
|
|
|
if (githubPreflight.ok !== true) {
|
|
|
|
|
return bootstrapResult(target, consumer, repository, mirror, options, githubPreflight, { ok: false, mutation: false, mode: "skipped", githubPreflight, valuesPrinted: false }, { ok: false, mutation: false, mode: "skipped", valuesPrinted: false });
|
|
|
|
|
return bootstrapResult(target, consumer, repository, mirror, options, githubPreflight, { ok: false, mutation: false, mode: "skipped", githubPreflight, valuesPrinted: false }, { ok: false, mutation: false, mode: "skipped", valuesPrinted: false }, pac.validationWarnings);
|
|
|
|
|
}
|
|
|
|
|
const giteaArgs = ["mirror", "bootstrap", "--target", target.id, "--repo", mirror.key, ...(options.confirm ? ["--confirm"] : []), "--full"];
|
|
|
|
|
const giteaBootstrap = record(await runPlatformInfraGiteaCommand(config, giteaArgs));
|
|
|
|
@@ -1009,10 +1228,10 @@ async function bootstrap(config: UniDeskConfig, options: ApplyOptions): Promise<
|
|
|
|
|
mode: "skipped",
|
|
|
|
|
error: "gitea-bootstrap-failed",
|
|
|
|
|
valuesPrinted: false,
|
|
|
|
|
});
|
|
|
|
|
}, pac.validationWarnings);
|
|
|
|
|
}
|
|
|
|
|
const pacApply = await apply(config, options);
|
|
|
|
|
return bootstrapResult(target, consumer, repository, mirror, options, githubPreflight, giteaBootstrap, pacApply);
|
|
|
|
|
return bootstrapResult(target, consumer, repository, mirror, options, githubPreflight, giteaBootstrap, pacApply, pac.validationWarnings);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bootstrapResult(
|
|
|
|
@@ -1024,6 +1243,7 @@ function bootstrapResult(
|
|
|
|
|
githubPreflight: Record<string, unknown>,
|
|
|
|
|
giteaBootstrap: Record<string, unknown>,
|
|
|
|
|
pacApply: Record<string, unknown>,
|
|
|
|
|
warnings: readonly PacConfigValidationWarning[],
|
|
|
|
|
): Record<string, unknown> {
|
|
|
|
|
const remote = record(pacApply.remote);
|
|
|
|
|
const repositoryMissing = remote.error === "gitea-repository-missing";
|
|
|
|
@@ -1053,6 +1273,7 @@ function bootstrapResult(
|
|
|
|
|
],
|
|
|
|
|
giteaBootstrap,
|
|
|
|
|
pipelinesAsCodeApply: pacApply,
|
|
|
|
|
warnings,
|
|
|
|
|
next: options.confirm
|
|
|
|
|
? {
|
|
|
|
|
deliveryTrigger: `Merge the prepared GitHub PR for ${mirror.upstream.repository}@${mirror.upstream.branch}; the automatic chain owns mirror, PaC, Tekton, GitOps, Argo, and runtime convergence.`,
|
|
|
|
@@ -1067,7 +1288,7 @@ function bootstrapResult(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function status(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
|
|
|
|
|
const pac = readPacConfig();
|
|
|
|
|
const pac = readPacConfig({ consumerId: options.consumerId, selectDefault: true });
|
|
|
|
|
const target = resolveTarget(pac, options.targetId);
|
|
|
|
|
const consumer = resolveConsumer(pac, options.consumerId);
|
|
|
|
|
const repository = resolveRepository(pac, consumer.repositoryRef);
|
|
|
|
@@ -1087,6 +1308,7 @@ async function status(config: UniDeskConfig, options: CommonOptions): Promise<Re
|
|
|
|
|
coverage: consumerCoverage(pac, target.id),
|
|
|
|
|
summary,
|
|
|
|
|
remote: parsed === null ? compactCapture(result, { full: true }) : options.raw ? parsed : undefined,
|
|
|
|
|
warnings: pac.validationWarnings,
|
|
|
|
|
next: nextCommands(target.id, consumer.id, pac.defaults.consumerId),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
@@ -1095,7 +1317,7 @@ export async function getPlatformInfraPipelinesAsCodeNodeStatus(config: UniDeskC
|
|
|
|
|
const pac = readPacConfig();
|
|
|
|
|
const target = resolveTarget(pac, options.targetId ?? options.nodeId);
|
|
|
|
|
const nodeId = options.nodeId ?? target.id;
|
|
|
|
|
const consumers = pac.consumers.filter((consumer) => consumer.node.toLowerCase() === nodeId.toLowerCase());
|
|
|
|
|
const consumers = validPacConsumers(pac).filter((consumer) => consumer.node.toLowerCase() === nodeId.toLowerCase());
|
|
|
|
|
const startedAt = Date.now();
|
|
|
|
|
const statuses = await Promise.all(consumers.map(async (consumer) => {
|
|
|
|
|
const repository = resolveRepository(pac, consumer.repositoryRef);
|
|
|
|
@@ -1139,6 +1361,7 @@ export async function getPlatformInfraPipelinesAsCodeNodeStatus(config: UniDeskC
|
|
|
|
|
valuesPrinted: false,
|
|
|
|
|
},
|
|
|
|
|
consumers: statuses,
|
|
|
|
|
warnings: pac.validationWarnings,
|
|
|
|
|
summary: {
|
|
|
|
|
ready: configured && ready,
|
|
|
|
|
status: configured ? (ready ? "ready" : "blocked") : "warning",
|
|
|
|
@@ -1157,7 +1380,7 @@ export async function getPlatformInfraPipelinesAsCodeNodeStatus(config: UniDeskC
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function closeout(config: UniDeskConfig, options: CloseoutOptions): Promise<Record<string, unknown>> {
|
|
|
|
|
const pac = readPacConfig();
|
|
|
|
|
const pac = readPacConfig({ consumerId: options.consumerId, selectDefault: true });
|
|
|
|
|
const target = resolveTarget(pac, options.targetId);
|
|
|
|
|
const consumer = resolveConsumer(pac, options.consumerId);
|
|
|
|
|
const startedAt = Date.now();
|
|
|
|
@@ -1278,9 +1501,9 @@ function printCloseoutProgress(target: PacTarget, consumer: PacConsumer, stage:
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function history(config: UniDeskConfig, options: HistoryOptions): Promise<Record<string, unknown>> {
|
|
|
|
|
const pac = readPacConfig();
|
|
|
|
|
const pac = readPacConfig({ consumerId: options.consumerId });
|
|
|
|
|
const target = resolveTarget(pac, options.targetId);
|
|
|
|
|
const selection = resolvePacHistoryConsumerSelection(pac.consumers, target.id, options.consumerId, options.detailId);
|
|
|
|
|
const selection = resolvePacHistoryConsumerSelection(validPacConsumers(pac), target.id, options.consumerId, options.detailId);
|
|
|
|
|
const selectedConsumers = selection.selectedConsumerIds.map((id) => resolveConsumer(pac, id));
|
|
|
|
|
const firstConsumer = selectedConsumers[0];
|
|
|
|
|
if (firstConsumer === undefined) throw new Error("no Pipelines-as-Code consumers are configured");
|
|
|
|
@@ -1306,6 +1529,7 @@ async function history(config: UniDeskConfig, options: HistoryOptions): Promise<
|
|
|
|
|
detailId: options.detailId,
|
|
|
|
|
rows: arrayRecords(record(remote).rows),
|
|
|
|
|
historyErrors,
|
|
|
|
|
warnings: pac.validationWarnings,
|
|
|
|
|
controlPlane: parsed === null ? undefined : {
|
|
|
|
|
crdPresent: parsed.crdPresent === true,
|
|
|
|
|
controllerReady: parsed.controllerReady,
|
|
|
|
@@ -1361,9 +1585,9 @@ export function resolvePacHistoryConsumerSelection(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function debugStep(config: UniDeskConfig, options: HistoryOptions): Promise<Record<string, unknown>> {
|
|
|
|
|
const pac = readPacConfig();
|
|
|
|
|
const pac = readPacConfig({ consumerId: options.consumerId });
|
|
|
|
|
const target = resolveTarget(pac, options.targetId);
|
|
|
|
|
const selection = resolvePacHistoryConsumerSelection(pac.consumers, target.id, options.consumerId, options.detailId);
|
|
|
|
|
const selection = resolvePacHistoryConsumerSelection(validPacConsumers(pac), target.id, options.consumerId, options.detailId);
|
|
|
|
|
if (selection.selectedConsumerIds.length !== 1) {
|
|
|
|
|
throw new Error("debug-step requires --consumer or --id resolving exactly one Pipelines-as-Code consumer");
|
|
|
|
|
}
|
|
|
|
@@ -1447,7 +1671,8 @@ function remoteScript(action: "apply" | "status" | "history" | "debug-step", pac
|
|
|
|
|
UNIDESK_PAC_TOKEN_KEY: repository.tokenKey,
|
|
|
|
|
UNIDESK_PAC_WEBHOOK_SECRET_KEY: repository.webhookSecretKey,
|
|
|
|
|
UNIDESK_PAC_CONCURRENCY_LIMIT: String(repository.concurrencyLimit),
|
|
|
|
|
UNIDESK_PAC_PARAMS_JSON: JSON.stringify(repository.params),
|
|
|
|
|
UNIDESK_PAC_REPOSITORY_PARAMS_JSON: JSON.stringify(repository.params),
|
|
|
|
|
UNIDESK_PAC_PARAMS_JSON: JSON.stringify(consumerParams(repository, consumer)),
|
|
|
|
|
UNIDESK_PAC_REGISTRY_APPLICABILITY: registryApplicability(consumer, repository),
|
|
|
|
|
UNIDESK_PAC_PIPELINE_NAME: consumer.pipeline,
|
|
|
|
|
UNIDESK_PAC_PIPELINE_RUN_PREFIX: consumer.pipelineRunPrefix,
|
|
|
|
@@ -1483,11 +1708,12 @@ function remoteScript(action: "apply" | "status" | "history" | "debug-step", pac
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function registryApplicability(consumer: PacConsumer, repository: PacRepository): "configured" | "not-configured" {
|
|
|
|
|
const params = consumerParams(repository, consumer);
|
|
|
|
|
const prefix = consumer.id === "unidesk-host" ? "unidesk_host_" : "";
|
|
|
|
|
const imageRepository = repository.params[`${prefix}image_repository`] ?? repository.params.image_repository;
|
|
|
|
|
const imageRepository = params[`${prefix}image_repository`] ?? params.image_repository;
|
|
|
|
|
if (typeof imageRepository !== "string" || imageRepository.length === 0) return "not-configured";
|
|
|
|
|
if (prefix.length > 0) return "configured";
|
|
|
|
|
return repository.params.pipeline_name === consumer.pipeline ? "configured" : "not-configured";
|
|
|
|
|
return params.pipeline_name === consumer.pipeline ? "configured" : "not-configured";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function historyConsumerConfig(pac: PacConfig, consumer: PacConsumer): Record<string, unknown> {
|
|
|
|
@@ -1889,6 +2115,7 @@ function consumerObservationSummary(consumer: PacConsumer): Record<string, unkno
|
|
|
|
|
argoNamespace: consumer.argoNamespace,
|
|
|
|
|
argoApplication: consumer.argoApplication,
|
|
|
|
|
repositoryRef: consumer.repositoryRef,
|
|
|
|
|
params: Object.keys(consumer.params).sort(),
|
|
|
|
|
runnerServiceAccount: consumer.runnerServiceAccount,
|
|
|
|
|
argoRepositoryCredential: consumer.argoBootstrap?.repositoryCredential === null || consumer.argoBootstrap?.repositoryCredential === undefined ? null : {
|
|
|
|
|
secretName: consumer.argoBootstrap.repositoryCredential.secretName,
|
|
|
|
@@ -1899,11 +2126,11 @@ function consumerObservationSummary(consumer: PacConsumer): Record<string, unkno
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function consumerCoverage(pac: PacConfig, targetId: string): Record<string, unknown>[] {
|
|
|
|
|
return pac.consumers.filter((consumer) => consumer.node.toLowerCase() === targetId.toLowerCase()).map((consumer) => {
|
|
|
|
|
return validPacConsumers(pac).filter((consumer) => consumer.node.toLowerCase() === targetId.toLowerCase()).map((consumer) => {
|
|
|
|
|
const repository = resolveRepository(pac, consumer.repositoryRef);
|
|
|
|
|
return {
|
|
|
|
|
consumer: consumer.id,
|
|
|
|
|
source: `${repository.owner}/${repository.repo}@${repository.params.source_branch ?? "-"}`,
|
|
|
|
|
source: `${repository.owner}/${repository.repo}@${consumerParams(repository, consumer).source_branch ?? "-"}`,
|
|
|
|
|
namespace: consumer.namespace,
|
|
|
|
|
pipeline: consumer.pipeline,
|
|
|
|
|
argoApplication: consumer.argoApplication,
|
|
|
|
@@ -1945,11 +2172,13 @@ function compactPlanJson(result: Record<string, unknown>): Record<string, unknow
|
|
|
|
|
argoNamespace: consumer.argoNamespace,
|
|
|
|
|
argoApplication: consumer.argoApplication,
|
|
|
|
|
repositoryRef: consumer.repositoryRef,
|
|
|
|
|
params: consumer.params,
|
|
|
|
|
runnerServiceAccount: consumer.runnerServiceAccount,
|
|
|
|
|
argoRepositoryCredential: consumer.argoRepositoryCredential,
|
|
|
|
|
},
|
|
|
|
|
secrets: result.secrets,
|
|
|
|
|
policy: result.policy,
|
|
|
|
|
warnings: result.warnings,
|
|
|
|
|
next: result.next,
|
|
|
|
|
valuesPrinted: false,
|
|
|
|
|
};
|
|
|
|
@@ -1972,6 +2201,7 @@ function compactStatusJson(result: Record<string, unknown>): Record<string, unkn
|
|
|
|
|
},
|
|
|
|
|
deliveryAuthority: result.deliveryAuthority,
|
|
|
|
|
capabilities: record(result.config).capabilities,
|
|
|
|
|
warnings: result.warnings,
|
|
|
|
|
summary: compactStatusSummary(record(result.summary)),
|
|
|
|
|
next: result.next,
|
|
|
|
|
valuesPrinted: false,
|
|
|
|
@@ -2176,6 +2406,7 @@ function renderPlan(result: Record<string, unknown>): RenderedCliResult {
|
|
|
|
|
const internalPublish = record(record(config.capabilities).sentinelInternalPublish);
|
|
|
|
|
const secrets = arrayRecords(result.secrets);
|
|
|
|
|
const policy = arrayRecords(result.policy);
|
|
|
|
|
const warnings = arrayRecords(result.warnings);
|
|
|
|
|
const lines = [
|
|
|
|
|
"PLATFORM-INFRA PIPELINES-AS-CODE PLAN",
|
|
|
|
|
...table(["TARGET", "NAMESPACE", "RELEASE", "REPOSITORY", "CONSUMER"], [[stringValue(target.id), stringValue(repository.namespace), stringValue(record(config.release).version), stringValue(repository.name), stringValue(consumer.id)]]),
|
|
|
|
@@ -2193,6 +2424,7 @@ function renderPlan(result: Record<string, unknown>): RenderedCliResult {
|
|
|
|
|
"",
|
|
|
|
|
"POLICY",
|
|
|
|
|
...table(["NAME", "OK", "DETAIL"], policy.map((item) => [stringValue(item.name), boolText(item.ok), stringValue(item.detail)])),
|
|
|
|
|
...renderPacConfigWarnings(warnings),
|
|
|
|
|
"",
|
|
|
|
|
"CAPABILITIES",
|
|
|
|
|
...table(["NAME", "ENABLED", "PROVENANCE", "TRACKING"], [[
|
|
|
|
@@ -2214,6 +2446,7 @@ function renderApply(result: Record<string, unknown>): RenderedCliResult {
|
|
|
|
|
const remote = record(result.remote);
|
|
|
|
|
const preflight = record(remote.preflight);
|
|
|
|
|
const errorText = compactLine(stringValue(remote.stderrTail, stringValue(remote.error, stringValue(result.error, ""))));
|
|
|
|
|
const warnings = arrayRecords(result.warnings);
|
|
|
|
|
const lines = [
|
|
|
|
|
"PLATFORM-INFRA PIPELINES-AS-CODE APPLY",
|
|
|
|
|
...table(["TARGET", "MODE", "MUTATION", "OK"], [[stringValue(target.id), stringValue(result.mode), boolText(result.mutation), boolText(result.ok)]]),
|
|
|
|
@@ -2227,6 +2460,7 @@ function renderApply(result: Record<string, unknown>): RenderedCliResult {
|
|
|
|
|
]),
|
|
|
|
|
...(result.ok === false && errorText.length > 0 ? ["", `ERROR ${errorText}`] : []),
|
|
|
|
|
...(remote.error === "gitea-repository-missing" ? ["HELP bun scripts/cli.ts platform-infra pipelines-as-code help platform-bootstrap"] : []),
|
|
|
|
|
...renderPacConfigWarnings(warnings),
|
|
|
|
|
"",
|
|
|
|
|
"NEXT",
|
|
|
|
|
` status: ${stringValue(record(result.next).status)}`,
|
|
|
|
@@ -2240,6 +2474,7 @@ export function renderStatus(result: Record<string, unknown>): RenderedCliResult
|
|
|
|
|
const deliveryAuthority = record(result.deliveryAuthority);
|
|
|
|
|
const internalPublish = record(record(record(result.config).capabilities).sentinelInternalPublish);
|
|
|
|
|
const coverage = arrayRecords(result.coverage);
|
|
|
|
|
const warnings = arrayRecords(result.warnings);
|
|
|
|
|
const latest = record(summary.latestPipelineRun);
|
|
|
|
|
const taskRuns = arrayRecords(summary.taskRuns);
|
|
|
|
|
const artifact = record(summary.artifact);
|
|
|
|
@@ -2258,6 +2493,7 @@ export function renderStatus(result: Record<string, unknown>): RenderedCliResult
|
|
|
|
|
const webhooks = arrayRecords(summary.webhooks);
|
|
|
|
|
const lines = [
|
|
|
|
|
"PLATFORM-INFRA PIPELINES-AS-CODE STATUS",
|
|
|
|
|
...renderPacConfigWarnings(warnings),
|
|
|
|
|
...(coverage.length === 0 ? [] : [
|
|
|
|
|
"COVERAGE",
|
|
|
|
|
...table(["CONSUMER", "SOURCE", "NAMESPACE", "PIPELINE", "ARGO_APP"], coverage.map((item) => [stringValue(item.consumer), stringValue(item.source), stringValue(item.namespace), stringValue(item.pipeline), stringValue(item.argoApplication)])),
|
|
|
|
@@ -2344,6 +2580,20 @@ export function renderStatus(result: Record<string, unknown>): RenderedCliResult
|
|
|
|
|
return rendered(result, "platform-infra pipelines-as-code status", lines);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderPacConfigWarnings(warnings: readonly Record<string, unknown>[]): string[] {
|
|
|
|
|
if (warnings.length === 0) return [];
|
|
|
|
|
return [
|
|
|
|
|
"",
|
|
|
|
|
"NON-BLOCKING WARNINGS",
|
|
|
|
|
...table(["OBJECT", "BLOCKING", "CODE", "CONFIG"], warnings.map((item) => [
|
|
|
|
|
`${stringValue(record(item.object).kind)}:${stringValue(record(item.object).id)}`,
|
|
|
|
|
boolText(item.blocking),
|
|
|
|
|
stringValue(item.code),
|
|
|
|
|
stringValue(item.configPath),
|
|
|
|
|
])),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderCloseout(result: Record<string, unknown>): RenderedCliResult {
|
|
|
|
|
const target = record(result.target);
|
|
|
|
|
const consumer = record(result.consumer);
|
|
|
|
@@ -2414,7 +2664,7 @@ function renderCloseout(result: Record<string, unknown>): RenderedCliResult {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deliveryTiming(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
|
|
|
|
|
const pac = readPacConfig();
|
|
|
|
|
const pac = readPacConfig({ consumerId: options.consumerId, selectDefault: true });
|
|
|
|
|
const target = resolveTarget(pac, options.targetId);
|
|
|
|
|
const consumer = resolveConsumer(pac, options.consumerId);
|
|
|
|
|
if (consumer.node.toLowerCase() !== target.id.toLowerCase()) throw new Error(`Pipelines-as-Code consumer ${consumer.id} belongs to ${consumer.node}, not target ${target.id}`);
|
|
|
|
@@ -2858,6 +3108,10 @@ function record(value: unknown): Record<string, unknown> {
|
|
|
|
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function arrayRecords(value: unknown): Record<string, unknown>[] {
|
|
|
|
|
return Array.isArray(value) ? value.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item)) as Record<string, unknown>[] : [];
|
|
|
|
|
}
|
|
|
|
|