@lanes-sh/link 0.1.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/LICENSE +202 -0
- package/README.md +144 -0
- package/bin/lanes +42 -0
- package/instructions/agents/lanes-link-scout.md +73 -0
- package/instructions/skills/lanes-link/SKILL.md +187 -0
- package/package.json +95 -0
- package/src/audit/chain.ts +278 -0
- package/src/audit/conformance.ts +374 -0
- package/src/audit/fanout.ts +97 -0
- package/src/audit/index.ts +218 -0
- package/src/audit/stdout.ts +60 -0
- package/src/auth/index.ts +220 -0
- package/src/auth/oauth/metadata.ts +75 -0
- package/src/auth/oauth/server.ts +385 -0
- package/src/auth/oauth/store.ts +215 -0
- package/src/auth/oidc.ts +206 -0
- package/src/auth/remote.ts +72 -0
- package/src/cli/argv.ts +94 -0
- package/src/cli/callback-page.ts +256 -0
- package/src/cli/commands/connect/accounts.ts +94 -0
- package/src/cli/commands/connect/authorise.ts +298 -0
- package/src/cli/commands/connect/client.ts +284 -0
- package/src/cli/commands/connect/index.ts +398 -0
- package/src/cli/commands/connect/outcome.ts +119 -0
- package/src/cli/commands/connect/requirements.ts +103 -0
- package/src/cli/commands/connect/scopes-gate.ts +146 -0
- package/src/cli/commands/connect/settle.ts +136 -0
- package/src/cli/commands/connect/setup.ts +276 -0
- package/src/cli/commands/mcp/assets.ts +189 -0
- package/src/cli/commands/mcp/harnesses.ts +143 -0
- package/src/cli/commands/mcp/list.ts +70 -0
- package/src/cli/commands/mcp/register.ts +199 -0
- package/src/cli/commands/mcp/stdio.ts +57 -0
- package/src/cli/commands/mcp.ts +22 -0
- package/src/cli/commands/operate/attach.ts +121 -0
- package/src/cli/commands/operate/audit.ts +119 -0
- package/src/cli/commands/operate/inspect.ts +355 -0
- package/src/cli/commands/operate/outputs.ts +206 -0
- package/src/cli/commands/operate/policy.ts +80 -0
- package/src/cli/commands/operate/serve.ts +55 -0
- package/src/cli/commands/operate/status.ts +133 -0
- package/src/cli/commands/operate/token.ts +70 -0
- package/src/cli/commands/operate.ts +27 -0
- package/src/cli/commands/owner/memory.ts +110 -0
- package/src/cli/commands/owner/shared.ts +125 -0
- package/src/cli/commands/owner/skills.ts +92 -0
- package/src/cli/commands/owner/vault.ts +140 -0
- package/src/cli/commands/owner.ts +46 -0
- package/src/cli/commands/profile/removal.ts +278 -0
- package/src/cli/commands/profile/remove.ts +315 -0
- package/src/cli/commands/profile.ts +156 -0
- package/src/cli/commands/secrets.ts +176 -0
- package/src/cli/commands/setup.ts +150 -0
- package/src/cli/commands/target.ts +310 -0
- package/src/cli/config-edit.ts +397 -0
- package/src/cli/endpoint-url.ts +77 -0
- package/src/cli/identity.ts +109 -0
- package/src/cli/lanes.ts +78 -0
- package/src/cli/main.ts +333 -0
- package/src/cli/oauth-error.ts +13 -0
- package/src/cli/oauth-exchange.ts +146 -0
- package/src/cli/oauth.ts +354 -0
- package/src/cli/output.ts +184 -0
- package/src/cli/prompt.ts +180 -0
- package/src/cli/publish.ts +185 -0
- package/src/cli/runtime/discovery.ts +123 -0
- package/src/cli/runtime/open.ts +342 -0
- package/src/cli/runtime/registry.ts +185 -0
- package/src/cli/runtime/select.ts +124 -0
- package/src/cli/runtime.ts +34 -0
- package/src/cli/scopes.ts +63 -0
- package/src/cli/usage.ts +111 -0
- package/src/cli/version.ts +25 -0
- package/src/connectivity/auth/README.md +36 -0
- package/src/connectivity/auth/api-key/index.ts +43 -0
- package/src/connectivity/auth/authorize.ts +49 -0
- package/src/connectivity/auth/basic/index.ts +68 -0
- package/src/connectivity/auth/bearer/index.ts +13 -0
- package/src/connectivity/auth/credential.ts +19 -0
- package/src/connectivity/auth/header/index.ts +18 -0
- package/src/connectivity/auth/index.ts +35 -0
- package/src/connectivity/auth/none/index.ts +12 -0
- package/src/connectivity/auth/oauth-authcode/broker.ts +261 -0
- package/src/connectivity/auth/oauth-authcode/index.ts +64 -0
- package/src/connectivity/auth/oauth-authcode/provider.ts +279 -0
- package/src/connectivity/auth/oauth-authcode/refresh.ts +118 -0
- package/src/connectivity/auth/resolve.ts +61 -0
- package/src/connectivity/auth/strategy/index.ts +14 -0
- package/src/connectivity/capability.ts +164 -0
- package/src/connectivity/connector.ts +175 -0
- package/src/connectivity/context.ts +77 -0
- package/src/connectivity/index.ts +94 -0
- package/src/connectivity/mail/attachments.ts +368 -0
- package/src/connectivity/mail/compose.ts +73 -0
- package/src/connectivity/mail/index.ts +40 -0
- package/src/connectivity/mail/message.ts +82 -0
- package/src/connectivity/mail/nodemailer.d.ts +83 -0
- package/src/connectivity/mail/staging.ts +143 -0
- package/src/connectivity/mail/url.ts +301 -0
- package/src/connectivity/manifest/auth.ts +135 -0
- package/src/connectivity/manifest/bundles.ts +23 -0
- package/src/connectivity/manifest/connector.ts +168 -0
- package/src/connectivity/manifest/credential-ref.ts +73 -0
- package/src/connectivity/manifest/identity.ts +43 -0
- package/src/connectivity/manifest/index.ts +46 -0
- package/src/connectivity/manifest/primitives.ts +20 -0
- package/src/connectivity/manifest/provider.ts +208 -0
- package/src/connectivity/manifest/requirements.ts +146 -0
- package/src/connectivity/manifest/setup.ts +55 -0
- package/src/connectivity/provider.ts +163 -0
- package/src/connectivity/transports/README.md +33 -0
- package/src/connectivity/transports/composite/index.ts +68 -0
- package/src/connectivity/transports/dav/calendar.ts +217 -0
- package/src/connectivity/transports/dav/capabilities.ts +151 -0
- package/src/connectivity/transports/dav/client.ts +226 -0
- package/src/connectivity/transports/dav/contacts.ts +75 -0
- package/src/connectivity/transports/dav/ical.ts +412 -0
- package/src/connectivity/transports/dav/index.ts +143 -0
- package/src/connectivity/transports/dav/operations.ts +43 -0
- package/src/connectivity/transports/dav/request.ts +161 -0
- package/src/connectivity/transports/dav/xml.ts +123 -0
- package/src/connectivity/transports/factory.ts +181 -0
- package/src/connectivity/transports/fs/capabilities.ts +88 -0
- package/src/connectivity/transports/fs/commands.ts +258 -0
- package/src/connectivity/transports/fs/index.ts +121 -0
- package/src/connectivity/transports/fs/operations.ts +45 -0
- package/src/connectivity/transports/fs/paths.ts +120 -0
- package/src/connectivity/transports/fs/result.ts +12 -0
- package/src/connectivity/transports/http/index.ts +255 -0
- package/src/connectivity/transports/imap/attachment.ts +166 -0
- package/src/connectivity/transports/imap/capabilities.ts +158 -0
- package/src/connectivity/transports/imap/client.ts +398 -0
- package/src/connectivity/transports/imap/commands.ts +385 -0
- package/src/connectivity/transports/imap/index.ts +152 -0
- package/src/connectivity/transports/imap/operations.ts +64 -0
- package/src/connectivity/transports/imap/parse.ts +130 -0
- package/src/connectivity/transports/imap/parser.ts +272 -0
- package/src/connectivity/transports/imap/result.ts +15 -0
- package/src/connectivity/transports/imap/send.ts +92 -0
- package/src/connectivity/transports/imap/socket.ts +111 -0
- package/src/connectivity/transports/imap/utf7.ts +136 -0
- package/src/connectivity/transports/index.ts +20 -0
- package/src/connectivity/transports/local/index.ts +173 -0
- package/src/connectivity/transports/mcp/index.ts +215 -0
- package/src/deployments/README.md +63 -0
- package/src/deployments/adapters/audit-blob.ts +203 -0
- package/src/deployments/adapters/filesystem.ts +184 -0
- package/src/deployments/adapters/gcp-secret-manager.ts +492 -0
- package/src/deployments/adapters/gcs.ts +191 -0
- package/src/deployments/adapters/otlp.ts +128 -0
- package/src/deployments/adapters/s3.ts +195 -0
- package/src/deployments/azure/README.md +21 -0
- package/src/deployments/bootstrap.ts +177 -0
- package/src/deployments/deploy.ts +290 -0
- package/src/deployments/driver.ts +157 -0
- package/src/deployments/drivers.ts +35 -0
- package/src/deployments/gcp/Dockerfile +70 -0
- package/src/deployments/gcp/cloudbuild.yaml +31 -0
- package/src/deployments/gcp/driver.ts +175 -0
- package/src/deployments/gcp/gcloud.ts +178 -0
- package/src/deployments/gcp/provision.ts +290 -0
- package/src/deployments/gcp/survey.ts +319 -0
- package/src/deployments/local/README.md +12 -0
- package/src/deployments/prepare.ts +257 -0
- package/src/deployments/steps.ts +137 -0
- package/src/deployments/target.ts +295 -0
- package/src/deployments/upload.ts +207 -0
- package/src/dispatch/context.ts +195 -0
- package/src/dispatch/dispatch.ts +350 -0
- package/src/dispatch/index.ts +32 -0
- package/src/dispatch/staging.ts +102 -0
- package/src/policy/index.ts +179 -0
- package/src/policy/limits.ts +77 -0
- package/src/profile/authorization.ts +81 -0
- package/src/profile/files.ts +71 -0
- package/src/profile/index.ts +76 -0
- package/src/profile/layout.ts +123 -0
- package/src/profile/load.ts +199 -0
- package/src/profile/primitives.ts +45 -0
- package/src/profile/schema.ts +347 -0
- package/src/profile/secret-detection.ts +162 -0
- package/src/profile/targets.ts +152 -0
- package/src/profile/workspace.ts +262 -0
- package/src/providers/custom/index.ts +21 -0
- package/src/providers/custom/load.ts +115 -0
- package/src/providers/custom/template.ts +156 -0
- package/src/providers/example/provider.ts +207 -0
- package/src/providers/google/calendar/index.ts +66 -0
- package/src/providers/google/calendar/redact.ts +40 -0
- package/src/providers/google/contacts/index.ts +50 -0
- package/src/providers/google/contacts/redact.ts +21 -0
- package/src/providers/google/docs/index.ts +45 -0
- package/src/providers/google/drive/hints.ts +28 -0
- package/src/providers/google/drive/index.ts +34 -0
- package/src/providers/google/drive/redact.ts +39 -0
- package/src/providers/google/drive-mcp/index.ts +21 -0
- package/src/providers/google/gmail/api.ts +42 -0
- package/src/providers/google/gmail/attachment.ts +142 -0
- package/src/providers/google/gmail/hints.ts +55 -0
- package/src/providers/google/gmail/index.ts +112 -0
- package/src/providers/google/gmail/redact.ts +56 -0
- package/src/providers/google/gmail/send.ts +365 -0
- package/src/providers/google/gmail-mcp/index.ts +35 -0
- package/src/providers/google/index.ts +10 -0
- package/src/providers/google/shared/oauth.ts +122 -0
- package/src/providers/google/shared/scopes.ts +99 -0
- package/src/providers/google/shared/setup.ts +80 -0
- package/src/providers/google/sheets/hints.ts +45 -0
- package/src/providers/google/sheets/index.ts +70 -0
- package/src/providers/google/sheets/redact.ts +45 -0
- package/src/providers/google/specs/calendar.v3.json +1829 -0
- package/src/providers/google/specs/docs.v1.json +381 -0
- package/src/providers/google/specs/drive.v3.json +2208 -0
- package/src/providers/google/specs/gmail.v1.json +2578 -0
- package/src/providers/google/specs/people.v1.json +506 -0
- package/src/providers/google/specs/sheets.v4.json +1269 -0
- package/src/providers/google/specs/tasks.v1.json +840 -0
- package/src/providers/google/specs/vendor.ts +661 -0
- package/src/providers/google/tasks/index.ts +53 -0
- package/src/providers/google/tasks/redact.ts +34 -0
- package/src/providers/harness.ts +95 -0
- package/src/providers/icloud/calendar/index.ts +27 -0
- package/src/providers/icloud/contacts/index.ts +17 -0
- package/src/providers/icloud/drive/index.ts +47 -0
- package/src/providers/icloud/index.ts +8 -0
- package/src/providers/icloud/mail/index.ts +37 -0
- package/src/providers/icloud/shared/setup.ts +66 -0
- package/src/providers/index.ts +93 -0
- package/src/providers/linear/index.ts +11 -0
- package/src/providers/linear/scopes.ts +7 -0
- package/src/providers/memory/provider.ts +429 -0
- package/src/providers/notion/index.ts +19 -0
- package/src/providers/owner.ts +49 -0
- package/src/providers/scopes.ts +26 -0
- package/src/providers/setup/plan.ts +141 -0
- package/src/providers/setup/provider.ts +323 -0
- package/src/providers/shared/frontmatter.ts +119 -0
- package/src/providers/skills/provider.ts +283 -0
- package/src/providers/skills/store.ts +252 -0
- package/src/providers/vault/provider.ts +194 -0
- package/src/registry/index.ts +36 -0
- package/src/registry/policy-bridge.ts +32 -0
- package/src/registry/reconcile.ts +313 -0
- package/src/registry/registry.ts +240 -0
- package/src/secrets/document.ts +293 -0
- package/src/secrets/index.ts +154 -0
- package/src/secrets/system.ts +173 -0
- package/src/secrets/vault.ts +336 -0
- package/src/server/attachments.ts +197 -0
- package/src/server/container.ts +96 -0
- package/src/server/edge.ts +53 -0
- package/src/server/endpoint.ts +352 -0
- package/src/server/generations.ts +362 -0
- package/src/server/harness.ts +400 -0
- package/src/server/index.ts +331 -0
- package/src/server/logging.ts +41 -0
- package/src/server/mcp/build.ts +68 -0
- package/src/server/mcp/icon.ts +145 -0
- package/src/server/mcp/index.ts +32 -0
- package/src/server/mcp/instructions.ts +245 -0
- package/src/server/mcp/naming.ts +39 -0
- package/src/server/mcp/prompts.ts +78 -0
- package/src/server/mcp/resources.ts +106 -0
- package/src/server/mcp/routing.ts +117 -0
- package/src/server/mcp/schema.ts +78 -0
- package/src/server/mcp/tools.ts +186 -0
- package/src/server/mcp/visibility.ts +132 -0
- package/src/server/oauth.ts +222 -0
- package/src/server/rebinding.ts +53 -0
- package/src/server/stdio.ts +192 -0
- package/src/stores/blobs/conformance.ts +223 -0
- package/src/stores/blobs/index.ts +125 -0
- package/src/stores/blobs/testing.ts +49 -0
- package/src/stores/state/index.ts +247 -0
- package/src/stores/state/keys.ts +68 -0
- package/src/stores/state/testing.ts +41 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CommandResult,
|
|
3
|
+
DeployDriver,
|
|
4
|
+
DeployStep,
|
|
5
|
+
PlanInput,
|
|
6
|
+
ProvisionInput,
|
|
7
|
+
SurveyInput,
|
|
8
|
+
SurveyResult,
|
|
9
|
+
} from '../driver.ts';
|
|
10
|
+
import type { DeployConfig } from '#profile';
|
|
11
|
+
import { encodeRef } from '../adapters/gcp-secret-manager.ts';
|
|
12
|
+
import {
|
|
13
|
+
captureGcloud,
|
|
14
|
+
gcloudPath,
|
|
15
|
+
requireProject,
|
|
16
|
+
runGcloud,
|
|
17
|
+
serviceUrl,
|
|
18
|
+
type CloudRunTarget,
|
|
19
|
+
} from './gcloud.ts';
|
|
20
|
+
import { provisionSteps } from './provision.ts';
|
|
21
|
+
import { surveyCloudRun } from './survey.ts';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Cloud Run.
|
|
25
|
+
*
|
|
26
|
+
* Everything Google-specific about deploying lives behind this object: the argv
|
|
27
|
+
* `gcloud` wants, the shape of an Artifact Registry reference, and the fact that
|
|
28
|
+
* a Cloud Run URL has to be asked for rather than constructed. The generic
|
|
29
|
+
* command in `../deploy.ts` knows none of it.
|
|
30
|
+
*
|
|
31
|
+
* Config travels one way — from the CLI into an image — and a deployed instance
|
|
32
|
+
* never mutates its own configuration and exposes no admin API. That is ADR-007
|
|
33
|
+
* extended to the deployment, and it is why this builds an image rather than
|
|
34
|
+
* pushing config to a running service.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** Where the image lives. Artifact Registry, one repository per project. */
|
|
38
|
+
export function imageReference(cloudrun: CloudRunTarget, tag: string): string {
|
|
39
|
+
return `${cloudrun.region}-docker.pkg.dev/${cloudrun.project}/lanes-link/${cloudrun.service}:${tag}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The commands a deploy runs, as data.
|
|
44
|
+
*
|
|
45
|
+
* Separated from running them so `--dry-run` shows exactly what will happen and
|
|
46
|
+
* so the argv construction is testable without a Google Cloud project. Every
|
|
47
|
+
* value is a separate argv element; nothing is interpolated into a shell.
|
|
48
|
+
*/
|
|
49
|
+
export function deployPlan(input: PlanInput): DeployStep[] {
|
|
50
|
+
const cloudrun = requireProject(input.deploy, input.target);
|
|
51
|
+
const image = imageReference(cloudrun, input.tag);
|
|
52
|
+
const scope = ['--project', cloudrun.project, '--region', cloudrun.region];
|
|
53
|
+
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
title: 'ensure the Artifact Registry repository exists',
|
|
57
|
+
argv: [
|
|
58
|
+
'artifacts',
|
|
59
|
+
'repositories',
|
|
60
|
+
'create',
|
|
61
|
+
'lanes-link',
|
|
62
|
+
'--repository-format',
|
|
63
|
+
'docker',
|
|
64
|
+
'--location',
|
|
65
|
+
cloudrun.region,
|
|
66
|
+
'--project',
|
|
67
|
+
cloudrun.project,
|
|
68
|
+
'--description',
|
|
69
|
+
'Lanes Link images',
|
|
70
|
+
],
|
|
71
|
+
// Second and subsequent deploys: already there, and that is the success
|
|
72
|
+
// case rather than a failure worth stopping for.
|
|
73
|
+
tolerateFailure: true,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
title: 'build and push the image',
|
|
77
|
+
argv: [
|
|
78
|
+
'builds',
|
|
79
|
+
'submit',
|
|
80
|
+
'--project',
|
|
81
|
+
cloudrun.project,
|
|
82
|
+
'--config',
|
|
83
|
+
'src/deployments/gcp/cloudbuild.yaml',
|
|
84
|
+
'--substitutions',
|
|
85
|
+
`_IMAGE=${image}`,
|
|
86
|
+
'.',
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
title: 'roll a revision',
|
|
91
|
+
argv: [
|
|
92
|
+
'run',
|
|
93
|
+
'deploy',
|
|
94
|
+
cloudrun.service,
|
|
95
|
+
...scope,
|
|
96
|
+
'--image',
|
|
97
|
+
image,
|
|
98
|
+
'--platform',
|
|
99
|
+
'managed',
|
|
100
|
+
'--port',
|
|
101
|
+
'8080',
|
|
102
|
+
// The container reads these; everything else it needs is baked in.
|
|
103
|
+
'--set-env-vars',
|
|
104
|
+
[
|
|
105
|
+
`LANES_LINK_TARGET=${input.target}`,
|
|
106
|
+
...(input.profile ? [`LANES_LINK_PROFILE=${input.profile}`] : []),
|
|
107
|
+
...(input.workspace ? [`LANES_LINK_HOME=${input.workspace}`] : []),
|
|
108
|
+
].join(','),
|
|
109
|
+
...secretMounts(input.secretEnv),
|
|
110
|
+
...(cloudrun.service_account ? ['--service-account', cloudrun.service_account] : []),
|
|
111
|
+
// `iam` is Cloud Run's own identity check, which admits only a caller
|
|
112
|
+
// holding a Google-signed identity token for this service. No agent
|
|
113
|
+
// harness can mint one — so a target reached by a remote MCP client
|
|
114
|
+
// declares `public` and gates the request in the application instead.
|
|
115
|
+
cloudrun.access === 'iam' ? '--no-allow-unauthenticated' : '--allow-unauthenticated',
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Environment the revision reads out of Secret Manager, as `--set-secrets`.
|
|
123
|
+
*
|
|
124
|
+
* The value never travels: Cloud Run is told a secret id and resolves it at
|
|
125
|
+
* instance start, as the runtime service account, so the key stays out of the
|
|
126
|
+
* argv this prints, out of the revision's stored description, and out of
|
|
127
|
+
* anything `gcloud run services describe` returns.
|
|
128
|
+
*
|
|
129
|
+
* The `/` → `__` encoding is the credential adapter's, applied here rather than
|
|
130
|
+
* duplicated: `vault/key` is the secret `vault__key`, and a driver that spelled
|
|
131
|
+
* that itself would drift from the store that writes it.
|
|
132
|
+
*/
|
|
133
|
+
function secretMounts(secretEnv: PlanInput['secretEnv']): string[] {
|
|
134
|
+
const entries = Object.entries(secretEnv ?? {});
|
|
135
|
+
if (entries.length === 0) return [];
|
|
136
|
+
|
|
137
|
+
return [
|
|
138
|
+
'--set-secrets',
|
|
139
|
+
entries.map(([variable, ref]) => `${variable}=${encodeRef(ref)}:latest`).join(','),
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const cloudRunDriver: DeployDriver = {
|
|
144
|
+
platform: 'cloudrun',
|
|
145
|
+
tool: 'gcloud',
|
|
146
|
+
|
|
147
|
+
preflight() {
|
|
148
|
+
// A property of *this* deployment, not of deployments generally — which is
|
|
149
|
+
// why it is asked here rather than by the generic command.
|
|
150
|
+
return gcloudPath()
|
|
151
|
+
? null
|
|
152
|
+
: 'gcloud is not on your PATH. Install the Google Cloud CLI, run `gcloud auth login`, ' +
|
|
153
|
+
'and try again — or use --dry-run to see the commands and run them yourself.';
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
survey(input: SurveyInput): Promise<SurveyResult> {
|
|
157
|
+
return surveyCloudRun(input);
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
provision(input: ProvisionInput): Promise<DeployStep[]> {
|
|
161
|
+
return provisionSteps(input);
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
plan: deployPlan,
|
|
165
|
+
|
|
166
|
+
url(deploy: DeployConfig): Promise<string | null> {
|
|
167
|
+
// `outputs` calls this for any deployable target and must degrade rather
|
|
168
|
+
// than throw: gcloud may be absent and the service may not exist yet.
|
|
169
|
+
return deploy.project ? serviceUrl(deploy as CloudRunTarget) : Promise.resolve(null);
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
run(argv: readonly string[], options?: { quiet?: boolean }): Promise<CommandResult> {
|
|
173
|
+
return options?.quiet === true ? captureGcloud(argv) : runGcloud(argv);
|
|
174
|
+
},
|
|
175
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { ConfigError, type DeployConfig } from '#profile';
|
|
2
|
+
import type { CommandResult } from '../driver.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shelling out to `gcloud`.
|
|
6
|
+
*
|
|
7
|
+
* `lanes link deploy` is a thin wrapper, not a deployment engine (`docs/detailed/init.md`), and
|
|
8
|
+
* the thinnest correct wrapper drives the tool the operator already has
|
|
9
|
+
* authenticated. Reimplementing Cloud Build and the Cloud Run Admin API over
|
|
10
|
+
* REST would mean owning an OAuth flow, a long-running-operation poller, and a
|
|
11
|
+
* revision spec — for a command whose entire job is "build this, roll that".
|
|
12
|
+
*
|
|
13
|
+
* The commands are constructed as argv arrays and never as shell strings, so a
|
|
14
|
+
* project or service name cannot become an argument to something else.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export function gcloudPath(): string | null {
|
|
18
|
+
return Bun.which('gcloud');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Run a `gcloud` invocation, streaming its output to the terminal.
|
|
23
|
+
*
|
|
24
|
+
* Streamed rather than captured because a build takes minutes and silence for
|
|
25
|
+
* the duration is indistinguishable from a hang.
|
|
26
|
+
*
|
|
27
|
+
* stderr is streamed *and* kept. It used to be inherited outright, which meant
|
|
28
|
+
* a failed step reached the caller as a bare exit code — and "should this be
|
|
29
|
+
* retried" is a question about the message, not the code. A freshly enabled API
|
|
30
|
+
* and a genuinely missing permission both exit 1 and say different things.
|
|
31
|
+
*
|
|
32
|
+
* Only the tail is kept. A build streams its entire log here, and no amount of
|
|
33
|
+
* it beyond the end explains an exit code.
|
|
34
|
+
*/
|
|
35
|
+
const KEPT_STDERR = 8192;
|
|
36
|
+
|
|
37
|
+
export async function runGcloud(argv: readonly string[]): Promise<CommandResult> {
|
|
38
|
+
const gcloud = gcloudPath();
|
|
39
|
+
if (!gcloud) {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
stdout: '',
|
|
43
|
+
stderr:
|
|
44
|
+
'gcloud is not on your PATH. Install the Google Cloud CLI and run `gcloud auth login`.',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const child = Bun.spawn([gcloud, ...argv], { stdout: 'inherit', stderr: 'pipe' });
|
|
49
|
+
|
|
50
|
+
let captured = '';
|
|
51
|
+
const decoder = new TextDecoder();
|
|
52
|
+
const reader = (child.stderr as ReadableStream<Uint8Array>).getReader();
|
|
53
|
+
|
|
54
|
+
for (;;) {
|
|
55
|
+
const { done, value } = await reader.read();
|
|
56
|
+
if (done) break;
|
|
57
|
+
|
|
58
|
+
// Written through as it arrives, so a long build reads exactly as it did
|
|
59
|
+
// when this was inherited.
|
|
60
|
+
const text = decoder.decode(value, { stream: true });
|
|
61
|
+
process.stderr.write(text);
|
|
62
|
+
captured = (captured + text).slice(-KEPT_STDERR);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const code = await child.exited;
|
|
66
|
+
return { ok: code === 0, stdout: '', stderr: captured };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Run a `gcloud` invocation for its output, without printing it. */
|
|
70
|
+
export async function captureGcloud(argv: readonly string[]): Promise<CommandResult> {
|
|
71
|
+
const gcloud = gcloudPath();
|
|
72
|
+
if (!gcloud) {
|
|
73
|
+
return { ok: false, stdout: '', stderr: 'gcloud is not on your PATH.' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const child = Bun.spawn([gcloud, ...argv], { stdout: 'pipe', stderr: 'pipe' });
|
|
77
|
+
const [stdout, stderr, code] = await Promise.all([
|
|
78
|
+
new Response(child.stdout).text(),
|
|
79
|
+
new Response(child.stderr).text(),
|
|
80
|
+
child.exited,
|
|
81
|
+
]);
|
|
82
|
+
return { ok: code === 0, stdout: stdout.trim(), stderr: stderr.trim() };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A deploy block that has the field Cloud Run cannot do without.
|
|
87
|
+
*
|
|
88
|
+
* `project` is optional in the schema because it means nothing to a platform
|
|
89
|
+
* without projects, on the same reasoning as `credentials.project` — so the
|
|
90
|
+
* driver that needs it is the thing that refuses without it, and says which
|
|
91
|
+
* platform is asking.
|
|
92
|
+
*/
|
|
93
|
+
export interface CloudRunTarget extends DeployConfig {
|
|
94
|
+
readonly project: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function requireProject(deploy: DeployConfig, target: string): CloudRunTarget {
|
|
98
|
+
if (!deploy.project) {
|
|
99
|
+
throw new ConfigError(
|
|
100
|
+
`targets.${target}.deploy.project is required for the cloudrun platform — ` +
|
|
101
|
+
'a Cloud Run service is addressed by project, region, and name.',
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return { ...deploy, project: deploy.project };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The public URL Cloud Run assigned this service, or null if it has none yet.
|
|
109
|
+
*
|
|
110
|
+
* The URL is not derivable from the service name — Cloud Run mixes in a
|
|
111
|
+
* project-specific hash — so it has to be asked for rather than constructed.
|
|
112
|
+
*/
|
|
113
|
+
export async function serviceUrl(target: CloudRunTarget): Promise<string | null> {
|
|
114
|
+
const result = await captureGcloud([
|
|
115
|
+
'run',
|
|
116
|
+
'services',
|
|
117
|
+
'describe',
|
|
118
|
+
target.service,
|
|
119
|
+
'--project',
|
|
120
|
+
target.project,
|
|
121
|
+
'--region',
|
|
122
|
+
target.region,
|
|
123
|
+
'--format',
|
|
124
|
+
'value(status.url)',
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
return result.ok && result.stdout ? result.stdout : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The project `gcloud` is currently pointed at, if any — a survey default. */
|
|
131
|
+
export async function activeProject(): Promise<string | null> {
|
|
132
|
+
const result = await captureGcloud(['config', 'get-value', 'project']);
|
|
133
|
+
const value = result.stdout.trim();
|
|
134
|
+
return result.ok && value && value !== '(unset)' ? value : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Whether a project id is already somebody's.
|
|
139
|
+
*
|
|
140
|
+
* Asked so the survey knows whether it is naming an existing project or one it
|
|
141
|
+
* has to create — which decides whether it needs a billing account at all.
|
|
142
|
+
* A project id that exists but belongs to someone else answers the same as one
|
|
143
|
+
* of yours, which is the right answer here: either way this deploy will not be
|
|
144
|
+
* creating it.
|
|
145
|
+
*/
|
|
146
|
+
export async function projectExists(project: string): Promise<boolean> {
|
|
147
|
+
const result = await captureGcloud(['projects', 'describe', project, '--format', 'value(projectId)']);
|
|
148
|
+
return result.ok && result.stdout.trim() === project;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Billing accounts this login can attach a project to.
|
|
153
|
+
*
|
|
154
|
+
* Only open ones: a closed account lists, links, and then fails every API
|
|
155
|
+
* enable behind it with a message about the API rather than about billing.
|
|
156
|
+
*/
|
|
157
|
+
export async function openBillingAccounts(): Promise<{ id: string; name: string }[]> {
|
|
158
|
+
const result = await captureGcloud([
|
|
159
|
+
'billing',
|
|
160
|
+
'accounts',
|
|
161
|
+
'list',
|
|
162
|
+
'--filter',
|
|
163
|
+
'open=true',
|
|
164
|
+
'--format',
|
|
165
|
+
'value(name,displayName)',
|
|
166
|
+
]);
|
|
167
|
+
if (!result.ok) return [];
|
|
168
|
+
|
|
169
|
+
return result.stdout
|
|
170
|
+
.split('\n')
|
|
171
|
+
.map((line) => line.trim())
|
|
172
|
+
.filter(Boolean)
|
|
173
|
+
.map((line) => {
|
|
174
|
+
const [id = '', ...rest] = line.split(/\s+/);
|
|
175
|
+
return { id: id.replace(/^billingAccounts\//, ''), name: rest.join(' ') || id };
|
|
176
|
+
})
|
|
177
|
+
.filter((account) => account.id.length > 0);
|
|
178
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { VAULT_DOCUMENT_REF } from '#secrets';
|
|
2
|
+
import type { DeployStep, ProvisionInput } from '../driver.ts';
|
|
3
|
+
import { encodeRef } from '../adapters/gcp-secret-manager.ts';
|
|
4
|
+
import { requireProject } from './gcloud.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The project-level things a Cloud Run deploy needs to already exist.
|
|
8
|
+
*
|
|
9
|
+
* These used to be a numbered list in `docs/detailed/deployment-cloudrun.md` that the
|
|
10
|
+
* operator worked through by hand before their first deploy could get past its
|
|
11
|
+
* first step. Everything here is derivable from what the target already
|
|
12
|
+
* declares, so asking someone to transcribe it into a console was work with no
|
|
13
|
+
* decision in it.
|
|
14
|
+
*
|
|
15
|
+
* Returned as steps rather than run, like the rollout itself, so `--dry-run`
|
|
16
|
+
* shows the whole first-run sequence and so this is testable without a project.
|
|
17
|
+
* Every one tolerates failure: the second deploy finds all of them present, and
|
|
18
|
+
* `ALREADY_EXISTS` is the success case.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Enabled together, in one call.
|
|
23
|
+
*
|
|
24
|
+
* Cloud Build is not optional even though nothing here names it after the build
|
|
25
|
+
* step: `gcloud builds submit` fails with a permission error rather than a
|
|
26
|
+
* "not enabled" one, which reads as a broken account.
|
|
27
|
+
*
|
|
28
|
+
* The last two are for the steps *below this one*, and they were missing. Every
|
|
29
|
+
* step here tolerates failure, so `iam.service-accounts create` and
|
|
30
|
+
* `projects add-iam-policy-binding` against a project without them failed
|
|
31
|
+
* silently — the deploy carried on, rolled a revision with no service account
|
|
32
|
+
* and no binding, and the first symptom was the revision failing to read a
|
|
33
|
+
* secret several minutes later. An API a later step needs belongs in the call
|
|
34
|
+
* that enables APIs.
|
|
35
|
+
*/
|
|
36
|
+
const REQUIRED_SERVICES = [
|
|
37
|
+
'run.googleapis.com',
|
|
38
|
+
'cloudbuild.googleapis.com',
|
|
39
|
+
'artifactregistry.googleapis.com',
|
|
40
|
+
'secretmanager.googleapis.com',
|
|
41
|
+
'storage.googleapis.com',
|
|
42
|
+
'iam.googleapis.com',
|
|
43
|
+
'cloudresourcemanager.googleapis.com',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
export function provisionSteps(input: ProvisionInput): Promise<DeployStep[]> {
|
|
47
|
+
const cloudrun = requireProject(input.deploy, input.target);
|
|
48
|
+
const { project, region, service_account: serviceAccount } = cloudrun;
|
|
49
|
+
const steps: DeployStep[] = [];
|
|
50
|
+
|
|
51
|
+
// Before everything, when the survey found no project by this name.
|
|
52
|
+
//
|
|
53
|
+
// `billing_account` is only written for a project that did not exist, so its
|
|
54
|
+
// presence is the record of "this deploy owns creating it". Linking billing is
|
|
55
|
+
// not optional and not deferrable: a project without it enables no API, and
|
|
56
|
+
// every step below would fail describing the API rather than the billing.
|
|
57
|
+
if (cloudrun.billing_account) {
|
|
58
|
+
steps.push({
|
|
59
|
+
title: `create the project ${project}`,
|
|
60
|
+
argv: ['projects', 'create', project],
|
|
61
|
+
tolerateFailure: true,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
steps.push({
|
|
65
|
+
title: `attach it to billing account ${cloudrun.billing_account}`,
|
|
66
|
+
argv: [
|
|
67
|
+
'billing',
|
|
68
|
+
'projects',
|
|
69
|
+
'link',
|
|
70
|
+
project,
|
|
71
|
+
'--billing-account',
|
|
72
|
+
cloudrun.billing_account,
|
|
73
|
+
],
|
|
74
|
+
tolerateFailure: true,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
steps.push({
|
|
79
|
+
title: `enable the APIs this deploy uses (${REQUIRED_SERVICES.length})`,
|
|
80
|
+
argv: ['services', 'enable', ...REQUIRED_SERVICES, '--project', project],
|
|
81
|
+
tolerateFailure: true,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (serviceAccount) {
|
|
85
|
+
// The local part of the address is the account id gcloud wants; passing the
|
|
86
|
+
// whole address creates `foo@bar.iam...@project.iam...`, which then fails
|
|
87
|
+
// every binding below with a name that looks almost right.
|
|
88
|
+
const accountId = serviceAccount.split('@')[0] || `${cloudrun.service}-run`;
|
|
89
|
+
|
|
90
|
+
steps.push({
|
|
91
|
+
title: `create the runtime service account ${accountId}`,
|
|
92
|
+
argv: [
|
|
93
|
+
'iam',
|
|
94
|
+
'service-accounts',
|
|
95
|
+
'create',
|
|
96
|
+
accountId,
|
|
97
|
+
'--project',
|
|
98
|
+
project,
|
|
99
|
+
'--display-name',
|
|
100
|
+
`Lanes Link (${cloudrun.service})`,
|
|
101
|
+
],
|
|
102
|
+
tolerateFailure: true,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Read, named one secret at a time, for the same reason the write side is:
|
|
106
|
+
// a resource-level grant needs no condition to be scoped.
|
|
107
|
+
//
|
|
108
|
+
// This was a project-wide `secretAccessor`. The adapter argued for it and
|
|
109
|
+
// the argument was half right — the line worth defending really is
|
|
110
|
+
// `secrets.create`, which stays with the operator — but project-wide read
|
|
111
|
+
// is broader than a revision ever uses, and `askProject` is happy to point
|
|
112
|
+
// a deploy at a project that already holds other things. An SSRF in the
|
|
113
|
+
// endpoint should reach this profile's credentials, not everything sharing
|
|
114
|
+
// a project with it.
|
|
115
|
+
//
|
|
116
|
+
// Affordable because the serving path reads by explicit ref: `list()` is a
|
|
117
|
+
// CLI call, and `secretAccessor` never carried `secrets.list` anyway.
|
|
118
|
+
// `readableRefs` derives the set from config and manifests at deploy time.
|
|
119
|
+
for (const ref of input.readable ?? []) {
|
|
120
|
+
steps.push({
|
|
121
|
+
title: `let the revision read ${ref}`,
|
|
122
|
+
argv: [
|
|
123
|
+
'secrets',
|
|
124
|
+
'add-iam-policy-binding',
|
|
125
|
+
encodeRef(ref),
|
|
126
|
+
'--project',
|
|
127
|
+
project,
|
|
128
|
+
'--member',
|
|
129
|
+
`serviceAccount:${serviceAccount}`,
|
|
130
|
+
'--role',
|
|
131
|
+
'roles/secretmanager.secretAccessor',
|
|
132
|
+
// Bindings are printed as the whole policy otherwise, which is pages
|
|
133
|
+
// of YAML per deploy and buries everything after it.
|
|
134
|
+
'--condition',
|
|
135
|
+
'None',
|
|
136
|
+
],
|
|
137
|
+
tolerateFailure: true,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// What a revision rewrites in its own credential store, named one secret at a
|
|
143
|
+
// time. Two kinds, and they arrive from different places:
|
|
144
|
+
//
|
|
145
|
+
// - the vault document, because `vault.put` is a capability an agent may
|
|
146
|
+
// hold under policy (ADR-022);
|
|
147
|
+
// - each connection's OAuth token, because a refresh persists and serving a
|
|
148
|
+
// request is what triggers it (ADR-026).
|
|
149
|
+
//
|
|
150
|
+
// The second was missing, and the shape of the miss is worth keeping in mind:
|
|
151
|
+
// nothing here was wrong, it was incomplete, and being incomplete looked
|
|
152
|
+
// exactly like being finished. Reading mail 403'd an hour after every deploy.
|
|
153
|
+
//
|
|
154
|
+
// Two steps each, and the first is what keeps the second narrow: the secret is
|
|
155
|
+
// created here so the revision only ever needs to *add a version*, never
|
|
156
|
+
// `secrets.create`, which is a project-level permission that would let it mint
|
|
157
|
+
// credential refs of its own. The binding is on the one secret, so it needs no
|
|
158
|
+
// condition to be scoped — a resource-level grant already is.
|
|
159
|
+
const writable = serviceAccount
|
|
160
|
+
? [
|
|
161
|
+
...(input.declared.vault?.adapter === 'secret'
|
|
162
|
+
? [input.declared.vault.ref ?? VAULT_DOCUMENT_REF]
|
|
163
|
+
: []),
|
|
164
|
+
...(input.rotatable ?? []),
|
|
165
|
+
]
|
|
166
|
+
: [];
|
|
167
|
+
|
|
168
|
+
for (const ref of writable) {
|
|
169
|
+
const id = encodeRef(ref);
|
|
170
|
+
|
|
171
|
+
steps.push({
|
|
172
|
+
title: `create the secret ${id}, so the revision never needs secrets.create`,
|
|
173
|
+
argv: ['secrets', 'create', id, '--project', project, '--replication-policy', 'automatic'],
|
|
174
|
+
tolerateFailure: true,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
steps.push({
|
|
178
|
+
title: `let the revision rewrite ${ref}, and nothing else in the store`,
|
|
179
|
+
argv: [
|
|
180
|
+
'secrets',
|
|
181
|
+
'add-iam-policy-binding',
|
|
182
|
+
id,
|
|
183
|
+
'--project',
|
|
184
|
+
project,
|
|
185
|
+
'--member',
|
|
186
|
+
`serviceAccount:${serviceAccount}`,
|
|
187
|
+
'--role',
|
|
188
|
+
'roles/secretmanager.secretVersionAdder',
|
|
189
|
+
'--condition',
|
|
190
|
+
'None',
|
|
191
|
+
],
|
|
192
|
+
tolerateFailure: true,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Any target that addresses a bucket, which deployed means all of them:
|
|
197
|
+
// config, state, the log, memory, skills and attachments are all objects in
|
|
198
|
+
// it, and it is the only stateful thing a deployment has besides Secret
|
|
199
|
+
// Manager. A target on the filesystem adapter has no bucket, and creating
|
|
200
|
+
// one it will never open would be a resource nobody asked for and nobody
|
|
201
|
+
// deletes.
|
|
202
|
+
const usesBucket =
|
|
203
|
+
input.declared.storage.adapter === 'gcs' || input.declared.storage.adapter === 's3';
|
|
204
|
+
const bucket = usesBucket ? input.declared.storage.bucket : undefined;
|
|
205
|
+
if (bucket) {
|
|
206
|
+
steps.push({
|
|
207
|
+
title: `create the bucket gs://${bucket}`,
|
|
208
|
+
argv: [
|
|
209
|
+
'storage',
|
|
210
|
+
'buckets',
|
|
211
|
+
'create',
|
|
212
|
+
`gs://${bucket}`,
|
|
213
|
+
'--project',
|
|
214
|
+
project,
|
|
215
|
+
'--location',
|
|
216
|
+
region,
|
|
217
|
+
// Blobs here are read and written by one instance at a time and never
|
|
218
|
+
// served publicly; uniform access removes per-object ACLs as a way to
|
|
219
|
+
// get that wrong.
|
|
220
|
+
'--uniform-bucket-level-access',
|
|
221
|
+
],
|
|
222
|
+
tolerateFailure: true,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (serviceAccount) {
|
|
226
|
+
// Two conditioned bindings rather than one blanket objectAdmin, because
|
|
227
|
+
// the bucket now holds the config as well as the data.
|
|
228
|
+
//
|
|
229
|
+
// ADR-007 says a deployed instance never mutates its own configuration.
|
|
230
|
+
// That used to be enforced by the image being read-only, which stopped
|
|
231
|
+
// being true when the workspace moved into the bucket (ADR-023). This is
|
|
232
|
+
// where the guarantee went: the revision may write what it owns and may
|
|
233
|
+
// only read what declares what it is.
|
|
234
|
+
const objectsUnder = (path: string): string =>
|
|
235
|
+
`resource.name.startsWith("projects/_/buckets/${bucket}/objects/${path}")`;
|
|
236
|
+
const objectIs = (path: string): string =>
|
|
237
|
+
`resource.name == "projects/_/buckets/${bucket}/objects/${path}"`;
|
|
238
|
+
|
|
239
|
+
// A provider manifest is configuration that happens to live inside the
|
|
240
|
+
// profile's directory (ADR-030), so `data/` alone no longer separates
|
|
241
|
+
// what the revision owns from what declares what it is. Anchored to the
|
|
242
|
+
// profile segment rather than matched loosely: `contains("/providers.d/")`
|
|
243
|
+
// would also catch a blob whose own key happened to spell it.
|
|
244
|
+
const providerManifests =
|
|
245
|
+
`resource.name.matches("^projects/_/buckets/${bucket}/objects/data/[^/]+/providers\\.d/")`;
|
|
246
|
+
|
|
247
|
+
steps.push({
|
|
248
|
+
title: 'let the revision write its own data, but not the manifests in it',
|
|
249
|
+
argv: [
|
|
250
|
+
'storage',
|
|
251
|
+
'buckets',
|
|
252
|
+
'add-iam-policy-binding',
|
|
253
|
+
`gs://${bucket}`,
|
|
254
|
+
'--member',
|
|
255
|
+
`serviceAccount:${serviceAccount}`,
|
|
256
|
+
// objectAdmin, not objectViewer: state, the log, attachments, memory
|
|
257
|
+
// and skills are all written by the running endpoint.
|
|
258
|
+
'--role',
|
|
259
|
+
'roles/storage.objectAdmin',
|
|
260
|
+
'--condition',
|
|
261
|
+
`title=owns-its-data,expression=${objectsUnder('data/')} && !${providerManifests}`,
|
|
262
|
+
],
|
|
263
|
+
tolerateFailure: true,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
steps.push({
|
|
267
|
+
title: 'let the revision read its config, and only read it',
|
|
268
|
+
argv: [
|
|
269
|
+
'storage',
|
|
270
|
+
'buckets',
|
|
271
|
+
'add-iam-policy-binding',
|
|
272
|
+
`gs://${bucket}`,
|
|
273
|
+
'--member',
|
|
274
|
+
`serviceAccount:${serviceAccount}`,
|
|
275
|
+
'--role',
|
|
276
|
+
'roles/storage.objectViewer',
|
|
277
|
+
// `expression=true` was here, which is every object in the bucket —
|
|
278
|
+
// the step title and ADR-023 both claim a narrowing this did not do.
|
|
279
|
+
// The config the revision reads is the workspace file, the profiles
|
|
280
|
+
// beside it, and each profile's own manifests, so name exactly those.
|
|
281
|
+
'--condition',
|
|
282
|
+
`title=reads-its-config,expression=${objectsUnder('profiles/')} || ${objectIs('lanes-link.yaml')} || ${providerManifests}`,
|
|
283
|
+
],
|
|
284
|
+
tolerateFailure: true,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return Promise.resolve(steps);
|
|
290
|
+
}
|