Compare commits
12
Commits
deploy-prep
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f672019c91 | ||
|
|
f553a87ce6 | ||
|
|
69d586bfcc | ||
|
|
25dda00eaf | ||
|
|
0959065d82 | ||
|
|
3d38240b2f | ||
|
|
a1669d6be1 | ||
|
|
d48f2f5e0c | ||
|
|
e70a5beac4 | ||
|
|
973c1bbcd3 | ||
|
|
31524e9ddb | ||
|
|
b85df9ee7a |
@@ -3,7 +3,8 @@
|
||||
|
||||
PROJECT ?= $(shell cd infra && pulumi config get gcp:project 2>/dev/null)
|
||||
REGION ?= $(shell cd infra && pulumi config get gcp:region 2>/dev/null || echo us-east1)
|
||||
ZONE ?= $(shell cd infra && pulumi config get gitea:zone 2>/dev/null || echo $(REGION)-a)
|
||||
# -b, matching the default in infra/pkg/config: us-east1 has no -a zone.
|
||||
ZONE ?= $(shell cd infra && pulumi config get gitea:zone 2>/dev/null || echo $(REGION)-b)
|
||||
VM ?= gitea-vm
|
||||
|
||||
.PHONY: help
|
||||
@@ -19,9 +20,11 @@ bootstrap: ## One-time project setup (run before the first `make up`)
|
||||
fmt: ## Format Go sources
|
||||
cd infra && gofmt -w .
|
||||
|
||||
# -o gitea-infra: Pulumi.yaml points the go runtime at this prebuilt binary, so
|
||||
# Pulumi runs it rather than compiling. Without it preview/up fail outright.
|
||||
.PHONY: check
|
||||
check: ## Build and vet the Pulumi program, and syntax-check the shell scripts
|
||||
cd infra && go build ./... && go vet ./...
|
||||
cd infra && go build -o gitea-infra . && go vet ./...
|
||||
bash -n vm/bootstrap.sh scripts/bootstrap.sh
|
||||
@command -v shellcheck >/dev/null && shellcheck -S warning vm/bootstrap.sh scripts/bootstrap.sh || echo "shellcheck not installed -- skipped"
|
||||
|
||||
@@ -41,12 +44,20 @@ up: check ## Apply the infrastructure
|
||||
# to cb-image@, not to whatever default Cloud Build account this project
|
||||
# happens to have -- and on newer projects the legacy default does not exist.
|
||||
# Without this the images push fine and the rollout step fails.
|
||||
# --gcs-source-staging-dir: running as cb-image@, the build must be able to read
|
||||
# the uploaded source. Pulumi grants that on this bucket only; the default
|
||||
# <project>_cloudbuild bucket is unreadable to it and the build fails at
|
||||
# "could not resolve source".
|
||||
# SHORT_SHA: Cloud Build fills it in only for triggered builds. For `builds
|
||||
# submit` it is empty, and image.yaml's `--tag <image>:$SHORT_SHA` becomes an
|
||||
# invalid reference that fails the build.
|
||||
.PHONY: build
|
||||
build: ## Build and roll out the container images via Cloud Build
|
||||
gcloud builds submit --config cloudbuild/image.yaml --project $(PROJECT) \
|
||||
--region=$(REGION) \
|
||||
--service-account=projects/$(PROJECT)/serviceAccounts/cb-image@$(PROJECT).iam.gserviceaccount.com \
|
||||
--substitutions=_REGION=$(REGION),_ZONE=$(ZONE)
|
||||
--gcs-source-staging-dir=gs://$(PROJECT)-gitea-build-source/source \
|
||||
--substitutions=_REGION=$(REGION),_ZONE=$(ZONE),SHORT_SHA=$(shell git rev-parse --short=7 HEAD)
|
||||
|
||||
.PHONY: rollout
|
||||
rollout: ## Pull the latest :prod images onto the VM right now
|
||||
|
||||
@@ -47,14 +47,23 @@ printf %s '<token>' | gcloud secrets versions add github-pat --data-file=- --pro
|
||||
# 3. Confirm the Cloud DNS zone is authoritative. DNS-01 cannot work otherwise.
|
||||
dig NS gitea.jasonmross.dev
|
||||
|
||||
# 4. Configure and apply.
|
||||
# 4. The ACME contact address. Kept in Secret Manager, not stack config, so it
|
||||
# stays out of this public repo; the VM reads it when rendering the Caddyfile.
|
||||
printf %s 'you@example.com' | gcloud secrets versions add gitea-acme-email --data-file=- --project <project-id>
|
||||
|
||||
# 5. Configure and apply. Pulumi's GCS backend and Google provider use
|
||||
# Application Default Credentials, not your gcloud login.
|
||||
gcloud auth application-default login
|
||||
# Use the passphrase bootstrap.sh generated. Letting `stack init` prompt for a
|
||||
# new one encrypts the stack with a key Cloud Build's infra trigger never sees.
|
||||
export PULUMI_CONFIG_PASSPHRASE=$(gcloud secrets versions access latest \
|
||||
--secret=pulumi-config-passphrase --project <project-id>)
|
||||
cd infra
|
||||
pulumi login gs://<project-id>-pulumi-state
|
||||
pulumi stack init prod
|
||||
pulumi config set gcp:project <project-id>
|
||||
pulumi config set gitea:domain gitea.jasonmross.dev
|
||||
pulumi config set gitea:dnsZone <cloud-dns-managed-zone-name> # gcloud dns managed-zones list
|
||||
pulumi config set gitea:acmeEmail you@example.com
|
||||
pulumi config set gitea:githubOwner <owner>
|
||||
pulumi config set gitea:githubAppInstallationId <id>
|
||||
pulumi config set gitea:infraBuildServiceAccount cb-infra@<project-id>.iam.gserviceaccount.com
|
||||
@@ -62,10 +71,10 @@ pulumi config set gitea:infraBuildServiceAccount cb-infra@<project-id>.iam.gserv
|
||||
pulumi config set gitea:wafMode DetectionOnly
|
||||
pulumi up
|
||||
|
||||
# 5. First image build. Until this runs, the :prod images do not exist.
|
||||
# 6. First image build. Until this runs, the :prod images do not exist.
|
||||
cd .. && make build
|
||||
|
||||
# 6. Create the admin user.
|
||||
# 7. Create the admin user.
|
||||
make ssh
|
||||
sudo podman exec -u 1000 gitea gitea admin user create \
|
||||
-c /etc/gitea/app.ini --admin --username <you> --email <you@example.com> --random-password
|
||||
@@ -73,7 +82,7 @@ sudo podman exec -u 1000 gitea gitea admin user create \
|
||||
|
||||
### Expected on the first run, not a bug
|
||||
|
||||
Between step 4 and step 5 the `:prod` images do not exist yet, so `gitea.service`
|
||||
Between step 5 and step 6 the `:prod` images do not exist yet, so `gitea.service`
|
||||
and `caddy.service` crash-loop. That is intentional: the units carry
|
||||
`Restart=always` with `StartLimitIntervalSec=0`, so they recover on their own
|
||||
within 30 seconds of the first successful push. Likewise, `app.ini` is not
|
||||
@@ -91,6 +100,11 @@ make backup # on-demand gitea dump to GCS
|
||||
make ssh # shell via IAP
|
||||
```
|
||||
|
||||
The targets read the project and zone from the Pulumi stack, which needs
|
||||
Application Default Credentials (`gcloud auth application-default login`).
|
||||
Without them `pulumi config get` fails quietly and gcloud runs with an empty
|
||||
`--project=`; pass `PROJECT=<project-id>` to skip the lookup.
|
||||
|
||||
A push to `main` under `image/**` builds, pushes, rolls out, and gates on
|
||||
`/api/healthz`. A push under `infra/**` or `vm/**` runs `pulumi up` and then
|
||||
re-syncs the VM configuration. Anything else does nothing.
|
||||
|
||||
@@ -36,6 +36,9 @@ steps:
|
||||
|
||||
- id: build-gitea
|
||||
name: gcr.io/cloud-builders/docker
|
||||
# Both Dockerfiles are written for BuildKit (`# syntax=`, COPY --chmod). This
|
||||
# builder image defaults to the legacy builder, which rejects --chmod.
|
||||
env: [DOCKER_BUILDKIT=1]
|
||||
entrypoint: bash
|
||||
args:
|
||||
- -c
|
||||
@@ -52,6 +55,7 @@ steps:
|
||||
|
||||
- id: build-caddy
|
||||
name: gcr.io/cloud-builders/docker
|
||||
env: [DOCKER_BUILDKIT=1]
|
||||
entrypoint: bash
|
||||
# xcaddy runs inside the Dockerfile's golang builder stage, so the plain
|
||||
# docker builder is all this step needs -- no Go toolchain out here.
|
||||
|
||||
@@ -29,6 +29,10 @@ steps:
|
||||
- -c
|
||||
- |
|
||||
set -euo pipefail
|
||||
# Pulumi.yaml points the go runtime at a prebuilt binary, so Pulumi
|
||||
# runs ./gitea-infra rather than compiling the program itself.
|
||||
go build -o gitea-infra .
|
||||
|
||||
# Self-managed GCS backend: no external SaaS dependency, and the state
|
||||
# bucket is versioned so history is recoverable.
|
||||
pulumi login "gs://$PROJECT_ID-pulumi-state"
|
||||
|
||||
@@ -58,6 +58,13 @@ curl -vI https://gitea.jasonmross.dev # valid Let's Encrypt cert
|
||||
DNS-01 means renewal does not need inbound port 80 at all. That is testable:
|
||||
temporarily remove the `gitea-allow-web` port 80 rule and force a renewal.
|
||||
|
||||
The ACME account and certificates live in the `caddy-data` podman volume, under
|
||||
`/var/lib/containers` on the **boot** disk, not the data disk. Replacing the
|
||||
instance therefore re-registers and re-issues on first start. That is fine
|
||||
occasionally, but Let's Encrypt allows 5 duplicate certificates per week, so
|
||||
several replacements in a few days can lock issuance out until the window
|
||||
rolls over.
|
||||
|
||||
### fail2ban — drill it, do not trust the status output
|
||||
|
||||
```bash
|
||||
|
||||
+4
-10
@@ -4,15 +4,12 @@
|
||||
# infrastructure metadata. The Gitea application secrets live in Secret Manager
|
||||
# and are never read by this program.
|
||||
config:
|
||||
gcp:project: CHANGEME-gitea-project-id
|
||||
gcp:project: gitea-496920
|
||||
gcp:region: us-east1
|
||||
|
||||
gitea:domain: gitea.jasonmross.dev
|
||||
# The Cloud DNS *resource* name of the existing managed zone, which is not
|
||||
# necessarily the DNS name. `gcloud dns managed-zones list` to find it.
|
||||
gitea:dnsZone: CHANGEME-managed-zone-name
|
||||
gitea:acmeEmail: CHANGEME@example.com
|
||||
|
||||
gitea:dnsZone: main
|
||||
# us-east1 has zones b, c and d -- there is no us-east1-a.
|
||||
gitea:zone: us-east1-b
|
||||
# e2-small: 2 shared vCPU, 2 GB RAM. See docs/runbook.md ("Memory on a 2 GB
|
||||
@@ -20,23 +17,20 @@ config:
|
||||
gitea:machineType: e2-small
|
||||
gitea:bootDiskGb: "20"
|
||||
gitea:dataDiskGb: "30"
|
||||
|
||||
gitea:appName: Gitea
|
||||
gitea:requireSigninView: "false"
|
||||
gitea:podmanSubnet: 10.89.10.0/24
|
||||
|
||||
# Coraza WAF: On | DetectionOnly | Off.
|
||||
# Start in DetectionOnly, review what it flags (docs/waf.md), then switch to
|
||||
# On. The fail2ban jail that bans on WAF verdicts follows this value.
|
||||
gitea:wafMode: DetectionOnly
|
||||
|
||||
# Cloud Build source. The GitHub App installation id comes from the URL of the
|
||||
# app's settings page after you install it on the repository.
|
||||
gitea:githubOwner: JMR-dev
|
||||
gitea:githubRepo: Gitea
|
||||
gitea:githubAppInstallationId: "0"
|
||||
gitea:githubPatSecret: github-pat
|
||||
|
||||
# Created by scripts/bootstrap.sh before the first `pulumi up`, because it is
|
||||
# the identity that runs Pulumi and therefore cannot be created by Pulumi.
|
||||
gitea:infraBuildServiceAccount: CHANGEME@CHANGEME.iam.gserviceaccount.com
|
||||
gitea:infraBuildServiceAccount: cb-infra@gitea-496920.iam.gserviceaccount.com
|
||||
encryptionsalt: v1:pIXPmM64Bzc=:v1:0pkb4B2RVM5LFu2v:ZEPaG4RB9ySlpmaHkaBy8v6FQ3paHg==
|
||||
|
||||
+26
-26
@@ -1,8 +1,8 @@
|
||||
module gitea-infra
|
||||
|
||||
go 1.25.11
|
||||
go 1.26.0
|
||||
|
||||
toolchain go1.26.6
|
||||
toolchain go1.26.9
|
||||
|
||||
require (
|
||||
github.com/pulumi/pulumi-gcp/sdk/v9 v9.34.1
|
||||
@@ -40,14 +40,14 @@ require (
|
||||
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
||||
github.com/go-git/go-billy/v6 v6.0.0-alpha.2 // indirect
|
||||
github.com/go-git/go-git/v6 v6.0.0-alpha.5 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/logr v1.4.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/glog v1.2.5 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
@@ -93,30 +93,30 @@ require (
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/collector/featuregate v1.53.0 // indirect
|
||||
go.opentelemetry.io/collector/pdata v1.53.0 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.18.0 // indirect
|
||||
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/log v0.19.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.20.1 // indirect
|
||||
go.opentelemetry.io/otel v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/log v0.22.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v0.21.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.46.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.54.0 // indirect
|
||||
golang.org/x/mod v0.38.0 // indirect
|
||||
golang.org/x/net v0.57.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/term v0.45.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
golang.org/x/tools v0.47.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
google.golang.org/grpc v1.82.1 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/mod v0.41.0 // indirect
|
||||
golang.org/x/net v0.60.0 // indirect
|
||||
golang.org/x/sync v0.23.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/term v0.46.0 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
golang.org/x/tools v0.49.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect
|
||||
google.golang.org/grpc v1.83.2 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/frand v1.4.2 // indirect
|
||||
|
||||
+57
-57
@@ -76,8 +76,8 @@ github.com/go-git/go-git-fixtures/v6 v6.0.0-alpha.1/go.mod h1:ECf1MqJlBdYpKggBrO
|
||||
github.com/go-git/go-git/v6 v6.0.0-alpha.5 h1:sE+OlkHgYWNMVmN1s9sR7uyFgsWLtxcNWse/vBYKxRE=
|
||||
github.com/go-git/go-git/v6 v6.0.0-alpha.5/go.mod h1:3IjhiZnM+uBmUrOGSeqrJpsmi4Vd0H2NZO/uK2a7d0s=
|
||||
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/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||
github.com/go-logr/logr v1.4.4/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/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
@@ -98,8 +98,8 @@ github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:E
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
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/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -128,9 +128,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
@@ -176,7 +175,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk=
|
||||
github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 h1:vkHw5I/plNdTr435cARxCW6q9gc0S/Yxz7Mkd38pOb0=
|
||||
github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231/go.mod h1:murToZ2N9hNJzewjHBgfFdXhZKjY3z5cYC1VXk+lbFE=
|
||||
@@ -207,8 +205,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
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/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U=
|
||||
github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8=
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
|
||||
@@ -231,32 +229,32 @@ go.opentelemetry.io/collector/internal/testutil v0.147.0 h1:DFlRxBRp23/sZnpTITK2
|
||||
go.opentelemetry.io/collector/internal/testutil v0.147.0/go.mod h1:Jkjs6rkqs973LqgZ0Fe3zrokQRKULYXPIf4HuqStiEE=
|
||||
go.opentelemetry.io/collector/pdata v1.53.0 h1:DlYDbRwammEZaxDZHINx5v0n8SEOVNniPbi6FRTlVkA=
|
||||
go.opentelemetry.io/collector/pdata v1.53.0/go.mod h1:LRSYGNjKXaUrZEwZv3Yl+8/zV2HmRGKXW62zB2bysms=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.18.0 h1:hhPGP3zvvy1xWT9RTy970wlniSxFttBIsAK1gvMguJM=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.18.0/go.mod h1:twJF7inoMza6kxMcF8JOdL3mPmtOZu7GEr34CUNE6Dg=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 h1:ao6Oe+wSebTlQ1OEht7jlYTzQKE+pnx/iNywFvTbuuI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0/go.mod h1:u3T6vz0gh/NVzgDgiwkgLxpsSF6PaPmo2il0apGJbls=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 h1:mq/Qcf28TWz719lE3/hMB4KkyDuLJIvgJnFGcd0kEUI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0/go.mod h1:yk5LXEYhsL2htyDNJbEq7fWzNEigeEdV5xBF/Y+kAv0=
|
||||
go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
|
||||
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
|
||||
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.20.1 h1:5sHc4ToTFjfSZCtGAAM6jPunICAmJX73htv372T4ipc=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.20.1/go.mod h1:oa6kgvyz/3GYW04dohd0++xJIH4xdQY8PAbpeCMaM8M=
|
||||
go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
|
||||
go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0 h1:WseeVYf5dJZTsyPiyW5L14k5qsSibqXAMTSiFEDiWr0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0/go.mod h1:SiLZnQS6Qk2eCpvr2CH/XMAOa64TWGXxEZJZCpD2Lmc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 h1:fG5MCxGz8+2VtrN/WgqSpJFctVz24gpxj8CxkKmc8Ww=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0/go.mod h1:BmAYTn+3ysbRe+IU2msxmf5Rx3g6DHvex+tWI3LdhYI=
|
||||
go.opentelemetry.io/otel/log v0.22.0 h1:5DBNnfvaJ6CVdkJ+Jle8Tzs50aSSv49TXGj9XRsEYw0=
|
||||
go.opentelemetry.io/otel/log v0.22.0/go.mod h1:gzOt/R67vF2GniAqWu8Qv0SXy89f71muHcrkz76PCdc=
|
||||
go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
|
||||
go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
|
||||
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
|
||||
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
|
||||
go.opentelemetry.io/otel/sdk/log v0.21.0 h1:QsE7XSR0ktQdKmRKGnR+f1ObGF32WG+7MER/P9KgmYc=
|
||||
go.opentelemetry.io/otel/sdk/log v0.21.0/go.mod h1:m9mApjCoD2/1QuKCAptjv+BrG9WKOvQLVdNx+iBldTo=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.21.0 h1:X+JBBgKlswCGYsmgL0CnoUUtlE//VB345c84jYAYkdQ=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.21.0/go.mod h1:HD1575K8e6sIFBBDd5tZB3t9DlMytWXq9FuR+Y4rfjE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
|
||||
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
|
||||
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
|
||||
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
|
||||
go.opentelemetry.io/proto/slim/otlp v1.9.0 h1:fPVMv8tP3TrsqlkH1HWYUpbCY9cAIemx184VGkS6vlE=
|
||||
go.opentelemetry.io/proto/slim/otlp v1.9.0/go.mod h1:xXdeJJ90Gqyll+orzUkY4bOd2HECo5JofeoLpymVqdI=
|
||||
go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0 h1:o13nadWDNkH/quoDomDUClnQBpdQQ2Qqv0lQBjIXjE8=
|
||||
@@ -270,31 +268,33 @@ 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.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
|
||||
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -305,34 +305,34 @@ golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
|
||||
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -63,6 +63,9 @@ func main() {
|
||||
if err := iam.GrantSecrets(ctx, cfg, accounts, secrets.Names); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := iam.GrantSecretRead(ctx, cfg, accounts, secrets.ACMEEmail); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buckets, err := storage.New(ctx, cfg, vmDir, apis)
|
||||
if err != nil {
|
||||
@@ -71,6 +74,9 @@ func main() {
|
||||
if err := iam.GrantBuckets(ctx, accounts, buckets.Config, buckets.Backup); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := iam.GrantBuildSource(ctx, accounts, buckets.BuildSource); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The zone already exists and is delegated; this only adds the A record
|
||||
// and the zone-scoped permission Caddy needs for DNS-01.
|
||||
@@ -97,6 +103,7 @@ func main() {
|
||||
ctx.Export("registry", pulumi.Sprintf("%s/%s/%s", cfg.ARHost(), cfg.Project, registry.RepoID))
|
||||
ctx.Export("configBucket", buckets.Config.Name)
|
||||
ctx.Export("backupBucket", buckets.Backup.Name)
|
||||
ctx.Export("buildSourceBucket", buckets.BuildSource.Name)
|
||||
ctx.Export("configHash", pulumi.String(buckets.ConfigHash))
|
||||
ctx.Export("vmServiceAccount", accounts.VM.Email)
|
||||
ctx.Export("imageServiceAccount", accounts.Image.Email)
|
||||
|
||||
@@ -169,7 +169,6 @@ func New(
|
||||
"image-caddy": pulumi.String(imageCaddy),
|
||||
|
||||
"domain": pulumi.String(cfg.Domain),
|
||||
"acme-email": pulumi.String(cfg.ACMEEmail),
|
||||
"app-name": pulumi.String(cfg.AppName),
|
||||
"require-signin-view": pulumi.String(strconv.FormatBool(cfg.RequireSigninView)),
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ type Config struct {
|
||||
|
||||
Domain string
|
||||
DNSZone string
|
||||
ACMEEmail string
|
||||
AppName string
|
||||
PodmanCIDR string
|
||||
|
||||
@@ -51,7 +50,6 @@ func Load(ctx *pulumi.Context) (*Config, error) {
|
||||
Zone: c.Get("zone"),
|
||||
Domain: c.Require("domain"),
|
||||
DNSZone: c.Require("dnsZone"),
|
||||
ACMEEmail: c.Require("acmeEmail"),
|
||||
AppName: c.Get("appName"),
|
||||
PodmanCIDR: c.Get("podmanSubnet"),
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ package dns
|
||||
import (
|
||||
"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/compute"
|
||||
"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/dns"
|
||||
"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/projects"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
|
||||
|
||||
"gitea-infra/pkg/config"
|
||||
@@ -60,5 +61,19 @@ func New(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The zone-scoped grant is not enough on its own: the googleclouddns plugin
|
||||
// maps the domain to a zone by LISTING the project's managed zones, and a
|
||||
// list is a project-level permission that no zone binding can confer. Without
|
||||
// this, presenting the challenge fails with a bare 403. dns.reader adds
|
||||
// read-only access and nothing else, and every write stays scoped to the zone
|
||||
// above.
|
||||
if _, err := projects.NewIAMMember(ctx, "gitea-vm-dns-reader", &projects.IAMMemberArgs{
|
||||
Project: pulumi.String(cfg.Project),
|
||||
Role: pulumi.String("roles/dns.reader"),
|
||||
Member: pulumi.Sprintf("serviceAccount:%s", vmServiceAccountEmail),
|
||||
}, pulumi.DependsOn(deps)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DNS{Zone: zone, Record: rec}, nil
|
||||
}
|
||||
|
||||
+25
-6
@@ -123,12 +123,7 @@ func GrantRegistry(ctx *pulumi.Context, cfg *config.Config, a *Accounts, repo *a
|
||||
// scripts/bootstrap.sh, not by Pulumi; see package secrets for why.
|
||||
func GrantSecrets(ctx *pulumi.Context, cfg *config.Config, a *Accounts, names []string) error {
|
||||
for _, name := range names {
|
||||
if _, err := secretmanager.NewSecretIamMember(ctx, "vm-read-"+name, &secretmanager.SecretIamMemberArgs{
|
||||
Project: pulumi.String(cfg.Project),
|
||||
SecretId: pulumi.String(name),
|
||||
Role: pulumi.String("roles/secretmanager.secretAccessor"),
|
||||
Member: pulumi.Sprintf("serviceAccount:%s", a.VM.Email),
|
||||
}); err != nil {
|
||||
if err := GrantSecretRead(ctx, cfg, a, name); err != nil {
|
||||
return err
|
||||
}
|
||||
// vm/bootstrap.sh's safety net adds a version if one is somehow missing.
|
||||
@@ -144,6 +139,30 @@ func GrantSecrets(ctx *pulumi.Context, cfg *config.Config, a *Accounts, names []
|
||||
return nil
|
||||
}
|
||||
|
||||
// GrantSecretRead gives the VM read-only access to one secret. On its own it is
|
||||
// for operator-supplied values the VM must never write; GrantSecrets adds
|
||||
// version-adder on top for the ones it may generate.
|
||||
func GrantSecretRead(ctx *pulumi.Context, cfg *config.Config, a *Accounts, name string) error {
|
||||
_, err := secretmanager.NewSecretIamMember(ctx, "vm-read-"+name, &secretmanager.SecretIamMemberArgs{
|
||||
Project: pulumi.String(cfg.Project),
|
||||
SecretId: pulumi.String(name),
|
||||
Role: pulumi.String("roles/secretmanager.secretAccessor"),
|
||||
Member: pulumi.Sprintf("serviceAccount:%s", a.VM.Email),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// GrantBuildSource lets cb-image@ read the tarballs `make build` stages, and
|
||||
// nothing else in storage. See storage.New for why the bucket exists.
|
||||
func GrantBuildSource(ctx *pulumi.Context, a *Accounts, sourceBucket *storage.Bucket) error {
|
||||
_, err := storage.NewBucketIAMMember(ctx, "img-build-source-reader", &storage.BucketIAMMemberArgs{
|
||||
Bucket: sourceBucket.Name,
|
||||
Role: pulumi.String("roles/storage.objectViewer"),
|
||||
Member: pulumi.Sprintf("serviceAccount:%s", a.Image.Email),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// GrantBuckets: read-only on config, write-only on backups. The VM can create a
|
||||
// backup but cannot read or delete existing ones, which limits what ransomware
|
||||
// on the box could do to the backup history.
|
||||
|
||||
@@ -26,3 +26,9 @@ var Names = []string{
|
||||
"gitea-oauth2-jwt-secret",
|
||||
"gitea-lfs-jwt-secret",
|
||||
}
|
||||
|
||||
// ACMEEmail holds the contact address Caddy registers with Let's Encrypt. It
|
||||
// is a secret not because it signs anything but to keep it out of this public
|
||||
// repository and out of instance metadata. Unlike Names it is supplied by the
|
||||
// operator, never generated, so the VM gets read access only.
|
||||
const ACMEEmail = "gitea-acme-email"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package storage holds the two buckets and, importantly, uploads the vm/ tree
|
||||
// Package storage holds the buckets and, importantly, uploads the vm/ tree
|
||||
// as Pulumi-managed objects.
|
||||
//
|
||||
// Uploading the VM configuration through Pulumi (rather than a `gcloud storage
|
||||
@@ -24,6 +24,8 @@ import (
|
||||
type Buckets struct {
|
||||
Config *storage.Bucket
|
||||
Backup *storage.Bucket
|
||||
// BuildSource stages the source tarball for `make build`. See New.
|
||||
BuildSource *storage.Bucket
|
||||
// ConfigHash changes whenever any file under vm/ changes. It is written into
|
||||
// instance metadata so a config change is visible from `describe`, and so
|
||||
// there is something to compare against when debugging drift.
|
||||
@@ -72,12 +74,37 @@ func New(ctx *pulumi.Context, cfg *config.Config, vmDir string, deps []pulumi.Re
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Where `gcloud builds submit` stages its source tarball. Builds run as
|
||||
// cb-image@, which needs storage.objects.get on that tarball. The default
|
||||
// staging bucket is <project>_cloudbuild, created by gcloud on the first
|
||||
// submit -- after `pulumi up`, so there is nothing to bind to in advance --
|
||||
// and project-wide objectViewer would also open the backup and state
|
||||
// buckets. A dedicated bucket keeps the grant exact. Triggered builds fetch
|
||||
// source through the GitHub connection and never touch it.
|
||||
buildSourceBucket, err := storage.NewBucket(ctx, "gitea-build-source", &storage.BucketArgs{
|
||||
Name: pulumi.Sprintf("%s-gitea-build-source", cfg.Project),
|
||||
Location: pulumi.String(strings.ToUpper(cfg.Region)),
|
||||
UniformBucketLevelAccess: pulumi.Bool(true),
|
||||
PublicAccessPrevention: pulumi.String("enforced"),
|
||||
// Tarballs are only read once, by the build they were uploaded for.
|
||||
LifecycleRules: storage.BucketLifecycleRuleArray{
|
||||
&storage.BucketLifecycleRuleArgs{
|
||||
Action: &storage.BucketLifecycleRuleActionArgs{Type: pulumi.String("Delete")},
|
||||
Condition: &storage.BucketLifecycleRuleConditionArgs{Age: pulumi.Int(7)},
|
||||
},
|
||||
},
|
||||
ForceDestroy: pulumi.Bool(true),
|
||||
}, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hash, err := uploadTree(ctx, configBucket, vmDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Buckets{Config: configBucket, Backup: backupBucket, ConfigHash: hash}, nil
|
||||
return &Buckets{Config: configBucket, Backup: backupBucket, BuildSource: buildSourceBucket, ConfigHash: hash}, nil
|
||||
}
|
||||
|
||||
// uploadTree mirrors vmDir into gs://<bucket>/vm/ and returns a content hash of
|
||||
|
||||
+11
-1
@@ -99,6 +99,16 @@ if ! has_version github-pat; then
|
||||
echo " printf %s '<token>' | gcloud secrets versions add github-pat --project=${PROJECT} --data-file=-"
|
||||
fi
|
||||
|
||||
# The ACME contact address Caddy registers with Let's Encrypt. A secret only to
|
||||
# keep it out of this public repository and out of instance metadata. Created
|
||||
# empty: the address is yours to choose, not something to generate.
|
||||
ensure_secret gitea-acme-email
|
||||
if ! has_version gitea-acme-email; then
|
||||
echo " NOTE: secret 'gitea-acme-email' has no value yet. Caddy still issues"
|
||||
echo " certificates without it, but with no contact address. Set it with:"
|
||||
echo " printf %s 'you@example.com' | gcloud secrets versions add gitea-acme-email --project=${PROJECT} --data-file=-"
|
||||
fi
|
||||
|
||||
# Gitea's signing secrets. These MUST come from `gitea generate secret`:
|
||||
# INTERNAL_TOKEN is a JWT, and a random string there produces an instance that
|
||||
# starts and then fails every internal API call in a confusing way.
|
||||
@@ -247,7 +257,7 @@ Next:
|
||||
pulumi stack init prod
|
||||
pulumi config set gcp:project ${PROJECT}
|
||||
pulumi config set gitea:infraBuildServiceAccount ${INFRA_SA_EMAIL}
|
||||
# ...plus domain, dnsZone, acmeEmail, githubOwner, githubAppInstallationId
|
||||
# ...plus domain, dnsZone, githubOwner, githubAppInstallationId
|
||||
pulumi up
|
||||
4. make build # or, spelled out:
|
||||
gcloud builds submit --config cloudbuild/image.yaml --project ${PROJECT} \\
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# GitHub → Gitea migration
|
||||
|
||||
These scripts moved every non-fork repository owned by `JMR-dev` from GitHub into this
|
||||
Gitea instance and made Gitea the primary. GitHub is now a push mirror: Gitea pushes
|
||||
every commit to it. They are kept here for re-runs and for rotating the mirror token.
|
||||
|
||||
All of them are standard-library Python. They read tokens from the environment, never
|
||||
from arguments or files. `verify.py`, `pushmirror.py` and `repoint.py` also read GitHub
|
||||
through the local `gh` CLI.
|
||||
|
||||
| Script | What it does | Writes to |
|
||||
|---|---|---|
|
||||
| `migrate.py` | Full one-time migration: code, issues, PRs, releases, labels, milestones, wiki, LFS. Archives on Gitea whatever is archived on GitHub. | Gitea only |
|
||||
| `verify.py` | Compares every branch and tag sha, the issue/PR/release counts, and the archived flag and visibility. | nothing |
|
||||
| `repoint.py` | Re-points local clones' `JMR-dev` GitHub remotes at Gitea, following renames. Dry run unless `--apply`. | local `.git/config` |
|
||||
| `pushmirror.py` | Creates a sync-on-commit push mirror to GitHub for each active repository. | Gitea, then GitHub through the mirror |
|
||||
|
||||
The input is a snapshot of the repository list:
|
||||
|
||||
```bash
|
||||
gh repo list JMR-dev --limit 1000 \
|
||||
--json name,visibility,isFork,isArchived,diskUsage,description,defaultBranchRef > repos.json
|
||||
```
|
||||
|
||||
## Tokens
|
||||
|
||||
Every token lives in Secret Manager in the Gitea project and is passed in by environment:
|
||||
|
||||
```bash
|
||||
export GITEA_TOKEN=$(gcloud secrets versions access latest --secret=gitea-migration-token --project=<project>)
|
||||
export GITHUB_TOKEN=$(gcloud secrets versions access latest --secret=github-migration-pat --project=<project>) # migrate.py
|
||||
export MIRROR_PAT=$(gcloud secrets versions access latest --secret=github-mirror-pat --project=<project>) # pushmirror.py
|
||||
```
|
||||
|
||||
- `GITEA_TOKEN` needs `write:repository` and `read:issue`.
|
||||
- `GITHUB_TOKEN` for migrating should be a **read-only** fine-grained PAT (Contents, Metadata,
|
||||
Issues, Pull requests). Read-only cannot see draft releases; copy those by hand.
|
||||
- `MIRROR_PAT` needs Contents and Workflows **read & write**. Without Workflows, GitHub rejects
|
||||
any push that touches `.github/workflows/`.
|
||||
|
||||
## Why they are safe to re-run
|
||||
|
||||
- `migrate.py` skips any repository that already exists on Gitea. It never deletes one in
|
||||
order to retry.
|
||||
- `pushmirror.py` skips repositories that already have a GitHub push mirror. It also refuses
|
||||
to create one unless every GitHub branch and tag already matches Gitea. A push mirror
|
||||
force-pushes and prunes, so this check is what guarantees the first sync cannot overwrite
|
||||
or delete anything on GitHub.
|
||||
- Archived repositories never get a push mirror, because GitHub rejects pushes to them.
|
||||
|
||||
## Rotating the mirror PAT
|
||||
|
||||
Each mirror stores its own copy of the PAT, and an expired PAT fails silently: the only sign
|
||||
is the error on the repository's *Settings → Mirror* page. To rotate:
|
||||
|
||||
1. Store the new token as a new version of `github-mirror-pat`.
|
||||
2. Delete each repository's GitHub mirror with
|
||||
`DELETE /api/v1/repos/JMR-dev/<repo>/push_mirrors/<remote_name>`.
|
||||
3. Re-run `pushmirror.py`.
|
||||
|
||||
The refs check runs again in step 3. Anything pushed to GitHub directly in the meantime
|
||||
shows up as `blocked` rather than being overwritten.
|
||||
|
||||
## Behaviour worth knowing
|
||||
|
||||
- New commits and branches reach GitHub within seconds.
|
||||
- A bare branch delete does not trigger a sync. It reaches GitHub at the next push that carries
|
||||
commits, or at the 8-hour interval.
|
||||
- Branches created on GitHub, such as Dependabot's, are pruned by the next sync.
|
||||
- GitHub Actions `on: push` workflows run for mirrored pushes.
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Copy GitHub repositories into Gitea as full, one-time migrations.
|
||||
|
||||
Non-destructive by construction:
|
||||
* GitHub is only ever read (clone + REST API through Gitea's downloader).
|
||||
* A repository that already exists in Gitea is never deleted or overwritten;
|
||||
it is skipped, so the script is safe to re-run after a partial failure.
|
||||
* The only Gitea-side change to an existing repository is setting the archived
|
||||
flag on a repository that is archived on GitHub.
|
||||
|
||||
Usage:
|
||||
GITEA_TOKEN=... [GITHUB_TOKEN=...] migrate.py repos.json results.jsonl [name ...]
|
||||
|
||||
repos.json is `gh repo list OWNER --json name,visibility,isFork,isArchived,
|
||||
diskUsage,description,defaultBranchRef`. Forks are always skipped. With names,
|
||||
only those repositories are processed, in the order given; otherwise every
|
||||
non-fork, smallest first.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
GITEA = os.environ.get("GITEA_URL", "https://gitea.jasonmross.dev").rstrip("/")
|
||||
OWNER = os.environ.get("OWNER", "JMR-dev")
|
||||
GITEA_TOKEN = os.environ["GITEA_TOKEN"]
|
||||
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "")
|
||||
|
||||
|
||||
def gitea(method, path, body=None, timeout=60):
|
||||
req = urllib.request.Request(
|
||||
f"{GITEA}/api/v1{path}",
|
||||
method=method,
|
||||
data=None if body is None else json.dumps(body).encode(),
|
||||
headers={"Authorization": f"token {GITEA_TOKEN}", "Content-Type": "application/json"},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
raw = r.read()
|
||||
return r.status, (json.loads(raw) if raw else None)
|
||||
except urllib.error.HTTPError as e:
|
||||
raw = e.read()
|
||||
try:
|
||||
return e.code, json.loads(raw)
|
||||
except ValueError:
|
||||
return e.code, {"message": raw.decode(errors="replace")[:500]}
|
||||
|
||||
|
||||
def migrate_one(repo):
|
||||
name = repo["name"]
|
||||
status, existing = gitea("GET", f"/repos/{OWNER}/{name}")
|
||||
if status == 200:
|
||||
result = {"result": "exists-skipped"}
|
||||
if repo["isArchived"] and not existing.get("archived"):
|
||||
s, _ = gitea("PATCH", f"/repos/{OWNER}/{name}", {"archived": True})
|
||||
result["archived_set"] = s == 200
|
||||
return result
|
||||
if status != 404:
|
||||
return {"result": "error", "stage": "lookup", "status": status, "detail": existing}
|
||||
|
||||
body = {
|
||||
"clone_addr": f"https://github.com/{OWNER}/{name}.git",
|
||||
"service": "github",
|
||||
"repo_owner": OWNER,
|
||||
"repo_name": name,
|
||||
"private": repo["visibility"] != "PUBLIC",
|
||||
"description": (repo.get("description") or "")[:2048],
|
||||
"mirror": False,
|
||||
"wiki": True,
|
||||
"issues": True,
|
||||
"labels": True,
|
||||
"milestones": True,
|
||||
"pull_requests": True,
|
||||
"releases": True,
|
||||
"lfs": True,
|
||||
}
|
||||
if GITHUB_TOKEN:
|
||||
body["auth_token"] = GITHUB_TOKEN
|
||||
|
||||
started = time.time()
|
||||
# The API migrates synchronously; a repository with hundreds of PRs takes
|
||||
# many minutes, mostly in Gitea's rate-limit-aware GitHub downloader.
|
||||
status, resp = gitea("POST", "/repos/migrate", body, timeout=4 * 3600)
|
||||
elapsed = round(time.time() - started, 1)
|
||||
if status != 201:
|
||||
return {"result": "error", "stage": "migrate", "status": status, "seconds": elapsed,
|
||||
"detail": (resp or {}).get("message", resp)}
|
||||
|
||||
result = {"result": "migrated", "seconds": elapsed, "private": resp.get("private")}
|
||||
if repo["isArchived"]:
|
||||
s, _ = gitea("PATCH", f"/repos/{OWNER}/{name}", {"archived": True})
|
||||
result["archived_set"] = s == 200
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
repos_file, results_file, *names = sys.argv[1:]
|
||||
repos = [r for r in json.load(open(repos_file)) if not r["isFork"]]
|
||||
if names:
|
||||
by_name = {r["name"]: r for r in repos}
|
||||
missing = [n for n in names if n not in by_name]
|
||||
if missing:
|
||||
sys.exit(f"not in {repos_file} (or a fork): {', '.join(missing)}")
|
||||
repos = [by_name[n] for n in names]
|
||||
else:
|
||||
repos.sort(key=lambda r: r["diskUsage"])
|
||||
|
||||
print(f"{len(repos)} repositories; github token: {'yes' if GITHUB_TOKEN else 'NO'}", flush=True)
|
||||
with open(results_file, "a") as out:
|
||||
for i, repo in enumerate(repos, 1):
|
||||
print(f"[{i}/{len(repos)}] {repo['name']} ({repo['diskUsage']} KB) ...", flush=True)
|
||||
try:
|
||||
result = migrate_one(repo)
|
||||
except Exception as e: # keep going; one bad repository must not stop the batch
|
||||
result = {"result": "error", "stage": "exception", "detail": repr(e)}
|
||||
result = {"name": repo["name"], "ts": time.strftime("%H:%M:%S"), **result}
|
||||
out.write(json.dumps(result) + "\n")
|
||||
out.flush()
|
||||
print(f" -> {result['result']} {json.dumps({k: v for k, v in result.items() if k not in ('name', 'result', 'ts')})}", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+117
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create Gitea -> GitHub push mirrors that sync on every commit.
|
||||
|
||||
Gitea's push mirror force-pushes and prunes, so it would delete or overwrite
|
||||
anything that exists only on GitHub. Guard: a mirror is created only when every
|
||||
GitHub branch and tag already exists on Gitea at the same commit. Then the first
|
||||
sync cannot remove or rewrite anything on GitHub. Repositories with an existing
|
||||
GitHub push mirror are skipped, so re-runs are safe.
|
||||
|
||||
Usage: GITEA_TOKEN=... MIRROR_PAT=... pushmirror.py repos.json results.jsonl [name ...]
|
||||
Archived (on GitHub) repositories and forks are always skipped: GitHub rejects
|
||||
pushes to archived repositories.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
GITEA = "https://gitea.jasonmross.dev"
|
||||
OWNER = "JMR-dev"
|
||||
GITEA_TOKEN = os.environ["GITEA_TOKEN"]
|
||||
MIRROR_PAT = os.environ["MIRROR_PAT"]
|
||||
INTERVAL = os.environ.get("MIRROR_INTERVAL", "8h") # backstop; sync_on_commit does the real work
|
||||
|
||||
|
||||
def gitea(method, path, body=None):
|
||||
req = urllib.request.Request(f"{GITEA}/api/v1{path}", method=method,
|
||||
data=None if body is None else json.dumps(body).encode(),
|
||||
headers={"Authorization": f"token {GITEA_TOKEN}", "Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=120) as r:
|
||||
raw = r.read()
|
||||
return r.status, (json.loads(raw) if raw else None)
|
||||
except urllib.error.HTTPError as e:
|
||||
raw = e.read()
|
||||
try:
|
||||
return e.code, json.loads(raw)
|
||||
except ValueError:
|
||||
return e.code, {"message": raw.decode(errors="replace")[:300]}
|
||||
|
||||
|
||||
def gitea_all(path):
|
||||
items, page = [], 1
|
||||
while True:
|
||||
_, batch = gitea("GET", f"{path}?limit=50&page={page}")
|
||||
batch = batch or []
|
||||
items += batch
|
||||
if len(batch) < 50:
|
||||
return items
|
||||
page += 1
|
||||
|
||||
|
||||
def gh_refs(name, kind):
|
||||
out = subprocess.run(["gh", "api", "--paginate", f"repos/{OWNER}/{name}/{kind}"],
|
||||
check=True, capture_output=True, text=True).stdout
|
||||
items = json.loads(out.replace("]\n[", ",").replace("][", ",")) if out.strip() else []
|
||||
return {i["name"]: i["commit"]["sha"] for i in items}
|
||||
|
||||
|
||||
def refs_problems(name):
|
||||
problems = []
|
||||
for kind, gitea_key in (("branches", "id"), ("tags", "sha")):
|
||||
gh = gh_refs(name, kind)
|
||||
gt = {i["name"]: i["commit"][gitea_key] for i in gitea_all(f"/repos/{OWNER}/{name}/{kind}")}
|
||||
for ref, sha in gh.items():
|
||||
if gt.get(ref) != sha:
|
||||
problems.append(f"{kind[:-1] if kind != 'branches' else 'branch'} {ref}: github={sha[:10]} gitea={(gt.get(ref) or 'missing')[:10]}")
|
||||
return problems
|
||||
|
||||
|
||||
def mirror_one(name):
|
||||
status, mirrors = gitea("GET", f"/repos/{OWNER}/{name}/push_mirrors")
|
||||
if status != 200:
|
||||
return {"result": "error", "stage": "list", "status": status, "detail": mirrors}
|
||||
if any("github.com" in (m.get("remote_address") or "") for m in mirrors or []):
|
||||
return {"result": "exists-skipped"}
|
||||
|
||||
problems = refs_problems(name)
|
||||
if problems:
|
||||
return {"result": "blocked", "problems": problems}
|
||||
|
||||
status, resp = gitea("POST", f"/repos/{OWNER}/{name}/push_mirrors", {
|
||||
"remote_address": f"https://github.com/{OWNER}/{name}.git",
|
||||
"remote_username": OWNER,
|
||||
"remote_password": MIRROR_PAT,
|
||||
"interval": INTERVAL,
|
||||
"sync_on_commit": True,
|
||||
})
|
||||
if status not in (200, 201):
|
||||
return {"result": "error", "stage": "create", "status": status, "detail": (resp or {}).get("message", resp)}
|
||||
gitea("POST", f"/repos/{OWNER}/{name}/push_mirrors-sync")
|
||||
return {"result": "created", "remote_name": resp.get("remote_name")}
|
||||
|
||||
|
||||
def main():
|
||||
repos_file, results_file, *names = sys.argv[1:]
|
||||
repos = [r for r in json.load(open(repos_file)) if not r["isFork"] and not r["isArchived"]]
|
||||
if names:
|
||||
repos = [r for r in repos if r["name"] in set(names)]
|
||||
print(f"{len(repos)} repositories", flush=True)
|
||||
with open(results_file, "a") as out:
|
||||
for i, r in enumerate(repos, 1):
|
||||
try:
|
||||
res = mirror_one(r["name"])
|
||||
except Exception as e:
|
||||
res = {"result": "error", "stage": "exception", "detail": repr(e)}
|
||||
res = {"name": r["name"], "ts": time.strftime("%H:%M:%S"), **res}
|
||||
out.write(json.dumps(res) + "\n")
|
||||
out.flush()
|
||||
print(f"[{i}/{len(repos)}] {r['name']}: {res['result']} {json.dumps({k: v for k, v in res.items() if k not in ('name', 'ts', 'result')})}", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Re-point local clones' remotes from GitHub (JMR-dev) to Gitea.
|
||||
|
||||
Only remotes whose URL is a JMR-dev GitHub repository that exists on Gitea are
|
||||
changed. Third-party remotes (upstreams, other owners) and JMR-dev repos that
|
||||
were not migrated (forks) are left untouched and reported. GitHub renames are
|
||||
followed via the API redirect. Every change is appended to a TSV so it can be
|
||||
reverted with `git remote set-url <remote> <old-url>`.
|
||||
|
||||
Usage: GITEA_TOKEN=... repoint.py <dirs-file> <workspace-root> <changes.tsv> [--apply]
|
||||
Without --apply it only prints the plan.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
OWNER = "JMR-dev"
|
||||
GITEA = "https://gitea.jasonmross.dev"
|
||||
GITEA_SSH = "ssh://git@gitea.jasonmross.dev:2222"
|
||||
GH_URL = re.compile(r"^(?:git@github\.com:|ssh://git@github\.com/|https://github\.com/)([^/]+)/(.+?)(?:\.git)?/?$")
|
||||
|
||||
|
||||
def git(cwd, *args):
|
||||
return subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True)
|
||||
|
||||
|
||||
def gitea_has(name):
|
||||
req = urllib.request.Request(f"{GITEA}/api/v1/repos/{OWNER}/{name}",
|
||||
headers={"Authorization": f"token {os.environ['GITEA_TOKEN']}"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return json.load(r)["name"]
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 404:
|
||||
return None
|
||||
raise
|
||||
|
||||
|
||||
def canonical(name):
|
||||
"""Follow a GitHub rename: the API redirects old names to the new repo."""
|
||||
r = subprocess.run(["gh", "api", f"repos/{OWNER}/{name}", "--jq", ".name"], capture_output=True, text=True)
|
||||
return r.stdout.strip() or name
|
||||
|
||||
|
||||
def main():
|
||||
dirs_file, root, changes_file, *flags = sys.argv[1:]
|
||||
apply = "--apply" in flags
|
||||
out = open(changes_file, "a") if apply else None
|
||||
for rel in open(dirs_file).read().split():
|
||||
d = os.path.normpath(os.path.join(root, rel))
|
||||
remotes = git(d, "remote").stdout.split()
|
||||
for remote in remotes:
|
||||
url = git(d, "remote", "get-url", remote).stdout.strip()
|
||||
m = GH_URL.match(url)
|
||||
if not m:
|
||||
print(f"SKIP {rel:38} {remote:9} not a GitHub URL: {url}")
|
||||
continue
|
||||
owner, name = m.groups()
|
||||
if owner != OWNER:
|
||||
print(f"SKIP {rel:38} {remote:9} third-party ({owner}/{name})")
|
||||
continue
|
||||
target = gitea_has(canonical(name))
|
||||
if not target:
|
||||
print(f"SKIP {rel:38} {remote:9} {owner}/{name} is not on Gitea (fork, not migrated)")
|
||||
continue
|
||||
new = f"{GITEA_SSH}/{OWNER}/{target}.git"
|
||||
if url == new:
|
||||
print(f"OK {rel:38} {remote:9} already {new}")
|
||||
continue
|
||||
print(f"{'CHANGE' if apply else 'PLAN '} {rel:38} {remote:9} {url} -> {new}")
|
||||
if apply:
|
||||
r = git(d, "remote", "set-url", remote, new)
|
||||
if r.returncode:
|
||||
print(f" !! set-url failed: {r.stderr.strip()}")
|
||||
continue
|
||||
out.write(f"{d}\t{remote}\t{url}\t{new}\n")
|
||||
out.flush()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+109
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare migrated repositories between GitHub and Gitea. Read-only on both.
|
||||
|
||||
Checks, per repository: every branch and tag points at the same commit; issue,
|
||||
pull request and release counts match; archived flag and visibility match.
|
||||
|
||||
Usage: GITEA_TOKEN=... verify.py repos.json [name ...]
|
||||
GitHub is read through the local `gh` CLI.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
GITEA = os.environ.get("GITEA_URL", "https://gitea.jasonmross.dev").rstrip("/")
|
||||
OWNER = os.environ.get("OWNER", "JMR-dev")
|
||||
GITEA_TOKEN = os.environ["GITEA_TOKEN"]
|
||||
|
||||
|
||||
def gh_api(path):
|
||||
out = subprocess.run(["gh", "api", "--paginate", path], check=True, capture_output=True, text=True).stdout
|
||||
# --paginate concatenates JSON arrays as `][`; join them into one.
|
||||
return json.loads(out.replace("]\n[", ",").replace("][", ",")) if out.strip() else []
|
||||
|
||||
|
||||
def gh_counts(name):
|
||||
q = ('query($o:String!,$n:String!){repository(owner:$o,name:$n){'
|
||||
'issues{totalCount} pullRequests{totalCount} releases{totalCount}}}')
|
||||
out = subprocess.run(["gh", "api", "graphql", "-f", f"query={q}", "-f", f"o={OWNER}", "-f", f"n={name}"],
|
||||
check=True, capture_output=True, text=True).stdout
|
||||
r = json.loads(out)["data"]["repository"]
|
||||
return {"issues": r["issues"]["totalCount"], "pulls": r["pullRequests"]["totalCount"],
|
||||
"releases": r["releases"]["totalCount"]}
|
||||
|
||||
|
||||
def gitea_get(path):
|
||||
req = urllib.request.Request(f"{GITEA}/api/v1{path}", headers={"Authorization": f"token {GITEA_TOKEN}"})
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
return json.loads(r.read()), r.headers.get("X-Total-Count")
|
||||
|
||||
|
||||
def gitea_all(path):
|
||||
items, page = [], 1
|
||||
sep = "&" if "?" in path else "?"
|
||||
while True:
|
||||
batch, _ = gitea_get(f"{path}{sep}limit=50&page={page}")
|
||||
batch = batch or [] # an empty repository returns null, not []
|
||||
items += batch
|
||||
if len(batch) < 50:
|
||||
return items
|
||||
page += 1
|
||||
|
||||
|
||||
def gitea_count(path):
|
||||
_, total = gitea_get(path + ("&" if "?" in path else "?") + "limit=1")
|
||||
return int(total or 0)
|
||||
|
||||
|
||||
def verify(repo):
|
||||
name = repo["name"]
|
||||
problems = []
|
||||
try:
|
||||
g, _ = gitea_get(f"/repos/{OWNER}/{name}")
|
||||
except urllib.error.HTTPError as e:
|
||||
return {"name": name, "ok": False, "problems": [f"not in gitea ({e.code})"]}
|
||||
|
||||
if g["archived"] != repo["isArchived"]:
|
||||
problems.append(f"archived gitea={g['archived']} github={repo['isArchived']}")
|
||||
if g["private"] != (repo["visibility"] != "PUBLIC"):
|
||||
problems.append(f"private gitea={g['private']} github={repo['visibility']}")
|
||||
|
||||
gh_branches = {b["name"]: b["commit"]["sha"] for b in gh_api(f"repos/{OWNER}/{name}/branches")}
|
||||
gt_branches = {b["name"]: b["commit"]["id"] for b in gitea_all(f"/repos/{OWNER}/{name}/branches")}
|
||||
gh_tags = {t["name"]: t["commit"]["sha"] for t in gh_api(f"repos/{OWNER}/{name}/tags")}
|
||||
gt_tags = {t["name"]: t["commit"]["sha"] for t in gitea_all(f"/repos/{OWNER}/{name}/tags")}
|
||||
for kind, a, b in (("branch", gh_branches, gt_branches), ("tag", gh_tags, gt_tags)):
|
||||
for ref in sorted(set(a) | set(b)):
|
||||
if a.get(ref) != b.get(ref):
|
||||
problems.append(f"{kind} {ref}: github={(a.get(ref) or 'missing')[:10]} gitea={(b.get(ref) or 'missing')[:10]}")
|
||||
|
||||
gh = gh_counts(name)
|
||||
gt = {"issues": gitea_count(f"/repos/{OWNER}/{name}/issues?state=all&type=issues"),
|
||||
"pulls": gitea_count(f"/repos/{OWNER}/{name}/issues?state=all&type=pulls"),
|
||||
"releases": gitea_count(f"/repos/{OWNER}/{name}/releases")}
|
||||
for k in gh:
|
||||
if gh[k] != gt[k]:
|
||||
problems.append(f"{k}: github={gh[k]} gitea={gt[k]}")
|
||||
|
||||
return {"name": name, "ok": not problems, "branches": len(gt_branches), "tags": len(gt_tags),
|
||||
**{f"gitea_{k}": v for k, v in gt.items()}, "problems": problems}
|
||||
|
||||
|
||||
def main():
|
||||
repos_file, *names = sys.argv[1:]
|
||||
repos = [r for r in json.load(open(repos_file)) if not r["isFork"]]
|
||||
if names:
|
||||
repos = [r for r in repos if r["name"] in set(names)]
|
||||
for repo in repos:
|
||||
try:
|
||||
res = verify(repo)
|
||||
except Exception as e:
|
||||
res = {"name": repo["name"], "ok": False, "problems": [f"verify error: {e!r}"]}
|
||||
print(json.dumps(res), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+33
-6
@@ -36,7 +36,6 @@ AR_HOST=$(meta ar-host)
|
||||
IMAGE_GITEA=$(meta image-gitea)
|
||||
IMAGE_CADDY=$(meta image-caddy)
|
||||
DOMAIN=$(meta domain)
|
||||
ACME_EMAIL=$(meta acme-email)
|
||||
APP_NAME=$(meta app-name)
|
||||
PODMAN_SUBNET=$(meta podman-subnet)
|
||||
PODMAN_GATEWAY=$(meta podman-gateway)
|
||||
@@ -57,7 +56,7 @@ case "${WAF_MODE}" in
|
||||
esac
|
||||
: "${DATA_DISK_DEVICE:=/dev/disk/by-id/google-gitea-data}"
|
||||
|
||||
export GCP_PROJECT AR_HOST IMAGE_GITEA IMAGE_CADDY DOMAIN ACME_EMAIL APP_NAME
|
||||
export GCP_PROJECT AR_HOST IMAGE_GITEA IMAGE_CADDY DOMAIN APP_NAME
|
||||
export PODMAN_SUBNET PODMAN_GATEWAY REQUIRE_SIGNIN_VIEW WAF_MODE
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -199,8 +198,21 @@ setup_nftables() {
|
||||
systemctl disable --now firewalld >/dev/null 2>&1 || true
|
||||
systemctl mask firewalld >/dev/null 2>&1 || true
|
||||
|
||||
install -m 0600 "${STATE_DIR}/nftables/gitea.nft" /etc/sysconfig/nftables.conf
|
||||
nft -c -f /etc/sysconfig/nftables.conf || die "nftables ruleset failed validation"
|
||||
[[ -n "${PODMAN_SUBNET}" && -n "${PODMAN_GATEWAY}" ]] \
|
||||
|| die "podman-subnet/podman-gateway metadata missing; cannot render the ruleset"
|
||||
|
||||
# Validate BEFORE installing. A ruleset that fails to parse must never land
|
||||
# in /etc/sysconfig: nftables.service would fail to load it on the next boot
|
||||
# and the host would come up with no gitea_filter table at all.
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
envsubst '${PODMAN_SUBNET} ${PODMAN_GATEWAY}' < "${STATE_DIR}/nftables/gitea.nft" > "${tmp}"
|
||||
if ! nft -c -f "${tmp}"; then
|
||||
rm -f "${tmp}"
|
||||
die "nftables ruleset failed validation"
|
||||
fi
|
||||
install -m 0600 "${tmp}" /etc/sysconfig/nftables.conf
|
||||
rm -f "${tmp}"
|
||||
systemctl enable --now nftables
|
||||
systemctl reload nftables
|
||||
|
||||
@@ -429,6 +441,20 @@ render_all() {
|
||||
rm -f /etc/sysctl.d/90-gitea-caddy.conf
|
||||
fi
|
||||
|
||||
# The ACME contact address lives in Secret Manager rather than instance
|
||||
# metadata, to keep it out of the public repository. Without it Caddy still
|
||||
# issues certificates, just under an account with no contact address -- far
|
||||
# better than an empty `email` directive, which fails to parse and leaves
|
||||
# nothing serving TLS.
|
||||
local acme_email caddy_email
|
||||
if acme_email=$(gcloud secrets versions access latest --secret=gitea-acme-email \
|
||||
--project="${GCP_PROJECT}" 2>/dev/null) && [[ -n "${acme_email}" ]]; then
|
||||
caddy_email="email ${acme_email}"
|
||||
else
|
||||
warn "secret gitea-acme-email unreadable -- Caddy will register without a contact address"
|
||||
caddy_email="# no ACME contact address: secret gitea-acme-email was unreadable at render time"
|
||||
fi
|
||||
|
||||
# Trust both the bridge CIDR and loopback so this value stays correct in
|
||||
# either Caddy networking mode. Rootful podman SNATs host-loopback traffic
|
||||
# to the bridge gateway, so the CIDR covers the host-network case too.
|
||||
@@ -444,7 +470,8 @@ render_all() {
|
||||
GITEA_UPSTREAM="${gitea_upstream}" \
|
||||
CADDY_NETWORK="${caddy_network}" \
|
||||
CADDY_PUBLISH_PORTS="${caddy_publish}" \
|
||||
CADDY_SYSCTL="${caddy_sysctl}"
|
||||
CADDY_SYSCTL="${caddy_sysctl}" \
|
||||
CADDY_EMAIL="${caddy_email}"
|
||||
|
||||
# app.ini is 0400 owned by uid 1000: it holds SECRET_KEY and INTERNAL_TOKEN,
|
||||
# and the container runs as that uid and must be able to read it.
|
||||
@@ -453,7 +480,7 @@ render_all() {
|
||||
&& changed=1
|
||||
|
||||
render "${STATE_DIR}/config/Caddyfile.tmpl" /etc/caddy/Caddyfile root:root 0644 \
|
||||
'${DOMAIN} ${ACME_EMAIL} ${GITEA_UPSTREAM} ${WAF_MODE}' \
|
||||
'${DOMAIN} ${CADDY_EMAIL} ${GITEA_UPSTREAM} ${WAF_MODE}' \
|
||||
&& changed=1
|
||||
|
||||
local unit
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# googleclouddns -- ACME DNS-01, so issuance and renewal never need inbound 80
|
||||
# coraza_waf -- OWASP Coraza with the Core Rule Set embedded in the binary
|
||||
{
|
||||
email ${ACME_EMAIL}
|
||||
${CADDY_EMAIL}
|
||||
admin 127.0.0.1:2019
|
||||
# Required by coraza-caddy: Caddy has no built-in ordering for a third-party
|
||||
# directive, and the WAF must run before anything that could act on the
|
||||
@@ -17,7 +17,8 @@ ${DOMAIN} {
|
||||
tls {
|
||||
dns googleclouddns {
|
||||
# Application Default Credentials come from the GCE metadata server.
|
||||
# The VM service account holds roles/dns.admin scoped to this zone only.
|
||||
# The VM service account holds roles/dns.admin scoped to this zone, plus
|
||||
# project-level dns.reader so the plugin can list zones to find it.
|
||||
gcp_project {env.GCP_PROJECT}
|
||||
}
|
||||
# Only used for propagation checks. If issuance stalls waiting for
|
||||
|
||||
+10
-1
@@ -1,6 +1,7 @@
|
||||
#!/usr/sbin/nft -f
|
||||
#
|
||||
# Host firewall. Installed by vm/bootstrap.sh as /etc/sysconfig/nftables.conf.
|
||||
# Host firewall. Rendered by vm/bootstrap.sh into /etc/sysconfig/nftables.conf,
|
||||
# substituting ${PODMAN_SUBNET} and ${PODMAN_GATEWAY}.
|
||||
#
|
||||
# CRITICAL: this file must NEVER contain `flush ruleset`. The stock nftables
|
||||
# config ships with one, and it would wipe podman/netavark's NAT and forward
|
||||
@@ -37,6 +38,14 @@ table inet gitea_filter {
|
||||
# DHCP renewal from the GCE metadata server.
|
||||
udp sport 67 udp dport 68 accept
|
||||
|
||||
# Container DNS. aardvark-dns answers on the bridge gateway, so lookups
|
||||
# from containers terminate on the host and arrive here, not in forward.
|
||||
# Netavark accepts them in its own table, but a packet has to survive
|
||||
# every input-hook chain, and this one's drop policy would discard it
|
||||
# anyway. Without this rule containers resolve nothing -- Caddy cannot
|
||||
# reach Let's Encrypt and Gitea cannot reach webhook or mirror hosts.
|
||||
ip saddr ${PODMAN_SUBNET} ip daddr ${PODMAN_GATEWAY} meta l4proto { tcp, udp } th dport 53 accept comment "container DNS"
|
||||
|
||||
# Admin SSH: IAP TCP forwarding range only. There is no other path in --
|
||||
# the VPC firewall enforces the same restriction as the outer layer.
|
||||
ip saddr 35.235.240.0/20 tcp dport 22 accept comment "IAP SSH"
|
||||
|
||||
Reference in New Issue
Block a user