@microagi/alchemy-gcp 0.11.3 → 0.11.5

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.
Files changed (81) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +13 -0
  3. package/lib/ArtifactRegistry/Operations.d.ts +2 -2
  4. package/lib/ArtifactRegistry/Operations.d.ts.map +1 -1
  5. package/lib/ArtifactRegistry/Repository.d.ts +1 -1
  6. package/lib/ArtifactRegistry/RepositoryIamMember.d.ts +40 -0
  7. package/lib/ArtifactRegistry/RepositoryIamMember.d.ts.map +1 -0
  8. package/lib/ArtifactRegistry/RepositoryIamMember.js +110 -0
  9. package/lib/ArtifactRegistry/RepositoryIamMember.js.map +1 -0
  10. package/lib/ArtifactRegistry/Validation.d.ts.map +1 -1
  11. package/lib/ArtifactRegistry/index.d.ts +2 -0
  12. package/lib/ArtifactRegistry/index.d.ts.map +1 -1
  13. package/lib/ArtifactRegistry/index.js +1 -0
  14. package/lib/ArtifactRegistry/index.js.map +1 -1
  15. package/lib/Auth/AuthProvider.d.ts.map +1 -1
  16. package/lib/Auth/AuthProvider.js.map +1 -1
  17. package/lib/Auth/Credentials.d.ts +1 -1
  18. package/lib/Auth/Credentials.d.ts.map +1 -1
  19. package/lib/CloudResourceManager/Project.d.ts +1 -1
  20. package/lib/CloudResourceManager/ProjectIamMember.d.ts.map +1 -1
  21. package/lib/Compute/Address.d.ts +1 -1
  22. package/lib/Compute/Address.d.ts.map +1 -1
  23. package/lib/Compute/Firewall.d.ts +1 -1
  24. package/lib/Compute/Firewall.d.ts.map +1 -1
  25. package/lib/Compute/ForwardingRule.d.ts +1 -1
  26. package/lib/Compute/ForwardingRule.d.ts.map +1 -1
  27. package/lib/Compute/GlobalAddress.d.ts +1 -1
  28. package/lib/Compute/GlobalAddress.d.ts.map +1 -1
  29. package/lib/Compute/Network.d.ts +1 -1
  30. package/lib/Compute/Network.d.ts.map +1 -1
  31. package/lib/Compute/Operations.d.ts +2 -7
  32. package/lib/Compute/Operations.d.ts.map +1 -1
  33. package/lib/Compute/ProjectDefaultNetworkTier.d.ts +1 -1
  34. package/lib/Compute/ProjectDefaultNetworkTier.d.ts.map +1 -1
  35. package/lib/Compute/SharedVpcHost.d.ts +1 -1
  36. package/lib/Compute/SharedVpcHost.d.ts.map +1 -1
  37. package/lib/Compute/SharedVpcServiceProject.d.ts +1 -1
  38. package/lib/Compute/SharedVpcServiceProject.d.ts.map +1 -1
  39. package/lib/Compute/Subnetwork.d.ts +1 -1
  40. package/lib/Compute/Subnetwork.d.ts.map +1 -1
  41. package/lib/Compute/SubnetworkIamMember.d.ts.map +1 -1
  42. package/lib/Container/Cluster.d.ts +1 -1
  43. package/lib/Container/Cluster.d.ts.map +1 -1
  44. package/lib/Container/NodePool.d.ts +14 -4
  45. package/lib/Container/NodePool.d.ts.map +1 -1
  46. package/lib/Container/NodePool.js +9 -6
  47. package/lib/Container/NodePool.js.map +1 -1
  48. package/lib/Container/Operations.d.ts +2 -2
  49. package/lib/Container/Operations.d.ts.map +1 -1
  50. package/lib/Iam/ServiceAccount.d.ts +1 -1
  51. package/lib/Iam/ServiceAccountIamMember.d.ts.map +1 -1
  52. package/lib/Kubernetes/Helm.d.ts +1 -1
  53. package/lib/Kubernetes/KubernetesManifest.d.ts +1 -1
  54. package/lib/Kubernetes/Secret.d.ts +1 -1
  55. package/lib/Kubernetes/client.d.ts.map +1 -1
  56. package/lib/Kubernetes/connection.d.ts.map +1 -1
  57. package/lib/ManagedLustre/Instance.d.ts +1 -1
  58. package/lib/Providers.d.ts +1 -1
  59. package/lib/Providers.d.ts.map +1 -1
  60. package/lib/Providers.js +3 -1
  61. package/lib/Providers.js.map +1 -1
  62. package/lib/Run/IamMember.d.ts.map +1 -1
  63. package/lib/Run/IamSync.d.ts.map +1 -1
  64. package/lib/Run/Job.d.ts +1 -1
  65. package/lib/Run/Operations.d.ts +2 -2
  66. package/lib/Run/Operations.d.ts.map +1 -1
  67. package/lib/Run/Service.d.ts +1 -1
  68. package/lib/Run/Validation.d.ts.map +1 -1
  69. package/lib/Sqladmin/Instance.d.ts +1 -1
  70. package/lib/Sqladmin/Operations.d.ts +2 -2
  71. package/lib/Sqladmin/Operations.d.ts.map +1 -1
  72. package/lib/Sqladmin/Types.d.ts.map +1 -1
  73. package/lib/Sqladmin/Validation.d.ts.map +1 -1
  74. package/lib/Storage/Bucket.d.ts +1 -1
  75. package/lib/Storage/BucketIamMember.d.ts.map +1 -1
  76. package/lib/Tags.d.ts.map +1 -1
  77. package/package.json +1 -1
  78. package/src/ArtifactRegistry/RepositoryIamMember.ts +205 -0
  79. package/src/ArtifactRegistry/index.ts +8 -0
  80. package/src/Container/NodePool.ts +29 -8
  81. package/src/Providers.ts +6 -0
@@ -0,0 +1,205 @@
1
+ import { ConfigError } from "@distilled.cloud/gcp";
2
+ import * as ar from "@distilled.cloud/gcp/artifactregistry-v1";
3
+ import { Resource } from "alchemy";
4
+ import { isResolved } from "alchemy/Diff";
5
+ import * as Provider from "alchemy/Provider";
6
+ import * as Duration from "effect/Duration";
7
+ import * as Effect from "effect/Effect";
8
+ import * as Schedule from "effect/Schedule";
9
+ import type * as GCP from "../Providers.ts";
10
+
11
+ /**
12
+ * A single unconditional `(role, member)` IAM grant on an Artifact Registry
13
+ * repository. The repository is referenced by its fully-qualified resource
14
+ * name and is not created or deleted by this resource.
15
+ *
16
+ * Reconciliation is an additive read-modify-write: foreign bindings,
17
+ * conditional bindings, members, policy version, and etag are preserved.
18
+ * Deletion revokes only this exact member and removes an emptied binding.
19
+ * The whole cycle retries boundedly on stale-etag conflicts so concurrent
20
+ * policy writers compose safely.
21
+ *
22
+ * @example Read access to one Docker repository
23
+ * ```typescript
24
+ * yield* GCP.ArtifactRegistryRepositoryIamMember("EvalsAppsReader", {
25
+ * repository: apps.fullyQualifiedName,
26
+ * role: "roles/artifactregistry.reader",
27
+ * member: "serviceAccount:evals@example-project.iam.gserviceaccount.com",
28
+ * });
29
+ * ```
30
+ */
31
+ export type ArtifactRegistryRepositoryIamMemberProps = {
32
+ /**
33
+ * Fully-qualified repository resource name:
34
+ * `projects/{project}/locations/{location}/repositories/{repository}`.
35
+ * Immutable — changing it replaces the grant.
36
+ */
37
+ repository: string;
38
+ /** IAM role, e.g. `roles/artifactregistry.reader`. Immutable. */
39
+ role: string;
40
+ /** IAM member string, e.g. `serviceAccount:name@project.iam.gserviceaccount.com`. Immutable. */
41
+ member: string;
42
+ };
43
+
44
+ export type ArtifactRegistryRepositoryIamMemberAttributes =
45
+ ArtifactRegistryRepositoryIamMemberProps;
46
+
47
+ export type ArtifactRegistryRepositoryIamMember = Resource<
48
+ "GCP.ArtifactRegistryRepositoryIamMember",
49
+ ArtifactRegistryRepositoryIamMemberProps,
50
+ ArtifactRegistryRepositoryIamMemberAttributes,
51
+ never,
52
+ GCP.Providers
53
+ >;
54
+
55
+ export const ArtifactRegistryRepositoryIamMember =
56
+ Resource<ArtifactRegistryRepositoryIamMember>(
57
+ "GCP.ArtifactRegistryRepositoryIamMember",
58
+ );
59
+
60
+ export const ArtifactRegistryRepositoryIamMemberProvider = () =>
61
+ Provider.effect(
62
+ ArtifactRegistryRepositoryIamMember,
63
+ Effect.gen(function* () {
64
+ const getIamPolicy = yield* ar.getIamPolicyProjectsLocationsRepositories;
65
+ const setIamPolicy = yield* ar.setIamPolicyProjectsLocationsRepositories;
66
+
67
+ const hasMember = (policy: ar.Policy, role: string, member: string) =>
68
+ (policy.bindings ?? []).some(
69
+ (binding) =>
70
+ binding.role === role &&
71
+ !binding.condition &&
72
+ (binding.members ?? []).includes(member),
73
+ );
74
+
75
+ const conflictRetry = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
76
+ effect.pipe(
77
+ Effect.retry({
78
+ while: (error) => (error as { _tag?: string })?._tag === "Conflict",
79
+ schedule: Schedule.exponential(Duration.millis(250)).pipe(
80
+ Schedule.both(Schedule.recurs(8)),
81
+ ),
82
+ }),
83
+ );
84
+
85
+ const readPolicy = (repository: string) =>
86
+ getIamPolicy({
87
+ resource: repository,
88
+ "options.requestedPolicyVersion": 3,
89
+ });
90
+
91
+ return {
92
+ stables: ["repository", "role", "member"],
93
+ diff: Effect.fn(function* ({ news, olds = {}, output }) {
94
+ if (!isResolved(news)) return undefined;
95
+ const priorRepository = output?.repository || olds.repository;
96
+ const priorRole = output?.role || olds.role;
97
+ const priorMember = output?.member || olds.member;
98
+ if (
99
+ (priorRepository && priorRepository !== news.repository) ||
100
+ (priorRole && priorRole !== news.role) ||
101
+ (priorMember && priorMember !== news.member)
102
+ ) {
103
+ return { action: "replace" } as const;
104
+ }
105
+ return undefined;
106
+ }),
107
+ reconcile: Effect.fn(function* ({ news }) {
108
+ yield* conflictRetry(
109
+ Effect.gen(function* () {
110
+ const current = yield* readPolicy(news.repository).pipe(
111
+ Effect.catchTag("NotFound", () =>
112
+ Effect.fail(
113
+ new ConfigError({
114
+ message: `Artifact Registry repository ${news.repository} does not exist — RepositoryIamMember grants on existing repositories only.`,
115
+ }),
116
+ ),
117
+ ),
118
+ );
119
+ if (hasMember(current, news.role, news.member)) return;
120
+
121
+ const bindings = (current.bindings ?? []).map((binding) => ({
122
+ ...binding,
123
+ members: [...(binding.members ?? [])],
124
+ }));
125
+ const existing = bindings.find(
126
+ (binding) => binding.role === news.role && !binding.condition,
127
+ );
128
+ if (existing) {
129
+ existing.members = [...(existing.members ?? []), news.member];
130
+ } else {
131
+ bindings.push({ role: news.role, members: [news.member] });
132
+ }
133
+
134
+ yield* setIamPolicy({
135
+ resource: news.repository,
136
+ body: {
137
+ policy: { ...current, bindings, version: 3 },
138
+ },
139
+ });
140
+ }),
141
+ );
142
+ return {
143
+ repository: news.repository,
144
+ role: news.role,
145
+ member: news.member,
146
+ };
147
+ }),
148
+ delete: Effect.fn(function* ({ output }) {
149
+ if (!output.repository || !output.role || !output.member) return;
150
+ yield* conflictRetry(
151
+ Effect.gen(function* () {
152
+ const current = yield* readPolicy(output.repository).pipe(
153
+ Effect.catchTag("NotFound", () =>
154
+ Effect.succeed(undefined as ar.Policy | undefined),
155
+ ),
156
+ );
157
+ if (!current || !hasMember(current, output.role, output.member)) {
158
+ return;
159
+ }
160
+
161
+ const bindings = (current.bindings ?? [])
162
+ .map((binding) =>
163
+ binding.role === output.role && !binding.condition
164
+ ? {
165
+ ...binding,
166
+ members: (binding.members ?? []).filter(
167
+ (member) => member !== output.member,
168
+ ),
169
+ }
170
+ : {
171
+ ...binding,
172
+ members: [...(binding.members ?? [])],
173
+ },
174
+ )
175
+ .filter((binding) => (binding.members?.length ?? 0) > 0);
176
+
177
+ yield* setIamPolicy({
178
+ resource: output.repository,
179
+ body: {
180
+ policy: { ...current, bindings, version: 3 },
181
+ },
182
+ }).pipe(Effect.catchTag("NotFound", () => Effect.void));
183
+ }),
184
+ );
185
+ }),
186
+ read: Effect.fn(function* ({ output, olds }) {
187
+ const repository = output?.repository ?? olds?.repository;
188
+ const role = output?.role ?? olds?.role;
189
+ const member = output?.member ?? olds?.member;
190
+ if (!repository || !role || !member) return undefined;
191
+
192
+ const current = yield* readPolicy(repository).pipe(
193
+ Effect.catchTag("NotFound", () =>
194
+ Effect.succeed(undefined as ar.Policy | undefined),
195
+ ),
196
+ Effect.catchTag("Forbidden", () =>
197
+ Effect.succeed(undefined as ar.Policy | undefined),
198
+ ),
199
+ );
200
+ if (!current || !hasMember(current, role, member)) return undefined;
201
+ return { repository, role, member };
202
+ }),
203
+ };
204
+ }),
205
+ );
@@ -7,4 +7,12 @@ export type {
7
7
  ArtifactRegistryRepositoryAttributes,
8
8
  ArtifactRegistryRepositoryProps,
9
9
  } from "./Repository.ts";
