Sitelet https://github.com/kubernetes/kops/pull/18623
Skip to content

gce: download nodeup from private GCS buckets with curl - #18623

Merged
kubernetes-prow[bot] merged 2 commits into
kubernetes:masterfrom
hakman:gce-nodeup-private-gcs-curl
Jul 30, 2026
Merged

kubernetes-prow[bot] merged 2 commits into
kubernetes:masterfrom
hakman:gce-nodeup-private-gcs-curl

Conversation

@hakman

@hakman hakman commented Jul 25, 2026 •

Copy link
Copy Markdown
Member

What this PR does / why we need it:

#18466 added support for fetching nodeup from a private GCS bucket by prepending gcloud storage cp to the bootstrap script's download list, which requires the gcloud CLI on the node image. This uses plain curl instead, authenticated with the instance service account token from the metadata server. The token is piped in via -H @-, so it never reaches the disk, the process arguments, or the serial console.

Since the download URLs are known when the script is generated, the method is chosen there rather than by testing the URL scheme at boot. Scripts using the default https sources are unchanged on every provider, and the GCE golden files return to their pre-#18466 form.

Two follow-ons make it usable and tested: spec.assets.fileRepository now accepts a gs:// URL on GCE, so node assets can live in the same private bucket (validation still rejects it elsewhere, as only GCE instances can authenticate to GCS), and GCE e2e jobs now pass kops the gs:// form of the staged artifacts so the path is covered by CI.

/cc @cheftako @justinsb @rifelpet

@kubernetes-prow

Copy link
Copy Markdown
Contributor

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@kubernetes-prow kubernetes-prow Bot added do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Jul 25, 2026
@hakman
hakman force-pushed the gce-nodeup-private-gcs-curl branch 4 times, most recently from fa05780 to e361409 Compare July 25, 2026 12:17
@kubernetes-prow kubernetes-prow Bot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. and removed size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Jul 25, 2026
@hakman
hakman force-pushed the gce-nodeup-private-gcs-curl branch from e361409 to 63b4bcc Compare July 25, 2026 12:43
@kubernetes-prow kubernetes-prow Bot added size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. and removed size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Jul 25, 2026
@hakman
hakman force-pushed the gce-nodeup-private-gcs-curl branch 2 times, most recently from b7735c5 to bf7d6fe Compare July 25, 2026 13:02
@hakman
hakman force-pushed the gce-nodeup-private-gcs-curl branch from bf7d6fe to 8edd197 Compare July 25, 2026 13:04
@hakman

hakman commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

/test pull-kops-e2e-k8s-gce-ipalias

@hakman
hakman force-pushed the gce-nodeup-private-gcs-curl branch from 8edd197 to 3cdd2c0 Compare July 25, 2026 13:42
@hakman
hakman marked this pull request as ready for review July 25, 2026 13:49
@kubernetes-prow kubernetes-prow Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jul 25, 2026
@hakman
hakman force-pushed the gce-nodeup-private-gcs-curl branch from 3cdd2c0 to ccc958f Compare July 25, 2026 15:34
@hakman

hakman commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

/test pull-kops-gce-master-scale-performance-100

@hakman

hakman commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

/test pull-kops-e2e-aws-load-balancer-controller

