Give manual image builds a source bucket cb-image can read
The first `make build` failed before any step ran: INVALID_ARGUMENT: could not resolve source: cb-image@... does not have storage.objects.get access to ... gitea-496920_cloudbuild/source/... `gcloud builds submit` uploads the source tarball to <project>_cloudbuild and the build, running as the user-specified cb-image@, must read it back. Nothing grants that. Binding on that bucket is not an option: gcloud creates it on the first submit, after `pulumi up` has already run. Project-wide objectViewer would also open the backup, config and state buckets. Pulumi now owns <project>-gitea-build-source, readable by cb-image@ and nothing else, with a 7-day delete rule since each tarball is read once. `make build` stages there via --gcs-source-staging-dir. Triggered builds fetch source through the GitHub connection and are unaffected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Jason Ross
co-authored by
Claude Opus 5.5
parent
d48f2f5e0c
commit
a1669d6be1
@@ -44,6 +44,10 @@ up: check ## Apply the infrastructure
|
||||
# to cb-image@, not to whatever default Cloud Build account this project
|
||||
# happens to have -- and on newer projects the legacy default does not exist.
|
||||
# Without this the images push fine and the rollout step fails.
|
||||
# --gcs-source-staging-dir: running as cb-image@, the build must be able to read
|
||||
# the uploaded source. Pulumi grants that on this bucket only; the default
|
||||
# <project>_cloudbuild bucket is unreadable to it and the build fails at
|
||||
# "could not resolve source".
|
||||
# SHORT_SHA: Cloud Build fills it in only for triggered builds. For `builds
|
||||
# submit` it is empty, and image.yaml's `--tag <image>:$SHORT_SHA` becomes an
|
||||
# invalid reference that fails the build.
|
||||
@@ -52,6 +56,7 @@ build: ## Build and roll out the container images via Cloud Build
|
||||
gcloud builds submit --config cloudbuild/image.yaml --project $(PROJECT) \
|
||||
--region=$(REGION) \
|
||||
--service-account=projects/$(PROJECT)/serviceAccounts/cb-image@$(PROJECT).iam.gserviceaccount.com \
|
||||
--gcs-source-staging-dir=gs://$(PROJECT)-gitea-build-source/source \
|
||||
--substitutions=_REGION=$(REGION),_ZONE=$(ZONE),SHORT_SHA=$(shell git rev-parse --short=7 HEAD)
|
||||
|
||||
.PHONY: rollout
|
||||
|
||||
@@ -74,6 +74,9 @@ func main() {
|
||||
if err := iam.GrantBuckets(ctx, accounts, buckets.Config, buckets.Backup); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := iam.GrantBuildSource(ctx, accounts, buckets.BuildSource); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The zone already exists and is delegated; this only adds the A record
|
||||
// and the zone-scoped permission Caddy needs for DNS-01.
|
||||
@@ -100,6 +103,7 @@ func main() {
|
||||
ctx.Export("registry", pulumi.Sprintf("%s/%s/%s", cfg.ARHost(), cfg.Project, registry.RepoID))
|
||||
ctx.Export("configBucket", buckets.Config.Name)
|
||||
ctx.Export("backupBucket", buckets.Backup.Name)
|
||||
ctx.Export("buildSourceBucket", buckets.BuildSource.Name)
|
||||
ctx.Export("configHash", pulumi.String(buckets.ConfigHash))
|
||||
ctx.Export("vmServiceAccount", accounts.VM.Email)
|
||||
ctx.Export("imageServiceAccount", accounts.Image.Email)
|
||||
|
||||
@@ -152,6 +152,17 @@ func GrantSecretRead(ctx *pulumi.Context, cfg *config.Config, a *Accounts, name
|
||||
return err
|
||||
}
|
||||
|
||||
// GrantBuildSource lets cb-image@ read the tarballs `make build` stages, and
|
||||
// nothing else in storage. See storage.New for why the bucket exists.
|
||||
func GrantBuildSource(ctx *pulumi.Context, a *Accounts, sourceBucket *storage.Bucket) error {
|
||||
_, err := storage.NewBucketIAMMember(ctx, "img-build-source-reader", &storage.BucketIAMMemberArgs{
|
||||
Bucket: sourceBucket.Name,
|
||||
Role: pulumi.String("roles/storage.objectViewer"),
|
||||
Member: pulumi.Sprintf("serviceAccount:%s", a.Image.Email),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// GrantBuckets: read-only on config, write-only on backups. The VM can create a
|
||||
// backup but cannot read or delete existing ones, which limits what ransomware
|
||||
// on the box could do to the backup history.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package storage holds the two buckets and, importantly, uploads the vm/ tree
|
||||
// Package storage holds the buckets and, importantly, uploads the vm/ tree
|
||||
// as Pulumi-managed objects.
|
||||
//
|
||||
// Uploading the VM configuration through Pulumi (rather than a `gcloud storage
|
||||
@@ -24,6 +24,8 @@ import (
|
||||
type Buckets struct {
|
||||
Config *storage.Bucket
|
||||
Backup *storage.Bucket
|
||||
// BuildSource stages the source tarball for `make build`. See New.
|
||||
BuildSource *storage.Bucket
|
||||
// ConfigHash changes whenever any file under vm/ changes. It is written into
|
||||
// instance metadata so a config change is visible from `describe`, and so
|
||||
// there is something to compare against when debugging drift.
|
||||
@@ -72,12 +74,37 @@ func New(ctx *pulumi.Context, cfg *config.Config, vmDir string, deps []pulumi.Re
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Where `gcloud builds submit` stages its source tarball. Builds run as
|
||||
// cb-image@, which needs storage.objects.get on that tarball. The default
|
||||
// staging bucket is <project>_cloudbuild, created by gcloud on the first
|
||||
// submit -- after `pulumi up`, so there is nothing to bind to in advance --
|
||||
// and project-wide objectViewer would also open the backup and state
|
||||
// buckets. A dedicated bucket keeps the grant exact. Triggered builds fetch
|
||||
// source through the GitHub connection and never touch it.
|
||||
buildSourceBucket, err := storage.NewBucket(ctx, "gitea-build-source", &storage.BucketArgs{
|
||||
Name: pulumi.Sprintf("%s-gitea-build-source", cfg.Project),
|
||||
Location: pulumi.String(strings.ToUpper(cfg.Region)),
|
||||
UniformBucketLevelAccess: pulumi.Bool(true),
|
||||
PublicAccessPrevention: pulumi.String("enforced"),
|
||||
// Tarballs are only read once, by the build they were uploaded for.
|
||||
LifecycleRules: storage.BucketLifecycleRuleArray{
|
||||
&storage.BucketLifecycleRuleArgs{
|
||||
Action: &storage.BucketLifecycleRuleActionArgs{Type: pulumi.String("Delete")},
|
||||
Condition: &storage.BucketLifecycleRuleConditionArgs{Age: pulumi.Int(7)},
|
||||
},
|
||||
},
|
||||
ForceDestroy: pulumi.Bool(true),
|
||||
}, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hash, err := uploadTree(ctx, configBucket, vmDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Buckets{Config: configBucket, Backup: backupBucket, ConfigHash: hash}, nil
|
||||
return &Buckets{Config: configBucket, Backup: backupBucket, BuildSource: buildSourceBucket, ConfigHash: hash}, nil
|
||||
}
|
||||
|
||||
// uploadTree mirrors vmDir into gs://<bucket>/vm/ and returns a content hash of
|
||||
|
||||
Reference in New Issue
Block a user