@microagi/alchemy-gcp 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/README.md +54 -2
- package/lib/Container/Cluster.d.ts +25 -0
- package/lib/Container/Cluster.d.ts.map +1 -1
- package/lib/Container/Cluster.js.map +1 -1
- package/lib/Providers.d.ts.map +1 -1
- package/lib/Providers.js +5 -1
- package/lib/Providers.js.map +1 -1
- package/lib/Run/IamMember.d.ts +70 -0
- package/lib/Run/IamMember.d.ts.map +1 -0
- package/lib/Run/IamMember.js +65 -0
- package/lib/Run/IamMember.js.map +1 -0
- package/lib/Run/IamSync.d.ts +80 -0
- package/lib/Run/IamSync.d.ts.map +1 -0
- package/lib/Run/IamSync.js +73 -0
- package/lib/Run/IamSync.js.map +1 -0
- package/lib/Run/Job.d.ts +167 -0
- package/lib/Run/Job.d.ts.map +1 -0
- package/lib/Run/Job.js +199 -0
- package/lib/Run/Job.js.map +1 -0
- package/lib/Run/Operations.d.ts +47 -0
- package/lib/Run/Operations.d.ts.map +1 -0
- package/lib/Run/Operations.js +46 -0
- package/lib/Run/Operations.js.map +1 -0
- package/lib/Run/Service.d.ts +193 -0
- package/lib/Run/Service.d.ts.map +1 -0
- package/lib/Run/Service.js +247 -0
- package/lib/Run/Service.js.map +1 -0
- package/lib/Run/Validation.d.ts +54 -0
- package/lib/Run/Validation.d.ts.map +1 -0
- package/lib/Run/Validation.js +129 -0
- package/lib/Run/Validation.js.map +1 -0
- package/lib/Run/index.d.ts +51 -0
- package/lib/Run/index.d.ts.map +1 -0
- package/lib/Run/index.js +4 -0
- package/lib/Run/index.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/package.json +5 -4
- package/src/Container/Cluster.ts +25 -0
- package/src/Providers.ts +6 -0
- package/src/Run/IamMember.ts +79 -0
- package/src/Run/IamSync.ts +134 -0
- package/src/Run/Job.ts +453 -0
- package/src/Run/Operations.ts +77 -0
- package/src/Run/Service.ts +541 -0
- package/src/Run/Validation.ts +153 -0
- package/src/Run/index.ts +52 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import * as run from "@distilled.cloud/gcp/run-v2";
|
|
2
|
+
import { Resource } from "alchemy";
|
|
3
|
+
import * as Provider from "alchemy/Provider";
|
|
4
|
+
import type * as GCP from "../Providers.ts";
|
|
5
|
+
import { type RunIamBindingContract } from "./IamSync.ts";
|
|
6
|
+
/**
|
|
7
|
+
* A Cloud Run v2 Service — a managed serverless HTTP endpoint. Cloud
|
|
8
|
+
* Run pulls a container image, scales it from zero up to the
|
|
9
|
+
* configured maximum based on request rate, and fronts it with a
|
|
10
|
+
* Google-managed URL (`https://{service}-{hash}-{region}.run.app`).
|
|
11
|
+
* Per-Service IAM controls who can invoke it.
|
|
12
|
+
*
|
|
13
|
+
* **Resource model.** A Service holds a `template` (the desired
|
|
14
|
+
* `RevisionTemplate`) plus traffic routing. Every change to `template`
|
|
15
|
+
* server-side spawns a new immutable `Revision`; `traffic` (this resource)
|
|
16
|
+
* picks which Revisions receive request percentages. Revisions and
|
|
17
|
+
* Executions are side-effects and are NOT modeled by this resource —
|
|
18
|
+
* inspect them via `getProjectsLocationsServicesRevisions` if needed.
|
|
19
|
+
*
|
|
20
|
+
* **Lifecycle.** observe → ensure (create, retrying on the well-known
|
|
21
|
+
* "API not yet enabled" 403) → sync (patch with updateMask of changed
|
|
22
|
+
* top-level fields) → sync IAM bindings → return.
|
|
23
|
+
*
|
|
24
|
+
* **Replace triggers.** Only identity fields (project, location, name)
|
|
25
|
+
* force replacement. Everything else — image, env, scaling, traffic,
|
|
26
|
+
* ingress, IAM — is in-place via `patch`. New revisions are auto-created
|
|
27
|
+
* by Cloud Run on any `template.*` change.
|
|
28
|
+
*
|
|
29
|
+
* **Optimistic concurrency.** Patch and delete pass through the
|
|
30
|
+
* observed `etag` so concurrent edits (e.g. from `gcloud run services
|
|
31
|
+
* update`) surface as `Conflict` instead of silently overwriting.
|
|
32
|
+
*
|
|
33
|
+
* **Adoption.** Label-gated: a Service whose `labels` lack our
|
|
34
|
+
* `alchemy_*` keys is wrapped in `Unowned(attrs)` from `read`, forcing
|
|
35
|
+
* `--adopt` before takeover.
|
|
36
|
+
*
|
|
37
|
+
* **IAM.** Use {@link import("./IamMember.ts").serviceIamMember} to
|
|
38
|
+
* bind `(role, member)` grants — the provider unions all bindings and
|
|
39
|
+
* applies them via a single `setIamPolicy` round-trip, preserving
|
|
40
|
+
* foreign roles + members.
|
|
41
|
+
*
|
|
42
|
+
* @section Creating a Cloud Run Service
|
|
43
|
+
* @example Minimal "hello" service, public via IAM
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const helloApi = yield* GCP.Service("HelloApi", {
|
|
46
|
+
* project: project.projectId,
|
|
47
|
+
* location: "europe-west4",
|
|
48
|
+
* template: {
|
|
49
|
+
* containers: [{ image: "gcr.io/cloudrun/hello" }],
|
|
50
|
+
* },
|
|
51
|
+
* });
|
|
52
|
+
* yield* GCP.serviceIamMember(helloApi, "PublicInvoker", {
|
|
53
|
+
* role: "roles/run.invoker",
|
|
54
|
+
* member: "allUsers",
|
|
55
|
+
* });
|
|
56
|
+
* // helloApi.uri → https://hello-api-XXXXX-ew.a.run.app
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @example Internal-only service with explicit service account and scaling
|
|
60
|
+
* ```typescript
|
|
61
|
+
* const internal = yield* GCP.Service("Internal", {
|
|
62
|
+
* project: project.projectId,
|
|
63
|
+
* location: "europe-west4",
|
|
64
|
+
* ingress: "INGRESS_TRAFFIC_INTERNAL_ONLY",
|
|
65
|
+
* scaling: { minInstanceCount: 1, maxInstanceCount: 10 },
|
|
66
|
+
* template: {
|
|
67
|
+
* serviceAccount: sa.email,
|
|
68
|
+
* containers: [{
|
|
69
|
+
* image: "europe-west4-docker.pkg.dev/proj/repo/app:latest",
|
|
70
|
+
* resources: { limits: { cpu: "1000m", memory: "512Mi" } },
|
|
71
|
+
* }],
|
|
72
|
+
* },
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export type ServiceProps = {
|
|
77
|
+
/** GCP project ID hosting the Service. Immutable — replace if changed. */
|
|
78
|
+
project: string;
|
|
79
|
+
/**
|
|
80
|
+
* Cloud Run region (e.g. `europe-west4`). Cloud Run is a regional
|
|
81
|
+
* product — there are no zonal Services. Immutable — replace if changed.
|
|
82
|
+
*/
|
|
83
|
+
location: string;
|
|
84
|
+
/**
|
|
85
|
+
* Service name. Defaults to `createPhysicalName({ id, lowercase: true,
|
|
86
|
+
* maxLength: 49 })`. Lowercase letters/digits/hyphens; must begin
|
|
87
|
+
* with a letter and not end with a hyphen; **fewer than 50 characters**.
|
|
88
|
+
* Immutable — replace if changed.
|
|
89
|
+
*/
|
|
90
|
+
name?: string;
|
|
91
|
+
/**
|
|
92
|
+
* User-visible description (≤512 chars). Mutable via `patch`.
|
|
93
|
+
*/
|
|
94
|
+
description?: string;
|
|
95
|
+
/**
|
|
96
|
+
* Resource labels. Alchemy internal labels (`alchemy_app`,
|
|
97
|
+
* `alchemy_stage`, `alchemy_id`) are merged on top automatically.
|
|
98
|
+
* Cloud Run rejects labels in `run.googleapis.com`,
|
|
99
|
+
* `cloud.googleapis.com`, `serving.knative.dev`, or
|
|
100
|
+
* `autoscaling.knative.dev` namespaces. Mutable via `patch`.
|
|
101
|
+
*/
|
|
102
|
+
labels?: Record<string, string>;
|
|
103
|
+
/**
|
|
104
|
+
* Free-form annotations for external tools. Cloud Run rejects the
|
|
105
|
+
* same reserved namespaces as `labels`. Mutable via `patch`.
|
|
106
|
+
*/
|
|
107
|
+
annotations?: Record<string, string>;
|
|
108
|
+
/**
|
|
109
|
+
* Launch stage. `BETA` (or higher) is required to use preview
|
|
110
|
+
* fields like GPU `nodeSelector` and Direct VPC `networkInterfaces`;
|
|
111
|
+
* leaving this unset defaults to `GA`, which silently rejects
|
|
112
|
+
* preview fields in the body. Mutable via `patch`.
|
|
113
|
+
*/
|
|
114
|
+
launchStage?: "ALPHA" | "BETA" | "GA" | "EARLY_ACCESS" | "PRELAUNCH" | "DEPRECATED";
|
|
115
|
+
/** Ingress traffic policy. Mutable via `patch`. */
|
|
116
|
+
ingress?: "INGRESS_TRAFFIC_ALL" | "INGRESS_TRAFFIC_INTERNAL_ONLY" | "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" | "INGRESS_TRAFFIC_NONE";
|
|
117
|
+
/**
|
|
118
|
+
* Disable the IAM permission check for invokers. When `true`, the
|
|
119
|
+
* Service is publicly reachable regardless of IAM policy — a
|
|
120
|
+
* deliberate footgun, surface it consciously. Prefer
|
|
121
|
+
* `serviceIamMember(svc, "Public", { role: "roles/run.invoker", member: "allUsers" })`
|
|
122
|
+
* for declarative public access. Mutable via `patch`.
|
|
123
|
+
*/
|
|
124
|
+
invokerIamDisabled?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Disable public resolution of the default `*.run.app` URI. When
|
|
127
|
+
* `true`, the service is reachable only via custom domains / load
|
|
128
|
+
* balancers. Mutable via `patch`.
|
|
129
|
+
*/
|
|
130
|
+
defaultUriDisabled?: boolean;
|
|
131
|
+
/** Service-level scaling. Mutable via `patch`. */
|
|
132
|
+
scaling?: run.GoogleCloudRunV2ServiceScaling;
|
|
133
|
+
/**
|
|
134
|
+
* Traffic routing across Revisions. Defaults server-side to 100% to
|
|
135
|
+
* the latest `Ready` revision. Mutable via `patch`.
|
|
136
|
+
*/
|
|
137
|
+
traffic?: ReadonlyArray<run.GoogleCloudRunV2TrafficTarget>;
|
|
138
|
+
/**
|
|
139
|
+
* Audiences encoded into the auth token, for cross-service auth.
|
|
140
|
+
* Mutable via `patch`.
|
|
141
|
+
*/
|
|
142
|
+
customAudiences?: ReadonlyArray<string>;
|
|
143
|
+
/**
|
|
144
|
+
* Binary Authorization policy. Mutable via `patch`.
|
|
145
|
+
*/
|
|
146
|
+
binaryAuthorization?: run.GoogleCloudRunV2BinaryAuthorization;
|
|
147
|
+
/**
|
|
148
|
+
* The Revision template — describes the desired Pod-equivalent.
|
|
149
|
+
* **Required at create.** Mutable via `patch`; any change spawns a
|
|
150
|
+
* new server-managed Revision.
|
|
151
|
+
*/
|
|
152
|
+
template: run.GoogleCloudRunV2RevisionTemplate;
|
|
153
|
+
};
|
|
154
|
+
export type ServiceAttributes = {
|
|
155
|
+
/** Service name (bare, without the `projects/.../services/` prefix). */
|
|
156
|
+
name: string;
|
|
157
|
+
/** Server-assigned UID — stable across rename-less mutations. */
|
|
158
|
+
uid: string;
|
|
159
|
+
/** Fully-qualified resource name `projects/{p}/locations/{l}/services/{n}`. */
|
|
160
|
+
resourceName: string;
|
|
161
|
+
/** GCP project ID, threaded through from props. */
|
|
162
|
+
project: string;
|
|
163
|
+
/** Region, threaded through from props. */
|
|
164
|
+
location: string;
|
|
165
|
+
/** Primary serving URI (`https://…run.app`). */
|
|
166
|
+
uri: string;
|
|
167
|
+
/** All URIs serving traffic (default + any tagged ones). */
|
|
168
|
+
urls: ReadonlyArray<string>;
|
|
169
|
+
/** Monotonically increasing generation, bumped on every patch. */
|
|
170
|
+
generation: string | undefined;
|
|
171
|
+
/** The generation currently serving traffic. */
|
|
172
|
+
observedGeneration: string | undefined;
|
|
173
|
+
/** Name of the latest Ready revision. */
|
|
174
|
+
latestReadyRevision: string | undefined;
|
|
175
|
+
/** Name of the latest created revision. */
|
|
176
|
+
latestCreatedRevision: string | undefined;
|
|
177
|
+
/** Overall readiness condition. */
|
|
178
|
+
terminalCondition: run.GoogleCloudRunV2Condition | undefined;
|
|
179
|
+
/** Per-traffic-target serving status. */
|
|
180
|
+
trafficStatuses: ReadonlyArray<run.GoogleCloudRunV2TrafficTargetStatus>;
|
|
181
|
+
/** Optimistic-concurrency etag. */
|
|
182
|
+
etag: string | undefined;
|
|
183
|
+
/** Labels currently set, including internals. */
|
|
184
|
+
labels: Record<string, string>;
|
|
185
|
+
/** Creation time. */
|
|
186
|
+
createTime: string | undefined;
|
|
187
|
+
/** Last-modified time. */
|
|
188
|
+
updateTime: string | undefined;
|
|
189
|
+
};
|
|
190
|
+
export type Service = Resource<"GCP.Service", ServiceProps, ServiceAttributes, RunIamBindingContract, GCP.Providers>;
|
|
191
|
+
export declare const Service: import("alchemy").ResourceClass<Service>;
|
|
192
|
+
export declare const ServiceProvider: () => import("effect/Layer").Layer<Provider.Provider<Service>, never, import("effect/unstable/http/HttpClient").HttpClient | import("alchemy").Stage | import("alchemy").Stack | import("@distilled.cloud/gcp").Credentials>;
|
|
193
|
+
//# sourceMappingURL=Service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Service.d.ts","sourceRoot":"","sources":["../../src/Run/Service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import * as run from "@distilled.cloud/gcp/run-v2";
|
|
2
|
+
import { Resource } from "alchemy";
|
|
3
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
4
|
+
import { deepEqual, isResolved, somePropsAreDifferent } from "alchemy/Diff";
|
|
5
|
+
import { createPhysicalName } from "alchemy/PhysicalName";
|
|
6
|
+
import * as Provider from "alchemy/Provider";
|
|
7
|
+
import { diffTags } from "alchemy/Tags";
|
|
8
|
+
import * as Duration from "effect/Duration";
|
|
9
|
+
import * as Effect from "effect/Effect";
|
|
10
|
+
import * as Schedule from "effect/Schedule";
|
|
11
|
+
import { gcpInternalLabels, hasAlchemyLabels } from "../Tags.js";
|
|
12
|
+
import { makeSyncIam } from "./IamSync.js";
|
|
13
|
+
import { makeAwaitOperation } from "./Operations.js";
|
|
14
|
+
import { reshapeBadRequest, validateAnnotations, validateContainers, validateLabels, validateRunName, } from "./Validation.js";
|
|
15
|
+
export const Service = Resource("GCP.Service");
|
|
16
|
+
const fqName = (project, location, name) => `projects/${project}/locations/${location}/services/${name}`;
|
|
17
|
+
const toServiceBody = (news, desiredLabels) => ({
|
|
18
|
+
...(news.description !== undefined ? { description: news.description } : {}),
|
|
19
|
+
labels: desiredLabels,
|
|
20
|
+
...(news.annotations ? { annotations: news.annotations } : {}),
|
|
21
|
+
...(news.launchStage ? { launchStage: news.launchStage } : {}),
|
|
22
|
+
...(news.ingress ? { ingress: news.ingress } : {}),
|
|
23
|
+
...(news.invokerIamDisabled !== undefined
|
|
24
|
+
? { invokerIamDisabled: news.invokerIamDisabled }
|
|
25
|
+
: {}),
|
|
26
|
+
...(news.defaultUriDisabled !== undefined
|
|
27
|
+
? { defaultUriDisabled: news.defaultUriDisabled }
|
|
28
|
+
: {}),
|
|
29
|
+
...(news.scaling ? { scaling: news.scaling } : {}),
|
|
30
|
+
...(news.traffic ? { traffic: news.traffic } : {}),
|
|
31
|
+
...(news.customAudiences ? { customAudiences: news.customAudiences } : {}),
|
|
32
|
+
...(news.binaryAuthorization
|
|
33
|
+
? { binaryAuthorization: news.binaryAuthorization }
|
|
34
|
+
: {}),
|
|
35
|
+
template: news.template,
|
|
36
|
+
});
|
|
37
|
+
const toAttributes = (s, parent) => ({
|
|
38
|
+
name: parent.name,
|
|
39
|
+
uid: s.uid ?? "",
|
|
40
|
+
resourceName: s.name ?? fqName(parent.project, parent.location, parent.name),
|
|
41
|
+
project: parent.project,
|
|
42
|
+
location: parent.location,
|
|
43
|
+
uri: s.uri ?? "",
|
|
44
|
+
urls: s.urls ?? [],
|
|
45
|
+
generation: s.generation,
|
|
46
|
+
observedGeneration: s.observedGeneration,
|
|
47
|
+
latestReadyRevision: s.latestReadyRevision,
|
|
48
|
+
latestCreatedRevision: s.latestCreatedRevision,
|
|
49
|
+
terminalCondition: s.terminalCondition,
|
|
50
|
+
trafficStatuses: s.trafficStatuses ?? [],
|
|
51
|
+
etag: s.etag,
|
|
52
|
+
labels: { ...(s.labels ?? {}) },
|
|
53
|
+
createTime: s.createTime,
|
|
54
|
+
updateTime: s.updateTime,
|
|
55
|
+
});
|
|
56
|
+
export const ServiceProvider = () => Provider.effect(Service, Effect.gen(function* () {
|
|
57
|
+
const getService = yield* run.getProjectsLocationsServices;
|
|
58
|
+
const createService = yield* run.createProjectsLocationsServices;
|
|
59
|
+
const patchService = yield* run.patchProjectsLocationsServices;
|
|
60
|
+
const deleteService = yield* run.deleteProjectsLocationsServices;
|
|
61
|
+
const getOperation = yield* run.getProjectsLocationsOperations;
|
|
62
|
+
const getIamPolicy = yield* run.getIamPolicyProjectsLocationsServices;
|
|
63
|
+
const setIamPolicy = yield* run.setIamPolicyProjectsLocationsServices;
|
|
64
|
+
const awaitOperation = makeAwaitOperation(getOperation);
|
|
65
|
+
const syncIam = makeSyncIam({ getIamPolicy, setIamPolicy });
|
|
66
|
+
const observe = (project, location, name) => getService({ name: fqName(project, location, name) }).pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)), Effect.catchTag("Forbidden", () => Effect.succeed(undefined)));
|
|
67
|
+
const syncMutable = Effect.fn(function* (args) {
|
|
68
|
+
const desiredBody = toServiceBody(args.news, args.desiredLabels);
|
|
69
|
+
const updateMaskFields = [];
|
|
70
|
+
if ((args.observed.description ?? undefined) !== args.news.description) {
|
|
71
|
+
updateMaskFields.push("description");
|
|
72
|
+
}
|
|
73
|
+
const labelDiff = diffTags({ ...(args.observed.labels ?? {}) }, args.desiredLabels);
|
|
74
|
+
if (labelDiff.removed.length > 0 || labelDiff.upsert.length > 0) {
|
|
75
|
+
updateMaskFields.push("labels");
|
|
76
|
+
}
|
|
77
|
+
if (!deepEqual(args.observed.annotations ?? {}, args.news.annotations ?? {})) {
|
|
78
|
+
updateMaskFields.push("annotations");
|
|
79
|
+
}
|
|
80
|
+
if (args.news.launchStage !== undefined &&
|
|
81
|
+
args.observed.launchStage !== args.news.launchStage) {
|
|
82
|
+
updateMaskFields.push("launchStage");
|
|
83
|
+
}
|
|
84
|
+
if (args.news.ingress !== undefined &&
|
|
85
|
+
args.observed.ingress !== args.news.ingress) {
|
|
86
|
+
updateMaskFields.push("ingress");
|
|
87
|
+
}
|
|
88
|
+
if (args.news.invokerIamDisabled !== undefined &&
|
|
89
|
+
(args.observed.invokerIamDisabled ?? false) !==
|
|
90
|
+
args.news.invokerIamDisabled) {
|
|
91
|
+
updateMaskFields.push("invokerIamDisabled");
|
|
92
|
+
}
|
|
93
|
+
if (args.news.defaultUriDisabled !== undefined &&
|
|
94
|
+
(args.observed.defaultUriDisabled ?? false) !==
|
|
95
|
+
args.news.defaultUriDisabled) {
|
|
96
|
+
updateMaskFields.push("defaultUriDisabled");
|
|
97
|
+
}
|
|
98
|
+
if (args.news.scaling && !deepEqual(args.observed.scaling, args.news.scaling)) {
|
|
99
|
+
updateMaskFields.push("scaling");
|
|
100
|
+
}
|
|
101
|
+
if (args.news.traffic && !deepEqual(args.observed.traffic ?? [], args.news.traffic)) {
|
|
102
|
+
updateMaskFields.push("traffic");
|
|
103
|
+
}
|
|
104
|
+
if (args.news.customAudiences &&
|
|
105
|
+
!deepEqual(args.observed.customAudiences ?? [], args.news.customAudiences)) {
|
|
106
|
+
updateMaskFields.push("customAudiences");
|
|
107
|
+
}
|
|
108
|
+
if (args.news.binaryAuthorization &&
|
|
109
|
+
!deepEqual(args.observed.binaryAuthorization, args.news.binaryAuthorization)) {
|
|
110
|
+
updateMaskFields.push("binaryAuthorization");
|
|
111
|
+
}
|
|
112
|
+
// The Revision template is a deeply nested struct; diffing field
|
|
113
|
+
// by field would be miles of code with no payoff. Deep-equal at
|
|
114
|
+
// the top — patch sends the whole desired template if anything
|
|
115
|
+
// inside differs. Cloud Run spawns a new Revision on patch only
|
|
116
|
+
// when the template's content (modulo server-injected defaults)
|
|
117
|
+
// actually changes, so resending an equivalent template after a
|
|
118
|
+
// no-op reconcile is cheap.
|
|
119
|
+
if (!deepEqual(args.observed.template, args.news.template)) {
|
|
120
|
+
updateMaskFields.push("template");
|
|
121
|
+
}
|
|
122
|
+
if (updateMaskFields.length === 0)
|
|
123
|
+
return;
|
|
124
|
+
// Pass the observed etag through the body so a concurrent edit
|
|
125
|
+
// racing us surfaces as Conflict instead of silently
|
|
126
|
+
// overwriting. Cloud Run reads etag from the body (it's a
|
|
127
|
+
// field on GoogleCloudRunV2Service, not a request-level param).
|
|
128
|
+
const bodyWithEtag = {
|
|
129
|
+
...desiredBody,
|
|
130
|
+
...(args.observed.etag ? { etag: args.observed.etag } : {}),
|
|
131
|
+
};
|
|
132
|
+
const op = yield* patchService({
|
|
133
|
+
name: args.name,
|
|
134
|
+
updateMask: updateMaskFields.join(","),
|
|
135
|
+
body: bodyWithEtag,
|
|
136
|
+
}).pipe(Effect.catchTag("BadRequest", reshapeBadRequest("Service", "patch")));
|
|
137
|
+
if (op.name)
|
|
138
|
+
yield* awaitOperation(op.name, args.session);
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
stables: ["name", "uid", "resourceName", "project", "location"],
|
|
142
|
+
diff: Effect.fn(function* ({ news, olds = {} }) {
|
|
143
|
+
if (!isResolved(news))
|
|
144
|
+
return undefined;
|
|
145
|
+
if (somePropsAreDifferent(olds, news, [
|
|
146
|
+
"project",
|
|
147
|
+
"location",
|
|
148
|
+
"name",
|
|
149
|
+
])) {
|
|
150
|
+
return { action: "replace" };
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}),
|
|
154
|
+
reconcile: Effect.fn(function* ({ id, news, session, bindings }) {
|
|
155
|
+
const internalLabels = yield* gcpInternalLabels(id);
|
|
156
|
+
// Cloud Run service IDs must be <50 chars (server hard cap).
|
|
157
|
+
const desiredName = news.name ??
|
|
158
|
+
(yield* createPhysicalName({ id, maxLength: 49 })).toLowerCase();
|
|
159
|
+
// Plan-time validation: surface common footguns as typed
|
|
160
|
+
// ConfigErrors before any state mutation. The server would
|
|
161
|
+
// reject these too, but a 30 s wait + half-built state is a
|
|
162
|
+
// worse experience than failing immediately.
|
|
163
|
+
yield* validateRunName("Service", desiredName);
|
|
164
|
+
yield* validateLabels(news.labels);
|
|
165
|
+
yield* validateAnnotations(news.annotations);
|
|
166
|
+
yield* validateContainers("Service", news.template.containers);
|
|
167
|
+
const parent = `projects/${news.project}/locations/${news.location}`;
|
|
168
|
+
const name = fqName(news.project, news.location, desiredName);
|
|
169
|
+
const desiredLabels = {
|
|
170
|
+
...(news.labels ?? {}),
|
|
171
|
+
...internalLabels,
|
|
172
|
+
};
|
|
173
|
+
// 1. Observe — collapse 403 to "missing" alongside 404 (GCP
|
|
174
|
+
// surfaces 403 for resources/projects we can't see).
|
|
175
|
+
let observed = yield* observe(news.project, news.location, desiredName);
|
|
176
|
+
// 2. Ensure — same API-enable race as Cluster: a create can
|
|
177
|
+
// fire faster than ApiEnable's effect propagates and the
|
|
178
|
+
// server returns 403 with "If you enabled this API
|
|
179
|
+
// recently, wait a few minutes …". Retry the create call
|
|
180
|
+
// on that specific 403 for ~5 min; other Forbiddens
|
|
181
|
+
// propagate.
|
|
182
|
+
if (!observed) {
|
|
183
|
+
const op = yield* createService({
|
|
184
|
+
parent,
|
|
185
|
+
serviceId: desiredName,
|
|
186
|
+
body: toServiceBody(news, desiredLabels),
|
|
187
|
+
}).pipe(Effect.retry({
|
|
188
|
+
while: (e) => e?._tag === "Forbidden" &&
|
|
189
|
+
/enabled this API recently|has not been used/i.test(e.message ?? ""),
|
|
190
|
+
schedule: Schedule.spaced(Duration.seconds(15)).pipe(Schedule.both(Schedule.recurs(20)), Schedule.tapOutput(() => session.note("Waiting for Cloud Run API enablement to propagate…"))),
|
|
191
|
+
}), Effect.catchTag("Conflict", () => Effect.succeed(undefined)), Effect.catchTag("BadRequest", reshapeBadRequest("Service", "create")));
|
|
192
|
+
if (op?.name)
|
|
193
|
+
yield* awaitOperation(op.name, session);
|
|
194
|
+
observed = yield* getService({ name });
|
|
195
|
+
}
|
|
196
|
+
// 3. Sync — single patch with an updateMask of changed
|
|
197
|
+
// top-level fields. Template is diff'd shallowly (deep
|
|
198
|
+
// equality on the whole struct).
|
|
199
|
+
yield* syncMutable({
|
|
200
|
+
name,
|
|
201
|
+
observed,
|
|
202
|
+
news,
|
|
203
|
+
desiredLabels,
|
|
204
|
+
session,
|
|
205
|
+
});
|
|
206
|
+
// 4. Apply IAM bindings AFTER the service exists. Single
|
|
207
|
+
// setIamPolicy call covers all bindings for this service;
|
|
208
|
+
// foreign bindings are preserved verbatim. Etag round-trip
|
|
209
|
+
// handles concurrent edits.
|
|
210
|
+
yield* syncIam({ resource: name, bindings });
|
|
211
|
+
const final = yield* getService({ name });
|
|
212
|
+
return toAttributes(final, {
|
|
213
|
+
project: news.project,
|
|
214
|
+
location: news.location,
|
|
215
|
+
name: desiredName,
|
|
216
|
+
});
|
|
217
|
+
}),
|
|
218
|
+
delete: Effect.fn(function* ({ output, session }) {
|
|
219
|
+
const name = fqName(output.project, output.location, output.name);
|
|
220
|
+
// Pass the etag we stored at last reconcile so a concurrent
|
|
221
|
+
// edit racing this delete surfaces as Conflict. If we never
|
|
222
|
+
// captured one (legacy state, adoption path), omit the
|
|
223
|
+
// parameter — Cloud Run treats it as "don't check".
|
|
224
|
+
yield* deleteService({
|
|
225
|
+
name,
|
|
226
|
+
...(output.etag ? { etag: output.etag } : {}),
|
|
227
|
+
}).pipe(Effect.flatMap((op) => op.name ? awaitOperation(op.name, session) : Effect.succeed(op)), Effect.catchTag("NotFound", () => Effect.void));
|
|
228
|
+
}),
|
|
229
|
+
read: Effect.fn(function* ({ id, output, olds }) {
|
|
230
|
+
const project = output?.project ?? olds?.project;
|
|
231
|
+
const location = output?.location ?? olds?.location;
|
|
232
|
+
if (!project || !location)
|
|
233
|
+
return undefined;
|
|
234
|
+
const name = output?.name ??
|
|
235
|
+
olds?.name ??
|
|
236
|
+
(yield* createPhysicalName({ id, maxLength: 49 })).toLowerCase();
|
|
237
|
+
const observed = yield* observe(project, location, name);
|
|
238
|
+
if (!observed)
|
|
239
|
+
return undefined;
|
|
240
|
+
const attrs = toAttributes(observed, { project, location, name });
|
|
241
|
+
return (yield* hasAlchemyLabels(id, observed.labels))
|
|
242
|
+
? attrs
|
|
243
|
+
: Unowned(attrs);
|
|
244
|
+
}),
|
|
245
|
+
};
|
|
246
|
+
}));
|
|
247
|
+
//# sourceMappingURL=Service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Service.js","sourceRoot":"","sources":["../../src/Run/Service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ConfigError } from "@distilled.cloud/gcp";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
/**
|
|
4
|
+
* Validate a Cloud Run resource name (Service or Job). Returns a
|
|
5
|
+
* `ConfigError` Effect on the failure channel if the name is invalid,
|
|
6
|
+
* otherwise succeeds.
|
|
7
|
+
*
|
|
8
|
+
* The name comes from `props.name` (when the user supplied one) or
|
|
9
|
+
* `createPhysicalName({ id, maxLength: 49 })` (when defaulted). The
|
|
10
|
+
* defaulted path is already capped at 49; this guard mostly protects
|
|
11
|
+
* the user-supplied path against a server-side reject after a long
|
|
12
|
+
* deploy.
|
|
13
|
+
*/
|
|
14
|
+
export declare const validateRunName: (kind: "Service" | "Job", name: string) => Effect.Effect<void, ConfigError>;
|
|
15
|
+
/**
|
|
16
|
+
* Reject user labels in any of the Cloud Run reserved namespaces — the
|
|
17
|
+
* server's 400 message ("Labels with run.googleapis.com namespace are
|
|
18
|
+
* not allowed") is clear enough, but failing at plan time means the
|
|
19
|
+
* stack never starts a deploy that's guaranteed to fail.
|
|
20
|
+
*
|
|
21
|
+
* Alchemy internals are merged *on top* of user labels and are NOT
|
|
22
|
+
* subject to this check — they use the un-namespaced `alchemy_*` keys.
|
|
23
|
+
*/
|
|
24
|
+
export declare const validateLabels: (labels: Record<string, string> | undefined) => Effect.Effect<void, ConfigError>;
|
|
25
|
+
/** Same check, applied to annotations (server has the same reservation). */
|
|
26
|
+
export declare const validateAnnotations: (annotations: Record<string, string> | undefined) => Effect.Effect<void, ConfigError>;
|
|
27
|
+
/**
|
|
28
|
+
* Cloud Run requires every Service template and Job task template to
|
|
29
|
+
* declare at least one container. A `containers: []` body sails through
|
|
30
|
+
* client-side type checks but the server rejects with a generic 400.
|
|
31
|
+
*/
|
|
32
|
+
export declare const validateContainers: (kind: "Service" | "Job", containers: ReadonlyArray<unknown> | undefined) => Effect.Effect<void, ConfigError>;
|
|
33
|
+
/**
|
|
34
|
+
* Reshape a `BadRequest` from a Cloud Run create or patch into a
|
|
35
|
+
* `ConfigError` with a remediation hint for the top failure modes:
|
|
36
|
+
*
|
|
37
|
+
* 1. **Billing not enabled** — the project's billing-account attach
|
|
38
|
+
* was missing or detached. Common after a fresh project + ApiEnable
|
|
39
|
+
* before billing reconciles.
|
|
40
|
+
* 2. **Image not in an approved registry** — Cloud Run defaults reject
|
|
41
|
+
* Docker Hub and most non-Google registries when Binary Authorization
|
|
42
|
+
* is on. Surface the registry-allow-list pointer.
|
|
43
|
+
* 3. **Reserved label/annotation namespace** — we catch user-supplied
|
|
44
|
+
* cases in `validateLabels`, but server-side checks also fire for
|
|
45
|
+
* fields nested inside `template` (we don't recurse) so leave this
|
|
46
|
+
* as a passthrough hint.
|
|
47
|
+
*
|
|
48
|
+
* Anything else propagates verbatim — the underlying GCP message is
|
|
49
|
+
* usually clear ("invalid image reference", "memory limit too low").
|
|
50
|
+
*/
|
|
51
|
+
export declare const reshapeBadRequest: (kind: "Service" | "Job", op: "create" | "patch") => (e: {
|
|
52
|
+
message?: string;
|
|
53
|
+
}) => Effect.Effect<never, ConfigError>;
|
|
54
|
+
//# sourceMappingURL=Validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Validation.d.ts","sourceRoot":"","sources":["../../src/Run/Validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AA0CxC;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,GAC1B,MAAM,SAAS,GAAG,KAAK,EACvB,MAAM,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,CAOjC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GACzB,QAAQ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,KACzC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,CAajC,CAAC;AAEF,4EAA4E;AAC5E,eAAO,MAAM,mBAAmB,GAC9B,aAAa,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,KAC9C,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,CAAgC,CAAC;AAEnE;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC7B,MAAM,SAAS,GAAG,KAAK,EACvB,YAAY,aAAa,CAAC,OAAO,CAAC,GAAG,SAAS,KAC7C,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,CAOjC,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,iBAAiB,GAC3B,MAAM,SAAS,GAAG,KAAK,EAAE,IAAI,QAAQ,GAAG,OAAO,MAC/C,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,KAAG,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,WAAW,CAkB1D,CAAC"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { ConfigError } from "@distilled.cloud/gcp";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
/**
|
|
4
|
+
* Plan-time validation for Cloud Run resource props. Failing fast with
|
|
5
|
+
* a typed `ConfigError` is much friendlier than letting the user wait
|
|
6
|
+
* 30 s for the server to reject the request — and unlike server-side
|
|
7
|
+
* errors, these surface before any state mutation happens.
|
|
8
|
+
*
|
|
9
|
+
* Used by both `Service` and `Job` since the constraints are shared:
|
|
10
|
+
* resource naming, label namespacing, and the "at least one container"
|
|
11
|
+
* rule on the template.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Cloud Run resource names must match the
|
|
15
|
+
* [RFC 1123](https://datatracker.ietf.org/doc/html/rfc1123) label
|
|
16
|
+
* subset:
|
|
17
|
+
* - lowercase letters, digits, and hyphens
|
|
18
|
+
* - begin with a letter, not end with a hyphen
|
|
19
|
+
* - <50 chars (server hard cap, distinct from the wider 63-char label
|
|
20
|
+
* limit other GCP resources use)
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
const RUN_NAME_RE = /^[a-z]([-a-z0-9]{0,47}[a-z0-9])?$/;
|
|
25
|
+
/**
|
|
26
|
+
* Cloud Run rejects labels and annotations with keys in any of these
|
|
27
|
+
* namespaces. Listed in `vendor/distilled/packages/gcp/src/services/run-v2.ts`
|
|
28
|
+
* doc comments next to every `labels` / `annotations` field. Failing
|
|
29
|
+
* fast here saves a 400 round-trip from the server.
|
|
30
|
+
*/
|
|
31
|
+
const RESERVED_LABEL_NAMESPACES = [
|
|
32
|
+
"run.googleapis.com/",
|
|
33
|
+
"cloud.googleapis.com/",
|
|
34
|
+
"serving.knative.dev/",
|
|
35
|
+
"autoscaling.knative.dev/",
|
|
36
|
+
];
|
|
37
|
+
const reservedNamespaceFor = (key) => RESERVED_LABEL_NAMESPACES.find((ns) => key.startsWith(ns));
|
|
38
|
+
/**
|
|
39
|
+
* Validate a Cloud Run resource name (Service or Job). Returns a
|
|
40
|
+
* `ConfigError` Effect on the failure channel if the name is invalid,
|
|
41
|
+
* otherwise succeeds.
|
|
42
|
+
*
|
|
43
|
+
* The name comes from `props.name` (when the user supplied one) or
|
|
44
|
+
* `createPhysicalName({ id, maxLength: 49 })` (when defaulted). The
|
|
45
|
+
* defaulted path is already capped at 49; this guard mostly protects
|
|
46
|
+
* the user-supplied path against a server-side reject after a long
|
|
47
|
+
* deploy.
|
|
48
|
+
*/
|
|
49
|
+
export const validateRunName = (kind, name) => {
|
|
50
|
+
if (RUN_NAME_RE.test(name) && name.length < 50)
|
|
51
|
+
return Effect.void;
|
|
52
|
+
return Effect.fail(new ConfigError({
|
|
53
|
+
message: `Cloud Run ${kind} name ${JSON.stringify(name)} is invalid: must match /^[a-z]([-a-z0-9]{0,47}[a-z0-9])?$/ (lowercase letters/digits/hyphens, starts with letter, no trailing hyphen, <50 chars).`,
|
|
54
|
+
}));
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Reject user labels in any of the Cloud Run reserved namespaces — the
|
|
58
|
+
* server's 400 message ("Labels with run.googleapis.com namespace are
|
|
59
|
+
* not allowed") is clear enough, but failing at plan time means the
|
|
60
|
+
* stack never starts a deploy that's guaranteed to fail.
|
|
61
|
+
*
|
|
62
|
+
* Alchemy internals are merged *on top* of user labels and are NOT
|
|
63
|
+
* subject to this check — they use the un-namespaced `alchemy_*` keys.
|
|
64
|
+
*/
|
|
65
|
+
export const validateLabels = (labels) => {
|
|
66
|
+
if (!labels)
|
|
67
|
+
return Effect.void;
|
|
68
|
+
for (const key of Object.keys(labels)) {
|
|
69
|
+
const ns = reservedNamespaceFor(key);
|
|
70
|
+
if (ns) {
|
|
71
|
+
return Effect.fail(new ConfigError({
|
|
72
|
+
message: `Label key ${JSON.stringify(key)} uses reserved namespace ${JSON.stringify(ns)} — Cloud Run will reject this on create/patch. Strip the prefix or pick a different key.`,
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return Effect.void;
|
|
77
|
+
};
|
|
78
|
+
/** Same check, applied to annotations (server has the same reservation). */
|
|
79
|
+
export const validateAnnotations = (annotations) => validateLabels(annotations);
|
|
80
|
+
/**
|
|
81
|
+
* Cloud Run requires every Service template and Job task template to
|
|
82
|
+
* declare at least one container. A `containers: []` body sails through
|
|
83
|
+
* client-side type checks but the server rejects with a generic 400.
|
|
84
|
+
*/
|
|
85
|
+
export const validateContainers = (kind, containers) => {
|
|
86
|
+
if (containers && containers.length > 0)
|
|
87
|
+
return Effect.void;
|
|
88
|
+
return Effect.fail(new ConfigError({
|
|
89
|
+
message: `Cloud Run ${kind} template must declare at least one container. Did you forget \`template.containers: [{ image: "…" }]\`?`,
|
|
90
|
+
}));
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Reshape a `BadRequest` from a Cloud Run create or patch into a
|
|
94
|
+
* `ConfigError` with a remediation hint for the top failure modes:
|
|
95
|
+
*
|
|
96
|
+
* 1. **Billing not enabled** — the project's billing-account attach
|
|
97
|
+
* was missing or detached. Common after a fresh project + ApiEnable
|
|
98
|
+
* before billing reconciles.
|
|
99
|
+
* 2. **Image not in an approved registry** — Cloud Run defaults reject
|
|
100
|
+
* Docker Hub and most non-Google registries when Binary Authorization
|
|
101
|
+
* is on. Surface the registry-allow-list pointer.
|
|
102
|
+
* 3. **Reserved label/annotation namespace** — we catch user-supplied
|
|
103
|
+
* cases in `validateLabels`, but server-side checks also fire for
|
|
104
|
+
* fields nested inside `template` (we don't recurse) so leave this
|
|
105
|
+
* as a passthrough hint.
|
|
106
|
+
*
|
|
107
|
+
* Anything else propagates verbatim — the underlying GCP message is
|
|
108
|
+
* usually clear ("invalid image reference", "memory limit too low").
|
|
109
|
+
*/
|
|
110
|
+
export const reshapeBadRequest = (kind, op) => (e) => {
|
|
111
|
+
const underlying = e.message ?? `unknown 400 from ${kind} ${op}`;
|
|
112
|
+
let hint = "";
|
|
113
|
+
if (/billing/i.test(underlying)) {
|
|
114
|
+
hint =
|
|
115
|
+
" Cloud Run requires billing to be enabled on the project. Attach a billing account (see `GCP.Project({ billingAccount })`) and retry.";
|
|
116
|
+
}
|
|
117
|
+
else if (/registry|image.*not.*allowed|disallowed.*image/i.test(underlying)) {
|
|
118
|
+
hint =
|
|
119
|
+
" Cloud Run rejects images from non-Google registries when Binary Authorization is on. Push to Artifact Registry (`*-docker.pkg.dev`) or relax the binary-auth policy.";
|
|
120
|
+
}
|
|
121
|
+
else if (/run\.googleapis\.com|knative\.dev|cloud\.googleapis\.com/i.test(underlying)) {
|
|
122
|
+
hint =
|
|
123
|
+
" A reserved label/annotation namespace is in use. Cloud Run forbids `run.googleapis.com/*`, `cloud.googleapis.com/*`, `serving.knative.dev/*`, and `autoscaling.knative.dev/*` on user fields.";
|
|
124
|
+
}
|
|
125
|
+
return Effect.fail(new ConfigError({
|
|
126
|
+
message: `Cloud Run ${kind} ${op} rejected: ${underlying}.${hint}`,
|
|
127
|
+
}));
|
|
128
|
+
};
|
|
129
|
+
//# sourceMappingURL=Validation.js.map
|