diff --git a/.github/workflows/live-compat.yml b/.github/workflows/live-compat.yml index 82c8d511..924c456e 100644 --- a/.github/workflows/live-compat.yml +++ b/.github/workflows/live-compat.yml @@ -133,6 +133,22 @@ jobs: if: env.LIVE_COMPAT_SUITE == 'client-e2e' uses: oras-project/setup-oras@v2 + - name: Set up Terraform 0.13 and current stable + if: env.LIVE_COMPAT_SUITE == 'client-e2e' + shell: bash + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/terraform/0.13.7" "$RUNNER_TEMP/terraform/1.15.8" + for version in 0.13.7 1.15.8; do + archive="terraform_${version}_linux_amd64.zip" + curl -fsSLO "https://releases.hashicorp.com/terraform/${version}/${archive}" + curl -fsSLO "https://releases.hashicorp.com/terraform/${version}/terraform_${version}_SHA256SUMS" + grep " ${archive}$" "terraform_${version}_SHA256SUMS" | sha256sum -c - + unzip -q "$archive" -d "$RUNNER_TEMP/terraform/$version" + done + echo "TERRAFORM_013_BIN=$RUNNER_TEMP/terraform/0.13.7/terraform" >> "$GITHUB_ENV" + echo "TERRAFORM_CURRENT_BIN=$RUNNER_TEMP/terraform/1.15.8/terraform" >> "$GITHUB_ENV" + - name: Install Python client build tools if: env.LIVE_COMPAT_SUITE == 'client-e2e' run: python -m pip install --upgrade build twine diff --git a/.github/workflows/migration-e2e.yml b/.github/workflows/migration-e2e.yml index 3a9910c2..cf244277 100644 --- a/.github/workflows/migration-e2e.yml +++ b/.github/workflows/migration-e2e.yml @@ -20,7 +20,7 @@ concurrency: jobs: nexus-migration-e2e: - name: Nexus ${{ matrix.nexus-version }} migration E2E + name: Nexus ${{ matrix.nexus-version }} (${{ matrix.metadata-engine }}) migration E2E runs-on: ubuntu-latest timeout-minutes: 120 if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-migration-e2e')) @@ -133,6 +133,27 @@ jobs: - name: Prepare migration fixtures run: scripts/ci/live-compat-setup.sh + - name: Set up Terraform current stable + if: matrix.nexus-version == '3.92.0' + shell: bash + run: | + set -euo pipefail + version=1.15.8 + archive="terraform_${version}_linux_amd64.zip" + mkdir -p "$RUNNER_TEMP/terraform/$version" + curl -fsSLO "https://releases.hashicorp.com/terraform/${version}/${archive}" + curl -fsSLO "https://releases.hashicorp.com/terraform/${version}/terraform_${version}_SHA256SUMS" + grep " ${archive}$" "terraform_${version}_SHA256SUMS" | sha256sum -c - + unzip -q "$archive" -d "$RUNNER_TEMP/terraform/$version" + echo "TERRAFORM_CURRENT_BIN=$RUNNER_TEMP/terraform/$version/terraform" >> "$GITHUB_ENV" + + - name: Prepare Nexus Terraform migration fixture + if: matrix.nexus-version == '3.92.0' + run: >- + mvn -B -ntp -pl compat-test -am + -Dtest=TerraformRepositoryBlackBoxCompatibilityTest#preparesNexusHostedMigrationFixtureWhenConfigured + -Dsurefire.failIfNoSpecifiedTests=false test + - name: Run Nexus metadata and repository-data migration E2E run: scripts/docker-compat/migration-e2e.sh @@ -140,14 +161,14 @@ jobs: if: always() run: | mkdir -p artifacts - docker compose -f "$COMPOSE_FILE" ps > "artifacts/migration-e2e-${{ matrix.nexus-version }}-ps.txt" || true - docker compose -f "$COMPOSE_FILE" logs --no-color --timestamps > "artifacts/migration-e2e-${{ matrix.nexus-version }}-compose.log" || true + docker compose -f "$COMPOSE_FILE" ps > "artifacts/migration-e2e-${{ matrix.nexus-version }}-${{ matrix.metadata-engine }}-ps.txt" || true + docker compose -f "$COMPOSE_FILE" logs --no-color --timestamps > "artifacts/migration-e2e-${{ matrix.nexus-version }}-${{ matrix.metadata-engine }}-compose.log" || true - name: Upload migration logs if: always() uses: actions/upload-artifact@v7 with: - name: migration-e2e-${{ matrix.nexus-version }}-logs + name: migration-e2e-${{ matrix.nexus-version }}-${{ matrix.metadata-engine }}-logs path: artifacts/ if-no-files-found: ignore diff --git a/AGENTS.md b/AGENTS.md index ea3305f2..878cbbc3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ 构建一个兼容 Nexus 的自托管制品仓库,包含: -- Maven、npm、PyPI、Go、Helm、Cargo/Rust、Dart/Pub、Composer/PHP、Docker/OCI、NuGet、RubyGems、Yum 和 Raw 制品支持;具体 hosted/proxy/group 边界以兼容性矩阵为准。 +- Maven、npm、PyPI、Go、Helm、Cargo/Rust、Dart/Pub、Composer/PHP、Terraform、Docker/OCI、NuGet、RubyGems、Yum 和 Raw 制品支持;具体 hosted/proxy/group 边界以兼容性矩阵为准。 - 面向用户、用户组、角色、仓库权限和 CI token 的完整权限管理。 - 兼容 Nexus 的客户端协议行为和 `/repository//...` URL 布局。 - 使用 MySQL 存储元数据、用户数据、权限数据、token 数据、审计数据和迁移状态。 @@ -51,6 +51,7 @@ - `protocol-cargo`:Cargo sparse registry、publish、index 和 crate 行为。 - `protocol-pub`:Dart / Flutter Pub hosted repository 行为。 - `protocol-composer`:Composer 2 repository metadata、dist、hosted、proxy 和 group 行为。 +- `protocol-terraform`:Terraform Module/Provider Registry path、metadata、版本、平台和 checksum 行为。 - `protocol-docker`:Docker Registry HTTP API V2 和 OCI 行为。 - `protocol-nuget`:NuGet 仓库行为。 - `protocol-rubygems`:RubyGems 仓库行为。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f4647696..d4cb9c40 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,7 +54,7 @@ When a protocol change affects behavior that real package clients exercise direc scripts/ci/run-live-compat.sh client-e2e ``` -It covers Maven, npm, PyPI, Go resolve, Helm, Cargo/Rust, Dart/Pub, Flutter Pub, Composer/PHP, NuGet, RubyGems, Yum, and Docker/OCI client flows. Logs and downloaded metadata are written under `artifacts/client-e2e/`. +It covers Maven, npm, PyPI, Go resolve, Helm, Cargo/Rust, Dart/Pub, Flutter Pub, Composer/PHP, Terraform 0.13/current, NuGet, RubyGems, Yum, and Docker/OCI client flows. Logs and downloaded metadata are written under `artifacts/client-e2e/`; credential-bearing Terraform metadata is redacted before artifacts are uploaded. See [compat-test/README.md](compat-test/README.md) for suite options, environment variables, and Maven properties. diff --git a/README.cn.md b/README.cn.md index f8ae0f27..bfbf4d10 100644 --- a/README.cn.md +++ b/README.cn.md @@ -9,11 +9,11 @@ [English](README.md) | **中文** -kkRepo 是一款社区驱动、完全开源的自托管制品仓库,旨在解决 Sonatype Nexus 社区版的各类限制与痛点,为社区提供开放、可靠且可持续演进的制品管理方案。目前已支持 Maven、npm、PyPI、Go、Helm、Cargo/Rust、Dart/Pub、Composer/PHP、Docker/OCI、NuGet、RubyGems、Yum 和 Raw 等制品格式。 +kkRepo 是一款社区驱动、完全开源的自托管制品仓库,旨在解决 Sonatype Nexus 社区版的各类限制与痛点,为社区提供开放、可靠且可持续演进的制品管理方案。目前已支持 Maven、npm、PyPI、Go、Helm、Cargo/Rust、Dart/Pub、Composer/PHP、Terraform、Docker/OCI、NuGet、RubyGems、Yum 和 Raw 等制品格式。 ## 功能特性 -- 支持 13+ 种主流仓库格式,覆盖 hosted、proxy 和 group 仓库管理。 +- 支持 14+ 种主流仓库格式,覆盖 hosted、proxy 和 group 仓库管理。 - 兼容 Sonatype Nexus API 协议、用户权限模型、和 `/repository//...` URL 布局。 - 可使用 kkRepo 平替 Sonatype Nexus,支持存量数据一键迁移并沿用原仓库域名和 URL,原有客户端配置与 CI 工作流无需改动。 - 支持完整的身份与访问控制,覆盖 Local、LDAP、OIDC 认证、匿名访问策略和细粒度权限管理。 @@ -74,13 +74,14 @@ curl -fsSL https://raw.githubusercontent.com/klboke/kkrepo/main/scripts/quicksta | Cargo / Rust | hosted / proxy / group | 支持 `cargo publish`、yank/unyank、`CargoToken` 认证和 UI/API `.crate` 上传 | 支持 sparse index 和 `cargo search` | 支持 Cargo 仓库迁移 | | Dart / Pub | hosted / proxy / group | 支持 `dart pub publish`、`dart pub get`、`flutter pub get`、`PubToken` 认证和 UI/API `.tar.gz` 上传 | 支持 package/version metadata、archive 属性和 Pub 搜索 | 支持 Nexus 3.92.0 Pub hosted 迁移,以及显式选择的 proxy cache 迁移 | | Composer / PHP | hosted / proxy / group | Composer 没有标准 publish 命令;支持 Components API 和 UI 上传 zip/tar archive,并通过 Composer 2 安装 | 支持 package/version、dist、HTML View、Browse/Search 和 Usage | Nexus 原生 Composer proxy 配置可迁移;cache 仅在管理员显式选择且 source profile 证明后迁移 | +| Terraform Provider / Module Registry | hosted / proxy / group | 支持 Nexus 兼容 PUT 和 UI/API archive 上传;`terraform init` 可通过 group 解析 hosted 与 proxy module/provider | 支持 module/provider coordinate、version、platform、Browse/Search 和 Usage | 支持 Nexus Terraform hosted 数据和显式选择的 proxy archive cache 迁移;同时迁移 proxy/group 配置 | | Docker / OCI | hosted / proxy / group | 支持 Registry V2 login、hosted push/pull、proxy pull、group pull、OCI referrers、cleanup 和 connector port 访问 | 支持 manifest/tag/blob metadata | Docker hosted 仓库数据迁移走 Nexus Repository Data 流程 | | NuGet | hosted / proxy / group | 支持 package push 和管理台上传 | 支持 v3 service index / search | 默认迁移 hosted;proxy 可作为可选仓库迁移 | | RubyGems | hosted / proxy / group | 支持 gem push/yank 和管理台上传 | 支持 | 默认迁移 hosted;proxy 可作为可选仓库迁移 | | Yum | hosted / proxy / group | 支持 RPM 上传和管理台上传 | 支持 repodata | 默认迁移 hosted;proxy 可作为可选仓库迁移 | | Raw | hosted / proxy / group | 支持 PUT 上传和管理台上传 | 支持 | 默认迁移 hosted;proxy 可作为可选仓库迁移 | -仓库数据迁移默认扫描 hosted 仓库;如需迁移源 Sonatype Nexus Repository 部署中作为历史备份或回源缓存使用的 proxy 仓库,可以在迁移页面的 `Optional proxy repositories` 中显式指定仓库名。Cargo / Rust、Dart / Pub 和 Nexus 原生 Composer proxy cache 迁移已支持;Composer proxy 必须由管理员显式选择。 +仓库数据迁移默认扫描 hosted 仓库;如需迁移源 Sonatype Nexus Repository 部署中作为历史备份或回源缓存使用的 proxy 仓库,可以在迁移页面的 `Optional proxy repositories` 中显式指定仓库名。Cargo / Rust、Dart / Pub、Nexus 原生 Composer 和 Terraform proxy cache 在显式选择且 source profile 接受时可迁移。Terraform 使用独立 proxy-cache writer 恢复 module/provider archive、保留 Nexus 公开 asset path,且不会把 cache asset 当作 hosted publication 重放。已恢复的 module archive 可不回源直接完成 module download discovery;Provider route 从已配置上游重建并校验 checksum/signature snapshot,其缓存 blob 在 metadata 有效期内保持固定。 ## 从 Sonatype Nexus Repository 迁移 @@ -99,7 +100,7 @@ curl -fsSL https://raw.githubusercontent.com/klboke/kkrepo/main/scripts/quicksta | 维度 | Sonatype Nexus Repository OSS / Community Edition | kkRepo | | --- | --- |-------------------------------------------------------------------------------------------------------------------| | 产品定位 | 通用制品仓库管理平台,功能完整,覆盖大量官方格式和管理能力 | 提供面向迁移的客户端行为、权限模型和 `/repository//...` URL 布局兼容,同时采用共享关系数据库、OSS/S3-first、适合多副本部署的架构 | -| 支持格式 | 官方支持格式更多,具体能力随版本和发行形态变化 | 聚焦常用制品格式,当前支持 Maven、npm、PyPI、Go、Helm、Cargo/Rust、Dart/Pub、Composer/PHP、Docker/OCI、NuGet、RubyGems、Yum 和 Raw;每个格式以独立 protocol 模块实现,便于按优先级扩展和验证 | +| 支持格式 | 官方支持格式更多,具体能力随版本和发行形态变化 | 聚焦常用制品格式,当前支持 Maven、npm、PyPI、Go、Helm、Cargo/Rust、Dart/Pub、Composer/PHP、Terraform、Docker/OCI、NuGet、RubyGems、Yum 和 Raw;每个格式以独立 protocol 模块实现,便于按优先级扩展和验证 | | 使用限制 | Community Edition 面向个人和小团队,官方限制为最多 40,000 components、100,000 requests/day;超过阈值后会暂停新增 component,直到用量回到限制以下 | 不内置 Community Edition 这类版本授权用量限制;容量边界由所选关系数据库、OSS/S3、运行副本数和部署规格决定,适合按实际业务规模扩容 | | 高可用部署 | 开源版适合单实例或基础 Kubernetes 部署;官方 HA deployment 属于 Pro 能力 | 从设计上默认支持多副本:session、认证 ticket、catalog 水位、锁、迁移进度和短生命周期协同状态都落 MySQL 或 PostgreSQL,进程内缓存只作为可重建热缓存 | | 稳定性和升级 | 版本边界复杂:3.70.x 是最后支持 OrientDB 的版本;3.71.0 起新安装默认 H2,但 H2 仍是内嵌数据库;Community Edition 到 3.77.0+ 才支持免费使用外部 PostgreSQL;3.88.0 起搜索才完全改为 SQL、替代 Elasticsearch。旧版 OrientDB/Elasticsearch/本地数据目录组合升级窗口重,文件损坏后恢复高度依赖备份、修复任务和人工介入 | 运行时支持 MySQL/PostgreSQL,不依赖 OrientDB 和内嵌 Elasticsearch;核心状态在共享关系数据库,blob 在 OSS/S3/File blob store,缓存和索引均可重建,更适合滚动升级、故障切换和数据恢复 | @@ -124,7 +125,7 @@ curl -fsSL https://raw.githubusercontent.com/klboke/kkrepo/main/scripts/quicksta ![前台仓库列表](docs/img/img_7.png) -按格式搜索组件,支持 Maven、npm、PyPI、Go、Helm、Cargo/Rust、Dart/Pub、Composer/PHP、Docker/OCI、NuGet、RubyGems、Yum 和 Raw 等仓库类型的制品检索。 +按格式搜索组件,支持 Maven、npm、PyPI、Go、Helm、Cargo/Rust、Dart/Pub、Composer/PHP、Terraform、Docker/OCI、NuGet、RubyGems、Yum 和 Raw 等仓库类型的制品检索。 ![前台制品搜索](docs/img/img.png) @@ -170,14 +171,14 @@ AI agent 和贡献者的开发说明见 [AGENTS.md](AGENTS.md)。 2. ✅ Cargo / Rust - 仓库能力已完成,包含搜索、UI/API 上传和迁移能力([设计说明](docs/zh/dev/cargo-rust-repository-design.md)) 3. ✅ Dart / Pub - 仓库能力已完成,包含 hosted/proxy/group、真实客户端 E2E、UI/API 上传、搜索和 Nexus 迁移能力([设计说明](docs/zh/dev/dart-pub-repository-design.md)) 4. ✅ Composer / PHP - hosted、proxy、group、UI/API 上传、搜索、真实客户端 E2E、强制 Nexus live 对比和显式选择的 Nexus proxy cache 迁移 E2E 已实现([设计说明](docs/zh/dev/composer-php-repository-design.md)) -5. ohpm / HarmonyOS - 规划中,覆盖 hosted、proxy、group、导入和管理端能力([设计说明](docs/zh/dev/ohpm-repository-design.md)) -6. Swift Package Registry -7. APT / Debian -8. Terraform Provider / Module Registry - 规划中,覆盖 hosted、proxy、group、Provider GPG 签名、Nexus 路径兼容、真实 Terraform CLI E2E 和 Nexus 迁移([设计说明](docs/zh/dev/terraform-repository-design.md)) +5. ✅ Terraform Provider / Module Registry - hosted、proxy、group、Provider GPG 签名、Nexus 路径兼容、UI/API 上传、搜索、真实 Terraform CLI E2E、Nexus hosted 数据迁移和显式选择的 proxy cache 迁移已实现([设计说明](docs/zh/dev/terraform-repository-design.md)) +6. ohpm / HarmonyOS - 规划中,覆盖 hosted、proxy、group、导入和管理端能力([设计说明](docs/zh/dev/ohpm-repository-design.md)) +7. Swift Package Registry +8. APT / Debian 9. Conan 10. Conda -用户和管理端 UI 已暴露的 token 类型包括协议专用 token(`NpmToken`、`CargoToken`、`PubToken`、`NuGetApiKey`、`RubyGemsApiKey`),以及面向 CI、脚本和自定义 HTTP 客户端的 `GenericToken`;`GenericToken` 适用于能够发送已配置 API-key header 或 bearer token 的调用方。 +用户和管理端 UI 已暴露的 token 类型包括协议专用 token(`NpmToken`、`CargoToken`、`PubToken`、`NuGetApiKey`、`RubyGemsApiKey`),以及面向 Terraform 服务 URL、CI、脚本和自定义 HTTP 客户端的 `GenericToken`;`GenericToken` 适用于能够发送已配置 API-key header 或 bearer token 的调用方。 ## 参与贡献 diff --git a/README.md b/README.md index 57f64810..0f6587c8 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ **English** | [中文](README.cn.md) -kkRepo is a community-driven, fully open-source, self-hosted artifact repository designed to address the limitations and pain points of Sonatype Nexus Community Edition and provide the community with an open, reliable, and sustainably evolving artifact management solution. It currently supports Maven, npm, PyPI, Go, Helm, Cargo/Rust, Dart/Pub, Composer/PHP, Docker/OCI, NuGet, RubyGems, Yum, Raw, and other artifact formats. +kkRepo is a community-driven, fully open-source, self-hosted artifact repository designed to address the limitations and pain points of Sonatype Nexus Community Edition and provide the community with an open, reliable, and sustainably evolving artifact management solution. It currently supports Maven, npm, PyPI, Go, Helm, Cargo/Rust, Dart/Pub, Composer/PHP, Terraform, Docker/OCI, NuGet, RubyGems, Yum, Raw, and other artifact formats. ## Features -- Support for 13+ mainstream repository formats across hosted, proxy, and group repository types. +- Support for 14+ mainstream repository formats across hosted, proxy, and group repository types. - Compatibility with Sonatype Nexus APIs, user permission model, and the `/repository//...` URL layout. - Use kkRepo as a drop-in replacement for Sonatype Nexus, with one-click migration of existing data while preserving repository domains and URLs, so client configurations and CI workflows continue unchanged. - Comprehensive identity and access control with Local, LDAP, and OIDC authentication, configurable anonymous access policies, and fine-grained permissions. @@ -74,13 +74,14 @@ Local hot-reload development and testing are documented in the [Development Guid | Cargo / Rust | hosted / proxy / group | `cargo publish`, yank/unyank, `CargoToken` auth, and UI/API `.crate` upload | Sparse index and `cargo search` supported | Cargo repository migration is supported | | Dart / Pub | hosted / proxy / group | `dart pub publish`, `dart pub get`, `flutter pub get`, `PubToken` auth, and UI/API `.tar.gz` upload | Package/version metadata, archive attributes, and Pub search supported | Nexus 3.92.0 Pub hosted migration and explicitly selected proxy cache migration are supported | | Composer / PHP | hosted / proxy / group | Composer has no standard publish command; Components API and UI zip/tar archive upload plus Composer 2 installation are supported | Package/version metadata, dist, HTML View, Browse/Search, and Usage supported | Native Nexus Composer proxy configuration is migrated; cache migration requires explicit administrator selection and a proven source profile | +| Terraform Provider / Module Registry | hosted / proxy / group | Nexus-compatible PUT and UI/API archive upload; `terraform init` resolves hosted and proxied modules/providers through groups | Module/provider coordinates, versions, platforms, Browse/Search, and Usage supported | Nexus Terraform hosted data and explicitly selected proxy archive caches are migrated; proxy/group configuration is also migrated | | Docker / OCI | hosted / proxy / group | Registry V2 login, hosted push/pull, proxy pull, group pull, OCI referrers, cleanup, and connector-port access | Manifest/tag/blob metadata supported | Hosted Docker repository data migration is supported through the Nexus Repository Data flow | | NuGet | hosted / proxy / group | package push and admin UI upload | v3 service index / search supported | Hosted repositories are migrated by default; proxy repositories can be migrated optionally | | RubyGems | hosted / proxy / group | gem push/yank and admin UI upload | Supported | Hosted repositories are migrated by default; proxy repositories can be migrated optionally | | Yum | hosted / proxy / group | RPM upload and admin UI upload | repodata supported | Hosted repositories are migrated by default; proxy repositories can be migrated optionally | | Raw | hosted / proxy / group | PUT upload and admin UI upload | Supported | Hosted repositories are migrated by default; proxy repositories can be migrated optionally | -Repository data migration scans hosted repositories by default. If you need to migrate proxy repositories from a source Sonatype Nexus Repository deployment as historical backup data or upstream cache data, explicitly specify repository names in `Optional proxy repositories` on the migration page. Cargo / Rust, Dart / Pub, and native Nexus Composer proxy-cache migration are supported; Composer proxy migration always requires explicit administrator selection. +Repository data migration scans hosted repositories by default. If you need to migrate proxy repositories from a source Sonatype Nexus Repository deployment as historical backup data or upstream cache data, explicitly specify repository names in `Optional proxy repositories` on the migration page. Cargo / Rust, Dart / Pub, native Nexus Composer, and Terraform proxy-cache migration are supported when explicitly selected and accepted by the source profile. Terraform restores module/provider archives through an independent proxy-cache writer and preserves Nexus public asset paths instead of replaying cache assets as hosted publications. Restored module archives can satisfy module download discovery without an upstream call; provider routes rebuild the verified checksum/signature snapshot from the configured upstream and pin its cached blobs for the metadata lifetime. ## Migrating From Sonatype Nexus Repository @@ -99,7 +100,7 @@ Migration supports interruption and resume. Completed data is skipped on later r | Dimension | Sonatype Nexus Repository OSS / Community Edition | kkRepo | | --- | --- | --- | | Product positioning | A general-purpose artifact repository management platform with broad format and management coverage | Provides migration-oriented client behavior, permission model, and `/repository//...` URL compatibility while using a relational-database, OSS/S3-first, multi-replica-friendly architecture | -| Supported formats | Officially supports more formats; exact capabilities vary by version and distribution | Focuses on common artifact formats. Currently supports Maven, npm, PyPI, Go, Helm, Cargo/Rust, Dart/Pub, Composer/PHP, Docker/OCI, NuGet, RubyGems, Yum, and Raw. Each format is implemented as an independent protocol module for prioritized extension and validation | +| Supported formats | Officially supports more formats; exact capabilities vary by version and distribution | Focuses on common artifact formats. Currently supports Maven, npm, PyPI, Go, Helm, Cargo/Rust, Dart/Pub, Composer/PHP, Terraform, Docker/OCI, NuGet, RubyGems, Yum, and Raw. Each format is implemented as an independent protocol module for prioritized extension and validation | | Usage limits | Community Edition targets individuals and small teams. Official limits are up to 40,000 components and 100,000 requests/day. When exceeded, new component creation is paused until usage returns below the limits | Does not include Community Edition-style license usage limits. Capacity is bounded by the selected relational database, OSS/S3, replica count, and deployment sizing, so it can scale with actual business needs | | High availability deployment | Open source editions are suitable for a single instance or basic Kubernetes deployment; official HA deployment is a Pro capability | Designed for multi-replica deployment by default: session, authentication tickets, catalog watermarks, locks, migration progress, and short-lived coordination state are stored in MySQL or PostgreSQL. In-process cache is only a rebuildable hot cache | | Stability and upgrade | Version boundaries are complex: 3.70.x is the last version supporting OrientDB; 3.71.0 defaults new installs to H2, but H2 is still embedded; Community Edition did not support free external PostgreSQL until 3.77.0+; search was fully moved to SQL and away from Elasticsearch only in 3.88.0. Older OrientDB/Elasticsearch/local-data-directory deployments carry heavy upgrade windows and recovery depends heavily on backups, repair tasks, and manual intervention | MySQL/PostgreSQL runtime with no dependency on OrientDB or embedded Elasticsearch. Core state is in the shared relational database, blobs are in OSS/S3/File blob store, and cache/index data is rebuildable, making rolling upgrade, failover, and recovery easier | @@ -124,7 +125,7 @@ The repository list shows hosted, proxy, and group repositories with format, sta ![User repository list](docs/img/img_7.png) -Search components by format across Maven, npm, PyPI, Go, Helm, Cargo/Rust, Dart/Pub, Composer/PHP, Docker/OCI, NuGet, RubyGems, Yum, Raw, and other repository types. +Search components by format across Maven, npm, PyPI, Go, Helm, Cargo/Rust, Dart/Pub, Composer/PHP, Terraform, Docker/OCI, NuGet, RubyGems, Yum, Raw, and other repository types. ![User artifact search](docs/img/img.png) @@ -170,14 +171,14 @@ Repository format roadmap: 2. ✅ Cargo / Rust - Repository support completed, including search, UI/API upload, and migration ([design notes](docs/en/dev/cargo-rust-repository-design.md)) 3. ✅ Dart / Pub - Repository support completed, including hosted/proxy/group, client E2E, UI/API upload, search, and Nexus migration ([Chinese design notes](docs/zh/dev/dart-pub-repository-design.md)) 4. ✅ Composer / PHP - Hosted, proxy, group, UI/API upload, search, real-client E2E, required Nexus live comparison, and explicitly selected Nexus proxy-cache migration E2E implemented ([Chinese design notes](docs/zh/dev/composer-php-repository-design.md)) -5. ohpm / HarmonyOS - Planned with hosted, proxy, group, import, and admin capabilities ([Chinese design notes](docs/zh/dev/ohpm-repository-design.md)) -6. Swift Package Registry -7. APT / Debian -8. Terraform Provider / Module Registry - Planned with hosted, proxy, group, provider GPG signing, Nexus-compatible paths, real Terraform CLI E2E, and Nexus migration ([Chinese design notes](docs/zh/dev/terraform-repository-design.md)) +5. ✅ Terraform Provider / Module Registry - Hosted, proxy, group, provider GPG signing, Nexus-compatible paths, UI/API upload, search, real Terraform CLI E2E, Nexus hosted-data migration, and explicitly selected proxy-cache migration implemented ([Chinese design notes](docs/zh/dev/terraform-repository-design.md)) +6. ohpm / HarmonyOS - Planned with hosted, proxy, group, import, and admin capabilities ([Chinese design notes](docs/zh/dev/ohpm-repository-design.md)) +7. Swift Package Registry +8. APT / Debian 9. Conan 10. Conda -Token types exposed in the user and admin UI include protocol-specific tokens (`NpmToken`, `CargoToken`, `PubToken`, `NuGetApiKey`, `RubyGemsApiKey`) plus `GenericToken` for CI, scripts, and custom HTTP clients that can send the configured API-key header or bearer token. +Token types exposed in the user and admin UI include protocol-specific tokens (`NpmToken`, `CargoToken`, `PubToken`, `NuGetApiKey`, `RubyGemsApiKey`) plus `GenericToken` for Terraform service URLs, CI, scripts, and custom HTTP clients that can send the configured API-key header or bearer token. ## Contributing diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/assets/admin.js b/admin-ui/src/main/resources/META-INF/resources/admin/assets/admin.js index db04240a..4c527799 100644 --- a/admin-ui/src/main/resources/META-INF/resources/admin/assets/admin.js +++ b/admin-ui/src/main/resources/META-INF/resources/admin/assets/admin.js @@ -371,6 +371,7 @@ const FORMAT_ICON_NAMES = Object.freeze({ nuget: "nuget", rubygems: "rubygems", yum: "yum", + terraform: "terraform", raw: "raw", }); @@ -387,6 +388,7 @@ const FORMAT_DISPLAY_NAMES = Object.freeze({ nuget: "NuGet", rubygems: "RubyGems", yum: "Yum / RPM", + terraform: "Terraform", raw: "Raw", }); @@ -1718,7 +1720,7 @@ function memberCandidates() { const recipe = currentRecipe(); const format = recipe ? recipe.format : null; if (!format) return []; - const allowNestedGroups = format === "pub" || format === "composer"; + const allowNestedGroups = format === "pub" || format === "composer" || format === "terraform"; return repositories.filter((repo) => { if (repo.format !== format) return false; if (repositoryFormMode === "edit" && repo.name === editingRepositoryName) return false; @@ -1950,7 +1952,8 @@ function refreshRepositoryRemoteDefaults(recipe) { docker: "https://registry-1.docker.io/", cargo: "https://index.crates.io/", pub: "https://pub.dev/", - composer: "https://repo.packagist.org/" + composer: "https://repo.packagist.org/", + terraform: "https://registry.terraform.io/" }; remote.placeholder = defaults[recipe.format] || "https://example.com/"; if (repositoryFormMode === "create" && !remote.value.trim() && defaults[recipe.format]) { diff --git a/browse-ui/src/main/resources/META-INF/resources/browse/assets/browse.js b/browse-ui/src/main/resources/META-INF/resources/browse/assets/browse.js index 4ecdbdc7..2a17e85f 100644 --- a/browse-ui/src/main/resources/META-INF/resources/browse/assets/browse.js +++ b/browse-ui/src/main/resources/META-INF/resources/browse/assets/browse.js @@ -9,6 +9,7 @@ const state = { mode: "repos", repo: null, path: "" }; const treeCache = new Map(); // key = `${repo}::${path}`, value = entries[] const expanded = new Set(); // keys (same shape as treeCache) currently expanded const treeMountLoads = new WeakMap(); +const treeNodeEntries = new WeakMap(); let userMenuCloseTimer = null; let repositoriesCache = []; @@ -42,6 +43,7 @@ const SEARCH_ROUTE_FORMAT = { nuget: "nuget", pub: "pub", composer: "composer", + terraform: "terraform", pypi: "pypi", rubygems: "rubygems", yum: "yum", @@ -55,6 +57,7 @@ const FORMAT_ROUTE_SEGMENT = { nuget: "nuget", pub: "pub", composer: "composer", + terraform: "terraform", pypi: "pypi", rubygems: "rubygems", yum: "yum", @@ -145,6 +148,7 @@ function componentBrowsePath(component) { if (format === "composer" && component.kind !== "composer-package") { return name; } + if (format === "terraform") return name; return ""; } @@ -622,6 +626,7 @@ const FORMAT_ICON_NAMES = Object.freeze({ cargo: "cargo", pub: "pub", composer: "composer", + terraform: "terraform", go: "go", helm: "helm", docker: "docker", @@ -1290,10 +1295,19 @@ async function ensureTreeLevelLoaded(path, mountEl, depth) { return true; } +async function activateTreeBranch(entry, toggleExpand) { + const expansion = toggleExpand(); + if (VERSION_DIR_RE.test(entry.name) || hasDirectoryUsage(entry)) { + await showComponentDetail(entry); + } + await expansion; +} + function buildTreeNode(entry) { const li = document.createElement("li"); li.className = entry.leaf ? "tree-node leaf" : "tree-node branch"; li.dataset.path = entry.path; + treeNodeEntries.set(li, entry); const row = document.createElement("div"); row.className = "tree-row"; @@ -1338,7 +1352,6 @@ function buildTreeNode(entry) { childMount.className = "tree-children"; childMount.style.display = isExpanded ? "" : "none"; li.appendChild(childMount); - const isVersionDir = VERSION_DIR_RE.test(entry.name); const ensureLoaded = async () => { await ensureTreeLevelLoaded(entry.path, childMount, 1); }; @@ -1360,12 +1373,7 @@ function buildTreeNode(entry) { label.addEventListener("click", async () => { selectRow(row); syncTreePath(entry); - if (isVersionDir || hasDirectoryUsage(entry)) { - await showComponentDetail(entry); - await setExpanded(true); - } else { - await toggleExpand(); - } + await activateTreeBranch(entry, toggleExpand); }); if (isExpanded) { ensureLoaded(); @@ -1410,11 +1418,12 @@ function pathSelectorValue(path) { async function selectInitialTreePath() { if (!state.path) return; await revealTreePath(state.path); - const row = document.querySelector(`.tree-node[data-path="${pathSelectorValue(state.path)}"] > .tree-row`); + const node = document.querySelector(`.tree-node[data-path="${pathSelectorValue(state.path)}"]`); + const row = node?.querySelector(":scope > .tree-row"); if (!row) return; selectRow(row); row.scrollIntoView({ block: "center" }); - const entry = findCachedTreeEntry(state.path); + const entry = treeNodeEntries.get(node) || findCachedTreeEntry(state.path); if (!entry) return; if (entry.leaf) await showAssetDetail(entry); else if (VERSION_DIR_RE.test(entry.name) || hasDirectoryUsage(entry)) await showComponentDetail(entry); @@ -1727,6 +1736,9 @@ function hasDirectoryUsage(entry) { const reserved = new Set(["p2", "providers", "packages", "_composer"]); return (parts.length === 2 || parts.length === 3) && !reserved.has(parts[0]); } + if (repo.format === "terraform") { + return parts[0] === "v1"; + } return false; } @@ -2393,6 +2405,69 @@ function renderDockerReferrers(referrers) { `; } +function terraformUsageDetail(entry) { + const parts = pathSegments(entry.path); + if (parts[0] !== "v1") return null; + const repoUrl = repositoryBaseUrl().replace(/\/+$/, ""); + const host = window.location.host; + const section = parts[1] || ""; + const isModule = section === "modules"; + const isProvider = section === "providers"; + const summaryRows = [ + ["Repository", state.repo], + ["Format", "terraform"], + ["Path", entry.path], + ["Scope", isModule ? "Terraform modules" : isProvider ? "Terraform providers" : "Terraform registry"], + ]; + const snippets = []; + + if (isModule) { + const [namespace, name, system, version] = parts.slice(2, 6); + if (namespace) summaryRows.push(["Namespace", namespace]); + if (name) summaryRows.push(["Name", name]); + if (system) summaryRows.push(["System", system]); + if (version) summaryRows.push(["Version", version]); + snippets.push(usageSnippet( + "Terraform CLI config", + `host "${host}" {\n services = {\n "modules.v1" = "${repoUrl}/v1/modules/"\n }\n}`, + )); + if (namespace && name && system) { + snippets.push(usageSnippet( + "module", + `module "${name}" {\n source = "${host}/${namespace}/${name}/${system}"\n version = "${version || "latest"}"\n}`, + )); + } + } else if (isProvider) { + const [namespace, type, version] = parts.slice(2, 5); + if (namespace) summaryRows.push(["Namespace", namespace]); + if (type) summaryRows.push(["Type", type]); + if (version) summaryRows.push(["Version", version]); + snippets.push(usageSnippet( + "Terraform CLI config", + `host "${host}" {\n services = {\n "providers.v1" = "${repoUrl}/v1/providers/"\n }\n}`, + )); + if (namespace && type) { + snippets.push(usageSnippet( + "required_providers", + `terraform {\n required_providers {\n ${type} = {\n source = "${host}/${namespace}/${type}"\n version = "${version || "latest"}"\n }\n }\n}`, + )); + } + } else if (section) { + summaryRows.push(["Scope", `Terraform path: ${section}`]); + } else { + snippets.push(usageSnippet( + "Terraform CLI config", + `host "${host}" {\n services = {\n "modules.v1" = "${repoUrl}/v1/modules/"\n "providers.v1" = "${repoUrl}/v1/providers/"\n }\n}`, + )); + } + + return { + crumbText: entry.path, + summaryRows, + snippets, + }; +} + async function usageDetailForEntry(entry, detail = null) { const repo = currentRepository(); if (!repo) return null; @@ -2404,6 +2479,7 @@ async function usageDetailForEntry(entry, detail = null) { if (repo.format === "go") return goUsageDetail(entry); if (repo.format === "pub") return pubUsageDetail(entry); if (repo.format === "composer") return composerUsageDetail(entry, detail); + if (repo.format === "terraform") return terraformUsageDetail(entry); if (repo.format === "docker") return dockerUsageDetail(entry); return null; } @@ -2417,7 +2493,7 @@ async function showComponentDetail(entry) { return; } renderDetailPane({ - crumbIcon: repoBreadcrumbIcon(), + crumbIcon: repositoryFormatIcon(), crumbText: detail.crumbText, summaryRows: detail.summaryRows, snippets: detail.snippets, @@ -2570,6 +2646,57 @@ function renderUploadFields() { bindRemoveAssetButtons(); return; } + if (repo.format === "terraform") { + fields.innerHTML = ` + + + + + + + + + + + `; + document.getElementById("upload-terraform-kind") + .addEventListener("change", updateTerraformUploadKind); + updateTerraformUploadKind(); + return; + } fields.innerHTML = `