diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0b27b7b2..f6cb44565 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,10 +136,8 @@ jobs: id-token: write env: API_IMAGE_ID_BASE: us-central1-docker.pkg.dev/deno-registry3-infra/registry/api - FRONTEND_IMAGE_ID_BASE: us-central1-docker.pkg.dev/deno-registry3-infra/registry/frontend outputs: api_image_id: ${{ steps.api_image_id.outputs.image_id }} - frontend_image_id: ${{ steps.frontend_image_id.outputs.image_id }} steps: - name: Clone repository uses: actions/checkout@v6 @@ -165,8 +163,7 @@ jobs: if: github.event_name == 'push' id: check_existing run: | - if docker manifest inspect ${{ env.API_IMAGE_ID_BASE }}:${{ github.sha }} > /dev/null 2>&1 && - docker manifest inspect ${{ env.FRONTEND_IMAGE_ID_BASE }}:${{ github.sha }} > /dev/null 2>&1; then + if docker manifest inspect ${{ env.API_IMAGE_ID_BASE }}:${{ github.sha }} > /dev/null 2>&1 then echo "exists=true" >> $GITHUB_OUTPUT else echo "exists=false" >> $GITHUB_OUTPUT @@ -191,20 +188,6 @@ jobs: cache-from: type=gha,scope=docker-api cache-to: type=gha,mode=max,scope=docker-api - # The legacy Cloud Run frontend lives alongside the new Cloudflare - # Worker frontend while traffic is cut over; tear this and the - # corresponding terraform resources down in a follow-up. - - name: Build and push frontend docker image - if: github.event_name != 'push' || steps.check_existing.outputs.exists != 'true' - uses: docker/build-push-action@v5 - id: frontend_push - with: - context: frontend - push: true - tags: ${{ env.FRONTEND_IMAGE_ID_BASE }}:${{ github.sha }} - cache-from: type=gha,scope=docker-frontend - cache-to: type=gha,mode=max,scope=docker-frontend - - name: Set api_image_id output id: api_image_id run: | @@ -214,15 +197,6 @@ jobs: echo "image_id=${{ env.API_IMAGE_ID_BASE }}:${{ github.sha }}" >> $GITHUB_OUTPUT fi - - name: Set frontend_image_id output - id: frontend_image_id - run: | - if [ -n "${{ steps.frontend_push.outputs.imageid }}" ]; then - echo "image_id=${{ env.FRONTEND_IMAGE_ID_BASE }}@${{ steps.frontend_push.outputs.imageid }}" >> $GITHUB_OUTPUT - else - echo "image_id=${{ env.FRONTEND_IMAGE_ID_BASE }}:${{ github.sha }}" >> $GITHUB_OUTPUT - fi - staging: if: github.event_name == 'merge_group' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'test-on-staging')) runs-on: ubuntu-22.04 @@ -269,7 +243,6 @@ jobs: deno task tf:staging:plan env: API_IMAGE_ID: ${{ needs.docker-images.outputs.api_image_id }} - FRONTEND_IMAGE_ID: ${{ needs.docker-images.outputs.frontend_image_id }} TF_VAR_github_client_secret: ${{ secrets.GH_CLIENT_SECRET }} TF_VAR_gitlab_client_secret: ${{ secrets.GITLAB_CLIENT_SECRET }} TF_VAR_postmark_token: ${{ secrets.POSTMARK_TOKEN }} @@ -281,6 +254,7 @@ jobs: TF_VAR_orama_symbols_data_source: ${{ vars.ORAMA_SYMBOLS_DATA_SOURCE }} TF_VAR_orama_docs_project_id: ${{ vars.ORAMA_DOCS_PROJECT_ID }} TF_VAR_cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }} + TF_VAR_otlp_headers: ${{ secrets.OTLP_HEADERS }} - name: terraform apply run: deno task tf:staging:apply @@ -341,7 +315,6 @@ jobs: deno task tf:prod:plan env: API_IMAGE_ID: ${{ needs.docker-images.outputs.api_image_id }} - FRONTEND_IMAGE_ID: ${{ needs.docker-images.outputs.frontend_image_id }} TF_VAR_github_client_secret: ${{ secrets.GH_CLIENT_SECRET }} TF_VAR_gitlab_client_secret: ${{ secrets.GITLAB_CLIENT_SECRET }} TF_VAR_postmark_token: ${{ secrets.POSTMARK_TOKEN }} @@ -353,6 +326,7 @@ jobs: TF_VAR_orama_symbols_data_source: ${{ vars.ORAMA_SYMBOLS_DATA_SOURCE }} TF_VAR_orama_docs_project_id: ${{ vars.ORAMA_DOCS_PROJECT_ID }} TF_VAR_cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }} + TF_VAR_otlp_headers: ${{ secrets.OTLP_HEADERS }} - name: terraform apply run: deno task tf:prod:apply diff --git a/Cargo.lock b/Cargo.lock index 49a7d771d..f5edf0851 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -371,7 +371,7 @@ dependencies = [ "async-std", "filetime", "libc", - "pin-project 1.1.5", + "pin-project", "redox_syscall 0.2.16", "xattr 0.2.3", ] @@ -591,7 +591,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools", "lazy_static", "lazycell", "log", @@ -1800,49 +1800,6 @@ dependencies = [ "slab", ] -[[package]] -name = "gcemeta" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d460327b24cc34c86d53d60a90e9e6044817f7906ebd9baa5c3d0ee13e1ecf" -dependencies = [ - "bytes", - "hyper 0.14.30", - "serde", - "serde_json", - "thiserror 1.0.63", - "tokio", - "tracing", -] - -[[package]] -name = "gcloud-sdk" -version = "0.20.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a24376e7850e7864bb326debc5765a1dda4fc47603c22e2bc0ebf30ff59141b" -dependencies = [ - "async-trait", - "chrono", - "futures", - "gcemeta", - "hyper 0.14.30", - "jsonwebtoken", - "once_cell", - "prost 0.11.9", - "prost-types 0.11.9", - "reqwest 0.11.27", - "secret-vault-value", - "serde", - "serde_json", - "tokio", - "tonic 0.9.2", - "tower 0.4.13", - "tower-layer", - "tower-util", - "tracing", - "url", -] - [[package]] name = "generator" version = "0.8.8" @@ -2528,15 +2485,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.14" @@ -3119,24 +3067,16 @@ dependencies = [ ] [[package]] -name = "opentelemetry-gcloud-trace" -version = "0.5.0" +name = "opentelemetry-http" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f67eb7da990bcd55159c3300c8d431d503892cf4f4d6a802fd30021b8233dec3" +checksum = "a819b71d6530c4297b49b3cae2939ab3a8cc1b9f382826a1bc29dd0ca3864906" dependencies = [ "async-trait", - "futures", - "futures-util", - "gcloud-sdk", - "opentelemetry", - "opentelemetry-semantic-conventions", - "prost-types 0.11.9", - "rsb_derive", - "rvstruct", - "tokio", - "tokio-stream", - "tonic 0.9.2", - "tracing", + "bytes", + "http 0.2.12", + "opentelemetry_api", + "reqwest 0.11.27", ] [[package]] @@ -3150,11 +3090,11 @@ dependencies = [ "futures-util", "http 0.2.12", "opentelemetry", + "opentelemetry-http", "opentelemetry-proto", - "prost 0.11.9", + "prost", + "reqwest 0.11.27", "thiserror 1.0.63", - "tokio", - "tonic 0.8.3", ] [[package]] @@ -3166,17 +3106,8 @@ dependencies = [ "futures", "futures-util", "opentelemetry", - "prost 0.11.9", - "tonic 0.8.3", -] - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e33428e6bf08c6f7fcea4ddb8e358fab0fe48ab877a87c70c6ebe20f673ce5" -dependencies = [ - "opentelemetry", + "prost", + "tonic", ] [[package]] @@ -3452,33 +3383,13 @@ dependencies = [ "siphasher", ] -[[package]] -name = "pin-project" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef0f924a5ee7ea9cbcea77529dba45f8a9ba9f622419fe3386ca581a3ae9d5a" -dependencies = [ - "pin-project-internal 0.4.30", -] - [[package]] name = "pin-project" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ - "pin-project-internal 1.1.5", -] - -[[package]] -name = "pin-project-internal" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "851c8d0ce9bebe43790dedfc86614c23494ac9f423dd618d3a61fc693eafe61e" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "pin-project-internal", ] [[package]] @@ -3637,17 +3548,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" dependencies = [ "bytes", - "prost-derive 0.11.9", -] - -[[package]] -name = "prost" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2ecbe40f08db5c006b5764a2645f7f3f141ce756412ac9e1dd6087e6d32995" -dependencies = [ - "bytes", - "prost-derive 0.13.2", + "prost-derive", ] [[package]] @@ -3657,43 +3558,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools", "proc-macro2", "quote", "syn 1.0.109", ] -[[package]] -name = "prost-derive" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf0c195eebb4af52c752bec4f52f645da98b6e92077a04110c7f349477ae5ac" -dependencies = [ - "anyhow", - "itertools 0.13.0", - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "prost-types" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" -dependencies = [ - "prost 0.11.9", -] - -[[package]] -name = "prost-types" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60caa6738c7369b940c3d49246a8d1749323674c65cb13010134f5c9bad5b519" -dependencies = [ - "prost 0.13.2", -] - [[package]] name = "psm" version = "0.1.23" @@ -3929,11 +3799,10 @@ dependencies = [ "oauth2", "once_cell", "opentelemetry", - "opentelemetry-gcloud-trace", "opentelemetry-otlp", "oramacore-client", "percent-encoding", - "pin-project 1.1.5", + "pin-project", "postmark", "pretty_assertions", "rand", @@ -4191,17 +4060,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rsb_derive" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2c53e42fccdc5f1172e099785fe78f89bc0c1e657d0c2ef591efbfac427e9a4" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "rust-ini" version = "0.21.3" @@ -4323,18 +4181,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile 1.0.4", - "schannel", - "security-framework", -] - [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -4391,26 +4237,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" -[[package]] -name = "rvs_derive" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1fa12378eb54f3d4f2db8dcdbe33af610b7e7d001961c1055858282ecef2a5" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "rvstruct" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5107860ec34506b64cf3680458074eac5c2c564f7ccc140918bbcd1714fd8d5d" -dependencies = [ - "rvs_derive", -] - [[package]] name = "ryu" version = "1.0.18" @@ -4454,19 +4280,6 @@ dependencies = [ "untrusted 0.9.0", ] -[[package]] -name = "secret-vault-value" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc32a777b53b3433b974c9c26b6d502a50037f8da94e46cb8ce2ced2cfdfaea0" -dependencies = [ - "prost 0.13.2", - "prost-types 0.13.2", - "serde", - "serde_json", - "zeroize", -] - [[package]] name = "security-framework" version = "2.11.1" @@ -5872,9 +5685,9 @@ dependencies = [ "hyper 0.14.30", "hyper-timeout", "percent-encoding", - "pin-project 1.1.5", - "prost 0.11.9", - "prost-derive 0.11.9", + "pin-project", + "prost", + "prost-derive", "tokio", "tokio-stream", "tokio-util", @@ -5885,38 +5698,6 @@ dependencies = [ "tracing-futures", ] -[[package]] -name = "tonic" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" -dependencies = [ - "async-stream", - "async-trait", - "axum", - "base64 0.21.7", - "bytes", - "futures-core", - "futures-util", - "h2", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.30", - "hyper-timeout", - "percent-encoding", - "pin-project 1.1.5", - "prost 0.11.9", - "rustls-native-certs", - "rustls-pemfile 1.0.4", - "tokio", - "tokio-rustls 0.24.1", - "tokio-stream", - "tower 0.4.13", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower" version = "0.4.13" @@ -5926,7 +5707,7 @@ dependencies = [ "futures-core", "futures-util", "indexmap 1.9.3", - "pin-project 1.1.5", + "pin-project", "pin-project-lite", "rand", "slab", @@ -5964,18 +5745,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" -[[package]] -name = "tower-util" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1093c19826d33807c72511e68f73b4a0469a3f22c2bd5f7d5212178b4b89674" -dependencies = [ - "futures-core", - "futures-util", - "pin-project 0.4.30", - "tower-service", -] - [[package]] name = "tracing" version = "0.1.44" @@ -6015,7 +5784,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" dependencies = [ - "pin-project 1.1.5", + "pin-project", "tracing", ] diff --git a/api/Cargo.toml b/api/Cargo.toml index 8ecdd081d..19e77129c 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -73,8 +73,15 @@ opentelemetry = { version = "0.19", features = [ "rt-tokio-current-thread", "trace", ] } -opentelemetry-otlp = "0.12" -opentelemetry-gcloud-trace = "0.5.0" +# OTLP over HTTP/protobuf (not gRPC): the managed Grafana Cloud OTLP gateway only +# speaks OTLP/HTTP. `reqwest-client` routes export through reqwest, the same HTTP +# stack every other outbound call in this crate already uses. Default +# `grpc-tonic` is off. +opentelemetry-otlp = { version = "0.12", default-features = false, features = [ + "trace", + "http-proto", + "reqwest-client", +] } rust-s3 = { version = "0.37.1", default-features = false, features = ["tokio-rustls-tls"] } deno_semver = "0.9.1" flate2 = "1" diff --git a/api/src/auth/github.rs b/api/src/auth/github.rs index d09640bc3..e61a613df 100644 --- a/api/src/auth/github.rs +++ b/api/src/auth/github.rs @@ -1,5 +1,4 @@ // Copyright 2024 the JSR authors. All rights reserved. MIT license. -// Copyright Deno Land Inc. All Rights Reserved. Proprietary and confidential. use crate::api::ApiError; use crate::db::*; diff --git a/api/src/auth/gitlab.rs b/api/src/auth/gitlab.rs index d92f660dd..e7bd13dd3 100644 --- a/api/src/auth/gitlab.rs +++ b/api/src/auth/gitlab.rs @@ -1,5 +1,4 @@ // Copyright 2024 the JSR authors. All rights reserved. MIT license. -// Copyright Deno Land Inc. All Rights Reserved. Proprietary and confidential. use crate::api::ApiError; use crate::db::*; diff --git a/api/src/auth/mod.rs b/api/src/auth/mod.rs index 40334ff04..657923e90 100644 --- a/api/src/auth/mod.rs +++ b/api/src/auth/mod.rs @@ -1,5 +1,4 @@ // Copyright 2024 the JSR authors. All rights reserved. MIT license. -// Copyright Deno Land Inc. All Rights Reserved. Proprietary and confidential. use crate::RegistryUrl; use crate::api::ApiError; diff --git a/api/src/config.rs b/api/src/config.rs index b6e0c158f..6c30a6655 100644 --- a/api/src/config.rs +++ b/api/src/config.rs @@ -115,13 +115,26 @@ pub struct Config { /// The Orama symbol data source pub orama_symbols_data_source: Option, - #[clap(long = "otlp_endpoint", env = "OTLP_ENDPOINT", group = "trace")] - /// OTLP endpoint to send traces to. + #[clap(long = "otlp_endpoint", env = "OTLP_ENDPOINT")] + /// Base OTLP/HTTP endpoint (e.g. Grafana Cloud's + /// `https://otlp-gateway-.grafana.net/otlp`), OTEL + /// `OTEL_EXPORTER_OTLP_ENDPOINT` style: the per-signal path (`/v1/traces`, + /// and `/v1/logs` in the future) is appended automatically. A full + /// signal URL is also accepted. Export is disabled when unset. pub otlp_endpoint: Option, - #[clap(long = "cloud_trace", group = "trace")] - /// Whether to enable cloud trace. - pub cloud_trace: bool, + #[clap(long = "otlp_headers", env = "OTLP_HEADERS")] + /// Extra headers sent with every OTLP request, as a comma-separated list of + /// `key=value` pairs (the OpenTelemetry `OTEL_EXPORTER_OTLP_HEADERS` format). + /// Used to carry the backend's auth, e.g. `Authorization=Basic ` for + /// Grafana Cloud. Only the first `=` in each pair separates key from value. + pub otlp_headers: Option, + + #[clap(long = "deployment_environment", env = "DEPLOYMENT_ENVIRONMENT")] + /// Deployment environment name (e.g. `staging`, `production`), exported as the + /// `deployment.environment` OTLP resource attribute so telemetry from each + /// environment can be told apart in the backend. Unset omits the attribute. + pub deployment_environment: Option, #[clap(long = "registry_url", env = "REGISTRY_URL")] /// The base URL of the registry, where module code and metadata can be @@ -169,10 +182,6 @@ pub struct Config { /// The ID of the npm tarball build queue. pub npm_tarball_build_queue_id: Option, - #[clap(long = "gcp_project_id", env = "GCP_PROJECT_ID")] - /// The ID of the project. - pub gcp_project_id: Option, - #[clap(long = "cloudflare_account_id", env = "CLOUDFLARE_ACCOUNT_ID")] /// The Cloudflare account ID for Analytics Engine. pub cloudflare_account_id: Option, @@ -222,7 +231,8 @@ impl std::fmt::Debug for Config { .field("github_client_id", &self.github_client_id) .field("github_client_secret", &"***") .field("otlp_endpoint", &self.otlp_endpoint) - .field("cloud_trace", &self.cloud_trace) + .field("otlp_headers", &self.otlp_headers.as_ref().map(|_| "***")) + .field("deployment_environment", &self.deployment_environment) .field("registry_url", &self.registry_url) .field("api", &self.api) .field("tasks", &self.tasks) diff --git a/api/src/db/models.rs b/api/src/db/models.rs index 87b206ee1..2ea437f74 100644 --- a/api/src/db/models.rs +++ b/api/src/db/models.rs @@ -1,5 +1,5 @@ // Copyright 2024 the JSR authors. All rights reserved. MIT license. -// Copyright Deno Land Inc. All Rights Reserved. Proprietary and confidential. + #![allow(dead_code)] use chrono::DateTime; diff --git a/api/src/docs.rs b/api/src/docs.rs index 37fe760f5..908ade2e5 100644 --- a/api/src/docs.rs +++ b/api/src/docs.rs @@ -604,7 +604,8 @@ fn get_url_rewriter( is_readme: bool, ) -> URLRewriter { Arc::new(move |current_file, url| { - if url.starts_with('#') || url.starts_with('/') { + // Anchors and protocol-relative URLs (`//host/...`) are left untouched. + if url.starts_with('#') || url.starts_with("//") { return url.to_string(); } @@ -639,6 +640,13 @@ fn get_url_rewriter( base.clone() }; + // Root-relative URLs (a single leading `/`) resolve against the + // repository root rather than the current file's directory. Without this + // they would be served relative to jsr.io and 404 (see #768). + if let Some(path) = url.strip_prefix('/') { + return format!("{base}/{path}"); + } + if !is_readme && let Some(current_file) = current_file { let (path, _file) = current_file .specifier @@ -1688,6 +1696,9 @@ mod tests { "/@foo/bar/1.2.3/src/assets/logo.svg" ); + // Root-relative links resolve against the package root (see #768). + assert_eq!(rewriter(None, "/LICENSE"), "/@foo/bar/1.2.3/LICENSE"); + assert_eq!( rewriter( Some(&ShortPath::new( @@ -1737,5 +1748,18 @@ mod tests { ), "https://raw.githubusercontent.com/foo/bar/HEAD/./src/assets/logo.svg" ); + + // Regression for #768: root-relative links resolve against the repository + // root rather than being left to 404 against jsr.io. + assert_eq!( + rewriter(None, "/LICENSE"), + "https://github.com/foo/bar/blob/HEAD/LICENSE" + ); + assert_eq!( + rewriter(None, "/assets/logo.svg"), + "https://raw.githubusercontent.com/foo/bar/HEAD/assets/logo.svg" + ); + // Protocol-relative URLs are left untouched. + assert_eq!(rewriter(None, "//example.com/x"), "//example.com/x"); } } diff --git a/api/src/external/github.rs b/api/src/external/github.rs index f38351f9e..f2f3bb921 100644 --- a/api/src/external/github.rs +++ b/api/src/external/github.rs @@ -1,5 +1,4 @@ // Copyright 2024 the JSR authors. All rights reserved. MIT license. -// Copyright Deno Land Inc. All Rights Reserved. Proprietary and confidential. use std::fmt::Display; use std::str::FromStr; diff --git a/api/src/external/gitlab.rs b/api/src/external/gitlab.rs index fe768d275..59840d7e7 100644 --- a/api/src/external/gitlab.rs +++ b/api/src/external/gitlab.rs @@ -1,5 +1,4 @@ // Copyright 2024 the JSR authors. All rights reserved. MIT license. -// Copyright Deno Land Inc. All Rights Reserved. Proprietary and confidential. use crate::util::shared_http_client; use hyper::StatusCode; diff --git a/api/src/external/orama.rs b/api/src/external/orama.rs index fcfd3864d..d4be65700 100644 --- a/api/src/external/orama.rs +++ b/api/src/external/orama.rs @@ -1,5 +1,4 @@ // Copyright 2024 the JSR authors. All rights reserved. MIT license. -// Copyright Deno Land Inc. All Rights Reserved. Proprietary and confidential. use std::sync::Arc; diff --git a/api/src/main.rs b/api/src/main.rs index c93124b49..ca1d586cf 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -163,14 +163,22 @@ async fn main() { let config = Config::parse(); println!("{config:?}"); - let export_target = if config.cloud_trace { - TracingExportTarget::CloudTrace - } else if let Some(otlp_endpoint) = config.otlp_endpoint { - TracingExportTarget::Otlp(otlp_endpoint) + // Treat a present-but-empty OTLP_ENDPOINT as unset: clap parses an empty env + // var as Some(""), which would otherwise build a schemeless endpoint and + // panic the exporter at boot. Filtering here means empty == export disabled. + let export_target = if let Some(endpoint) = + config.otlp_endpoint.filter(|s| !s.trim().is_empty()) + { + TracingExportTarget::Otlp { + endpoint, + headers: crate::tracing::parse_otlp_headers( + config.otlp_headers.as_deref(), + ), + } } else { TracingExportTarget::None }; - setup_tracing("api", export_target).await; + setup_tracing("api", export_target, config.deployment_environment).await; let database = Database::connect( &config.database_url, diff --git a/api/src/provenance.rs b/api/src/provenance.rs index f8f190a1b..15938985c 100644 --- a/api/src/provenance.rs +++ b/api/src/provenance.rs @@ -2,6 +2,7 @@ use anyhow::{Result, bail}; use base64::Engine as _; use base64::prelude::BASE64_STANDARD; +use base64::prelude::BASE64_URL_SAFE; use serde::Deserialize; use serde::Serialize; use x509_parser::parse_x509_certificate; @@ -117,14 +118,23 @@ pub enum ProvenanceAttestationSubject { Subject(Subject), } +/// Decode a DSSE envelope payload. The payload is base64-encoded, but some +/// clients emit it using the URL-safe alphabet (`-`/`_` instead of `+`/`/`), +/// so fall back to URL-safe decoding when standard decoding fails. +fn decode_payload(payload: &str) -> Result> { + match BASE64_STANDARD.decode(payload) { + Ok(bytes) => Ok(bytes), + Err(_) => Ok(BASE64_URL_SAFE.decode(payload)?), + } +} + pub fn verify( subject_name: String, bundle: ProvenanceBundle, ) -> Result { // Extract subject from the DSSE envelope let subject = { - let payload = - BASE64_STANDARD.decode(&bundle.content.dsse_envelope.payload)?; + let payload = decode_payload(&bundle.content.dsse_envelope.payload)?; serde_json::from_slice::(&payload)?.subject }; @@ -162,3 +172,29 @@ pub fn verify( let tls = &bundle.verification_material.tlog_entries[0]; Ok(tls.log_index.to_string()) } + +#[cfg(test)] +mod tests { + use super::decode_payload; + use base64::Engine as _; + use base64::prelude::BASE64_STANDARD; + use base64::prelude::BASE64_URL_SAFE; + + #[test] + fn decode_payload_accepts_standard_and_url_safe() { + // These bytes encode to "+/8=" in standard base64 and "-_8=" in URL-safe + // base64, exercising both alphabet-specific characters (`+`/`/` vs `-`/`_`). + let raw = [0xfb_u8, 0xff]; + + let standard = BASE64_STANDARD.encode(raw); + assert!(standard.contains('+') && standard.contains('/')); + assert_eq!(decode_payload(&standard).unwrap(), raw); + + // Regression test for jsr-io/jsr#1312: some clients emit the DSSE payload + // using the URL-safe alphabet, which the standard decoder rejected with + // "Invalid symbol 45, offset ..." (45 being `-`). + let url_safe = BASE64_URL_SAFE.encode(raw); + assert!(url_safe.contains('-') && url_safe.contains('_')); + assert_eq!(decode_payload(&url_safe).unwrap(), raw); + } +} diff --git a/api/src/token.rs b/api/src/token.rs index 4f1cacbe9..ade8617ce 100644 --- a/api/src/token.rs +++ b/api/src/token.rs @@ -1,5 +1,4 @@ // Copyright 2024 the JSR authors. All rights reserved. MIT license. -// Copyright Deno Land Inc. All Rights Reserved. Proprietary and confidential. use crate::db::*; use chrono::DateTime; diff --git a/api/src/traced_router.rs b/api/src/traced_router.rs index 37b156252..db2889c2f 100644 --- a/api/src/traced_router.rs +++ b/api/src/traced_router.rs @@ -1,5 +1,4 @@ // Copyright 2024 the JSR authors. All rights reserved. MIT license. -// Copyright Deno Land Inc. All Rights Reserved. Proprietary and confidential. //! This module implements a hyper service that wraps routerify and handles //! tracing. It starts a span for each request, and records successes and diff --git a/api/src/tracing.rs b/api/src/tracing.rs index 121450da3..a03840b55 100644 --- a/api/src/tracing.rs +++ b/api/src/tracing.rs @@ -1,5 +1,4 @@ // Copyright 2024 the JSR authors. All rights reserved. MIT license. -// Copyright Deno Land Inc. All Rights Reserved. Proprietary and confidential. use opentelemetry::KeyValue; use opentelemetry::global; @@ -21,11 +20,44 @@ use tracing_subscriber::registry::LookupSpan; use tracing_subscriber::reload; pub enum TracingExportTarget { - Otlp(String), - CloudTrace, + Otlp { + endpoint: String, + headers: std::collections::HashMap, + }, None, } +/// Append an OTLP signal subpath to the configured base endpoint, OTEL +/// `OTEL_EXPORTER_OTLP_ENDPOINT` style: the endpoint is the base (e.g. Grafana +/// Cloud's `.../otlp`) and each signal posts to its own path (`/v1/traces`, +/// later `/v1/logs`). The opentelemetry-otlp 0.12 HTTP exporter uses the +/// endpoint verbatim and does NOT do this itself, so posting to the bare base +/// 404s. Tolerates a trailing slash and an endpoint that already carries the +/// signal path. +fn otlp_signal_endpoint(base: &str, signal_path: &str) -> String { + let base = base.trim_end_matches('/'); + if base.ends_with(signal_path) { + base.to_string() + } else { + format!("{base}{signal_path}") + } +} + +/// Parse the `OTLP_HEADERS` value (`key1=value1,key2=value2`, the OpenTelemetry +/// `OTEL_EXPORTER_OTLP_HEADERS` format) into a header map. Splits each pair on +/// its first `=` only, so values containing `=` (e.g. base64 padding in a +/// `Basic` auth header) survive intact. +pub fn parse_otlp_headers( + raw: Option<&str>, +) -> std::collections::HashMap { + raw + .into_iter() + .flat_map(|s| s.split(',')) + .filter_map(|pair| pair.split_once('=')) + .map(|(k, v)| (k.trim().to_string(), v.trim().to_string())) + .collect() +} + /// Initialize tracing infrastructure. /// /// `tracing` has a three core concepts. These are Spans, Events, and subscribers. @@ -39,18 +71,31 @@ pub enum TracingExportTarget { pub async fn setup_tracing( name: &'static str, export_target: TracingExportTarget, + deployment_environment: Option, ) -> (LogFilterHandle, String) { - let trace_config = trace::config().with_resource(Resource::new(vec![ + let mut resource = vec![ KeyValue::new("service.name", name), KeyValue::new("service.namespace", "registry"), - ])); + ]; + // Distinguishes staging from prod telemetry when both export to the same + // backend. Empty/unset omits it rather than reporting a blank environment. + if let Some(env) = deployment_environment.filter(|s| !s.trim().is_empty()) { + resource.push(KeyValue::new("deployment.environment", env)); + } + let trace_config = trace::config().with_resource(Resource::new(resource)); let tracer = match export_target { - TracingExportTarget::Otlp(otlp_endpoint) => { + TracingExportTarget::Otlp { endpoint, headers } => { + // OTLP/HTTP (protobuf), not gRPC: the managed Grafana Cloud gateway only + // accepts HTTP, and it also works directly from the Cloudflare Container. + // `endpoint` is the base; the `/v1/traces` signal path is appended here. + // `headers` carries the backend auth, e.g. `Authorization: Basic ` + // for Grafana Cloud. let exporter = opentelemetry_otlp::new_exporter() - .tonic() - .with_endpoint(otlp_endpoint) - .with_protocol(opentelemetry_otlp::Protocol::Grpc); + .http() + .with_endpoint(otlp_signal_endpoint(&endpoint, "/v1/traces")) + .with_protocol(opentelemetry_otlp::Protocol::HttpBinary) + .with_headers(headers); let tracer = opentelemetry_otlp::new_pipeline() .tracing() .with_trace_config(trace_config) @@ -59,16 +104,6 @@ pub async fn setup_tracing( .unwrap(); Some(tracer) } - TracingExportTarget::CloudTrace => { - let tracer = opentelemetry_gcloud_trace::GcpCloudTraceExporterBuilder::for_default_project_id() - .await - .unwrap() - .with_trace_config(trace_config) - .install_batch(opentelemetry::runtime::Tokio) - .await - .unwrap(); - Some(tracer) - } TracingExportTarget::None => None, }; @@ -145,3 +180,55 @@ where let otel_data = extensions.get::()?; Some(otel_data.parent_cx.span().span_context().trace_id()) } + +#[cfg(test)] +mod tests { + use super::otlp_signal_endpoint; + use super::parse_otlp_headers; + + #[test] + fn appends_signal_path_to_base() { + assert_eq!( + otlp_signal_endpoint("https://x.grafana.net/otlp", "/v1/traces"), + "https://x.grafana.net/otlp/v1/traces" + ); + } + + #[test] + fn tolerates_trailing_slash_and_existing_path() { + assert_eq!( + otlp_signal_endpoint("https://x.grafana.net/otlp/", "/v1/traces"), + "https://x.grafana.net/otlp/v1/traces" + ); + assert_eq!( + otlp_signal_endpoint( + "https://x.grafana.net/otlp/v1/traces", + "/v1/traces" + ), + "https://x.grafana.net/otlp/v1/traces" + ); + } + + #[test] + fn none_is_empty() { + assert!(parse_otlp_headers(None).is_empty()); + assert!(parse_otlp_headers(Some("")).is_empty()); + } + + #[test] + fn keeps_equals_in_value() { + // A `Basic` auth header's base64 value can contain `=` padding; only the + // first `=` of each pair separates key from value. + let headers = + parse_otlp_headers(Some("Authorization=Basic dXNlcjpwYXNz==")); + assert_eq!(headers.len(), 1); + assert_eq!(headers["Authorization"], "Basic dXNlcjpwYXNz=="); + } + + #[test] + fn multiple_pairs_are_trimmed() { + let headers = parse_otlp_headers(Some("a=1, b=2")); + assert_eq!(headers["a"], "1"); + assert_eq!(headers["b"], "2"); + } +} diff --git a/deno.json b/deno.json index 0f4fcf98b..4912f7b85 100644 --- a/deno.json +++ b/deno.json @@ -24,10 +24,10 @@ "tf:infra:plan": "cd terraform_infra && terraform plan -var-file=infra.tfvars -out=infra.tfplan -input=false", "tf:infra:apply": "cd terraform_infra && terraform apply -input=false infra.tfplan", "tf:staging:init": "gcloud config set project deno-registry3-staging && cd terraform && terraform init -backend-config bucket=deno-registry3-staging-terraform", - "tf:staging:plan": "cd terraform && terraform plan -var-file=staging.tfvars -var-file=staging.secret.tfvars -out=staging.tfplan -input=false -var \"api_image_id=$API_IMAGE_ID\" -var \"frontend_image_id=$FRONTEND_IMAGE_ID\"", + "tf:staging:plan": "cd terraform && terraform plan -var-file=staging.tfvars -var-file=staging.secret.tfvars -out=staging.tfplan -input=false -var \"api_image_id=$API_IMAGE_ID\"", "tf:staging:apply": "cd terraform && terraform apply -input=false staging.tfplan", "tf:prod:init": "gcloud config set project deno-registry3-prod && cd terraform && terraform init -backend-config bucket=deno-registry3-prod-terraform", - "tf:prod:plan": "cd terraform && terraform plan -var-file=prod.tfvars -var-file=prod.secret.tfvars -out=prod.tfplan -input=false -var \"api_image_id=$API_IMAGE_ID\" -var \"frontend_image_id=$FRONTEND_IMAGE_ID\"", + "tf:prod:plan": "cd terraform && terraform plan -var-file=prod.tfvars -var-file=prod.secret.tfvars -out=prod.tfplan -input=false -var \"api_image_id=$API_IMAGE_ID\"", "tf:prod:apply": "cd terraform && terraform apply -input=false prod.tfplan", "e2e:staging": "cd e2e && JSR_URL=https://deno-registry-staging.net/ JSR_API_URL=https://api.deno-registry-staging.net/ deno test -A", diff --git a/frontend/components/NavOverflow.tsx b/frontend/components/NavOverflow.tsx index b4c10e12f..b492fdaab 100644 --- a/frontend/components/NavOverflow.tsx +++ b/frontend/components/NavOverflow.tsx @@ -64,6 +64,8 @@ export function NavOverflow() {