import type { TImmutableContainerPlatform, TOciManifestMediaType, TSha256Digest, } from './immutableimage.js'; import { normalizeImmutableReleaseTag, normalizeSha256Digest } from './immutableimage.js'; import type { TServiceDeploymentCapability } from './user.js'; import type { IDeploymentRouteRequest, IServiceDeploymentOperation, TDeploymentOperationMode, TDeploymentOperationPhase, } from './deploymentoperation.js'; import { canonicalizeDeploymentHostname, getDeploymentRouteVerificationHostname, normalizeDeploymentReadinessPath, validateDeploymentProposedConfiguration, } from './deploymentoperation.js'; export type TDeploymentPreflightDecision = 'GO' | 'NO-GO'; export type TDeploymentPreflightBlockerCode = | 'INVALID_REQUEST' | 'AUTHORIZATION_MISSING' | 'ORGANIZATION_SCOPE_MISSING' | 'SERVICE_OWNERSHIP_MISMATCH' | 'DEPLOYMENT_OPERATION_NOT_FOUND' | 'DEPLOYMENT_OPERATION_CONFLICT' | 'DEPLOY_ON_PUSH_MUST_BE_DISABLED' | 'REGISTRY_AUTHENTICATION_UNAVAILABLE' | 'REGISTRY_REPOSITORY_MISMATCH' | 'REGISTRY_TAG_CONFLICT' | 'SOURCE_REVISION_INVALID' | 'SOURCE_WORKTREE_DIRTY' | 'VERSION_NOT_RESERVED' | 'VERSION_COLLISION' | 'NAMESPACE_NOT_RESERVED' | 'NAMESPACE_COLLISION' | 'NAMESPACE_NOT_PROVEN_BLANK' | 'CONFIGURATION_DIGEST_INVALID' | 'INTENT_DIGEST_INVALID' | 'PROPOSED_CONFIGURATION_INCOMPLETE' | 'CURRENT_REFERENCE_MUTABLE' | 'CURRENT_RUNTIME_DIGEST_MISSING' | 'OCI_INDEX_DIGEST_REQUIRED' | 'OCI_INDEX_NOT_FOUND' | 'OCI_INDEX_MEDIA_TYPE_REQUIRED' | 'OCI_PLATFORM_MISSING' | 'OCI_PLATFORM_MANIFEST_MISSING' | 'IMMUTABLE_RELEASE_TAG_REQUIRED' | 'IMMUTABLE_RELEASE_TAG_NOT_RESERVED' | 'RELEASE_EVIDENCE_MISSING' | 'RELEASE_EVIDENCE_INVALID' | 'RELEASE_EVIDENCE_AMBIGUOUS' | 'RELEASE_NOT_AUTHORIZED' | 'GREENFIELD_EXECUTION_UNAVAILABLE' | 'ROUTE_PROMOTION_FENCE_UNAVAILABLE' | 'POST_DEPLOYMENT_VERIFICATION_UNAVAILABLE' | 'WOULD_REPLACE_EXISTING_STATE' | 'PLATFORM_CAPABILITY_UNAVAILABLE' | 'CORESTORE_INVENTORY_UNAVAILABLE' | 'RUNTIME_ATTESTATION_MISMATCH' | 'ROLLOUT_ACTIVE_CONFLICT' | 'ROLLOUT_NOT_SUCCEEDED' | 'ROUTE_UNAVAILABLE' | 'ROUTE_VERIFICATION_FAILED' | 'TLS_VERIFICATION_FAILED'; export interface IDeploymentPreflightBlocker { code: TDeploymentPreflightBlockerCode; message: string; } export type TDeploymentPreflightWarningCode = 'RECOVERY_REDEPLOY_OVER_FAILED_RUNTIME'; /** * Auditable non-blocking preflight finding. Warnings never veto a decision. * `RECOVERY_REDEPLOY_OVER_FAILED_RUNTIME` documents that an existing-service * reserve preflight suppressed the CURRENT_RUNTIME_DIGEST_MISSING and * RUNTIME_ATTESTATION_MISMATCH blockers because the current runtime provides * no consistent attested digest and the service's most recent deployment * operation ended 'failed' or 'cleaned' (a recovery redeploy over a known-bad * runtime). No other blocker is ever relaxed. */ export interface IDeploymentPreflightWarning { code: TDeploymentPreflightWarningCode; message: string; } export interface IDeploymentPreflightAuthorizationCheck { capability: TServiceDeploymentCapability; granted: boolean; } export interface IOciPlatformDescriptorSummary { os: string; architecture: string; digest?: TSha256Digest; mediaType?: string; runnable: boolean; manifestPresent: boolean; } export interface IDeploymentPreflightRuntimeTask { deploymentId: string; nodeName?: string; status: string; healthStatus?: string; imageReference?: string; observedDigest?: TSha256Digest; reportedDigest?: TSha256Digest; verificationStatus?: 'not-required' | 'pending' | 'verified' | 'missing' | 'mismatched'; } export interface IDeploymentPreflightPlatformBinding { capability: 'database' | 'objectstorage' | 'pushnotification'; desiredState: 'enabled' | 'disabled'; status: 'requested' | 'provisioning' | 'ready' | 'degraded' | 'failed' | 'disabled'; } export interface IDeploymentPreflightCorestoreResource { capability: 'database' | 'objectstorage'; provider: string; resourceName: string; } export interface IDeploymentPreflightProposedCorestoreResource { capability: 'database' | 'objectstorage'; resourceName: string; } export interface IDeploymentPreflightProposedVolumeMount { mountPath: string; storageClass: 'corestore' | 'ephemeral'; capability?: 'database' | 'objectstorage'; } /** * Secret-free candidate configuration declared by the source checkout. * Cloudly derives namespace and resource names; callers may not probe arbitrary * cluster names through this contract. */ export interface IDeploymentPreflightProposedConfiguration { schemaVersion: 1; declarationComplete: boolean; projectName: string; targetPlatforms: TImmutableContainerPlatform[]; publicDomains: string[]; environmentVariableNames: string[]; /** Exact OCI/Docker argument vector appended after the image entrypoint. */ containerArgs?: string[]; containerPorts: number[]; volumeMounts: IDeploymentPreflightProposedVolumeMount[]; requiredCapabilities: Array<'database' | 'objectstorage' | 'pushnotification'>; } export interface IDeploymentReleaseEvidenceReference { kind: 'provenance' | 'sbom' | 'signature' | 'vulnerability-policy' | 'release-authorization'; statementDigest: TSha256Digest; subjectDigest: TSha256Digest; verifier: string; verifiedAt: number; } /** Evidence is descriptive input until a trusted Cloudly verifier validates it. */ export interface IDeploymentReleaseEvidence { sourceRevision: string; version: string; configurationDigest: TSha256Digest; ociIndexDigest: TSha256Digest; releaseAuthorized: boolean; attestations: IDeploymentReleaseEvidenceReference[]; } export interface IDeploymentPreflightRequestData { requestId: string; serviceId: string; organizationId?: string; mode?: TDeploymentOperationMode; phase?: TDeploymentOperationPhase; operationId?: string; sourceRevision: string; sourceDirty: boolean; version: string; intentDigest: string; proposedConfiguration: IDeploymentPreflightProposedConfiguration; ociIndexDigest?: string; releaseTag?: string; releaseEvidence?: IDeploymentReleaseEvidence; routes?: IDeploymentRouteRequest[]; /** Legacy v14 discriminator. Equivalent to mode: 'greenfield'. */ greenfield?: true; } export interface IDeploymentPreflightReport { schemaVersion: 2; decision: TDeploymentPreflightDecision; mutationPerformed: false; generatedAt: number; service: { organizationId?: string; serviceId: string; serviceName: string; workspaceIdentity: string; registryHost: string; registryRepository: string; deployOnPush: boolean; }; mode?: TDeploymentOperationMode; phase?: TDeploymentOperationPhase; operation?: IServiceDeploymentOperation; source: { revision: string; dirty: boolean; version: string; intentDigest: TSha256Digest; configurationDigest: TSha256Digest; }; currentImage: { tag?: string; digest?: TSha256Digest; imageReference?: string; mutableReference: boolean; mediaType?: TOciManifestMediaType; platforms: IOciPlatformDescriptorSummary[]; }; requestedImage: { digest?: TSha256Digest; releaseTag?: string; tagReserved: boolean; mediaType?: TOciManifestMediaType; platforms: IOciPlatformDescriptorSummary[]; }; runtimeTasks: IDeploymentPreflightRuntimeTask[]; runtimeAttestation: { requiredReplicaCount: number; observedReplicaCount: number; healthyReplicaCount: number; verifiedReplicaCount: number; observedDigestReplicaCount: number; observedDigestConsistent: boolean; consistentDigest: boolean; }; platformBindings: IDeploymentPreflightPlatformBinding[]; corestoreResources: IDeploymentPreflightCorestoreResource[]; proposed: { namespace: string; configuration: IDeploymentPreflightProposedConfiguration; corestoreResources: IDeploymentPreflightProposedCorestoreResource[]; namespaceReserved: boolean; inventoryComplete: boolean; namespaceBlank: boolean | 'unknown'; versionReserved: boolean; wouldReplaceExistingState: boolean | 'unknown'; }; authorizationChecks: IDeploymentPreflightAuthorizationCheck[]; compatibilityBlockers: IDeploymentPreflightBlocker[]; /** Additive auditable non-blocking findings; absent when nothing was relaxed. */ warnings?: IDeploymentPreflightWarning[]; } export interface IGreenfieldDeploymentFence { sourceServiceId: string; sourceRevision: string; version: string; ociIndexDigest: TSha256Digest; configurationDigest: TSha256Digest; targetNamespace: string; } /** @deprecated Use IServiceDeploymentOperation. */ export type IGreenfieldDeploymentOperation = IServiceDeploymentOperation; const fullGitRevisionRegex = /^[a-f0-9]{40}$/; const semanticVersionRegex = /^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/; const requiredEvidenceKinds = new Set([ 'provenance', 'sbom', 'signature', 'vulnerability-policy', 'release-authorization', ]); const isRecord = (valueArg: unknown): valueArg is Record => Boolean(valueArg) && typeof valueArg === 'object' && !Array.isArray(valueArg); export const validateDeploymentPreflightRequest = ( requestArg: unknown, ): IDeploymentPreflightBlocker[] => { const blockers: IDeploymentPreflightBlocker[] = []; if (!isRecord(requestArg)) { return [{ code: 'INVALID_REQUEST', message: 'Deployment preflight request must be an object.' }]; } if (typeof requestArg.requestId !== 'string' || requestArg.requestId.length > 200 || !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(requestArg.requestId)) { blockers.push({ code: 'INVALID_REQUEST', message: 'A bounded request identifier is required.' }); } if (typeof requestArg.serviceId !== 'string' || !requestArg.serviceId.trim() || requestArg.serviceId.length > 200) { blockers.push({ code: 'INVALID_REQUEST', message: 'A bounded service identifier is required.' }); } if (typeof requestArg.sourceRevision !== 'string' || !fullGitRevisionRegex.test(requestArg.sourceRevision)) { blockers.push({ code: 'SOURCE_REVISION_INVALID', message: 'Source revision must be a full lower-case Git commit SHA.' }); if (typeof requestArg.sourceRevision !== 'string') { blockers.push({ code: 'INVALID_REQUEST', message: 'Source revision is malformed.' }); } } if (typeof requestArg.sourceDirty !== 'boolean') { blockers.push({ code: 'INVALID_REQUEST', message: 'Source worktree state must be reported.' }); } else if (requestArg.sourceDirty) { blockers.push({ code: 'SOURCE_WORKTREE_DIRTY', message: 'The source worktree contains uncommitted changes.' }); } if (typeof requestArg.version !== 'string' || requestArg.version.length > 100 || !semanticVersionRegex.test(requestArg.version)) { blockers.push({ code: 'VERSION_NOT_RESERVED', message: 'A canonical semantic release version is required.' }); if (typeof requestArg.version !== 'string') { blockers.push({ code: 'INVALID_REQUEST', message: 'Release version is malformed.' }); } } if (typeof requestArg.intentDigest !== 'string' || !normalizeSha256Digest(requestArg.intentDigest)) { blockers.push({ code: 'INTENT_DIGEST_INVALID', message: 'Deployment intent digest must be a canonical sha256 digest.' }); if (typeof requestArg.intentDigest !== 'string') { blockers.push({ code: 'INVALID_REQUEST', message: 'Deployment intent digest is malformed.' }); } } const configuration = isRecord(requestArg.proposedConfiguration) ? requestArg.proposedConfiguration : undefined; const configurationContainerPorts = Array.isArray(configuration?.containerPorts) ? configuration.containerPorts : []; const configurationPublicDomains = Array.isArray(configuration?.publicDomains) ? configuration.publicDomains.filter((domainArg): domainArg is string => ( typeof domainArg === 'string' )) : []; if (validateDeploymentProposedConfiguration(configuration).length > 0) { blockers.push({ code: 'INVALID_REQUEST', message: 'The proposed deployment configuration is malformed.' }); } if (requestArg.ociIndexDigest !== undefined && (typeof requestArg.ociIndexDigest !== 'string' || !normalizeSha256Digest(requestArg.ociIndexDigest))) { blockers.push({ code: 'OCI_INDEX_DIGEST_REQUIRED', message: 'OCI index digest must be a canonical sha256 digest.' }); blockers.push({ code: 'INVALID_REQUEST', message: 'OCI index digest is malformed.' }); } if (requestArg.releaseTag !== undefined && !normalizeImmutableReleaseTag(requestArg.releaseTag)) { blockers.push({ code: 'INVALID_REQUEST', message: 'Release tag is malformed.' }); } const mode = requestArg.mode === undefined && requestArg.greenfield === true ? 'greenfield' : requestArg.mode; if (mode !== 'greenfield' && mode !== 'existing-service') { blockers.push({ code: 'INVALID_REQUEST', message: 'A supported deployment mode is required.' }); } if (requestArg.greenfield !== undefined && (requestArg.greenfield !== true || mode !== 'greenfield')) { blockers.push({ code: 'INVALID_REQUEST', message: 'Legacy greenfield mode is contradictory.' }); } if (requestArg.phase !== undefined && requestArg.phase !== 'reserve' && requestArg.phase !== 'promote' && requestArg.phase !== 'route') { blockers.push({ code: 'INVALID_REQUEST', message: 'Deployment preflight phase is invalid.' }); } if (requestArg.organizationId !== undefined && (typeof requestArg.organizationId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(requestArg.organizationId))) { blockers.push({ code: 'INVALID_REQUEST', message: 'Deployment organization identifier is malformed.' }); } if (requestArg.operationId !== undefined && (typeof requestArg.operationId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(requestArg.operationId))) { blockers.push({ code: 'INVALID_REQUEST', message: 'Deployment operation identifier is malformed.' }); } const routeHostnames = Array.isArray(requestArg.routes) ? requestArg.routes.map((routeArg) => ( isRecord(routeArg) && typeof routeArg.hostname === 'string' ? routeArg.hostname : undefined )) : []; if (requestArg.routes !== undefined && ( !Array.isArray(requestArg.routes) || requestArg.routes.length > 32 || new Set(routeHostnames).size !== routeHostnames.length || requestArg.routes.some((routeArg) => !isRecord(routeArg) || typeof routeArg.hostname !== 'string' || canonicalizeDeploymentHostname(routeArg.hostname) !== routeArg.hostname || !Number.isSafeInteger(routeArg.targetPort) || (typeof routeArg.targetPort === 'number' && (routeArg.targetPort < 1 || routeArg.targetPort > 65535)) || (routeArg.proxied !== undefined && typeof routeArg.proxied !== 'boolean') || !configurationContainerPorts.includes(routeArg.targetPort) || (routeArg.verificationHostname !== undefined && (typeof routeArg.verificationHostname !== 'string' || canonicalizeDeploymentHostname(routeArg.verificationHostname) !== routeArg.verificationHostname)) || getDeploymentRouteVerificationHostname( routeArg as unknown as IDeploymentRouteRequest, ) === undefined || (routeArg.readinessPath !== undefined && (typeof routeArg.readinessPath !== 'string' || normalizeDeploymentReadinessPath(routeArg.readinessPath) !== routeArg.readinessPath)) || (routeArg.expectedStatusCodes !== undefined && (!Array.isArray(routeArg.expectedStatusCodes) || routeArg.expectedStatusCodes.length < 1 || routeArg.expectedStatusCodes.length > 16 || new Set(routeArg.expectedStatusCodes).size !== routeArg.expectedStatusCodes.length || routeArg.expectedStatusCodes.some((statusArg) => ( !Number.isSafeInteger(statusArg) || statusArg < 200 || statusArg > 299 ))))) )) { blockers.push({ code: 'INVALID_REQUEST', message: 'Deployment route declaration is malformed.' }); } if (mode === 'greenfield' && Array.isArray(requestArg.routes) && JSON.stringify([...new Set(requestArg.routes.map((routeArg) => ( isRecord(routeArg) && typeof routeArg.hostname === 'string' ? routeArg.hostname : '' )))].sort()) !== JSON.stringify([...new Set(configurationPublicDomains)].sort())) { blockers.push({ code: 'INVALID_REQUEST', message: 'Greenfield routes must exactly match proposed public domains.', }); } return blockers; }; export const validateDeploymentReleaseEvidence = (argsArg: { evidence: unknown; sourceRevision: string; version: string; configurationDigest: TSha256Digest; ociIndexDigest: TSha256Digest; }): IDeploymentPreflightBlocker[] => { const blockers: IDeploymentPreflightBlocker[] = []; if (!isRecord(argsArg.evidence)) { return [{ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release evidence must be an object.' }]; } const evidence = argsArg.evidence; if (evidence.releaseAuthorized !== true) { blockers.push({ code: 'RELEASE_NOT_AUTHORIZED', message: 'Release authorization evidence is explicitly false.' }); } if ( evidence.sourceRevision !== argsArg.sourceRevision || evidence.version !== argsArg.version || evidence.configurationDigest !== argsArg.configurationDigest || evidence.ociIndexDigest !== argsArg.ociIndexDigest ) { blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release evidence is not bound to the requested deployment fence.' }); } const seenKinds = new Set(); const attestations = Array.isArray(evidence.attestations) ? evidence.attestations : []; if (!Array.isArray(evidence.attestations)) { blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release attestations must be an array.' }); } for (const attestation of attestations) { if (!isRecord(attestation)) { blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release attestation must be an object.' }); continue; } const kind = attestation.kind; if (typeof kind !== 'string' || !requiredEvidenceKinds.has(kind as IDeploymentReleaseEvidenceReference['kind'])) { blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release attestation kind is invalid.' }); continue; } seenKinds.add(kind as IDeploymentReleaseEvidenceReference['kind']); if ( attestation.subjectDigest !== argsArg.ociIndexDigest || typeof attestation.statementDigest !== 'string' || !normalizeSha256Digest(attestation.statementDigest) || typeof attestation.verifier !== 'string' || !attestation.verifier.trim() || typeof attestation.verifiedAt !== 'number' || !Number.isSafeInteger(attestation.verifiedAt) || attestation.verifiedAt < 1 ) { blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: `Invalid ${kind} evidence.` }); } } for (const requiredKind of requiredEvidenceKinds) { if (!seenKinds.has(requiredKind)) { blockers.push({ code: 'RELEASE_EVIDENCE_MISSING', message: `Missing ${requiredKind} evidence.` }); } } return blockers; };