@microagi/alchemy-gcp 0.11.6 → 0.11.8
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 +28 -0
- package/lib/ArtifactRegistry/Operations.d.ts +1 -1
- package/lib/ArtifactRegistry/Repository.d.ts +2 -2
- package/lib/ArtifactRegistry/Repository.d.ts.map +1 -1
- package/lib/ArtifactRegistry/Repository.js +5 -5
- package/lib/ArtifactRegistry/Repository.js.map +1 -1
- package/lib/ArtifactRegistry/RepositoryIamMember.d.ts +2 -1
- package/lib/ArtifactRegistry/RepositoryIamMember.d.ts.map +1 -1
- package/lib/ArtifactRegistry/RepositoryIamMember.js +1 -1
- package/lib/CloudResourceManager/Project.d.ts +2 -1
- package/lib/CloudResourceManager/Project.d.ts.map +1 -1
- package/lib/CloudResourceManager/Project.js +6 -6
- package/lib/CloudResourceManager/Project.js.map +1 -1
- package/lib/Compute/Address.d.ts +2 -1
- package/lib/Compute/Address.d.ts.map +1 -1
- package/lib/Compute/Address.js +4 -4
- package/lib/Compute/Address.js.map +1 -1
- package/lib/Compute/Firewall.d.ts +2 -1
- package/lib/Compute/Firewall.d.ts.map +1 -1
- package/lib/Compute/Firewall.js +1 -1
- package/lib/Compute/ForwardingRule.d.ts +2 -1
- package/lib/Compute/ForwardingRule.d.ts.map +1 -1
- package/lib/Compute/ForwardingRule.js +4 -4
- package/lib/Compute/ForwardingRule.js.map +1 -1
- package/lib/Compute/GlobalAddress.d.ts +2 -1
- package/lib/Compute/GlobalAddress.d.ts.map +1 -1
- package/lib/Compute/GlobalAddress.js +4 -4
- package/lib/Compute/GlobalAddress.js.map +1 -1
- package/lib/Compute/Network.d.ts +2 -1
- package/lib/Compute/Network.d.ts.map +1 -1
- package/lib/Compute/Network.js +1 -1
- package/lib/Compute/Operations.d.ts +1 -1
- package/lib/Compute/ProjectDefaultNetworkTier.d.ts +2 -1
- package/lib/Compute/ProjectDefaultNetworkTier.d.ts.map +1 -1
- package/lib/Compute/ProjectDefaultNetworkTier.js +1 -1
- package/lib/Compute/SharedVpcHost.d.ts +2 -1
- package/lib/Compute/SharedVpcHost.d.ts.map +1 -1
- package/lib/Compute/SharedVpcHost.js +1 -1
- package/lib/Compute/SharedVpcServiceProject.d.ts +2 -1
- package/lib/Compute/SharedVpcServiceProject.d.ts.map +1 -1
- package/lib/Compute/SharedVpcServiceProject.js +1 -1
- package/lib/Compute/Subnetwork.d.ts +2 -1
- package/lib/Compute/Subnetwork.d.ts.map +1 -1
- package/lib/Compute/Subnetwork.js +2 -2
- package/lib/Compute/Subnetwork.js.map +1 -1
- package/lib/Container/Cluster.d.ts +4 -2
- package/lib/Container/Cluster.d.ts.map +1 -1
- package/lib/Container/Cluster.js +28 -10
- package/lib/Container/Cluster.js.map +1 -1
- package/lib/Container/NodePool.d.ts +2 -2
- package/lib/Container/NodePool.d.ts.map +1 -1
- package/lib/Container/NodePool.js +22 -12
- package/lib/Container/NodePool.js.map +1 -1
- package/lib/Container/Operations.d.ts +1 -1
- package/lib/Iam/Operations.d.ts +42 -0
- package/lib/Iam/Operations.d.ts.map +1 -0
- package/lib/Iam/Operations.js +48 -0
- package/lib/Iam/Operations.js.map +1 -0
- package/lib/Iam/ServiceAccount.d.ts +2 -1
- package/lib/Iam/ServiceAccount.d.ts.map +1 -1
- package/lib/Iam/ServiceAccount.js +1 -1
- package/lib/Iam/ServiceAccountKey.d.ts +2 -1
- package/lib/Iam/ServiceAccountKey.d.ts.map +1 -1
- package/lib/Iam/ServiceAccountKey.js +1 -1
- package/lib/Iam/WorkloadIdentityPool.d.ts +101 -0
- package/lib/Iam/WorkloadIdentityPool.d.ts.map +1 -0
- package/lib/Iam/WorkloadIdentityPool.js +175 -0
- package/lib/Iam/WorkloadIdentityPool.js.map +1 -0
- package/lib/Iam/WorkloadIdentityPoolProvider.d.ts +125 -0
- package/lib/Iam/WorkloadIdentityPoolProvider.d.ts.map +1 -0
- package/lib/Iam/WorkloadIdentityPoolProvider.js +222 -0
- package/lib/Iam/WorkloadIdentityPoolProvider.js.map +1 -0
- package/lib/Iam/index.d.ts +4 -0
- package/lib/Iam/index.d.ts.map +1 -1
- package/lib/Iam/index.js +2 -0
- package/lib/Iam/index.js.map +1 -1
- package/lib/Kubernetes/Secret.d.ts.map +1 -1
- package/lib/Kubernetes/Secret.js.map +1 -1
- package/lib/ManagedLustre/Instance.d.ts +2 -1
- package/lib/ManagedLustre/Instance.d.ts.map +1 -1
- package/lib/ManagedLustre/Instance.js +6 -6
- package/lib/ManagedLustre/Instance.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/IamSync.d.ts +1 -1
- package/lib/Run/Job.d.ts +3 -3
- package/lib/Run/Job.d.ts.map +1 -1
- package/lib/Run/Job.js +5 -5
- package/lib/Run/Job.js.map +1 -1
- package/lib/Run/Operations.d.ts +1 -1
- package/lib/Run/Service.d.ts +2 -2
- package/lib/Run/Service.d.ts.map +1 -1
- package/lib/Run/Service.js +9 -7
- package/lib/Run/Service.js.map +1 -1
- package/lib/Run/index.d.ts +2 -2
- package/lib/ServiceNetworking/PsaConnection.d.ts +2 -1
- package/lib/ServiceNetworking/PsaConnection.d.ts.map +1 -1
- package/lib/ServiceNetworking/PsaConnection.js +1 -1
- package/lib/ServiceUsage/ApiEnable.d.ts +2 -1
- package/lib/ServiceUsage/ApiEnable.d.ts.map +1 -1
- package/lib/ServiceUsage/ApiEnable.js +1 -1
- package/lib/Sqladmin/Database.d.ts +2 -1
- package/lib/Sqladmin/Database.d.ts.map +1 -1
- package/lib/Sqladmin/Database.js +1 -1
- package/lib/Sqladmin/Instance.d.ts +2 -1
- package/lib/Sqladmin/Instance.d.ts.map +1 -1
- package/lib/Sqladmin/Instance.js +7 -7
- package/lib/Sqladmin/Instance.js.map +1 -1
- package/lib/Sqladmin/Operations.d.ts +1 -1
- package/lib/Sqladmin/User.d.ts +2 -1
- package/lib/Sqladmin/User.d.ts.map +1 -1
- package/lib/Sqladmin/User.js +1 -1
- package/lib/Storage/Bucket.d.ts +2 -2
- package/lib/Storage/Bucket.d.ts.map +1 -1
- package/lib/Storage/Bucket.js +1 -1
- package/lib/Storage/StorageBucketIamMember.d.ts +2 -1
- package/lib/Storage/StorageBucketIamMember.d.ts.map +1 -1
- package/lib/Storage/StorageBucketIamMember.js +1 -1
- package/lib/Tags.d.ts +3 -1
- package/lib/Tags.d.ts.map +1 -1
- package/lib/Tags.js +9 -1
- package/lib/Tags.js.map +1 -1
- package/package.json +9 -9
- package/src/ArtifactRegistry/Operations.ts +1 -1
- package/src/ArtifactRegistry/Repository.ts +5 -5
- package/src/ArtifactRegistry/RepositoryIamMember.ts +1 -1
- package/src/CloudResourceManager/Project.ts +6 -6
- package/src/Compute/Address.ts +4 -4
- package/src/Compute/Firewall.ts +1 -1
- package/src/Compute/ForwardingRule.ts +4 -4
- package/src/Compute/GlobalAddress.ts +4 -4
- package/src/Compute/Network.ts +1 -1
- package/src/Compute/Operations.ts +1 -1
- package/src/Compute/ProjectDefaultNetworkTier.ts +1 -1
- package/src/Compute/SharedVpcHost.ts +1 -1
- package/src/Compute/SharedVpcServiceProject.ts +1 -1
- package/src/Compute/Subnetwork.ts +2 -2
- package/src/Container/Cluster.ts +36 -11
- package/src/Container/NodePool.ts +30 -13
- package/src/Container/Operations.ts +1 -1
- package/src/Iam/Operations.ts +81 -0
- package/src/Iam/ServiceAccount.ts +1 -1
- package/src/Iam/ServiceAccountKey.ts +1 -1
- package/src/Iam/WorkloadIdentityPool.ts +309 -0
- package/src/Iam/WorkloadIdentityPoolProvider.ts +429 -0
- package/src/Iam/index.ts +10 -0
- package/src/Kubernetes/Secret.ts +4 -1
- package/src/ManagedLustre/Instance.ts +6 -6
- package/src/Providers.ts +12 -0
- package/src/Run/IamSync.ts +1 -1
- package/src/Run/Job.ts +6 -6
- package/src/Run/Operations.ts +1 -1
- package/src/Run/Service.ts +9 -7
- package/src/Run/index.ts +2 -2
- package/src/ServiceNetworking/PsaConnection.ts +1 -1
- package/src/ServiceUsage/ApiEnable.ts +1 -1
- package/src/Sqladmin/Database.ts +1 -1
- package/src/Sqladmin/Instance.ts +13 -7
- package/src/Sqladmin/Operations.ts +1 -1
- package/src/Sqladmin/User.ts +1 -1
- package/src/Storage/Bucket.ts +1 -1
- package/src/Storage/StorageBucketIamMember.ts +1 -1
- package/src/Tags.ts +16 -1
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { ConfigError } from "@distilled.cloud/gcp";
|
|
2
|
+
import * as iam from "@distilled.cloud/gcp/unstable/iam_v1";
|
|
3
|
+
import { Resource } from "alchemy";
|
|
4
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
5
|
+
import { isResolved } from "alchemy/Diff";
|
|
6
|
+
import * as Provider from "alchemy/Provider";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
import {
|
|
9
|
+
descriptionHasAlchemyMarker,
|
|
10
|
+
gcpAlchemyDescription,
|
|
11
|
+
normalizeStringMap,
|
|
12
|
+
stripAlchemyMarker,
|
|
13
|
+
} from "../Tags.ts";
|
|
14
|
+
import type * as GCP from "../Providers.ts";
|
|
15
|
+
import { makeAwaitOperation } from "./Operations.ts";
|
|
16
|
+
import { poolResourceName } from "./WorkloadIdentityPool.ts";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* An OIDC **provider** inside a
|
|
20
|
+
* {@link import("./WorkloadIdentityPool.ts").WorkloadIdentityPool} — the
|
|
21
|
+
* thing that actually declares "I trust tokens issued by this URL".
|
|
22
|
+
*
|
|
23
|
+
* The canonical use is keyless federation from another cloud: point
|
|
24
|
+
* `oidc.issuerUri` at an EKS/AKS/GKE cluster's OIDC issuer, and pods holding
|
|
25
|
+
* a projected ServiceAccount token can impersonate a GSA with no key material
|
|
26
|
+
* anywhere in the system.
|
|
27
|
+
*
|
|
28
|
+
* ### `attributeMapping` is required and `google.subject` is mandatory
|
|
29
|
+
*
|
|
30
|
+
* The mapping turns claims on the incoming token into Google attributes. At
|
|
31
|
+
* minimum `google.subject` must be mapped; it is what `principal://` members
|
|
32
|
+
* match on. For a Kubernetes issuer the subject claim is
|
|
33
|
+
* `system:serviceaccount:{namespace}:{name}`, so:
|
|
34
|
+
*
|
|
35
|
+
* ```typescript
|
|
36
|
+
* attributeMapping: { "google.subject": "assertion.sub" }
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* ### Set `allowedAudiences`, or the default is surprising
|
|
40
|
+
*
|
|
41
|
+
* When `oidc.allowedAudiences` is empty, GCP accepts only the *default*
|
|
42
|
+
* audience — the full provider resource name prefixed with
|
|
43
|
+
* `https://iam.googleapis.com/`. A Kubernetes projected token minted for
|
|
44
|
+
* audience `sts.googleapis.com` (or anything else) is then rejected with a
|
|
45
|
+
* generic audience error. Set this explicitly to whatever audience the
|
|
46
|
+
* token is actually minted for.
|
|
47
|
+
*
|
|
48
|
+
* ### Constrain trust with `attributeCondition`
|
|
49
|
+
*
|
|
50
|
+
* An issuer-only trust accepts **every** identity that issuer can mint — on
|
|
51
|
+
* a Kubernetes cluster, that is every ServiceAccount in every namespace. Pair
|
|
52
|
+
* the provider with an `attributeCondition` (and a narrow `principal://`
|
|
53
|
+
* member on the GSA binding) so a token from an unrelated namespace cannot
|
|
54
|
+
* impersonate anything.
|
|
55
|
+
*
|
|
56
|
+
* @section Creating a WorkloadIdentityPoolProvider
|
|
57
|
+
* @example Trust one Kubernetes ServiceAccount on an EKS cluster
|
|
58
|
+
* ```typescript
|
|
59
|
+
* const provider = yield* GCP.WorkloadIdentityPoolProvider("EksOidc", {
|
|
60
|
+
* project: "123456789",
|
|
61
|
+
* poolId: pool.poolId,
|
|
62
|
+
* providerId: "eks-oidc",
|
|
63
|
+
* oidc: {
|
|
64
|
+
* issuerUri: "https://oidc.eks.us-east-1.amazonaws.com/id/ABC123",
|
|
65
|
+
* allowedAudiences: ["sts.googleapis.com"],
|
|
66
|
+
* },
|
|
67
|
+
* attributeMapping: { "google.subject": "assertion.sub" },
|
|
68
|
+
* attributeCondition:
|
|
69
|
+
* "assertion.sub == 'system:serviceaccount:admin:eks-auth-reconciler'",
|
|
70
|
+
* });
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export type WorkloadIdentityPoolProviderProps = {
|
|
74
|
+
/** Project that owns the pool. Prefer the project **number**. */
|
|
75
|
+
project: string;
|
|
76
|
+
/** ID of the enclosing pool. */
|
|
77
|
+
poolId: string;
|
|
78
|
+
/**
|
|
79
|
+
* Provider ID, 4–32 characters of `[a-z0-9-]`. Immutable — changing it
|
|
80
|
+
* replaces the provider. The `gcp-` prefix is reserved by Google.
|
|
81
|
+
*/
|
|
82
|
+
providerId: string;
|
|
83
|
+
/** The OIDC issuer this provider trusts. */
|
|
84
|
+
oidc: {
|
|
85
|
+
/** Issuer URL. Must be HTTPS and serve an OIDC discovery document. */
|
|
86
|
+
issuerUri: string;
|
|
87
|
+
/**
|
|
88
|
+
* Acceptable `aud` values. Leave unset only if the token is minted for
|
|
89
|
+
* the provider's default audience — see the note above.
|
|
90
|
+
*/
|
|
91
|
+
allowedAudiences?: string[];
|
|
92
|
+
/**
|
|
93
|
+
* Inline JWKS, for issuers whose keys are not publicly reachable. When
|
|
94
|
+
* unset, GCP fetches `jwks_uri` from the issuer's discovery document,
|
|
95
|
+
* which requires the issuer to be reachable from Google's network.
|
|
96
|
+
*/
|
|
97
|
+
jwksJson?: string;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Claim-to-attribute mapping. Must include `google.subject`.
|
|
101
|
+
*/
|
|
102
|
+
attributeMapping: Record<string, string>;
|
|
103
|
+
/** CEL expression further restricting which credentials are accepted. */
|
|
104
|
+
attributeCondition?: string;
|
|
105
|
+
/** Display name, max 32 characters. */
|
|
106
|
+
displayName?: string;
|
|
107
|
+
/** Description, max 256 characters. */
|
|
108
|
+
description?: string;
|
|
109
|
+
/** Disable the provider without deleting it. */
|
|
110
|
+
disabled?: boolean;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export type WorkloadIdentityPoolProviderAttributes = {
|
|
114
|
+
/** Full resource name, `…/workloadIdentityPools/{pool}/providers/{id}`. */
|
|
115
|
+
name: string;
|
|
116
|
+
project: string;
|
|
117
|
+
poolId: string;
|
|
118
|
+
providerId: string;
|
|
119
|
+
issuerUri: string | undefined;
|
|
120
|
+
// `readonly` because that is how the SDK models repeated fields; widening it
|
|
121
|
+
// to a mutable array here would only force a copy at every use site.
|
|
122
|
+
allowedAudiences: readonly string[] | undefined;
|
|
123
|
+
attributeMapping: Record<string, string> | undefined;
|
|
124
|
+
attributeCondition: string | undefined;
|
|
125
|
+
displayName: string | undefined;
|
|
126
|
+
description: string | undefined;
|
|
127
|
+
state: string | undefined;
|
|
128
|
+
disabled: boolean | undefined;
|
|
129
|
+
/**
|
|
130
|
+
* The audience string to put in a credential configuration, i.e.
|
|
131
|
+
* `//iam.googleapis.com/{name}`. Provided because assembling it by hand is
|
|
132
|
+
* easy to get subtly wrong (the leading `//`, and no scheme).
|
|
133
|
+
*/
|
|
134
|
+
audience: string;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export interface WorkloadIdentityPoolProvider
|
|
138
|
+
extends Resource<
|
|
139
|
+
"GCP.WorkloadIdentityPoolProvider",
|
|
140
|
+
WorkloadIdentityPoolProviderProps,
|
|
141
|
+
WorkloadIdentityPoolProviderAttributes,
|
|
142
|
+
never,
|
|
143
|
+
GCP.Providers
|
|
144
|
+
> {}
|
|
145
|
+
|
|
146
|
+
export const WorkloadIdentityPoolProvider =
|
|
147
|
+
Resource<WorkloadIdentityPoolProvider>("GCP.WorkloadIdentityPoolProvider");
|
|
148
|
+
|
|
149
|
+
export const providerResourceName = (
|
|
150
|
+
project: string,
|
|
151
|
+
poolId: string,
|
|
152
|
+
providerId: string,
|
|
153
|
+
) => `${poolResourceName(project, poolId)}/providers/${providerId}`;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Compare two claim mappings by CONTENT, not serialisation.
|
|
157
|
+
*
|
|
158
|
+
* `JSON.stringify` preserves key insertion order, and the order GCP returns
|
|
159
|
+
* these keys in need not match the order they were sent in. A raw stringify
|
|
160
|
+
* comparison therefore reports drift for two identical mappings and fires a
|
|
161
|
+
* patch LRO on every single deploy.
|
|
162
|
+
*/
|
|
163
|
+
const sameMapping = (
|
|
164
|
+
a: Record<string, string> | undefined,
|
|
165
|
+
b: Record<string, string> | undefined,
|
|
166
|
+
) => {
|
|
167
|
+
const entries = (r: Record<string, string> | undefined) =>
|
|
168
|
+
Object.entries(r ?? {}).sort(([x], [y]) => (x < y ? -1 : x > y ? 1 : 0));
|
|
169
|
+
return JSON.stringify(entries(a)) === JSON.stringify(entries(b));
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Compare audience allow-lists as SETS.
|
|
174
|
+
*
|
|
175
|
+
* `allowedAudiences` is an allow-list: membership is what matters, order is
|
|
176
|
+
* not meaningful, and GCP is free to return it in any order. Comparing it
|
|
177
|
+
* positionally would make a reordering look like drift forever.
|
|
178
|
+
*/
|
|
179
|
+
const sameAudiences = (
|
|
180
|
+
a: readonly string[] | undefined,
|
|
181
|
+
b: readonly string[] | undefined,
|
|
182
|
+
) => {
|
|
183
|
+
const sorted = (v: readonly string[] | undefined) => [...(v ?? [])].sort();
|
|
184
|
+
return JSON.stringify(sorted(a)) === JSON.stringify(sorted(b));
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Treat absent and empty-string as the same value.
|
|
189
|
+
*
|
|
190
|
+
* The patch below uses a FIXED `updateMask` while the request body omits
|
|
191
|
+
* unset optional fields — which is deliberate: under a field mask, an omitted
|
|
192
|
+
* field means "clear it", so dropping `displayName` from the props really
|
|
193
|
+
* should clear it on GCP. The hazard is only in the comparison afterwards. If
|
|
194
|
+
* GCP echoes a cleared field back as `""` rather than omitting it, a raw
|
|
195
|
+
* `!==` against `undefined` would report drift on every single deploy and
|
|
196
|
+
* churn a long-running patch operation forever.
|
|
197
|
+
*/
|
|
198
|
+
const sameText = (a: string | undefined, b: string | undefined) =>
|
|
199
|
+
(a ?? "") === (b ?? "");
|
|
200
|
+
|
|
201
|
+
export const WorkloadIdentityPoolProviderProvider = () =>
|
|
202
|
+
Provider.effect(
|
|
203
|
+
WorkloadIdentityPoolProvider,
|
|
204
|
+
Effect.gen(function* () {
|
|
205
|
+
const getProvider =
|
|
206
|
+
yield* iam.getProjectsLocationsWorkloadIdentityPoolsProviders;
|
|
207
|
+
const createProvider =
|
|
208
|
+
yield* iam.createProjectsLocationsWorkloadIdentityPoolsProviders;
|
|
209
|
+
const patchProvider =
|
|
210
|
+
yield* iam.patchProjectsLocationsWorkloadIdentityPoolsProviders;
|
|
211
|
+
const deleteProvider =
|
|
212
|
+
yield* iam.deleteProjectsLocationsWorkloadIdentityPoolsProviders;
|
|
213
|
+
const undeleteProvider =
|
|
214
|
+
yield* iam.undeleteProjectsLocationsWorkloadIdentityPoolsProviders;
|
|
215
|
+
const getOperations =
|
|
216
|
+
yield* iam.getProjectsLocationsWorkloadIdentityPoolsProvidersOperations;
|
|
217
|
+
const awaitOperation = makeAwaitOperation(
|
|
218
|
+
getOperations,
|
|
219
|
+
"Workload Identity provider",
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const observeProvider = (
|
|
223
|
+
project: string,
|
|
224
|
+
poolId: string,
|
|
225
|
+
providerId: string,
|
|
226
|
+
) =>
|
|
227
|
+
getProvider({
|
|
228
|
+
name: providerResourceName(project, poolId, providerId),
|
|
229
|
+
}).pipe(
|
|
230
|
+
Effect.catchTag("NotFound", () =>
|
|
231
|
+
Effect.succeed(
|
|
232
|
+
undefined as iam.WorkloadIdentityPoolProvider | undefined,
|
|
233
|
+
),
|
|
234
|
+
),
|
|
235
|
+
Effect.catchTag("Forbidden", () =>
|
|
236
|
+
Effect.succeed(
|
|
237
|
+
undefined as iam.WorkloadIdentityPoolProvider | undefined,
|
|
238
|
+
),
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const toAttrs = (
|
|
243
|
+
project: string,
|
|
244
|
+
poolId: string,
|
|
245
|
+
providerId: string,
|
|
246
|
+
provider: iam.WorkloadIdentityPoolProvider,
|
|
247
|
+
): WorkloadIdentityPoolProviderAttributes => {
|
|
248
|
+
const name =
|
|
249
|
+
provider.name ?? providerResourceName(project, poolId, providerId);
|
|
250
|
+
return {
|
|
251
|
+
name,
|
|
252
|
+
project,
|
|
253
|
+
poolId,
|
|
254
|
+
providerId,
|
|
255
|
+
issuerUri: provider.oidc?.issuerUri,
|
|
256
|
+
allowedAudiences: provider.oidc?.allowedAudiences,
|
|
257
|
+
attributeMapping: normalizeStringMap(provider.attributeMapping),
|
|
258
|
+
attributeCondition: provider.attributeCondition,
|
|
259
|
+
displayName: provider.displayName,
|
|
260
|
+
description: stripAlchemyMarker(provider.description),
|
|
261
|
+
state: provider.state,
|
|
262
|
+
disabled: provider.disabled,
|
|
263
|
+
audience: `//iam.googleapis.com/${name}`,
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
stables: ["name", "project", "poolId", "providerId", "audience"],
|
|
269
|
+
diff: Effect.fn(function* ({ olds = {}, news, output }) {
|
|
270
|
+
if (!isResolved(news)) return undefined;
|
|
271
|
+
const oldProps = olds as Partial<WorkloadIdentityPoolProviderProps>;
|
|
272
|
+
// Prefer live attributes over persisted props, and fall back to the
|
|
273
|
+
// DESIRED value when neither is known — see the equivalent comment
|
|
274
|
+
// in WorkloadIdentityPool.ts. Defaulting to `undefined` would make
|
|
275
|
+
// an adoption replace a live, correct provider.
|
|
276
|
+
//
|
|
277
|
+
// All three fields are part of the resource name; the API cannot
|
|
278
|
+
// move a provider between pools or rename it.
|
|
279
|
+
const currentProject = output?.project || oldProps.project || news.project;
|
|
280
|
+
const currentPoolId = output?.poolId || oldProps.poolId || news.poolId;
|
|
281
|
+
const currentProviderId =
|
|
282
|
+
output?.providerId || oldProps.providerId || news.providerId;
|
|
283
|
+
if (
|
|
284
|
+
currentProject !== news.project ||
|
|
285
|
+
currentPoolId !== news.poolId ||
|
|
286
|
+
currentProviderId !== news.providerId
|
|
287
|
+
) {
|
|
288
|
+
return { action: "replace" } as const;
|
|
289
|
+
}
|
|
290
|
+
}),
|
|
291
|
+
read: Effect.fn(function* ({ id, olds, output }) {
|
|
292
|
+
const project = output?.project || olds?.project;
|
|
293
|
+
const poolId = output?.poolId || olds?.poolId;
|
|
294
|
+
const providerId = output?.providerId || olds?.providerId;
|
|
295
|
+
if (!project || !poolId || !providerId) return undefined;
|
|
296
|
+
const provider = yield* observeProvider(project, poolId, providerId);
|
|
297
|
+
if (!provider) return undefined;
|
|
298
|
+
const attrs = toAttrs(project, poolId, providerId, provider);
|
|
299
|
+
return (yield* descriptionHasAlchemyMarker(id, provider.description))
|
|
300
|
+
? attrs
|
|
301
|
+
: Unowned(attrs);
|
|
302
|
+
}),
|
|
303
|
+
reconcile: Effect.fn(function* ({ id, news, session }) {
|
|
304
|
+
const { project, poolId, providerId } = news;
|
|
305
|
+
if (!news.attributeMapping["google.subject"]) {
|
|
306
|
+
// Caught here rather than at the API, which rejects it with a
|
|
307
|
+
// message that does not name the missing key.
|
|
308
|
+
return yield* new ConfigError({
|
|
309
|
+
message: `Workload Identity provider ${providerId} must map 'google.subject' in attributeMapping.`,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
const description = yield* gcpAlchemyDescription(
|
|
313
|
+
id,
|
|
314
|
+
news.description,
|
|
315
|
+
);
|
|
316
|
+
// Scalars are TOTAL over the updateMask below — always present,
|
|
317
|
+
// explicit empty/default when unset — so clearing is stated in code
|
|
318
|
+
// rather than implied by field-mask semantics.
|
|
319
|
+
//
|
|
320
|
+
// `attributeCondition` and `oidc.jwksJson` are the exception and
|
|
321
|
+
// stay conditional: both are validated formats (a CEL expression
|
|
322
|
+
// and a JWKS document), and sending "" risks a parse rejection
|
|
323
|
+
// where omission is unambiguous. They are still cleared correctly
|
|
324
|
+
// when unset — `oidc` is masked as a whole message, so the body's
|
|
325
|
+
// `oidc` replaces it outright.
|
|
326
|
+
const body: iam.WorkloadIdentityPoolProvider = {
|
|
327
|
+
displayName: news.displayName ?? "",
|
|
328
|
+
description,
|
|
329
|
+
disabled: news.disabled ?? false,
|
|
330
|
+
attributeMapping: news.attributeMapping,
|
|
331
|
+
...(news.attributeCondition
|
|
332
|
+
? { attributeCondition: news.attributeCondition }
|
|
333
|
+
: {}),
|
|
334
|
+
oidc: {
|
|
335
|
+
issuerUri: news.oidc.issuerUri,
|
|
336
|
+
allowedAudiences: news.oidc.allowedAudiences ?? [],
|
|
337
|
+
...(news.oidc.jwksJson ? { jwksJson: news.oidc.jwksJson } : {}),
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
let provider = yield* observeProvider(project, poolId, providerId);
|
|
342
|
+
|
|
343
|
+
// Same soft-delete reasoning as the pool: the ID is held until the
|
|
344
|
+
// purge, so undelete rather than strand the deploy.
|
|
345
|
+
if (provider?.state === "DELETED") {
|
|
346
|
+
yield* session.note(
|
|
347
|
+
`Undeleting soft-deleted Workload Identity provider ${providerId}…`,
|
|
348
|
+
);
|
|
349
|
+
const op = yield* undeleteProvider({
|
|
350
|
+
name: providerResourceName(project, poolId, providerId),
|
|
351
|
+
body: {},
|
|
352
|
+
});
|
|
353
|
+
if (op.name) yield* awaitOperation(op.name, session);
|
|
354
|
+
provider = yield* observeProvider(project, poolId, providerId);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (!provider) {
|
|
358
|
+
const op = yield* createProvider({
|
|
359
|
+
parent: poolResourceName(project, poolId),
|
|
360
|
+
workloadIdentityPoolProviderId: providerId,
|
|
361
|
+
body,
|
|
362
|
+
});
|
|
363
|
+
if (op.name) yield* awaitOperation(op.name, session);
|
|
364
|
+
provider = yield* observeProvider(project, poolId, providerId);
|
|
365
|
+
if (!provider) {
|
|
366
|
+
return yield* new ConfigError({
|
|
367
|
+
message: `Workload Identity provider ${providerId} in pool ${poolId} was not readable after create.`,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
const needsPatch =
|
|
372
|
+
!sameText(provider.displayName, news.displayName) ||
|
|
373
|
+
!sameText(provider.description, description) ||
|
|
374
|
+
(provider.disabled ?? false) !== (news.disabled ?? false) ||
|
|
375
|
+
!sameText(provider.attributeCondition, news.attributeCondition) ||
|
|
376
|
+
!sameMapping(
|
|
377
|
+
normalizeStringMap(provider.attributeMapping),
|
|
378
|
+
news.attributeMapping,
|
|
379
|
+
) ||
|
|
380
|
+
!sameText(provider.oidc?.issuerUri, news.oidc.issuerUri) ||
|
|
381
|
+
!sameAudiences(
|
|
382
|
+
provider.oidc?.allowedAudiences,
|
|
383
|
+
news.oidc.allowedAudiences,
|
|
384
|
+
) ||
|
|
385
|
+
!sameText(provider.oidc?.jwksJson, news.oidc.jwksJson);
|
|
386
|
+
if (needsPatch) {
|
|
387
|
+
const op = yield* patchProvider({
|
|
388
|
+
name: providerResourceName(project, poolId, providerId),
|
|
389
|
+
updateMask:
|
|
390
|
+
"displayName,description,disabled,attributeMapping,attributeCondition,oidc",
|
|
391
|
+
body,
|
|
392
|
+
});
|
|
393
|
+
if (op.name) yield* awaitOperation(op.name, session);
|
|
394
|
+
provider =
|
|
395
|
+
(yield* observeProvider(project, poolId, providerId)) ??
|
|
396
|
+
provider;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
yield* session.note(
|
|
401
|
+
providerResourceName(project, poolId, providerId),
|
|
402
|
+
);
|
|
403
|
+
return toAttrs(project, poolId, providerId, provider);
|
|
404
|
+
}),
|
|
405
|
+
delete: Effect.fn(function* ({ olds, output, session }) {
|
|
406
|
+
// Props fallback for the same reason as the pool: incomplete
|
|
407
|
+
// attributes must not turn destroy into a silent no-op that leaks a
|
|
408
|
+
// soft-deleted-name-holding resource.
|
|
409
|
+
// `||` not `??`: an empty string is never a valid identity here,
|
|
410
|
+
// and a persisted `""` must fall through to the next source rather
|
|
411
|
+
// than be taken as real. With `??` a corrupt/partial state entry
|
|
412
|
+
// would short-circuit the fallback and turn destroy into a silent
|
|
413
|
+
// no-op (or make diff replace a live resource).
|
|
414
|
+
const project = output?.project || olds?.project;
|
|
415
|
+
const poolId = output?.poolId || olds?.poolId;
|
|
416
|
+
const providerId = output?.providerId || olds?.providerId;
|
|
417
|
+
if (!project || !poolId || !providerId) return;
|
|
418
|
+
const op = yield* deleteProvider({
|
|
419
|
+
name: providerResourceName(project, poolId, providerId),
|
|
420
|
+
}).pipe(
|
|
421
|
+
Effect.catchTag("NotFound", () =>
|
|
422
|
+
Effect.succeed({ name: undefined } as iam.Operation),
|
|
423
|
+
),
|
|
424
|
+
);
|
|
425
|
+
if (op.name) yield* awaitOperation(op.name, session);
|
|
426
|
+
}),
|
|
427
|
+
};
|
|
428
|
+
}),
|
|
429
|
+
);
|
package/src/Iam/index.ts
CHANGED
|
@@ -6,3 +6,13 @@ export {
|
|
|
6
6
|
ServiceAccountKeyProvider,
|
|
7
7
|
} from "./ServiceAccountKey.ts";
|
|
8
8
|
export type { ServiceAccountKeyProps } from "./ServiceAccountKey.ts";
|
|
9
|
+
export {
|
|
10
|
+
WorkloadIdentityPool,
|
|
11
|
+
WorkloadIdentityPoolResourceProvider,
|
|
12
|
+
} from "./WorkloadIdentityPool.ts";
|
|
13
|
+
export type { WorkloadIdentityPoolProps } from "./WorkloadIdentityPool.ts";
|
|
14
|
+
export {
|
|
15
|
+
WorkloadIdentityPoolProvider,
|
|
16
|
+
WorkloadIdentityPoolProviderProvider,
|
|
17
|
+
} from "./WorkloadIdentityPoolProvider.ts";
|
|
18
|
+
export type { WorkloadIdentityPoolProviderProps } from "./WorkloadIdentityPoolProvider.ts";
|
package/src/Kubernetes/Secret.ts
CHANGED
|
@@ -123,7 +123,10 @@ type SecretObject = {
|
|
|
123
123
|
namespace?: string;
|
|
124
124
|
uid?: string;
|
|
125
125
|
resourceVersion?: string;
|
|
126
|
-
|
|
126
|
+
// Distilled's generated `…ObjectMetaLabelsMap` index signature includes
|
|
127
|
+
// `undefined`, so the observed object is only assignable to this local
|
|
128
|
+
// subset if we mirror that.
|
|
129
|
+
labels?: Record<string, string | undefined>;
|
|
127
130
|
};
|
|
128
131
|
type?: string;
|
|
129
132
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConfigError } from "@distilled.cloud/gcp";
|
|
2
|
-
import * as lustre from "@distilled.cloud/gcp/
|
|
2
|
+
import * as lustre from "@distilled.cloud/gcp/lustre_v1";
|
|
3
3
|
import { Resource } from "alchemy";
|
|
4
4
|
import { Unowned } from "alchemy/AdoptPolicy";
|
|
5
5
|
import type { ScopedPlanStatusSession } from "alchemy/Cli/Cli";
|
|
@@ -11,7 +11,7 @@ import * as Duration from "effect/Duration";
|
|
|
11
11
|
import * as Effect from "effect/Effect";
|
|
12
12
|
import * as Schedule from "effect/Schedule";
|
|
13
13
|
import type { NetworkRefById } from "../Compute/Network.ts";
|
|
14
|
-
import { gcpInternalLabels, hasAlchemyLabels } from "../Tags.ts";
|
|
14
|
+
import { gcpInternalLabels, hasAlchemyLabels, normalizeStringMap } from "../Tags.ts";
|
|
15
15
|
import type * as GCP from "../Providers.ts";
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -221,7 +221,7 @@ const toAttributes = (
|
|
|
221
221
|
perUnitStorageThroughput: i.perUnitStorageThroughput,
|
|
222
222
|
gkeSupportEnabled: i.gkeSupportEnabled,
|
|
223
223
|
kmsKey: i.kmsKey,
|
|
224
|
-
labels:
|
|
224
|
+
labels: normalizeStringMap(i.labels) ?? {},
|
|
225
225
|
});
|
|
226
226
|
|
|
227
227
|
export const ManagedLustreInstanceProvider = () =>
|
|
@@ -295,7 +295,7 @@ export const ManagedLustreInstanceProvider = () =>
|
|
|
295
295
|
if ((args.observed.description ?? undefined) !== args.desired.description) {
|
|
296
296
|
updateMaskFields.push("description");
|
|
297
297
|
}
|
|
298
|
-
const observedLabels =
|
|
298
|
+
const observedLabels = normalizeStringMap(args.observed.labels) ?? {};
|
|
299
299
|
const labelDiff = diffTags(observedLabels, args.desired.labels);
|
|
300
300
|
if (labelDiff.removed.length > 0 || labelDiff.upsert.length > 0) {
|
|
301
301
|
updateMaskFields.push("labels");
|
|
@@ -434,7 +434,7 @@ export const ManagedLustreInstanceProvider = () =>
|
|
|
434
434
|
),
|
|
435
435
|
);
|
|
436
436
|
for (const candidate of page?.instances ?? []) {
|
|
437
|
-
if (yield* hasAlchemyLabels(id, candidate.labels)) {
|
|
437
|
+
if (yield* hasAlchemyLabels(id, normalizeStringMap(candidate.labels))) {
|
|
438
438
|
observed = candidate;
|
|
439
439
|
break;
|
|
440
440
|
}
|
|
@@ -450,7 +450,7 @@ export const ManagedLustreInstanceProvider = () =>
|
|
|
450
450
|
instanceId,
|
|
451
451
|
network,
|
|
452
452
|
});
|
|
453
|
-
return (yield* hasAlchemyLabels(id, observed.labels))
|
|
453
|
+
return (yield* hasAlchemyLabels(id, normalizeStringMap(observed.labels)))
|
|
454
454
|
? attrs
|
|
455
455
|
: Unowned(attrs);
|
|
456
456
|
}),
|
package/src/Providers.ts
CHANGED
|
@@ -39,6 +39,14 @@ import {
|
|
|
39
39
|
ServiceAccountKey,
|
|
40
40
|
ServiceAccountKeyProvider,
|
|
41
41
|
} from "./Iam/ServiceAccountKey.ts";
|
|
42
|
+
import {
|
|
43
|
+
WorkloadIdentityPool,
|
|
44
|
+
WorkloadIdentityPoolResourceProvider,
|
|
45
|
+
} from "./Iam/WorkloadIdentityPool.ts";
|
|
46
|
+
import {
|
|
47
|
+
WorkloadIdentityPoolProvider,
|
|
48
|
+
WorkloadIdentityPoolProviderProvider,
|
|
49
|
+
} from "./Iam/WorkloadIdentityPoolProvider.ts";
|
|
42
50
|
import {
|
|
43
51
|
HelmRelease,
|
|
44
52
|
HelmReleaseProvider,
|
|
@@ -123,6 +131,8 @@ export const providers = () =>
|
|
|
123
131
|
ArtifactRegistryRepositoryIamMember,
|
|
124
132
|
ServiceAccount,
|
|
125
133
|
ServiceAccountKey,
|
|
134
|
+
WorkloadIdentityPool,
|
|
135
|
+
WorkloadIdentityPoolProvider,
|
|
126
136
|
KubernetesSecret,
|
|
127
137
|
KubernetesManifest,
|
|
128
138
|
HelmRelease,
|
|
@@ -156,6 +166,8 @@ export const providers = () =>
|
|
|
156
166
|
ArtifactRegistryRepositoryIamMemberProvider(),
|
|
157
167
|
ServiceAccountProvider(),
|
|
158
168
|
ServiceAccountKeyProvider(),
|
|
169
|
+
WorkloadIdentityPoolResourceProvider(),
|
|
170
|
+
WorkloadIdentityPoolProviderProvider(),
|
|
159
171
|
KubernetesSecretProvider(),
|
|
160
172
|
KubernetesManifestProvider(),
|
|
161
173
|
HelmReleaseProvider(),
|
package/src/Run/IamSync.ts
CHANGED
package/src/Run/Job.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as run from "@distilled.cloud/gcp/
|
|
1
|
+
import * as run from "@distilled.cloud/gcp/run_v2";
|
|
2
2
|
import { Resource } from "alchemy";
|
|
3
3
|
import { Unowned } from "alchemy/AdoptPolicy";
|
|
4
4
|
import type { ScopedPlanStatusSession } from "alchemy/Cli/Cli";
|
|
@@ -10,7 +10,7 @@ import * as Duration from "effect/Duration";
|
|
|
10
10
|
import * as Effect from "effect/Effect";
|
|
11
11
|
import * as Schedule from "effect/Schedule";
|
|
12
12
|
import type * as GCP from "../Providers.ts";
|
|
13
|
-
import { gcpInternalLabels, hasAlchemyLabels } from "../Tags.ts";
|
|
13
|
+
import { gcpInternalLabels, hasAlchemyLabels, normalizeStringMap } from "../Tags.ts";
|
|
14
14
|
import { makeSyncIam, type RunIamBindingContract } from "./IamSync.ts";
|
|
15
15
|
import { makeAwaitOperation } from "./Operations.ts";
|
|
16
16
|
import {
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
* converge-to-desired-state model) and is left to user-side code:
|
|
35
35
|
*
|
|
36
36
|
* ```typescript
|
|
37
|
-
* import * as run from "@distilled.cloud/gcp/
|
|
37
|
+
* import * as run from "@distilled.cloud/gcp/run_v2";
|
|
38
38
|
* const job = yield* GCP.Job("Nightly", { ... });
|
|
39
39
|
* const runJob = yield* run.runProjectsLocationsJobs;
|
|
40
40
|
* yield* runJob({
|
|
@@ -234,7 +234,7 @@ const toAttributes = (
|
|
|
234
234
|
latestCreatedExecution: j.latestCreatedExecution,
|
|
235
235
|
reconciling: j.reconciling,
|
|
236
236
|
etag: j.etag,
|
|
237
|
-
labels:
|
|
237
|
+
labels: normalizeStringMap(j.labels) ?? {},
|
|
238
238
|
createTime: j.createTime,
|
|
239
239
|
updateTime: j.updateTime,
|
|
240
240
|
});
|
|
@@ -260,7 +260,7 @@ const jobMutated = (
|
|
|
260
260
|
desiredLabels: Record<string, string>,
|
|
261
261
|
): boolean => {
|
|
262
262
|
const labelDiff = diffTags(
|
|
263
|
-
|
|
263
|
+
normalizeStringMap(observed.labels) ?? {},
|
|
264
264
|
desiredLabels,
|
|
265
265
|
);
|
|
266
266
|
if (labelDiff.removed.length > 0 || labelDiff.upsert.length > 0) return true;
|
|
@@ -448,7 +448,7 @@ export const JobProvider = () =>
|
|
|
448
448
|
const observed = yield* observe(project, location, name);
|
|
449
449
|
if (!observed) return undefined;
|
|
450
450
|
const attrs = toAttributes(observed, { project, location, name });
|
|
451
|
-
return (yield* hasAlchemyLabels(id, observed.labels))
|
|
451
|
+
return (yield* hasAlchemyLabels(id, normalizeStringMap(observed.labels)))
|
|
452
452
|
? attrs
|
|
453
453
|
: Unowned(attrs);
|
|
454
454
|
}),
|
package/src/Run/Operations.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConfigError } from "@distilled.cloud/gcp";
|
|
2
|
-
import * as run from "@distilled.cloud/gcp/
|
|
2
|
+
import * as run from "@distilled.cloud/gcp/run_v2";
|
|
3
3
|
import type { ScopedPlanStatusSession } from "alchemy/Cli/Cli";
|
|
4
4
|
import * as Duration from "effect/Duration";
|
|
5
5
|
import * as Effect from "effect/Effect";
|
package/src/Run/Service.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as run from "@distilled.cloud/gcp/
|
|
1
|
+
import * as run from "@distilled.cloud/gcp/run_v2";
|
|
2
2
|
import { Resource } from "alchemy";
|
|
3
3
|
import { Unowned } from "alchemy/AdoptPolicy";
|
|
4
4
|
import type { ScopedPlanStatusSession } from "alchemy/Cli/Cli";
|
|
@@ -10,7 +10,7 @@ import * as Duration from "effect/Duration";
|
|
|
10
10
|
import * as Effect from "effect/Effect";
|
|
11
11
|
import * as Schedule from "effect/Schedule";
|
|
12
12
|
import type * as GCP from "../Providers.ts";
|
|
13
|
-
import { gcpInternalLabels, hasAlchemyLabels } from "../Tags.ts";
|
|
13
|
+
import { gcpInternalLabels, hasAlchemyLabels, normalizeStringMap } from "../Tags.ts";
|
|
14
14
|
import { makeSyncIam, type RunIamBindingContract } from "./IamSync.ts";
|
|
15
15
|
import { makeAwaitOperation } from "./Operations.ts";
|
|
16
16
|
import {
|
|
@@ -245,8 +245,10 @@ const toServiceBody = (
|
|
|
245
245
|
? { defaultUriDisabled: news.defaultUriDisabled }
|
|
246
246
|
: {}),
|
|
247
247
|
...(news.scaling ? { scaling: news.scaling } : {}),
|
|
248
|
-
...(news.traffic ? { traffic: news.traffic } : {}),
|
|
249
|
-
...(news.customAudiences
|
|
248
|
+
...(news.traffic ? { traffic: [...news.traffic] } : {}),
|
|
249
|
+
...(news.customAudiences
|
|
250
|
+
? { customAudiences: [...news.customAudiences] }
|
|
251
|
+
: {}),
|
|
250
252
|
...(news.binaryAuthorization
|
|
251
253
|
? { binaryAuthorization: news.binaryAuthorization }
|
|
252
254
|
: {}),
|
|
@@ -271,7 +273,7 @@ const toAttributes = (
|
|
|
271
273
|
terminalCondition: s.terminalCondition,
|
|
272
274
|
trafficStatuses: s.trafficStatuses ?? [],
|
|
273
275
|
etag: s.etag,
|
|
274
|
-
labels:
|
|
276
|
+
labels: normalizeStringMap(s.labels) ?? {},
|
|
275
277
|
createTime: s.createTime,
|
|
276
278
|
updateTime: s.updateTime,
|
|
277
279
|
});
|
|
@@ -314,7 +316,7 @@ export const ServiceProvider = () =>
|
|
|
314
316
|
updateMaskFields.push("description");
|
|
315
317
|
}
|
|
316
318
|
const labelDiff = diffTags(
|
|
317
|
-
|
|
319
|
+
normalizeStringMap(args.observed.labels) ?? {},
|
|
318
320
|
args.desiredLabels,
|
|
319
321
|
);
|
|
320
322
|
if (labelDiff.removed.length > 0 || labelDiff.upsert.length > 0) {
|
|
@@ -536,7 +538,7 @@ export const ServiceProvider = () =>
|
|
|
536
538
|
const observed = yield* observe(project, location, name);
|
|
537
539
|
if (!observed) return undefined;
|
|
538
540
|
const attrs = toAttributes(observed, { project, location, name });
|
|
539
|
-
return (yield* hasAlchemyLabels(id, observed.labels))
|
|
541
|
+
return (yield* hasAlchemyLabels(id, normalizeStringMap(observed.labels)))
|
|
540
542
|
? attrs
|
|
541
543
|
: Unowned(attrs);
|
|
542
544
|
}),
|
package/src/Run/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type * as run from "@distilled.cloud/gcp/
|
|
1
|
+
import type * as run from "@distilled.cloud/gcp/run_v2";
|
|
2
2
|
|
|
3
3
|
export { Job, JobProvider } from "./Job.ts";
|
|
4
4
|
export type { JobAttributes, JobProps } from "./Job.ts";
|
|
@@ -9,7 +9,7 @@ export type { ServiceAttributes, ServiceProps } from "./Service.ts";
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Clean aliases for the most commonly-used Cloud Run v2 SDK types,
|
|
12
|
-
* re-exported from `@distilled.cloud/gcp/
|
|
12
|
+
* re-exported from `@distilled.cloud/gcp/run_v2` so consumers don't
|
|
13
13
|
* need to import from the underlying SDK directly. The aliases are
|
|
14
14
|
* type-only — they share identity with the distilled types, so values
|
|
15
15
|
* are interchangeable.
|