Comment thread pkg/apis/kops/validation/validation.go Outdated
case "gs":
// Only GCE instances can authenticate to GCS with their service account.
if cloudProvider != kops.CloudProviderGCE {
allErrs = append(allErrs, field.Invalid(fieldPath, s, "gs:// fileRepository is only supported on GCE"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be nice to include the current cloud provider in the error message. If they think they are using GCE (likely with a gs:// protocol) then knowing what its actually set to may help debugging.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The message now includes the configured cloud provider.

func buildVFSPath(target string) (string, error) {
if !strings.Contains(target, "://") || strings.HasPrefix(target, "memfs://") || strings.HasPrefix(target, "file://") {
if !strings.Contains(target, "://") || strings.HasPrefix(target, "memfs://") || strings.HasPrefix(target, "file://") ||
strings.HasPrefix(target, "gs://") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we now exits early on "gs://" which is the expected form, then comment and code from lines 196-206 maybe should reflect they are dealing with the "http(s)://storage.googleapis.com/" edge case?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Updated the comment to make it clear that this branch now only handles the https://storage.googleapis.com/ form, and the error hint mentions both forms.

Comment thread pkg/model/resources/nodeup.go Outdated
local token
echo "== Downloading ${url} =="
# Pipe the service account token to curl, so that it stays out of the logs.
if ! token=$(curl -s -f --noproxy '*' --connect-timeout 2 --max-time 5 -H 'Metadata-Flavor: Google' 'http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token' | grep -o '"access_token" *: *"[^"]*"' | cut -d '"' -f 4); then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like creating an entry for 169.254.169.254 to a hostname like LOCAL_METADATA would make this easier to understand. Would also allow us to change the IP to something like its IPv6 equivalent more easily.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, factored into a metadata_server variable. Kept the IP rather than metadata.google.internal so that early boot does not depend on DNS, which matches what the Go metadata client does. GCE does have an IPv6 endpoint these days, http://[fd20:ce::254], though only for IPv6-only VMs, which kOps does not create on GCE yet, as nodes are always dual-stack.

# Pipe the service account token to curl, so that it stays out of the logs.
if ! token=$(curl -s -f --noproxy '*' --connect-timeout 2 --max-time 5 -H 'Metadata-Flavor: Google' 'http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token' | grep -o '"access_token" *: *"[^"]*"' | cut -d '"' -f 4); then
echo "== Failed to get a service account token =="
elif ! echo "Authorization: Bearer ${token}" | curl -f -Lo "${file}" --connect-timeout 20 --retry 6 --retry-delay 10 -H @- "https://storage.googleapis.com/${url#gs://}"; then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm used to using a few extra headers from https://docs.cloud.google.com/apis/docs/system-parameters but if this works for you, lets keep it simple.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested the plain Authorization: Bearer header against a real private bucket and it works, so keeping it simple.

continue
}
for _, location := range asset.Locations {
if strings.HasPrefix(location, "gs://") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any chance this could be in the unsanitized "http(s)://storage.googleapis.com/" form?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can, and that form takes the standard unauthenticated download path, same as before this PR: it works for public buckets and gets 403 for private ones. gs:// is the explicit opt-in for the authenticated download.

if asset == nil {
continue
}
for _, location := range asset.Locations {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any chance that we get a mix of gs:// and not gs:// ? Seems like this should be decided per location?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice no, mirrors are only added for the default artifacts.k8s.io base. A custom base, which is the only way to get gs://, always yields a single location.

hakman added 2 commits July 29, 2026 08:09
PR kubernetes#18466 added support for fetching nodeup from a private GCS bucket
by calling "gcloud storage cp", which requires the gcloud CLI to be
installed on the node image. Use curl with the instance service account
token instead, so that no extra tooling is needed on the node.

The source URLs are known when the bootstrap script is generated, so
the download method is chosen there rather than by inspecting the URL
at boot. For the default https sources, the script goes back to the
standard download commands, identical to the other providers.

On GCE, spec.assets.fileRepository can now be a gs:// URL, so that the
node assets can be hosted in the same private bucket. Nodes elsewhere
cannot authenticate to GCS, so validation keeps rejecting it there.
"kops get assets --copy" can now also write to a gs:// repository.

GCE e2e jobs and dev-build-gce.sh now use the gs:// form of the staged
artifacts, so that the download is covered by CI and by dev clusters.
Only kops is given this form; the tooling that downloads the kops
binary keeps using https.
@hakman
hakman force-pushed the gce-nodeup-private-gcs-curl branch from ccc958f to be10c9e Compare July 29, 2026 05:11
@hakman

hakman commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

/test pull-kops-verify-terraform

@hakman

hakman commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

/retest

// nodes download the staged artifacts with their instance service-account credentials. Any other
// URL, and any other cloud provider, passes through unchanged. Only kops invocations get the gs://
// form; the scripts that download the kops binary run outside the deployer and keep using https.
func (d *deployer) maybeGSurl(/sitelet?url=https%3A%2F%2Fgithub.com%2Fkubernetes%2Fkops%2Fpull%2FbaseURL%2520string) string {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to have. I don't find the outcome of a method called "maybeGSURL" intuitive. I would suggest something like "canonicalizeGSURL" or "standardizeGSURL" as better indicating what the method does.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

canonicalizeBaseURL seems like a good option, thanks for the suggestion.
I will update the name in the followup.

@cheftako

Copy link
Copy Markdown
Member

This seems cleaner. I like it. It would be nice if we can come up with a better name than "maybeGSURL".
/lgtm

@kubernetes-prow kubernetes-prow Bot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Jul 29, 2026
@hakman

hakman commented Jul 30, 2026

Copy link
Copy Markdown
Member Author

/approve

@kubernetes-prow

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: hakman

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubernetes-prow kubernetes-prow Bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Jul 30, 2026
@kubernetes-prow
kubernetes-prow Bot merged commit a4ab99c into kubernetes:master Jul 30, 2026
28 checks passed
@hakman
hakman deleted the gce-nodeup-private-gcs-curl branch July 30, 2026 06:56
hakman added a commit to hakman/kops that referenced this pull request Jul 30, 2026
When KOPS_BASE_URL is an s3:// URL, the bootstrap script downloads nodeup
with curl, signing the request with the instance profile credentials from
IMDS using curl's native AWS SigV4 support. The credentials are piped in
via --config -, so they never reach the disk, the process arguments, or
the console log. An explicit x-amz-content-sha256 header lowers the
required curl version from 8.0 to 7.86.

The s3:// URL does not carry the bucket region, which SigV4 needs, so
kops resolves it at script generation time through vfs, which already
discovered and cached it when reading the nodeup hash from the bucket.

spec.assets.fileRepository now accepts an s3:// URL on AWS, so node assets
can live in the same private bucket. nodeup reads such assets through vfs,
which signs requests with the instance credentials and resolves the bucket
region.

Also rename the kubetest2 deployer's maybeGSURL to canonicalizeBaseURL, as
promised in the review of kubernetes#18623, and teach it to convert the public S3
https forms of staged artifacts to s3:// on AWS.
hakman added a commit to hakman/kops that referenced this pull request Jul 30, 2026
When KOPS_BASE_URL is an s3:// URL, the bootstrap script downloads nodeup
with curl, signing the request with the instance profile credentials from
IMDS using curl's native AWS SigV4 support. The credentials are piped in
via --config -, so they never reach the disk, the process arguments, or
the console log. An explicit x-amz-content-sha256 header lowers the
required curl version from 8.0 to 7.86.

The s3:// URL does not carry the bucket region, which SigV4 needs, so
kops resolves it at script generation time through vfs, which already
discovered and cached it when reading the nodeup hash from the bucket.

spec.assets.fileRepository now accepts an s3:// URL on AWS, so node assets
can live in the same private bucket. nodeup reads such assets through vfs,
which signs requests with the instance credentials and resolves the bucket
region.

Also rename the kubetest2 deployer's maybeGSURL to canonicalizeBaseURL, as
promised in the review of kubernetes#18623, and teach it to convert the public S3
https forms of staged artifacts to s3:// on AWS.
hakman added a commit to hakman/kops that referenced this pull request Aug 1, 2026
When KOPS_BASE_URL is an s3:// URL, the bootstrap script downloads nodeup
with curl, signing the request with the instance profile credentials from
IMDS using curl's native AWS SigV4 support. The credentials are piped in
via --config -, so they never reach the disk, the process arguments, or
the console log. An explicit x-amz-content-sha256 header lowers the
required curl version from 8.0 to 7.86.

The s3:// URL does not carry the bucket region, which SigV4 needs, so
kops resolves it at script generation time through vfs, which already
discovered and cached it when reading the nodeup hash from the bucket.

spec.assets.fileRepository now accepts an s3:// URL on AWS, so node assets
can live in the same private bucket. nodeup reads such assets through vfs,
which signs requests with the instance credentials and resolves the bucket
region.

Also rename the kubetest2 deployer's maybeGSURL to canonicalizeBaseURL, as
promised in the review of kubernetes#18623, and teach it to convert the public S3
https forms of staged artifacts to s3:// on AWS.
hakman added a commit to hakman/kops that referenced this pull request Aug 1, 2026
When KOPS_BASE_URL is an s3:// URL, the bootstrap script downloads nodeup
with curl, signing the request with the instance profile credentials from
IMDS using curl's native AWS SigV4 support. The credentials are piped in
via --config -, so they never reach the disk, the process arguments, or
the console log. An explicit x-amz-content-sha256 header lowers the
required curl version from 8.0 to 7.86.

The s3:// URL does not carry the bucket region, which SigV4 needs, so
kops resolves it at script generation time through vfs, which already
discovered and cached it when reading the nodeup hash from the bucket.

spec.assets.fileRepository now accepts an s3:// URL on AWS, so node assets
can live in the same private bucket. nodeup reads such assets through vfs,
which signs requests with the instance credentials and resolves the bucket
region.

Also rename the kubetest2 deployer's maybeGSURL to canonicalizeBaseURL, as
promised in the review of kubernetes#18623, and teach it to convert the public S3
https forms of staged artifacts to s3:// on AWS.
@rifelpet

rifelpet commented Aug 7, 2026

Copy link
Copy Markdown
Member

@hakman Looks like the curl version in some distros doesn't support gs://:

https://storage.googleapis.com/kubernetes-ci-logs/logs/e2e-kops-grid-gce-calico-deb13-k33-ko35/2083211374263013376/artifacts/136.109.232.232/journal.log

google_metadata_script_runner[1154]: Metadata key("startup-script"), command("/bin/bash"): == Failed to download gs://k8s-staging-kops/kops/releases/1.35.3+v1.35.1-39-gc89e13599b/linux/amd64/nodeup using wget --compression=auto -O nodeup --connect-timeout=20 --tries=6 --wait=10 ==
google_metadata_script_runner[1154]: Metadata key("startup-script"), command("/bin/bash"): == Downloading gs://k8s-staging-kops/kops/releases/1.35.3+v1.35.1-39-gc89e13599b/linux/amd64/nodeup using curl -f -Lo nodeup --connect-timeout 20 --retry 6 --retry-delay 10 ==
google_metadata_script_runner[1154]: Metadata key("startup-script"), command("/bin/bash"): curl: (1) Protocol "gs" not supported

https://storage.googleapis.com/kubernetes-ci-logs/logs/e2e-kops-grid-gce-calico-cos121arm64-k34-ko35/2084495582784655360/artifacts/136.115.187.38/journal.log

cloud-init[1242]: == Downloading gs://k8s-staging-kops/kops/releases/1.35.3+v1.35.1-39-gc89e13599b/linux/arm64/nodeup using curl -f --compressed -Lo nodeup --connect-timeout 20 --retry 6 --retry-delay 10 ==
cloud-init[1242]: curl: (1) Protocol "gs" not supported
cloud-init[1242]: == Failed to download gs://k8s-staging-kops/kops/releases/1.35.3+v1.35.1-39-gc89e13599b/linux/arm64/nodeup using curl -f --compressed -Lo nodeup --connect-timeout 20 --retry 6 --retry-delay 10 ==

https://storage.googleapis.com/kubernetes-ci-logs/logs/e2e-kops-grid-gce-calico-umini2404arm64-k33-ko33/2085325656857513984/artifacts/104.154.114.135/journal.log

cloud-init[1160]: == Failed to download gs://k8s-staging-kops/kops/releases/1.33.3+v1.33.1-46-g1d5f38108d/linux/arm64/nodeup using wget --compression=auto -O nodeup --connect-timeout=20 --tries=6 --wait=10 ==
cloud-init[1160]: == Downloading gs://k8s-staging-kops/kops/releases/1.33.3+v1.33.1-46-g1d5f38108d/linux/arm64/nodeup using curl -f -Lo nodeup --connect-timeout 20 --retry 6 --retry-delay 10 ==
cloud-init[1160]: curl: (1) Protocol "gs" not supported or disabled in libcurl

@hakman

hakman commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

@rifelpet it looks like bug, curl should not get any 'gs://' url.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. area/api area/documentation cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. lgtm "Looks good to me", indicates that a PR is ready to be merged. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants