@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.
- package/CHANGELOG.md +18 -0
- package/README.md +13 -0
- package/lib/ArtifactRegistry/Operations.d.ts +2 -2
- package/lib/ArtifactRegistry/Operations.d.ts.map +1 -1
- package/lib/ArtifactRegistry/Repository.d.ts +1 -1
- package/lib/ArtifactRegistry/RepositoryIamMember.d.ts +40 -0
- package/lib/ArtifactRegistry/RepositoryIamMember.d.ts.map +1 -0
- package/lib/ArtifactRegistry/RepositoryIamMember.js +110 -0
- package/lib/ArtifactRegistry/RepositoryIamMember.js.map +1 -0
- package/lib/ArtifactRegistry/Validation.d.ts.map +1 -1
- package/lib/ArtifactRegistry/index.d.ts +2 -0
- package/lib/ArtifactRegistry/index.d.ts.map +1 -1
- package/lib/ArtifactRegistry/index.js +1 -0
- package/lib/ArtifactRegistry/index.js.map +1 -1
- package/lib/Auth/AuthProvider.d.ts.map +1 -1
- package/lib/Auth/AuthProvider.js.map +1 -1
- package/lib/Auth/Credentials.d.ts +1 -1
- package/lib/Auth/Credentials.d.ts.map +1 -1
- package/lib/CloudResourceManager/Project.d.ts +1 -1
- package/lib/CloudResourceManager/ProjectIamMember.d.ts.map +1 -1
- package/lib/Compute/Address.d.ts +1 -1
- package/lib/Compute/Address.d.ts.map +1 -1
- package/lib/Compute/Firewall.d.ts +1 -1
- package/lib/Compute/Firewall.d.ts.map +1 -1
- package/lib/Compute/ForwardingRule.d.ts +1 -1
- package/lib/Compute/ForwardingRule.d.ts.map +1 -1
- package/lib/Compute/GlobalAddress.d.ts +1 -1
- package/lib/Compute/GlobalAddress.d.ts.map +1 -1
- package/lib/Compute/Network.d.ts +1 -1
- package/lib/Compute/Network.d.ts.map +1 -1
- package/lib/Compute/Operations.d.ts +2 -7
- package/lib/Compute/Operations.d.ts.map +1 -1
- package/lib/Compute/ProjectDefaultNetworkTier.d.ts +1 -1
- package/lib/Compute/ProjectDefaultNetworkTier.d.ts.map +1 -1
- package/lib/Compute/SharedVpcHost.d.ts +1 -1
- package/lib/Compute/SharedVpcHost.d.ts.map +1 -1
- package/lib/Compute/SharedVpcServiceProject.d.ts +1 -1
- package/lib/Compute/SharedVpcServiceProject.d.ts.map +1 -1
- package/lib/Compute/Subnetwork.d.ts +1 -1
- package/lib/Compute/Subnetwork.d.ts.map +1 -1
- package/lib/Compute/SubnetworkIamMember.d.ts.map +1 -1
- package/lib/Container/Cluster.d.ts +1 -1
- package/lib/Container/Cluster.d.ts.map +1 -1
- package/lib/Container/NodePool.d.ts +14 -4
- package/lib/Container/NodePool.d.ts.map +1 -1
- package/lib/Container/NodePool.js +9 -6
- package/lib/Container/NodePool.js.map +1 -1
- package/lib/Container/Operations.d.ts +2 -2
- package/lib/Container/Operations.d.ts.map +1 -1
- package/lib/Iam/ServiceAccount.d.ts +1 -1
- package/lib/Iam/ServiceAccountIamMember.d.ts.map +1 -1
- package/lib/Kubernetes/Helm.d.ts +1 -1
- package/lib/Kubernetes/KubernetesManifest.d.ts +1 -1
- package/lib/Kubernetes/Secret.d.ts +1 -1
- package/lib/Kubernetes/client.d.ts.map +1 -1
- package/lib/Kubernetes/connection.d.ts.map +1 -1
- package/lib/ManagedLustre/Instance.d.ts +1 -1
- package/lib/Providers.d.ts +1 -1
- package/lib/Providers.d.ts.map +1 -1
- package/lib/Providers.js +3 -1
- package/lib/Providers.js.map +1 -1
- package/lib/Run/IamMember.d.ts.map +1 -1
- package/lib/Run/IamSync.d.ts.map +1 -1
- package/lib/Run/Job.d.ts +1 -1
- package/lib/Run/Operations.d.ts +2 -2
- package/lib/Run/Operations.d.ts.map +1 -1
- package/lib/Run/Service.d.ts +1 -1
- package/lib/Run/Validation.d.ts.map +1 -1
- package/lib/Sqladmin/Instance.d.ts +1 -1
- package/lib/Sqladmin/Operations.d.ts +2 -2
- package/lib/Sqladmin/Operations.d.ts.map +1 -1
- package/lib/Sqladmin/Types.d.ts.map +1 -1
- package/lib/Sqladmin/Validation.d.ts.map +1 -1
- package/lib/Storage/Bucket.d.ts +1 -1
- package/lib/Storage/BucketIamMember.d.ts.map +1 -1
- package/lib/Tags.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ArtifactRegistry/RepositoryIamMember.ts +205 -0
- package/src/ArtifactRegistry/index.ts +8 -0
- package/src/Container/NodePool.ts +29 -8
- 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`
|
|
38
|
-
*
|
|
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
|
-
|
|
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(),
|