10
+ export {
11
+ ArtifactRegistryRepositoryIamMember,
12
+ ArtifactRegistryRepositoryIamMemberProvider,
13
+ } from "./RepositoryIamMember.ts";
14
+ export type {
15
+ ArtifactRegistryRepositoryIamMemberAttributes,
16
+ ArtifactRegistryRepositoryIamMemberProps,
17
+ } from "./RepositoryIamMember.ts";
10
18
  export type { ArtifactRegistryFormat, ArtifactRegistryMode } from "./Types.ts";
@@ -34,11 +34,19 @@ export type NodePoolProps = {
34
34
  * without a positive node count, so when omitted we default to
35
35
  * `autoscaling.minNodeCount ?? 1` on the create path.
36
36
  *
37
- * Subsequent size changes go via `setSize` when `autoscaling` is
38
- * disabled, or are GKE-managed when `autoscaling.enabled` is true
39
- * (the size sync is skipped to avoid fighting the autoscaler).
37
+ * Subsequent size changes go via `setSize` unless autoscaling or
38
+ * `externallyManagedSize` delegates steady-state size ownership.
40
39
  */
41
40
  initialNodeCount?: number;
41
+ /**
42
+ * Treat the pool's live size as externally owned after creation.
43
+ *
44
+ * `initialNodeCount` is still used to bootstrap a missing pool, but
45
+ * subsequent reconciles do not call GKE `setSize`. This is intended for
46
+ * imperative controllers that resize a pool while Alchemy continues to own
47
+ * its machine shape, labels, taints, management, and upgrade settings.
48
+ */
49
+ externallyManagedSize?: boolean;
42
50
  /**
43
51
  * Per-pool zones. Override the cluster default. Mutable via
44
52
  * `update` — sent in a standalone update body to avoid the SDK's
@@ -235,6 +243,19 @@ export type NodePool = Resource<
235
243
  >;
236
244
  export const NodePool = Resource<NodePool>("GCP.NodePool");
237
245
 
246
+ /** Whether a reconcile should bring a node pool back to its declared size. */
247
+ export const nodePoolSizeNeedsSync = (
248
+ observedCount: number | undefined,
249
+ news: Pick<
250
+ NodePoolProps,
251
+ "initialNodeCount" | "autoscaling" | "externallyManagedSize"
252
+ >,
253
+ ): boolean =>
254
+ news.externallyManagedSize !== true &&
255
+ news.autoscaling?.enabled !== true &&
256
+ news.initialNodeCount !== undefined &&
257
+ observedCount !== news.initialNodeCount;
258
+
238
259
  /**
239
260
  * Translate a public `accelerators` spec into the SDK's
240
261
  * `AcceleratorConfig`. Same shape on both create and update paths.
@@ -449,14 +470,14 @@ export const NodePoolProvider = () =>
449
470
  news: NodePoolProps;
450
471
  session: ScopedPlanStatusSession;
451
472
  }) {
452
- // Skip when the autoscaler is enabled; fighting it creates churn.
453
- if (args.news.autoscaling?.enabled) return;
454
- if (args.news.initialNodeCount === undefined) return;
455
473
  const observedCount = args.observed.initialNodeCount;
456
- if (observedCount === args.news.initialNodeCount) return;
474
+ // Skip when either GKE autoscaling or an external controller owns
475
+ // size; fighting either creates churn. Externally-managed pools still
476
+ // use initialNodeCount on the create path above.
477
+ if (!nodePoolSizeNeedsSync(observedCount, args.news)) return;
457
478
  const op = yield* setSize({
458
479
  name: args.fqName,
459
- body: { nodeCount: args.news.initialNodeCount },
480
+ body: { nodeCount: args.news.initialNodeCount! },
460
481
  });
461
482
  if (op.name) yield* awaitOperation(qualifyOp(args.fqName, op.name), args.session);
462
483
  });
package/src/Providers.ts CHANGED
@@ -4,6 +4,10 @@ import {
4
4
  ArtifactRegistryRepository,
5
5
  ArtifactRegistryRepositoryProvider,
6
6
  } from "./ArtifactRegistry/Repository.ts";
7
+ import {
8
+ ArtifactRegistryRepositoryIamMember,
9
+ ArtifactRegistryRepositoryIamMemberProvider,
10
+ } from "./ArtifactRegistry/RepositoryIamMember.ts";
7
11
  import { GCPAuth } from "./Auth/AuthProvider.ts";
8
12
  import { fromAuthProvider } from "./Auth/Credentials.ts";
9
13
  import { Project, ProjectProvider } from "./CloudResourceManager/Project.ts";
@@ -116,6 +120,7 @@ export const providers = () =>
116
120
  SqlDatabase,
117
121
  SqlUser,
118
122
  ArtifactRegistryRepository,
123
+ ArtifactRegistryRepositoryIamMember,
119
124
  ServiceAccount,
120
125
  ServiceAccountKey,
121
126
  KubernetesSecret,
@@ -148,6 +153,7 @@ export const providers = () =>
148
153
  SqlDatabaseProvider(),
149
154
  SqlUserProvider(),
150
155
  ArtifactRegistryRepositoryProvider(),
156
+ ArtifactRegistryRepositoryIamMemberProvider(),
151
157
  ServiceAccountProvider(),
152
158
  ServiceAccountKeyProvider(),
153
159
  KubernetesSecretProvider(),