mirror of
https://github.com/imjasonh/npm-snoop
synced 2026-07-07 00:32:54 +00:00
Poll npm registry changes feed and record releases to BigQuery
Go cron job that tails the npm CouchDB _changes feed, fetches package metadata, and emits CloudEvents for releases and deletions. Events flow through a Chainguard cloudevent-broker to a cloudevent-recorder that streams them into BigQuery. Infrastructure is managed with Terraform using chainguard-dev/terraform- infra-common modules for networking, broker, recorder, and cron. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
a22a473782
13 changed files with 1106 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Go binary
|
||||
npm-snoop
|
||||
|
||||
# Terraform
|
||||
terraform/.terraform/
|
||||
terraform/terraform.tfstate*
|
||||
terraform/.terraform.lock.hcl
|
||||
35
CLAUDE.md
Normal file
35
CLAUDE.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# npm-snoop
|
||||
|
||||
Go cron job that tails the npm registry changes feed and records releases/deletions to BigQuery via CloudEvents.
|
||||
|
||||
## Structure
|
||||
|
||||
- `main.go` — the poller; all application logic is in this one file
|
||||
- `terraform/` — all infrastructure (networking, broker, recorder, cron job)
|
||||
- `terraform/schema.json` — BigQuery schema for release events
|
||||
- `terraform/schema-deleted.json` — BigQuery schema for deletion events
|
||||
|
||||
## Build and deploy
|
||||
|
||||
The `cron` Terraform module builds the Go binary with `ko` automatically. To redeploy after code changes:
|
||||
|
||||
```sh
|
||||
cd terraform && terraform apply -var=project_id=jason-491415
|
||||
```
|
||||
|
||||
To just rebuild the cron job image without touching other infra, use `-target=module.cron`.
|
||||
|
||||
## Key design decisions
|
||||
|
||||
- The poller sends CloudEvents to the broker using an ID-token-authenticated HTTP client (`google.golang.org/api/idtoken`), because the broker is a private Cloud Run service with `ingress: internal`.
|
||||
- The cron job uses `vpc_access` with `PRIVATE_RANGES_ONLY` egress so broker traffic routes through the VPC (via private DNS for `*.run.app`) while npm registry traffic goes directly over the internet.
|
||||
- Event IDs replace `/` with `_` because the recorder uses the event ID as a filename, and scoped npm packages contain slashes (e.g., `@scope/name`).
|
||||
- The poller loops through batches until fully caught up, saving progress to GCS after each batch. This prevents missed events when more changes occur in a 15-minute window than a single batch can handle.
|
||||
- On first run (no state file), the poller bootstraps from the registry's current `update_seq` rather than processing the entire history.
|
||||
- CloudEvent sends are checked with `IsACK` (not `IsUndelivered`) to catch non-2xx HTTP responses that the SDK would otherwise treat as delivered.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- The npm `license` field can be a string or an object; `extractLicense` handles both.
|
||||
- Packages that return 404 from the registry (unpublished between the changes feed and metadata fetch) are emitted as deletion events.
|
||||
- Terraform state is local. Back it up or move to a GCS backend.
|
||||
202
LICENSE
Normal file
202
LICENSE
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
55
README.md
Normal file
55
README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# npm-snoop
|
||||
|
||||
Polls the npm registry's CouchDB changes feed and records package releases and deletions to BigQuery via CloudEvents.
|
||||
|
||||
## Architecture
|
||||
|
||||
A Cloud Run **cron job** runs every 15 minutes:
|
||||
|
||||
1. Reads the last processed sequence number from a GCS bucket
|
||||
2. Polls `https://replicate.npmjs.com/registry/_changes` for new changes
|
||||
3. Fetches metadata for each changed package from `https://registry.npmjs.org/<name>`
|
||||
4. Emits CloudEvents to a **broker** (Chainguard `cloudevent-broker`)
|
||||
5. The **recorder** (Chainguard `cloudevent-recorder`) writes events to BigQuery
|
||||
|
||||
Two event types are recorded:
|
||||
|
||||
- `dev.npm.package.released.v1` — name, version, description, license, homepage
|
||||
- `dev.npm.package.deleted.v1` — name, deleted_at
|
||||
|
||||
The poller loops through batches until caught up, saving progress after each batch.
|
||||
|
||||
## Deploy
|
||||
|
||||
Requires `gcloud` auth, `terraform`, and `ko`.
|
||||
|
||||
```sh
|
||||
cd terraform
|
||||
terraform init
|
||||
terraform apply -var=project_id=YOUR_PROJECT_ID
|
||||
```
|
||||
|
||||
Key variables (see `terraform/variables.tf`):
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `project_id` | (required) | GCP project |
|
||||
| `region` | `us-east4` | GCP region |
|
||||
| `schedule` | `*/15 * * * *` | Cron schedule |
|
||||
| `bq_retention_days` | `365` | BigQuery retention |
|
||||
| `deletion_protection` | `false` | Protect resources from deletion |
|
||||
|
||||
## Manual run
|
||||
|
||||
```sh
|
||||
gcloud run jobs execute npm-snoop-cron --project YOUR_PROJECT_ID --region us-east4 --wait
|
||||
```
|
||||
|
||||
## Query
|
||||
|
||||
```sql
|
||||
SELECT name, version, license
|
||||
FROM `cloudevents_npm_snoop_recorder.dev_npm_package_released_v1`
|
||||
ORDER BY _PARTITIONTIME DESC
|
||||
LIMIT 100
|
||||
```
|
||||
57
design.md
Normal file
57
design.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
### Architecture Overview
|
||||
To avoid downloading the entire npm registry history every time the cron job runs, the system needs to maintain state.
|
||||
|
||||
1. **State Storage (GCS):** A lightweight Google Cloud Storage bucket to store the `last_seq` integer.
|
||||
2. **The Poller (Cron Job):** A Go application, triggered on a schedule, that reads the sequence, polls the `_changes` endpoint, fetches full package metadata, wraps it in a CloudEvent, and pushes it to the broker.
|
||||
3. **The Broker (Event Broker):** The Chainguard `cloudevent-broker` module receives the events and acts as the central routing hub.
|
||||
4. **The Recorder (BigQuery):** The Chainguard `cloudevent-recorder` module listens to the broker and streams the package metadata directly into a BigQuery table.
|
||||
|
||||
---
|
||||
|
||||
### Part 1: Go Application Logic (The Poller)
|
||||
Instruct your coding agent to build a Go application with the following execution flow:
|
||||
|
||||
* **Initialization:**
|
||||
* Initialize a Google Cloud Storage client (`cloud.google.com/go/storage`).
|
||||
* Initialize a CloudEvents Go SDK client (`github.com/cloudevents/sdk-go/v2`).
|
||||
* **State Retrieval:** * Fetch the `last_seq` string/integer from a specific object in the GCS bucket (the bucket name should be passed via an environment variable like `STATE_BUCKET`).
|
||||
* **Polling the Feed:** * Make an HTTP GET request to `https://replicate.npmjs.com/registry/_changes?since=<last_seq>`.
|
||||
* **Fetching Metadata & Emitting Events:**
|
||||
* Parse the JSON response and iterate through the `.results` array.
|
||||
* For each item, extract the `id` (package name) and `seq`.
|
||||
* Make an HTTP GET request to `https://registry.npmjs.org/<id>` to fetch the full metadata payload.
|
||||
* Construct a CloudEvent:
|
||||
* `Type`: `dev.npm.package.released.v1`
|
||||
* `Source`: `npm-poller-cron`
|
||||
* `Data`: The raw JSON bytes of the full package metadata.
|
||||
* Send the CloudEvent to the URI specified in the `$K_SINK` environment variable (this variable is standard in Knative/Chainguard architectures for the target destination).
|
||||
* **State Update:** * After successfully processing the batch and emitting the events, upload the highest `.seq` processed back to the GCS bucket object. This persists the state for the next cron execution.
|
||||
|
||||
---
|
||||
|
||||
### Part 2: Infrastructure as Code (Terraform)
|
||||
The agent will need to write a Terraform configuration utilizing the `chainguard-dev/terraform-infra-common` repository.
|
||||
|
||||
* **Prerequisites:**
|
||||
* Configure the Google Cloud provider.
|
||||
* Enable necessary APIs: Cloud Run, Cloud Scheduler, Pub/Sub, BigQuery, and Eventarc.
|
||||
* **State Storage Resource:**
|
||||
* Define a `google_storage_bucket` and a `google_storage_bucket_object` to hold the initial state file (e.g., `seq.txt` initialized to `0` or a recent starting point).
|
||||
* **Module 1: The Broker (`cloudevent-broker`)**
|
||||
* **Source:** `github.com/chainguard-dev/terraform-infra-common//modules/cloudevent-broker`
|
||||
* **Purpose:** Deploys the regional event broker.
|
||||
* **Outputs:** This module will output a broker URI that the cron job will use to dispatch events.
|
||||
* **Module 2: The Recorder (`cloudevent-recorder`)**
|
||||
* **Source:** `github.com/chainguard-dev/terraform-infra-common//modules/cloudevent-recorder`
|
||||
* **Purpose:** Automatically creates a BigQuery dataset/table and sets up a Cloud Run service to listen for events and write them to BigQuery.
|
||||
* **Configuration:** Pass the broker's name to bind it to the broker. Configure the target dataset and table names.
|
||||
* **Module 3: The Cron Job (`cron`)**
|
||||
* **Source:** `github.com/chainguard-dev/terraform-infra-common//modules/cron`
|
||||
* **Purpose:** Deploys the Go container and sets up the Cloud Scheduler trigger.
|
||||
* **Configuration:**
|
||||
* Set the schedule (e.g., `"*/15 * * * *"` for every 15 minutes).
|
||||
* Set the container image (recommend using `ko` to build the Go binary).
|
||||
* Set environment variables: Map `K_SINK` to the URI output from the broker module, and map `STATE_BUCKET` to the GCS bucket name.
|
||||
* Create and assign a Google Service Account (GSA) with `roles/storage.objectAdmin` (to read/write the state file) and permissions to invoke the broker.
|
||||
|
||||
> **Note for the Agent:** Because Chainguard modules heavily rely on standard Knative Eventing patterns, ensure that the Poller's Service Account has the required IAM bindings to publish directly to the Broker's ingress URL.
|
||||
64
go.mod
Normal file
64
go.mod
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
module github.com/imjasonh/npm-snoop
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
cloud.google.com/go/storage v1.61.3
|
||||
github.com/chainguard-dev/clog v1.8.0
|
||||
github.com/cloudevents/sdk-go/v2 v2.16.2
|
||||
github.com/sethvargo/go-envconfig v1.3.0
|
||||
google.golang.org/api v0.271.0
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/auth v0.18.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.3 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.3 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
google.golang.org/grpc v1.79.2 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
152
go.sum
Normal file
152
go.sum
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
|
||||
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
||||
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
||||
cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
|
||||
cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
|
||||
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
|
||||
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
|
||||
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
|
||||
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
|
||||
cloud.google.com/go/storage v1.61.3 h1:VS//ZfBuPGDvakfD9xyPW1RGF1Vy3BWUoVZXgW1KMOg=
|
||||
cloud.google.com/go/storage v1.61.3/go.mod h1:JtqK8BBB7TWv0HVGHubtUdzYYrakOQIsMLffZ2Z/HWk=
|
||||
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
|
||||
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chainguard-dev/clog v1.8.0 h1:frlTMEdg3XQR+ioQ6O9i92uigY8GTUcWKpuCFkhcCHA=
|
||||
github.com/chainguard-dev/clog v1.8.0/go.mod h1:5MQOZi+Iu7fV7GcJG8ag8rCB5elEOpqRMKEASgnGVdo=
|
||||
github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK1D+NnQM=
|
||||
github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
|
||||
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sethvargo/go-envconfig v1.3.0 h1:gJs+Fuv8+f05omTpwWIu6KmuseFAXKrIaOZSh8RMt0U=
|
||||
github.com/sethvargo/go-envconfig v1.3.0/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY=
|
||||
google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
315
main.go
Normal file
315
main.go
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/storage"
|
||||
"github.com/chainguard-dev/clog"
|
||||
cloudevents "github.com/cloudevents/sdk-go/v2"
|
||||
cehttp "github.com/cloudevents/sdk-go/v2/protocol/http"
|
||||
"github.com/sethvargo/go-envconfig"
|
||||
"google.golang.org/api/idtoken"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
StateBucket string `env:"STATE_BUCKET,required"`
|
||||
Sink string `env:"K_SINK,required"`
|
||||
BatchLimit int `env:"BATCH_LIMIT,default=1000"`
|
||||
}
|
||||
|
||||
type changesResponse struct {
|
||||
Results []change `json:"results"`
|
||||
LastSeq int64 `json:"last_seq"`
|
||||
}
|
||||
|
||||
type change struct {
|
||||
Seq int64 `json:"seq"`
|
||||
ID string `json:"id"`
|
||||
Deleted bool `json:"deleted"`
|
||||
}
|
||||
|
||||
// npmMetadata is the subset of npm package metadata we parse.
|
||||
type npmMetadata struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
DistTags map[string]string `json:"dist-tags"`
|
||||
Homepage string `json:"homepage"`
|
||||
License json.RawMessage `json:"license"`
|
||||
}
|
||||
|
||||
// packageRelease is the structured CloudEvent data payload,
|
||||
// matching the BigQuery schema in terraform/schema.json.
|
||||
type packageRelease struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
License string `json:"license"`
|
||||
Homepage string `json:"homepage"`
|
||||
}
|
||||
|
||||
type packageDeletion struct {
|
||||
Name string `json:"name"`
|
||||
DeletedAt string `json:"deleted_at"`
|
||||
}
|
||||
|
||||
const stateObject = "seq.txt"
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
var cfg config
|
||||
if err := envconfig.Process(ctx, &cfg); err != nil {
|
||||
clog.FatalContext(ctx, "failed to process config", "error", err)
|
||||
}
|
||||
|
||||
gcs, err := storage.NewClient(ctx)
|
||||
if err != nil {
|
||||
clog.FatalContext(ctx, "failed to create GCS client", "error", err)
|
||||
}
|
||||
defer gcs.Close()
|
||||
|
||||
// Use an ID-token-authenticated HTTP client to call the private
|
||||
// Cloud Run broker service.
|
||||
idClient, err := idtoken.NewClient(ctx, cfg.Sink)
|
||||
if err != nil {
|
||||
clog.FatalContext(ctx, "failed to create ID token client", "error", err)
|
||||
}
|
||||
ce, err := cloudevents.NewClientHTTP(
|
||||
cloudevents.WithTarget(cfg.Sink),
|
||||
cehttp.WithClient(*idClient),
|
||||
)
|
||||
if err != nil {
|
||||
clog.FatalContext(ctx, "failed to create CloudEvents client", "error", err)
|
||||
}
|
||||
|
||||
seq, err := readLastSeq(ctx, gcs, cfg.StateBucket)
|
||||
if err != nil {
|
||||
clog.FatalContext(ctx, "failed to read last_seq", "error", err)
|
||||
}
|
||||
clog.InfoContext(ctx, "starting poll", "last_seq", seq)
|
||||
|
||||
// Process batches until we've caught up with the feed.
|
||||
for {
|
||||
changes, err := pollChanges(ctx, seq, cfg.BatchLimit)
|
||||
if err != nil {
|
||||
clog.FatalContext(ctx, "failed to poll changes", "error", err)
|
||||
}
|
||||
if len(changes.Results) == 0 {
|
||||
break
|
||||
}
|
||||
clog.InfoContext(ctx, "fetched changes", "count", len(changes.Results), "since", seq)
|
||||
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
var batchErr bool
|
||||
for _, c := range changes.Results {
|
||||
if c.Deleted {
|
||||
if err := sendDeletion(ctx, ce, c, now); err != nil {
|
||||
clog.ErrorContext(ctx, "failed to send deletion event, stopping", "package", c.ID, "error", err)
|
||||
batchErr = true
|
||||
break
|
||||
}
|
||||
if c.Seq > seq {
|
||||
seq = c.Seq
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
rel, err := fetchAndTransform(ctx, c.ID)
|
||||
if err != nil {
|
||||
clog.ErrorContext(ctx, "failed to fetch metadata, stopping", "package", c.ID, "error", err)
|
||||
batchErr = true
|
||||
break
|
||||
}
|
||||
if rel == nil {
|
||||
if err := sendDeletion(ctx, ce, c, now); err != nil {
|
||||
clog.ErrorContext(ctx, "failed to send deletion event, stopping", "package", c.ID, "error", err)
|
||||
batchErr = true
|
||||
break
|
||||
}
|
||||
if c.Seq > seq {
|
||||
seq = c.Seq
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
event := cloudevents.NewEvent()
|
||||
event.SetType("dev.npm.package.released.v1")
|
||||
event.SetSource("npm-poller-cron")
|
||||
event.SetID(fmt.Sprintf("%s-%d", strings.ReplaceAll(c.ID, "/", "_"), c.Seq))
|
||||
if err := event.SetData(cloudevents.ApplicationJSON, rel); err != nil {
|
||||
clog.ErrorContext(ctx, "failed to set event data", "package", c.ID, "error", err)
|
||||
batchErr = true
|
||||
break
|
||||
}
|
||||
|
||||
if result := ce.Send(ctx, event); !cloudevents.IsACK(result) {
|
||||
clog.ErrorContext(ctx, "failed to send event, stopping", "package", c.ID, "result", result)
|
||||
batchErr = true
|
||||
break
|
||||
}
|
||||
|
||||
if c.Seq > seq {
|
||||
seq = c.Seq
|
||||
}
|
||||
clog.InfoContext(ctx, "sent event", "package", c.ID, "seq", c.Seq)
|
||||
}
|
||||
|
||||
// Save progress after each batch so we don't reprocess on restart.
|
||||
if err := writeLastSeq(ctx, gcs, cfg.StateBucket, seq); err != nil {
|
||||
clog.FatalContext(ctx, "failed to update last_seq", "error", err)
|
||||
}
|
||||
clog.InfoContext(ctx, "saved progress", "last_seq", seq)
|
||||
|
||||
if batchErr {
|
||||
clog.FatalContext(ctx, "exiting due to batch error")
|
||||
}
|
||||
}
|
||||
|
||||
clog.InfoContext(ctx, "caught up", "last_seq", seq)
|
||||
}
|
||||
|
||||
// readLastSeq reads the last processed sequence number from GCS.
|
||||
// If the state object doesn't exist yet, it bootstraps from the
|
||||
// registry's current sequence number.
|
||||
func readLastSeq(ctx context.Context, client *storage.Client, bucket string) (int64, error) {
|
||||
r, err := client.Bucket(bucket).Object(stateObject).NewReader(ctx)
|
||||
if err == storage.ErrObjectNotExist {
|
||||
return getCurrentSeq(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("reading %s: %w", stateObject, err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("reading data: %w", err)
|
||||
}
|
||||
return strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
}
|
||||
|
||||
// getCurrentSeq fetches the registry's current update_seq to
|
||||
// bootstrap state on the first run.
|
||||
func getCurrentSeq(ctx context.Context) (int64, error) {
|
||||
resp, err := http.Get("https://replicate.npmjs.com/registry/")
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("fetching registry info: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var info struct {
|
||||
UpdateSeq int64 `json:"update_seq"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
||||
return 0, fmt.Errorf("decoding registry info: %w", err)
|
||||
}
|
||||
clog.InfoContext(ctx, "bootstrapped from registry", "update_seq", info.UpdateSeq)
|
||||
return info.UpdateSeq, nil
|
||||
}
|
||||
|
||||
func writeLastSeq(ctx context.Context, client *storage.Client, bucket string, seq int64) error {
|
||||
w := client.Bucket(bucket).Object(stateObject).NewWriter(ctx)
|
||||
if _, err := fmt.Fprintf(w, "%d", seq); err != nil {
|
||||
w.Close()
|
||||
return err
|
||||
}
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
func pollChanges(ctx context.Context, since int64, limit int) (*changesResponse, error) {
|
||||
url := fmt.Sprintf("https://replicate.npmjs.com/registry/_changes?since=%d&limit=%d", since, limit)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching changes: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, body)
|
||||
}
|
||||
|
||||
var changes changesResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&changes); err != nil {
|
||||
return nil, fmt.Errorf("decoding changes: %w", err)
|
||||
}
|
||||
return &changes, nil
|
||||
}
|
||||
|
||||
func sendDeletion(ctx context.Context, ce cloudevents.Client, c change, deletedAt string) error {
|
||||
event := cloudevents.NewEvent()
|
||||
event.SetType("dev.npm.package.deleted.v1")
|
||||
event.SetSource("npm-poller-cron")
|
||||
event.SetID(fmt.Sprintf("%s-%d", strings.ReplaceAll(c.ID, "/", "_"), c.Seq))
|
||||
if err := event.SetData(cloudevents.ApplicationJSON, &packageDeletion{
|
||||
Name: c.ID,
|
||||
DeletedAt: deletedAt,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if result := ce.Send(ctx, event); !cloudevents.IsACK(result) {
|
||||
return fmt.Errorf("sending deletion event: %s", result)
|
||||
}
|
||||
clog.InfoContext(ctx, "sent deletion event", "package", c.ID, "seq", c.Seq)
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchAndTransform fetches full package metadata from the npm registry
|
||||
// and extracts the fields we care about.
|
||||
func fetchAndTransform(ctx context.Context, name string) (*packageRelease, error) {
|
||||
url := fmt.Sprintf("https://registry.npmjs.org/%s", name)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching %s: %w", name, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, nil // package was unpublished/deleted
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status %d for %s", resp.StatusCode, name)
|
||||
}
|
||||
|
||||
var meta npmMetadata
|
||||
if err := json.NewDecoder(resp.Body).Decode(&meta); err != nil {
|
||||
return nil, fmt.Errorf("decoding %s: %w", name, err)
|
||||
}
|
||||
|
||||
rel := &packageRelease{
|
||||
Name: meta.Name,
|
||||
Description: meta.Description,
|
||||
Homepage: meta.Homepage,
|
||||
License: extractLicense(meta.License),
|
||||
}
|
||||
if v, ok := meta.DistTags["latest"]; ok {
|
||||
rel.Version = v
|
||||
}
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
// extractLicense handles the npm license field, which can be
|
||||
// either a string ("MIT") or an object ({"type": "MIT", "url": "..."}).
|
||||
func extractLicense(raw json.RawMessage) string {
|
||||
if len(raw) == 0 {
|
||||
return ""
|
||||
}
|
||||
var s string
|
||||
if err := json.Unmarshal(raw, &s); err == nil {
|
||||
return s
|
||||
}
|
||||
var obj struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &obj); err == nil {
|
||||
return obj.Type
|
||||
}
|
||||
return ""
|
||||
}
|
||||
166
terraform/main.tf
Normal file
166
terraform/main.tf
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
}
|
||||
google-beta = {
|
||||
source = "hashicorp/google-beta"
|
||||
}
|
||||
ko = {
|
||||
source = "ko-build/ko"
|
||||
}
|
||||
cosign = {
|
||||
source = "chainguard-dev/cosign"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "google" {
|
||||
project = var.project_id
|
||||
}
|
||||
|
||||
provider "google-beta" {
|
||||
project = var.project_id
|
||||
}
|
||||
|
||||
provider "ko" {
|
||||
repo = "gcr.io/${var.project_id}"
|
||||
}
|
||||
|
||||
# Enable required GCP APIs.
|
||||
resource "google_project_service" "apis" {
|
||||
for_each = toset([
|
||||
"run.googleapis.com",
|
||||
"cloudscheduler.googleapis.com",
|
||||
"pubsub.googleapis.com",
|
||||
"bigquery.googleapis.com",
|
||||
"bigquerydatatransfer.googleapis.com",
|
||||
"storage.googleapis.com",
|
||||
"compute.googleapis.com",
|
||||
"artifactregistry.googleapis.com",
|
||||
"dns.googleapis.com",
|
||||
])
|
||||
service = each.value
|
||||
disable_on_destroy = false
|
||||
}
|
||||
|
||||
# --- Networking ---
|
||||
|
||||
module "networking" {
|
||||
source = "chainguard-dev/common/infra//modules/networking"
|
||||
|
||||
name = "npm-snoop-net"
|
||||
project_id = var.project_id
|
||||
regions = [var.region]
|
||||
team = "npm-snoop"
|
||||
hosted_zone_logging_enabled = false
|
||||
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
data "google_client_openid_userinfo" "me" {}
|
||||
|
||||
locals {
|
||||
provisioner = "user:${data.google_client_openid_userinfo.me.email}"
|
||||
}
|
||||
|
||||
# --- State Storage ---
|
||||
|
||||
resource "google_storage_bucket" "state" {
|
||||
name = "${var.project_id}-npm-snoop-state"
|
||||
location = var.region
|
||||
uniform_bucket_level_access = true
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
# --- Service Account ---
|
||||
|
||||
resource "google_service_account" "poller" {
|
||||
account_id = "npm-snoop-poller"
|
||||
display_name = "npm-snoop poller"
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
resource "google_storage_bucket_iam_member" "poller_state" {
|
||||
bucket = google_storage_bucket.state.name
|
||||
role = "roles/storage.objectAdmin"
|
||||
member = "serviceAccount:${google_service_account.poller.email}"
|
||||
}
|
||||
|
||||
# --- Event Broker ---
|
||||
|
||||
module "broker" {
|
||||
source = "chainguard-dev/common/infra//modules/cloudevent-broker"
|
||||
|
||||
project_id = var.project_id
|
||||
name = "npm-snoop"
|
||||
regions = module.networking.regional-networks
|
||||
notification_channels = []
|
||||
team = "npm-snoop"
|
||||
deletion_protection = var.deletion_protection
|
||||
}
|
||||
|
||||
# Authorize the poller SA to publish to the broker's ingress.
|
||||
module "authorize-poller" {
|
||||
source = "chainguard-dev/common/infra//modules/authorize-private-service"
|
||||
|
||||
project_id = var.project_id
|
||||
region = var.region
|
||||
name = module.broker.ingress.name
|
||||
service-account = google_service_account.poller.email
|
||||
}
|
||||
|
||||
# --- Event Recorder (BigQuery) ---
|
||||
|
||||
module "recorder" {
|
||||
source = "chainguard-dev/common/infra//modules/cloudevent-recorder"
|
||||
|
||||
project_id = var.project_id
|
||||
name = "npm-snoop-recorder"
|
||||
provisioner = local.provisioner
|
||||
retention-period = var.bq_retention_days
|
||||
regions = module.networking.regional-networks
|
||||
broker = module.broker.broker
|
||||
notification_channels = []
|
||||
team = "npm-snoop"
|
||||
deletion_protection = var.deletion_protection
|
||||
|
||||
types = {
|
||||
"dev.npm.package.released.v1" = {
|
||||
schema = file("${path.module}/schema.json")
|
||||
}
|
||||
"dev.npm.package.deleted.v1" = {
|
||||
schema = file("${path.module}/schema-deleted.json")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Cron Job (The Poller) ---
|
||||
|
||||
module "cron" {
|
||||
source = "chainguard-dev/common/infra//modules/cron"
|
||||
|
||||
name = "npm-snoop"
|
||||
project_id = var.project_id
|
||||
region = var.region
|
||||
schedule = var.schedule
|
||||
service_account = google_service_account.poller.email
|
||||
working_dir = "${path.module}/.."
|
||||
importpath = "."
|
||||
notification_channels = []
|
||||
team = "npm-snoop"
|
||||
deletion_protection = var.deletion_protection
|
||||
|
||||
vpc_access = {
|
||||
network_interfaces = [{
|
||||
network = module.networking.network_id
|
||||
subnetwork = module.networking.regional-networks[var.region].subnet
|
||||
}]
|
||||
egress = "PRIVATE_RANGES_ONLY"
|
||||
}
|
||||
|
||||
env = {
|
||||
"K_SINK" = module.authorize-poller.uri
|
||||
"STATE_BUCKET" = google_storage_bucket.state.name
|
||||
}
|
||||
}
|
||||
14
terraform/outputs.tf
Normal file
14
terraform/outputs.tf
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
output "dataset_id" {
|
||||
value = module.recorder.dataset_id
|
||||
description = "BigQuery dataset ID."
|
||||
}
|
||||
|
||||
output "table_ids" {
|
||||
value = module.recorder.table_ids
|
||||
description = "BigQuery table IDs by event type."
|
||||
}
|
||||
|
||||
output "state_bucket" {
|
||||
value = google_storage_bucket.state.name
|
||||
description = "GCS bucket holding the poller sequence state."
|
||||
}
|
||||
4
terraform/schema-deleted.json
Normal file
4
terraform/schema-deleted.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[
|
||||
{"name": "name", "type": "STRING", "mode": "NULLABLE", "description": "Package name"},
|
||||
{"name": "deleted_at", "type": "STRING", "mode": "NULLABLE", "description": "Deletion timestamp (RFC3339)"}
|
||||
]
|
||||
7
terraform/schema.json
Normal file
7
terraform/schema.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[
|
||||
{"name": "name", "type": "STRING", "mode": "NULLABLE", "description": "Package name"},
|
||||
{"name": "version", "type": "STRING", "mode": "NULLABLE", "description": "Latest version from dist-tags"},
|
||||
{"name": "description", "type": "STRING", "mode": "NULLABLE", "description": "Package description"},
|
||||
{"name": "license", "type": "STRING", "mode": "NULLABLE", "description": "Package license"},
|
||||
{"name": "homepage", "type": "STRING", "mode": "NULLABLE", "description": "Package homepage URL"}
|
||||
]
|
||||
28
terraform/variables.tf
Normal file
28
terraform/variables.tf
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
variable "project_id" {
|
||||
type = string
|
||||
description = "GCP project ID to deploy into."
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
type = string
|
||||
description = "GCP region for all resources."
|
||||
default = "us-east4"
|
||||
}
|
||||
|
||||
variable "schedule" {
|
||||
type = string
|
||||
description = "Cron schedule for the poller."
|
||||
default = "*/15 * * * *"
|
||||
}
|
||||
|
||||
variable "bq_retention_days" {
|
||||
type = number
|
||||
description = "Days to retain data in BigQuery."
|
||||
default = 365
|
||||
}
|
||||
|
||||
variable "deletion_protection" {
|
||||
type = bool
|
||||
description = "Enable deletion protection on resources."
|
||||
default = false
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue