diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fa672b28a37..895fe068ed9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -23,6 +23,24 @@ updates: schedule: interval: "weekly" labels: - - "release-note/update" - - # More will be needed \ No newline at end of file + - "release-note/bump-version" + - "branch/main" + + - package-ecosystem: "gomod" + directory: "/src" + schedule: + interval: "weekly" + target-branch: "release-2.14.0" + labels: + - "release-note/bump-version" + - "branch/release-2.14.0" + + - package-ecosystem: "npm" + directory: "/src/portal" + schedule: + interval: "weekly" + open-pull-requests-limit: 0 + labels: + - "release-note/bump-version" + + # More will be needed diff --git a/.github/release.yml b/.github/release.yml index 0cd23c92ad8..2f97c5f5a42 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -31,6 +31,10 @@ changelog: labels: - release-note/deprecation + - title: Bump Component Version 🤖 + labels: + - release-note/bump-version + - title: Other Changes labels: - - "*" \ No newline at end of file + - "*" diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 95b938cbb20..72827276e81 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -15,6 +15,8 @@ env: UI_BUILDER_VERSION: 1.6.0 on: + # the paths-ignore is the same as the paths in pass-CI.yml, they should be synced together + # see https://web.archive.org/web/20230506145443/https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/troubleshooting-required-status-checks#handling-skipped-but-required-checks pull_request: paths-ignore: - 'docs/**' @@ -23,7 +25,11 @@ on: - '!tests/**.sh' - '!tests/apitests/**' - '!tests/ci/**' + - '!tests/resources/**' + - '!tests/robot-cases/**' + - '!tests/robot-cases/Group1-Nightly/**' push: + # the paths-ignore is the same as the paths in pass-CI.yml, they should be synced together paths-ignore: - 'docs/**' - '**.md' @@ -31,6 +37,9 @@ on: - '!tests/**.sh' - '!tests/apitests/**' - '!tests/ci/**' + - '!tests/resources/**' + - '!tests/robot-cases/**' + - '!tests/robot-cases/Group1-Nightly/**' jobs: UTTEST: @@ -38,7 +47,7 @@ jobs: UTTEST: true runs-on: #- self-hosted - - ubuntu-latest + - oracle-vm-24cpu-96gb-x86-64 timeout-minutes: 100 steps: - name: Set up Go 1.23 @@ -99,7 +108,7 @@ jobs: APITEST_DB: true runs-on: #- self-hosted - - ubuntu-latest + - oracle-vm-24cpu-96gb-x86-64 timeout-minutes: 100 steps: - name: Set up Go 1.23 @@ -149,7 +158,7 @@ jobs: bash ./tests/showtime.sh ./tests/ci/api_run.sh DB $IP df -h - name: upload_logs - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: db-api-harbor-logs.tar.gz path: /home/runner/work/harbor/harbor/src/github.com/goharbor/harbor/integration_logs.tar.gz @@ -159,7 +168,7 @@ jobs: APITEST_DB: true runs-on: #- self-hosted - - ubuntu-latest + - oracle-vm-24cpu-96gb-x86-64 timeout-minutes: 100 steps: - name: Set up Go 1.23 @@ -209,7 +218,7 @@ jobs: bash ./tests/showtime.sh ./tests/ci/api_run.sh PROXY_CACHE $IP df -h - name: upload_logs - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: proxy-api-harbor-logs.tar.gz path: /home/runner/work/harbor/harbor/src/github.com/goharbor/harbor/integration_logs.tar.gz @@ -219,7 +228,7 @@ jobs: APITEST_LDAP: true runs-on: #- self-hosted - - ubuntu-latest + - oracle-vm-24cpu-96gb-x86-64 timeout-minutes: 100 steps: - name: Set up Go 1.23 @@ -267,7 +276,7 @@ jobs: bash ./tests/showtime.sh ./tests/ci/api_run.sh LDAP $IP df -h - name: upload_logs - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ldap-api-harbor-logs.tar.gz path: /home/runner/work/harbor/harbor/src/github.com/goharbor/harbor/integration_logs.tar.gz @@ -277,7 +286,7 @@ jobs: OFFLINE: true runs-on: #- self-hosted - - ubuntu-latest + - oracle-vm-24cpu-96gb-x86-64 timeout-minutes: 100 steps: - name: Set up Go 1.23 @@ -329,10 +338,10 @@ jobs: UI_UT: true runs-on: #- self-hosted - - ubuntu-latest + - oracle-vm-24cpu-96gb-x86-64 timeout-minutes: 100 steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: '18' - uses: actions/checkout@v5 diff --git a/.github/workflows/build-package.yml b/.github/workflows/build-package.yml index 09e1d72706c..e21420f9161 100644 --- a/.github/workflows/build-package.yml +++ b/.github/workflows/build-package.yml @@ -13,10 +13,10 @@ jobs: env: BUILD_PACKAGE: true runs-on: - - ubuntu-22.04 + - oracle-vm-24cpu-96gb-x86-64 steps: - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4.2.1 + uses: aws-actions/configure-aws-credentials@v5.0.0 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/conformance_test.yml b/.github/workflows/conformance_test.yml index 8ec4f8f8d94..668f4290b80 100644 --- a/.github/workflows/conformance_test.yml +++ b/.github/workflows/conformance_test.yml @@ -15,10 +15,10 @@ jobs: CONFORMANCE_TEST: true runs-on: #- self-hosted - - ubuntu-latest + - oracle-vm-24cpu-96gb-x86-64 steps: - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4.2.1 + uses: aws-actions/configure-aws-credentials@v5.0.0 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/housekeeping-stale-issues-prs.yaml b/.github/workflows/housekeeping-stale-issues-prs.yaml index a44665e22ef..c0a7de21e1a 100644 --- a/.github/workflows/housekeeping-stale-issues-prs.yaml +++ b/.github/workflows/housekeeping-stale-issues-prs.yaml @@ -7,7 +7,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9.1.0 + - uses: actions/stale@v10.0.0 with: stale-issue-message: 'This issue is being marked stale due to a period of inactivity. If this issue is still relevant, please comment or remove the stale label. Otherwise, this issue will close in 30 days.' stale-pr-message: 'This PR is being marked stale due to a period of inactivty. If this PR is still relevant, please comment or remove the stale label. Otherwise, this PR will close in 30 days.' diff --git a/.github/workflows/label_check.yaml b/.github/workflows/label_check.yaml index 6c2cd53adcc..8b24e6c6c16 100644 --- a/.github/workflows/label_check.yaml +++ b/.github/workflows/label_check.yaml @@ -1,7 +1,7 @@ name: Release Note Label Check # Trigger the workflow on pull requests only -on: +on: pull_request: types: [opened, labeled, unlabeled, synchronize] @@ -20,4 +20,5 @@ jobs: with: mode: minimum count: 1 - labels: "release-note/ignore-for-release, release-note/new-feature, release-note/update, release-note/enhancement, release-note/community, release-note/breaking-change, release-note/docs, release-note/infra, release-note/deprecation" + labels: "release-note/.*" + use_regex: true diff --git a/.github/workflows/pass-CI.yml b/.github/workflows/pass-CI.yml index a54567f6102..0669218cb8b 100644 --- a/.github/workflows/pass-CI.yml +++ b/.github/workflows/pass-CI.yml @@ -2,6 +2,7 @@ name: CI on: pull_request: + # the paths is the same as the paths-ignore in CI.yml, they should be synced together paths: - 'docs/**' - '**.md' @@ -13,6 +14,7 @@ on: - '!tests/robot-cases/**' - '!tests/robot-cases/Group1-Nightly/**' push: + # the paths is the same as the paths-ignore in CI.yml, they should be synced together paths: - 'docs/**' - '**.md' diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index cbf0cf23706..22cf6f282cb 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -7,7 +7,7 @@ on: jobs: release: - runs-on: ubuntu-22.04 + runs-on: oracle-vm-24cpu-96gb-x86-64 steps: - uses: actions/checkout@v5 - name: Setup env @@ -20,7 +20,7 @@ jobs: echo "BRANCH=$(echo $release | jq -r '.target_commitish')" >> $GITHUB_ENV echo "PRERELEASE=$(echo $release | jq -r '.prerelease')" >> $GITHUB_ENV - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4.2.1 + uses: aws-actions/configure-aws-credentials@v5.0.0 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/Makefile b/Makefile index 88a02f31d13..2619fd7758d 100644 --- a/Makefile +++ b/Makefile @@ -140,7 +140,7 @@ DOCKERRMIMAGE=$(DOCKERCMD) rmi DOCKERPULL=$(DOCKERCMD) pull DOCKERIMAGES=$(DOCKERCMD) images DOCKERSAVE=$(DOCKERCMD) save -DOCKERCOMPOSECMD=$(shell which docker-compose) +DOCKERCOMPOSECMD=$(shell which docker-compose 2>/dev/null || echo "docker compose") DOCKERTAG=$(DOCKERCMD) tag # go parameters diff --git a/README.md b/README.md index 6cfac0901d4..198c2f9fd29 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,13 @@ # Harbor - -[![CI](https://github.com/goharbor/harbor/workflows/CI/badge.svg?branch=main&event=push)](https://github.com/goharbor/harbor/actions?query=event%3Apush+branch%3Amain+workflow%3ACI+) -[![Coverage Status](https://codecov.io/gh/goharbor/harbor/branch/main/graph/badge.svg)](https://codecov.io/gh/goharbor/harbor) +[![CI](https://github.com/goharbor/harbor/actions/workflows/CI.yml/badge.svg)](https://github.com/goharbor/harbor/actions/workflows/CI.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/goharbor/harbor)](https://goreportcard.com/report/github.com/goharbor/harbor) +[![Coverage Status](https://codecov.io/gh/goharbor/harbor/branch/main/graph/badge.svg)](https://codecov.io/gh/goharbor/harbor) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/2095/badge)](https://bestpractices.coreinfrastructure.org/projects/2095) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/792fe1755edc4d6e91f4c3469f553389)](https://www.codacy.com/gh/goharbor/harbor/dashboard?utm_source=github.com&utm_medium=referral&utm_content=goharbor/harbor&utm_campaign=Badge_Grade) ![Code scanning - action](https://github.com/goharbor/harbor/workflows/Code%20scanning%20-%20action/badge.svg) -[![Nightly Status](https://us-central1-eminent-nation-87317.cloudfunctions.net/harbor-nightly-result)](https://www.googleapis.com/storage/v1/b/harbor-nightly/o) -![CONFORMANCE_TEST](https://github.com/goharbor/harbor/workflows/CONFORMANCE_TEST/badge.svg) +![OCI Distribution Conformance Tests](https://github.com/goharbor/harbor/workflows/CONFORMANCE_TEST/badge.svg) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fgoharbor%2Fharbor.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fgoharbor%2Fharbor?ref=badge_shield) -[![Artifact HUB](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/harbor)](https://artifacthub.io/packages/helm/harbor/harbor) +[![Helm Chart on Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/harbor)](https://artifacthub.io/packages/helm/harbor/harbor)
|![notification](https://raw.githubusercontent.com/goharbor/website/master/docs/img/readme/bell-outline-badged.svg)Community Meeting| diff --git a/VERSION b/VERSION index d4174a4e192..f855fc0f82d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v2.14.0 +v2.15.0 diff --git a/api/v2.0/swagger.yaml b/api/v2.0/swagger.yaml index 16d5d120440..6821ed65ca3 100644 --- a/api/v2.0/swagger.yaml +++ b/api/v2.0/swagger.yaml @@ -7321,6 +7321,10 @@ definitions: type: string description: 'The bandwidth limit of proxy cache, in Kbps (kilobits per second). It limits the communication between Harbor and the upstream registry, not the client and the Harbor.' x-nullable: true + max_upstream_conn: + type: string + description: 'The max connection per artifact to the upstream registry in current proxy cache project, if it is -1, no limit to upstream registry connections' + x-nullable: true ProjectSummary: type: object properties: diff --git a/make/migrations/postgresql/0180_2.15.0_schema.up.sql b/make/migrations/postgresql/0180_2.15.0_schema.up.sql new file mode 100644 index 00000000000..a4383fc9f8d --- /dev/null +++ b/make/migrations/postgresql/0180_2.15.0_schema.up.sql @@ -0,0 +1,17 @@ +/* +Initialize skip_audit_log_database configuration based on existing audit log usage - Only insert the configuration if it doesn't already exist +1. If tables exist and show evidence of previous usage + set skip_audit_log_database to false +2. If tables exist but show no evidence of usage, don't create the configuration record +*/ +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM properties WHERE k = 'skip_audit_log_database') THEN + RETURN; + END IF; + + IF (SELECT last_value FROM audit_log_id_seq) > 1 + OR (SELECT last_value FROM audit_log_ext_id_seq) > 1 THEN + INSERT INTO properties (k, v) VALUES ('skip_audit_log_database', 'false'); + END IF; +END $$; diff --git a/src/common/api/base.go b/src/common/api/base.go index 213fdefc7a4..9984922b011 100644 --- a/src/common/api/base.go +++ b/src/common/api/base.go @@ -21,6 +21,7 @@ import ( "fmt" "net/http" "strconv" + "strings" "github.com/beego/beego/v2/core/validation" "github.com/beego/beego/v2/server/web" @@ -98,11 +99,11 @@ func (b *BaseAPI) Validate(v any) (bool, error) { } if !isValid { - message := "" + var message strings.Builder for _, e := range validator.Errors { - message += fmt.Sprintf("%s %s \n", e.Field, e.Message) + message.WriteString(fmt.Sprintf("%s %s \n", e.Field, e.Message)) } - return false, errors.New(message) + return false, errors.New(message.String()) } return true, nil } diff --git a/src/common/secret/request.go b/src/common/secret/request.go index 36be468dc7e..fb247994900 100644 --- a/src/common/secret/request.go +++ b/src/common/secret/request.go @@ -31,8 +31,8 @@ func FromRequest(req *http.Request) string { return "" } auth := req.Header.Get("Authorization") - if strings.HasPrefix(auth, HeaderPrefix) { - return strings.TrimPrefix(auth, HeaderPrefix) + if after, ok := strings.CutPrefix(auth, HeaderPrefix); ok { + return after } return "" } diff --git a/src/controller/gc/controller.go b/src/controller/gc/controller.go index 0e715a0d291..9359d143df4 100644 --- a/src/controller/gc/controller.go +++ b/src/controller/gc/controller.go @@ -77,6 +77,7 @@ type controller struct { func (c *controller) Start(ctx context.Context, policy Policy, trigger string) (int64, error) { para := make(map[string]any) para["delete_untagged"] = policy.DeleteUntagged + para["delete_tag"] = policy.DeleteTag para["dry_run"] = policy.DryRun para["workers"] = policy.Workers para["redis_url_reg"] = policy.ExtraAttrs["redis_url_reg"] @@ -205,6 +206,7 @@ func (c *controller) GetSchedule(ctx context.Context) (*scheduler.Schedule, erro func (c *controller) CreateSchedule(ctx context.Context, cronType, cron string, policy Policy) (int64, error) { extras := make(map[string]any) extras["delete_untagged"] = policy.DeleteUntagged + extras["delete_tag"] = policy.DeleteTag extras["workers"] = policy.Workers return c.schedulerMgr.Schedule(ctx, job.GarbageCollectionVendorType, -1, cronType, cron, job.GarbageCollectionVendorType, policy, extras) } @@ -234,6 +236,7 @@ func convertTask(task *task.Task) *Task { StatusMessage: task.StatusMessage, RunCount: task.RunCount, DeleteUntagged: task.GetBoolFromExtraAttrs("delete_untagged"), + DeleteTag: task.GetBoolFromExtraAttrs("delete_tag"), DryRun: task.GetBoolFromExtraAttrs("dry_run"), Workers: int(task.GetNumFromExtraAttrs("workers")), JobID: task.JobID, diff --git a/src/controller/gc/controller_test.go b/src/controller/gc/controller_test.go index a7f1611d2ca..1e7af0d6d4b 100644 --- a/src/controller/gc/controller_test.go +++ b/src/controller/gc/controller_test.go @@ -41,6 +41,7 @@ func (g *gcCtrTestSuite) TestStart() { dataMap := make(map[string]any) p := Policy{ DeleteUntagged: true, + DeleteTag: true, ExtraAttrs: dataMap, } id, err := g.ctl.Start(nil, p, task.ExecutionTriggerManual) @@ -149,6 +150,7 @@ func (g *gcCtrTestSuite) TestCreateSchedule() { dataMap := make(map[string]any) p := Policy{ DeleteUntagged: true, + DeleteTag: true, ExtraAttrs: dataMap, Workers: 3, } diff --git a/src/controller/gc/model.go b/src/controller/gc/model.go index 9425958cb07..a7f82addf2f 100644 --- a/src/controller/gc/model.go +++ b/src/controller/gc/model.go @@ -22,6 +22,7 @@ import ( type Policy struct { Trigger *Trigger `json:"trigger"` DeleteUntagged bool `json:"deleteuntagged"` + DeleteTag bool `json:"deletetag"` DryRun bool `json:"dryrun"` Workers int `json:"workers"` ExtraAttrs map[string]any `json:"extra_attrs"` @@ -60,6 +61,7 @@ type Task struct { StatusMessage string RunCount int32 DeleteUntagged bool + DeleteTag bool DryRun bool Workers int JobID string diff --git a/src/controller/registry/controller.go b/src/controller/registry/controller.go index af8b4481908..b2b8038b330 100644 --- a/src/controller/registry/controller.go +++ b/src/controller/registry/controller.go @@ -214,7 +214,7 @@ func getWhitelistedAdapters(ctx context.Context) map[string]struct{} { return nil } adapterWhitelist := make(map[string]struct{}) - for _, adapter := range strings.Split(adapterWhitelistRaw, ",") { + for adapter := range strings.SplitSeq(adapterWhitelistRaw, ",") { adapter = strings.TrimSpace(adapter) if adapter != "" { adapterWhitelist[adapter] = struct{}{} diff --git a/src/core/main.go b/src/core/main.go index 24b58c063f8..1834cc6820e 100644 --- a/src/core/main.go +++ b/src/core/main.go @@ -22,12 +22,14 @@ import ( "net/url" "os" "os/signal" + "strconv" "strings" "syscall" "time" "github.com/beego/beego/v2/server/web" + "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/dao" common_http "github.com/goharbor/harbor/src/common/http" configCtl "github.com/goharbor/harbor/src/controller/config" @@ -222,6 +224,10 @@ func main() { log.Error(err) } + // Allow user to disable writing audit log to db by env while initialize + if err := initSkipAuditDBbyEnv(ctx); err != nil { + log.Errorf("Failed to initialize SkipAuditDB by ENV: %v", err) + } // Init API handler if err := api.Init(); err != nil { log.Fatalf("Failed to initialize API handlers with error: %s", err.Error()) @@ -356,3 +362,34 @@ func getDefaultScannerName() string { } return "" } + +func initSkipAuditDBbyEnv(ctx context.Context) error { + var err error + skipAuditEnv := false + s := os.Getenv("SKIP_LOG_AUDIT_DATABASE") + if s != "" { + skipAuditEnv, err = strconv.ParseBool(s) + if err != nil { + log.Warningf("Failed to parse SKIP_LOG_AUDIT_DATABASE to bool with error: %v, Will use SKIP_LOG_AUDIT_DATABASE env as false", err) + } + } + log.Debugf("get SKIP_LOG_AUDIT_DATABASE from Env is %v", skipAuditEnv) + + // get from db + mgr := config.GetCfgManager(ctx) + cfg, err := mgr.GetItemFromDriver(ctx, common.SkipAuditLogDatabase) + if err != nil { + return err + } + // if key not exist in the db, set default ENV value + if val, ok := cfg[common.SkipAuditLogDatabase]; !ok { + log.Debugf("key SkipAuditLogDatabase do not exist in the db, will initialize as %v", skipAuditEnv) + cfg[common.SkipAuditLogDatabase] = skipAuditEnv + if err := mgr.UpdateConfig(ctx, cfg); err != nil { + return err + } + } else { + log.Debugf("key SkipAuditLogDatabase aleady exist in the db with value %v", val) + } + return nil +} diff --git a/src/go.mod b/src/go.mod index ca897a06fde..d0f2e942ce1 100644 --- a/src/go.mod +++ b/src/go.mod @@ -8,19 +8,19 @@ require ( github.com/Masterminds/semver v1.5.0 github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 - github.com/aws/aws-sdk-go v1.55.6 + github.com/aws/aws-sdk-go v1.55.8 github.com/beego/beego/v2 v2.3.8 github.com/beego/i18n v0.0.0-20140604031826-e87155e8f0c0 github.com/bmatcuk/doublestar v1.3.4 github.com/casbin/casbin v1.9.1 github.com/cenkalti/backoff/v4 v4.3.0 github.com/cloudevents/sdk-go/v2 v2.16.1 - github.com/coreos/go-oidc/v3 v3.12.0 + github.com/coreos/go-oidc/v3 v3.15.0 github.com/dghubble/sling v1.4.2 github.com/docker/distribution v2.8.3+incompatible github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 - github.com/go-asn1-ber/asn1-ber v1.5.7 - github.com/go-ldap/ldap/v3 v3.4.10 + github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 + github.com/go-ldap/ldap/v3 v3.4.11 github.com/go-openapi/errors v0.22.0 github.com/go-openapi/loads v0.22.0 // indirect github.com/go-openapi/runtime v0.28.0 @@ -32,7 +32,7 @@ require ( github.com/gocarina/gocsv v0.0.0-20210516172204-ca9e8a8ddea8 github.com/gocraft/work v0.5.1 github.com/golang-jwt/jwt/v5 v5.2.2 - github.com/golang-migrate/migrate/v4 v4.18.1 + github.com/golang-migrate/migrate/v4 v4.19.0 github.com/gomodule/redigo v2.0.0+incompatible github.com/google/go-containerregistry v0.20.2 github.com/google/uuid v1.6.0 @@ -53,32 +53,32 @@ require ( github.com/prometheus/client_model v0.6.2 github.com/robfig/cron/v3 v3.0.1 github.com/spf13/viper v1.19.0 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/tencentcloud/tencentcloud-sdk-go v3.0.233+incompatible github.com/vmihailenco/msgpack/v5 v5.4.1 - github.com/volcengine/volcengine-go-sdk v1.1.29 - go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.59.0 + github.com/volcengine/volcengine-go-sdk v1.1.44 + go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 - go.opentelemetry.io/otel v1.35.0 + go.opentelemetry.io/otel v1.38.0 go.opentelemetry.io/otel/exporters/jaeger v1.17.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 - go.opentelemetry.io/otel/sdk v1.35.0 - go.opentelemetry.io/otel/trace v1.35.0 + go.opentelemetry.io/otel/sdk v1.38.0 + go.opentelemetry.io/otel/trace v1.38.0 go.pinniped.dev v0.37.0 go.uber.org/ratelimit v0.3.1 - golang.org/x/crypto v0.40.0 - golang.org/x/net v0.41.0 + golang.org/x/crypto v0.43.0 + golang.org/x/net v0.46.0 golang.org/x/oauth2 v0.28.0 - golang.org/x/sync v0.16.0 - golang.org/x/text v0.27.0 - golang.org/x/time v0.12.0 + golang.org/x/sync v0.17.0 + golang.org/x/text v0.30.0 + golang.org/x/time v0.13.0 gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/yaml.v2 v2.4.0 helm.sh/helm/v3 v3.18.5 - k8s.io/api v0.33.3 - k8s.io/apimachinery v0.33.3 - k8s.io/client-go v0.33.3 - sigs.k8s.io/yaml v1.5.0 + k8s.io/api v0.34.1 + k8s.io/apimachinery v0.34.1 + k8s.io/client-go v0.34.1 + sigs.k8s.io/yaml v1.6.0 ) require ( @@ -108,9 +108,9 @@ require ( github.com/docker/go-metrics v0.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-jose/go-jose/v4 v4.0.5 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.23.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -143,7 +143,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect @@ -172,15 +172,14 @@ require ( go.mongodb.org/mongo-driver v1.14.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect - go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/term v0.33.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect google.golang.org/api v0.171.0 // indirect google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect @@ -191,10 +190,10 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect ) replace ( diff --git a/src/go.sum b/src/go.sum index 642c04e1396..f616f83fc58 100644 --- a/src/go.sum +++ b/src/go.sum @@ -66,8 +66,8 @@ github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsW github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= -github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= -github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= +github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= github.com/beego/beego/v2 v2.3.8 h1:wplhB1pF4TxR+2SS4PUej8eDoH4xGfxuHfS7wAk9VBc= github.com/beego/beego/v2 v2.3.8/go.mod h1:8vl9+RrXqvodrl9C8yivX1e6le6deCK6RWeq8R7gTTg= github.com/beego/i18n v0.0.0-20140604031826-e87155e8f0c0 h1:fQaDnUQvBXHHQdGBu9hz8nPznB4BeiPQokvmQVjmNEw= @@ -93,10 +93,14 @@ github.com/cloudevents/sdk-go/v2 v2.16.1 h1:G91iUdqvl88BZ1GYYr9vScTj5zzXSyEuqbfE github.com/cloudevents/sdk-go/v2 v2.16.1/go.mod h1:v/kVOaWjNfbvc6tkhhlkhvLapj8Aa8kvXiH5GiOHCKI= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= -github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo= -github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= +github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg= +github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -113,8 +117,8 @@ github.com/dghubble/sling v1.4.2/go.mod h1:o0arCOz0HwfqYQJLrRtqunaWOn4X6jxE/6ORK github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0= -github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs= +github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= +github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU= github.com/distribution/distribution v2.8.2+incompatible h1:k9+4DKdOG+quPFZXT/mUsiQrGu9vYCp+dXpuPkuqhk8= github.com/distribution/distribution v2.8.2+incompatible/go.mod h1:EgLm2NgWtdKgzF9NpMzUKgzmR7AMmb0VQi2B+ZzDRjc= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -123,8 +127,8 @@ github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE= github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= -github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= +github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= @@ -137,8 +141,8 @@ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw= github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= -github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= -github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -150,23 +154,23 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= -github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= -github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= +github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= +github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= @@ -211,8 +215,8 @@ github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXe github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y= -github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks= +github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE= +github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -233,8 +237,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s= github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -244,8 +248,6 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo= @@ -267,10 +269,8 @@ github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyE github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/graph-gophers/dataloader v5.0.0+incompatible h1:R+yjsbrNq1Mo3aPG+Z/EKYrXrXXUNJHOgbRt+U6jOug= github.com/graph-gophers/dataloader v5.0.0+incompatible/go.mod h1:jk4jk0c5ZISbKaMe8WsVopGB5/15GvGHMdMdPtwlRp4= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= @@ -282,7 +282,6 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= @@ -418,8 +417,9 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= @@ -543,8 +543,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tencentcloud/tencentcloud-sdk-go v3.0.233+incompatible h1:q+D/Y9jla3afgsIihtyhwyl0c2W+eRWNM9ohVwPiiPw= @@ -564,38 +564,39 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/volcengine/volc-sdk-golang v1.0.23 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9PY+4OehIk5R8= github.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU= -github.com/volcengine/volcengine-go-sdk v1.1.29 h1:wAc8VbZvCAHsE6KqVawvZA4epB6k+eVVUvso9h0n274= -github.com/volcengine/volcengine-go-sdk v1.1.29/go.mod h1:EyKoi6t6eZxoPNGr2GdFCZti2Skd7MO3eUzx7TtSvNo= +github.com/volcengine/volcengine-go-sdk v1.1.44 h1:WLoLlzt67ZlJeow55PPx65/Mh52DewVXqkHcFSodM9w= +github.com/volcengine/volcengine-go-sdk v1.1.44/go.mod h1:oxoVo+A17kvkwPkIeIHPVLjSw7EQAm+l/Vau1YGHN+A= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.59.0 h1:/h/biJ5H2DVotLp4HHqmBlNwNwwUOJLwgOTiezmO1YE= -go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.59.0/go.mod h1:j8fjcXBZndAJ/nvp7DzPa7mKujTTPlWRLCCPkxxcPZQ= +go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63.0 h1:rATLgFjv0P9qyXQR/aChJ6JVbMtXOQjt49GgT36cBbk= +go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63.0/go.mod h1:34csimR1lUhdT5HH4Rii9aKPrvBcnFRwxLwcevsU+Kk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.pinniped.dev v0.37.0 h1:9Bf9RWqEP8mbDirqqQt/A5G09T6vixxxE/DTiHiwJu0= @@ -623,8 +624,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= -go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= @@ -637,13 +638,8 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -665,13 +661,8 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -682,23 +673,14 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= @@ -708,14 +690,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ 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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -740,46 +716,23 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= -golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -798,12 +751,8 @@ golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -843,6 +792,7 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= @@ -883,26 +833,24 @@ helm.sh/helm/v3 v3.18.5/go.mod h1:L/dXDR2r539oPlFP1PJqKAC1CUgqHJDLkxKpDGrWnyg= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= -k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= -k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= -k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= -k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= -k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= -sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= -sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/src/jobservice/job/impl/gc/garbage_collection.go b/src/jobservice/job/impl/gc/garbage_collection.go index b63fff7da59..b480dfb7e1e 100644 --- a/src/jobservice/job/impl/gc/garbage_collection.go +++ b/src/jobservice/job/impl/gc/garbage_collection.go @@ -64,6 +64,7 @@ type GarbageCollector struct { logger logger.Interface redisURL string deleteUntagged bool + deleteTag bool dryRun bool // holds all of trashed artifacts' digest and repositories. // The source data of trashedArts is the table ArtifactTrash and it's only used as a dictionary by sweep when to delete a manifest. @@ -130,6 +131,12 @@ func (gc *GarbageCollector) parseParams(params job.Parameters) { } } + // delete tag: default is to delete the tag in the backend storage + gc.deleteTag = true + if deleteTag, ok := params["delete_tag"].(bool); ok { + gc.deleteTag = deleteTag + } + // time window: default is 2 hours, and for testing/debugging, it can be set to 0. gc.timeWindowHours = 2 timeWindow, exist := params["time_window"] @@ -159,8 +166,8 @@ func (gc *GarbageCollector) parseParams(params job.Parameters) { } } - gc.logger.Infof("Garbage Collection parameters: [delete_untagged: %t, dry_run: %t, time_window: %d, workers: %d]", - gc.deleteUntagged, gc.dryRun, gc.timeWindowHours, gc.workers) + gc.logger.Infof("Garbage Collection parameters: [delete_untagged: %t, delete_tag: %t, dry_run: %t, time_window: %d, workers: %d]", + gc.deleteUntagged, gc.deleteTag, gc.dryRun, gc.timeWindowHours, gc.workers) } // Run implements the interface in job/Interface @@ -332,34 +339,37 @@ func (gc *GarbageCollector) sweep(ctx job.Context) error { skippedBlob := false if _, exist := gc.trashedArts[blob.Digest]; exist && blob.IsManifest() { for _, art := range gc.trashedArts[blob.Digest] { - // Harbor cannot know the existing tags in the backend from its database, so let the v2 DELETE manifest to remove all of them. - gc.logger.Infof("[%s][%d/%d] delete the manifest with registry v2 API: %s, %s, %s", - uid, localIndex, total, art.RepositoryName, blob.ContentType, blob.Digest) - if err := retry.Retry(func() error { - return ignoreNotFound(func() error { - err := v2DeleteManifest(art.RepositoryName, blob.Digest) - // if the system is in read-only mode, return an Abort error to skip retrying + // if the deleteTag is enabled, call the distribution api to perform the tag deletion. + if gc.deleteTag { + // Harbor cannot know the existing tags in the backend from its database, so let the v2 DELETE manifest to remove all of them. + gc.logger.Infof("[%s][%d/%d] delete the manifest with registry v2 API: %s, %s, %s", + uid, localIndex, total, art.RepositoryName, blob.ContentType, blob.Digest) + if err := retry.Retry(func() error { + return ignoreNotFound(func() error { + err := v2DeleteManifest(art.RepositoryName, blob.Digest) + // if the system is in read-only mode, return an Abort error to skip retrying + if err == readonly.Err { + return retry.Abort(err) + } + return err + }) + }, retry.Callback(func(err error, sleep time.Duration) { + gc.logger.Infof("[%s][%d/%d] failed to exec v2DeleteManifest, error: %v, will retry again after: %s", uid, localIndex, total, err, sleep) + })); err != nil { + gc.logger.Errorf("[%s][%d/%d] failed to delete manifest with v2 API, %s, %s, %v", uid, localIndex, total, art.RepositoryName, blob.Digest, err) + if err := ignoreNotFound(func() error { + return gc.markDeleteFailed(ctx, blob) + }); err != nil { + gc.logger.Errorf("[%s][%d/%d] failed to call gc.markDeleteFailed() after v2DeleteManifest() error out: %s, %v", uid, localIndex, total, blob.Digest, err) + return err + } + // if the system is set to read-only mode, return directly if err == readonly.Err { - return retry.Abort(err) + return err } - return err - }) - }, retry.Callback(func(err error, sleep time.Duration) { - gc.logger.Infof("[%s][%d/%d] failed to exec v2DeleteManifest, error: %v, will retry again after: %s", uid, localIndex, total, err, sleep) - })); err != nil { - gc.logger.Errorf("[%s][%d/%d] failed to delete manifest with v2 API, %s, %s, %v", uid, localIndex, total, art.RepositoryName, blob.Digest, err) - if err := ignoreNotFound(func() error { - return gc.markDeleteFailed(ctx, blob) - }); err != nil { - gc.logger.Errorf("[%s][%d/%d] failed to call gc.markDeleteFailed() after v2DeleteManifest() error out: %s, %v", uid, localIndex, total, blob.Digest, err) - return err - } - // if the system is set to read-only mode, return directly - if err == readonly.Err { - return err + skippedBlob = true + continue } - skippedBlob = true - continue } // for manifest, it has to delete the revisions folder of each repository gc.logger.Infof("[%s][%d/%d] delete manifest from storage: %s", uid, localIndex, total, blob.Digest) diff --git a/src/jobservice/job/impl/gc/garbage_collection_test.go b/src/jobservice/job/impl/gc/garbage_collection_test.go index b9446b7a705..26bbd677aa3 100644 --- a/src/jobservice/job/impl/gc/garbage_collection_test.go +++ b/src/jobservice/job/impl/gc/garbage_collection_test.go @@ -159,6 +159,7 @@ func (suite *gcTestSuite) TestInit() { } params := map[string]any{ "delete_untagged": true, + "delete_tag": true, "redis_url_reg": "redis url", "time_window": 1, "workers": float64(3), @@ -167,27 +168,33 @@ func (suite *gcTestSuite) TestInit() { mock.OnAnything(gc.registryCtlClient, "Health").Return(nil) suite.Nil(gc.init(ctx, params)) suite.True(gc.deleteUntagged) + suite.True(gc.deleteTag) suite.Equal(3, gc.workers) params = map[string]any{ "delete_untagged": "unsupported", + "delete_tag": "unsupported", "redis_url_reg": "redis url", } suite.Nil(gc.init(ctx, params)) suite.True(gc.deleteUntagged) + suite.True(gc.deleteTag) params = map[string]any{ "delete_untagged": false, + "delete_tag": false, "redis_url_reg": "redis url", } suite.Nil(gc.init(ctx, params)) suite.False(gc.deleteUntagged) + suite.False(gc.deleteTag) params = map[string]any{ "redis_url_reg": "redis url", } suite.Nil(gc.init(ctx, params)) suite.True(gc.deleteUntagged) + suite.True(gc.deleteTag) } func (suite *gcTestSuite) TestStop() { @@ -210,6 +217,7 @@ func (suite *gcTestSuite) TestStop() { registryCtlClient: suite.registryCtlClient, artCtl: suite.artifactCtl, deleteUntagged: true, + deleteTag: true, } suite.Equal(errGcStop, gc.mark(ctx)) diff --git a/src/jobservice/migration/manager_test.go b/src/jobservice/migration/manager_test.go index 3517ea2b734..39e11cd9cc6 100644 --- a/src/jobservice/migration/manager_test.go +++ b/src/jobservice/migration/manager_test.go @@ -58,7 +58,7 @@ func (suite *ManagerTestSuite) SetupSuite() { suite.manager = New(suite.pool, suite.namespace) } -// SetupTestSuite sets up env for each test case +// SetupTest sets up env for each test case func (suite *ManagerTestSuite) SetupTest() { // Mock fake data conn := suite.pool.Get() @@ -141,7 +141,7 @@ func (suite *ManagerTestSuite) SetupTest() { require.Equal(suite.T(), 1, count) } -// SetupTestSuite clears up env for each test case +// TearDownTest clears up env for each test case func (suite *ManagerTestSuite) TearDownTest() { conn := suite.pool.Get() defer func() { diff --git a/src/lib/config/config.go b/src/lib/config/config.go index ce1d5766a1e..9d83a9caef7 100644 --- a/src/lib/config/config.go +++ b/src/lib/config/config.go @@ -48,6 +48,7 @@ type Manager interface { Set(ctx context.Context, key string, value any) Save(ctx context.Context) error Get(ctx context.Context, key string) *metadata.ConfigureValue + GetItemFromDriver(ctx context.Context, key string) (map[string]any, error) UpdateConfig(ctx context.Context, cfgs map[string]any) error GetUserCfgs(ctx context.Context) map[string]any ValidateCfg(ctx context.Context, cfgs map[string]any) error diff --git a/src/lib/q/builder.go b/src/lib/q/builder.go index 66e625e2e6f..d4e59866942 100644 --- a/src/lib/q/builder.go +++ b/src/lib/q/builder.go @@ -85,8 +85,8 @@ func ParseSorting(sort string) []*Sort { for sorting := range strings.SplitSeq(sort, ",") { key := sorting desc := false - if strings.HasPrefix(sorting, "-") { - key = strings.TrimPrefix(sorting, "-") + if after, ok := strings.CutPrefix(sorting, "-"); ok { + key = after desc = true } sorts = append(sorts, &Sort{ diff --git a/src/pkg/cached/manager.go b/src/pkg/cached/manager.go index affd2bbbf96..7f8b2f4dcea 100644 --- a/src/pkg/cached/manager.go +++ b/src/pkg/cached/manager.go @@ -17,6 +17,7 @@ package cached import ( "context" "fmt" + "strings" "github.com/goharbor/harbor/src/lib/cache" "github.com/goharbor/harbor/src/lib/errors" @@ -74,7 +75,8 @@ func (ok *ObjectKey) Format(keysAndValues ...any) (string, error) { return "", errors.Errorf("invalid keysAndValues: %v", keysAndValues...) } - s := ok.namespace + var s strings.Builder + s.WriteString(ok.namespace) for i := range len(keysAndValues) { // even is key if i%2 == 0 { @@ -83,18 +85,18 @@ func (ok *ObjectKey) Format(keysAndValues ...any) (string, error) { return "", errors.Errorf("key must be string, invalid key type: %#v", keysAndValues[i]) } - s += fmt.Sprintf(":%s", key) + s.WriteString(fmt.Sprintf(":%s", key)) } else { switch keysAndValues[i].(type) { case int, int16, int32, int64: - s += fmt.Sprintf(":%d", keysAndValues[i]) + s.WriteString(fmt.Sprintf(":%d", keysAndValues[i])) case string: - s += fmt.Sprintf(":%s", keysAndValues[i]) + s.WriteString(fmt.Sprintf(":%s", keysAndValues[i])) default: return "", errors.Errorf("unsupported value type: %#v", keysAndValues[i]) } } } - return s, nil + return s.String(), nil } diff --git a/src/pkg/config/db/cache.go b/src/pkg/config/db/cache.go index 902b85eaf8b..b0d2ce2e090 100644 --- a/src/pkg/config/db/cache.go +++ b/src/pkg/config/db/cache.go @@ -61,6 +61,11 @@ func (d *Cache) Save(ctx context.Context, cfg map[string]any) error { return nil } +// Get - delegate to driver +func (d *Cache) Get(ctx context.Context, key string) (map[string]any, error) { + return d.driver.Get(ctx, key) +} + // NewCacheDriver returns driver with cache func NewCacheDriver(cache cache.Cache, driver store.Driver) store.Driver { return &Cache{ diff --git a/src/pkg/config/db/dao/dao.go b/src/pkg/config/db/dao/dao.go index 809d56ee3de..2d1829b0cae 100644 --- a/src/pkg/config/db/dao/dao.go +++ b/src/pkg/config/db/dao/dao.go @@ -22,6 +22,7 @@ import ( "github.com/goharbor/harbor/src/lib/config/models" "github.com/goharbor/harbor/src/lib/errors" "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/lib/q" ) // DAO the dao for configure items @@ -30,6 +31,8 @@ type DAO interface { GetConfigEntries(ctx context.Context) ([]*models.ConfigEntry, error) // SaveConfigEntries save configure items provided SaveConfigEntries(ctx context.Context, entries []models.ConfigEntry) error + // GetConfigItem get configure item by key + GetConfigItem(ctx context.Context, query *q.Query) ([]*models.ConfigEntry, error) } type dao struct { @@ -85,3 +88,17 @@ func (d *dao) SaveConfigEntries(ctx context.Context, entries []models.ConfigEntr } return nil } + +// GetConfigItem get configure item by query +func (d *dao) GetConfigItem(ctx context.Context, query *q.Query) ([]*models.ConfigEntry, error) { + query = q.MustClone(query) + qs, err := orm.QuerySetter(ctx, &models.ConfigEntry{}, query) + if err != nil { + return nil, err + } + var configs []*models.ConfigEntry + if _, err := qs.All(&configs); err != nil { + return nil, err + } + return configs, nil +} diff --git a/src/pkg/config/db/db.go b/src/pkg/config/db/db.go index 8be70311eb5..a7bf67b228b 100644 --- a/src/pkg/config/db/db.go +++ b/src/pkg/config/db/db.go @@ -23,6 +23,7 @@ import ( "github.com/goharbor/harbor/src/lib/config/models" "github.com/goharbor/harbor/src/lib/encrypt" "github.com/goharbor/harbor/src/lib/log" + "github.com/goharbor/harbor/src/lib/q" "github.com/goharbor/harbor/src/pkg/config/db/dao" ) @@ -84,3 +85,18 @@ func (d *Database) Save(ctx context.Context, cfgs map[string]any) error { } return d.cfgDAO.SaveConfigEntries(ctx, configEntries) } + +// Get - Get config item from db +func (d *Database) Get(ctx context.Context, key string) (map[string]any, error) { + resultMap := map[string]any{} + configEntries, err := d.cfgDAO.GetConfigItem(ctx, q.New(q.KeyWords{"k": key})) + if err != nil { + log.Debugf("get config db error: %v", err) + return resultMap, err + } + // convert to map if there's any record + for _, item := range configEntries { + resultMap[item.Key] = item.Value + } + return resultMap, nil +} diff --git a/src/pkg/config/inmemory/manager.go b/src/pkg/config/inmemory/manager.go index 3cbfaf3316c..3f2beacd99e 100644 --- a/src/pkg/config/inmemory/manager.go +++ b/src/pkg/config/inmemory/manager.go @@ -16,6 +16,7 @@ package inmemory import ( "context" + "errors" "maps" "sync" @@ -54,6 +55,11 @@ func (d *Driver) Save(_ context.Context, cfg map[string]any) error { return nil } +// TODO +func (d *Driver) Get(_ context.Context, _ string) (map[string]any, error) { + return nil, errors.ErrUnsupported +} + // NewInMemoryManager create a manager for unit testing, doesn't involve database or REST func NewInMemoryManager() *config.CfgManager { manager := &config.CfgManager{Store: store.NewConfigStore(&Driver{cfgMap: map[string]any{}})} diff --git a/src/pkg/config/manager.go b/src/pkg/config/manager.go index c4628c7a37d..429a6facf01 100644 --- a/src/pkg/config/manager.go +++ b/src/pkg/config/manager.go @@ -180,6 +180,11 @@ func (c *CfgManager) UpdateConfig(ctx context.Context, cfgs map[string]any) erro return c.Store.Update(ctx, cfgs) } +// GetItemFromDriver ... +func (c *CfgManager) GetItemFromDriver(ctx context.Context, key string) (map[string]any, error) { + return c.Store.GetFromDriver(ctx, key) +} + // ValidateCfg validate config by metadata. return the first error if exist. func (c *CfgManager) ValidateCfg(ctx context.Context, cfgs map[string]any) error { for key, value := range cfgs { diff --git a/src/pkg/config/manager_test.go b/src/pkg/config/manager_test.go new file mode 100644 index 00000000000..9c28393d712 --- /dev/null +++ b/src/pkg/config/manager_test.go @@ -0,0 +1,153 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "context" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + + "github.com/goharbor/harbor/src/common" + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/pkg/config/store" +) + +// MockDriver is a mock implementation of store.Driver +type MockDriver struct { + mock.Mock +} + +func (m *MockDriver) Load(ctx context.Context) (map[string]any, error) { + args := m.Called(ctx) + return args.Get(0).(map[string]any), args.Error(1) +} + +func (m *MockDriver) Save(ctx context.Context, cfg map[string]any) error { + args := m.Called(ctx, cfg) + return args.Error(0) +} + +func (m *MockDriver) Get(ctx context.Context, key string) (map[string]any, error) { + args := m.Called(ctx, key) + return args.Get(0).(map[string]any), args.Error(1) +} + +// GetItemFromDriverTestSuite tests the GetItemFromDriver method +type GetItemFromDriverTestSuite struct { + suite.Suite + ctx context.Context + manager *CfgManager + driver *MockDriver +} + +func (suite *GetItemFromDriverTestSuite) SetupTest() { + suite.ctx = context.Background() + suite.driver = &MockDriver{} + suite.manager = &CfgManager{ + Store: store.NewConfigStore(suite.driver), + } +} + +func (suite *GetItemFromDriverTestSuite) TestGetItemFromDriverSuccess() { + key := common.SkipAuditLogDatabase + expectedResult := map[string]any{ + common.SkipAuditLogDatabase: true, + } + + suite.driver.On("Get", suite.ctx, key).Return(expectedResult, nil) + + result, err := suite.manager.GetItemFromDriver(suite.ctx, key) + + suite.Require().NoError(err) + suite.Equal(expectedResult, result) + suite.driver.AssertExpectations(suite.T()) +} + +func (suite *GetItemFromDriverTestSuite) TestGetItemFromDriverError() { + key := common.SkipAuditLogDatabase + expectedError := errors.New("database connection failed") + + suite.driver.On("Get", suite.ctx, key).Return(map[string]any{}, expectedError) + + result, err := suite.manager.GetItemFromDriver(suite.ctx, key) + + suite.Require().Error(err) + suite.Equal(expectedError, err) + suite.Empty(result) + suite.driver.AssertExpectations(suite.T()) +} + +func (suite *GetItemFromDriverTestSuite) TestGetItemFromDriverEmptyResult() { + key := common.SkipAuditLogDatabase + expectedResult := map[string]any{} + + suite.driver.On("Get", suite.ctx, key).Return(expectedResult, nil) + + result, err := suite.manager.GetItemFromDriver(suite.ctx, key) + + suite.Require().NoError(err) + suite.Equal(expectedResult, result) + suite.driver.AssertExpectations(suite.T()) +} + +func (suite *GetItemFromDriverTestSuite) TestGetItemFromDriverMultipleKeys() { + key := common.AuditLogForwardEndpoint + expectedResult := map[string]any{ + common.AuditLogForwardEndpoint: "syslog://localhost:514", + common.SkipAuditLogDatabase: false, + } + + suite.driver.On("Get", suite.ctx, key).Return(expectedResult, nil) + + result, err := suite.manager.GetItemFromDriver(suite.ctx, key) + + suite.Require().NoError(err) + suite.Equal(expectedResult, result) + suite.driver.AssertExpectations(suite.T()) +} + +func (suite *GetItemFromDriverTestSuite) TestGetItemFromDriverNilContext() { + key := common.SkipAuditLogDatabase + expectedResult := map[string]any{ + common.SkipAuditLogDatabase: false, + } + + suite.driver.On("Get", mock.Anything, key).Return(expectedResult, nil) + + result, err := suite.manager.GetItemFromDriver(nil, key) + + suite.Require().NoError(err) + suite.Equal(expectedResult, result) + suite.driver.AssertExpectations(suite.T()) +} + +func (suite *GetItemFromDriverTestSuite) TestGetItemFromDriverEmptyKey() { + key := "" + expectedResult := map[string]any{} + + suite.driver.On("Get", suite.ctx, key).Return(expectedResult, nil) + + result, err := suite.manager.GetItemFromDriver(suite.ctx, key) + + suite.Require().NoError(err) + suite.Equal(expectedResult, result) + suite.driver.AssertExpectations(suite.T()) +} + +func TestGetItemFromDriverTestSuite(t *testing.T) { + suite.Run(t, new(GetItemFromDriverTestSuite)) +} diff --git a/src/pkg/config/rest/rest.go b/src/pkg/config/rest/rest.go index 138e91af9bc..5d933f7059b 100644 --- a/src/pkg/config/rest/rest.go +++ b/src/pkg/config/rest/rest.go @@ -62,3 +62,8 @@ func (h *Driver) Load(_ context.Context) (map[string]any, error) { func (h *Driver) Save(_ context.Context, cfg map[string]any) error { return h.client.Put(h.configRESTURL, cfg) } + +// TODO +func (h *Driver) Get(_ context.Context, _ string) (map[string]any, error) { + return nil, errors.ErrUnsupported +} diff --git a/src/pkg/config/store/driver.go b/src/pkg/config/store/driver.go index 853fab6f667..e6769a19409 100644 --- a/src/pkg/config/store/driver.go +++ b/src/pkg/config/store/driver.go @@ -23,4 +23,6 @@ type Driver interface { Load(ctx context.Context) (map[string]any, error) // Save - save config item into config driver Save(ctx context.Context, cfg map[string]any) error + // Get - get config item from config driver + Get(ctx context.Context, key string) (map[string]any, error) } diff --git a/src/pkg/config/store/store.go b/src/pkg/config/store/store.go index 6c1e61b6519..a0ec20cd233 100644 --- a/src/pkg/config/store/store.go +++ b/src/pkg/config/store/store.go @@ -51,6 +51,18 @@ func (c *ConfigStore) Get(key string) (*metadata.ConfigureValue, error) { return nil, metadata.ErrValueNotSet } +// GetFromDriver ... +func (c *ConfigStore) GetFromDriver(ctx context.Context, key string) (map[string]any, error) { + if c.cfgDriver == nil { + return nil, errors.New("failed to load store, cfgDriver is nil") + } + cfgs, err := c.cfgDriver.Get(ctx, key) + if err != nil { + return nil, err + } + return cfgs, nil +} + // GetAnyType get any type for config items func (c *ConfigStore) GetAnyType(key string) (any, error) { if value, ok := c.cfgValues.Load(key); ok { diff --git a/src/pkg/config/store/store_test.go b/src/pkg/config/store/store_test.go new file mode 100644 index 00000000000..79d9a7f2f97 --- /dev/null +++ b/src/pkg/config/store/store_test.go @@ -0,0 +1,212 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package store + +import ( + "context" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + + "github.com/goharbor/harbor/src/common" + "github.com/goharbor/harbor/src/lib/errors" +) + +// MockDriver is a mock implementation of store.Driver +type MockDriver struct { + mock.Mock +} + +func (m *MockDriver) Load(ctx context.Context) (map[string]any, error) { + args := m.Called(ctx) + return args.Get(0).(map[string]any), args.Error(1) +} + +func (m *MockDriver) Save(ctx context.Context, cfg map[string]any) error { + args := m.Called(ctx, cfg) + return args.Error(0) +} + +func (m *MockDriver) Get(ctx context.Context, key string) (map[string]any, error) { + args := m.Called(ctx, key) + return args.Get(0).(map[string]any), args.Error(1) +} + +// GetFromDriverTestSuite tests the GetFromDriver method in ConfigStore +type GetFromDriverTestSuite struct { + suite.Suite + ctx context.Context + store *ConfigStore + driver *MockDriver +} + +func (suite *GetFromDriverTestSuite) SetupTest() { + suite.ctx = context.Background() + suite.driver = &MockDriver{} + suite.store = &ConfigStore{ + cfgDriver: suite.driver, + } +} + +func (suite *GetFromDriverTestSuite) TestGetFromDriverSuccess() { + key := common.SkipAuditLogDatabase + expectedResult := map[string]any{ + common.SkipAuditLogDatabase: true, + } + + suite.driver.On("Get", suite.ctx, key).Return(expectedResult, nil) + + result, err := suite.store.GetFromDriver(suite.ctx, key) + + suite.Require().NoError(err) + suite.Equal(expectedResult, result) + suite.driver.AssertExpectations(suite.T()) +} + +func (suite *GetFromDriverTestSuite) TestGetFromDriverNilDriver() { + key := common.SkipAuditLogDatabase + suite.store.cfgDriver = nil + + result, err := suite.store.GetFromDriver(suite.ctx, key) + + suite.Require().Error(err) + suite.Contains(err.Error(), "failed to load store, cfgDriver is nil") + suite.Nil(result) +} + +func (suite *GetFromDriverTestSuite) TestGetFromDriverError() { + key := common.SkipAuditLogDatabase + expectedError := errors.New("database connection failed") + + suite.driver.On("Get", suite.ctx, key).Return(map[string]any{}, expectedError) + + result, err := suite.store.GetFromDriver(suite.ctx, key) + + suite.Require().Error(err) + suite.Equal(expectedError, err) + suite.Empty(result) + suite.driver.AssertExpectations(suite.T()) +} + +func (suite *GetFromDriverTestSuite) TestGetFromDriverEmptyResult() { + key := common.SkipAuditLogDatabase + expectedResult := map[string]any{} + + suite.driver.On("Get", suite.ctx, key).Return(expectedResult, nil) + + result, err := suite.store.GetFromDriver(suite.ctx, key) + + suite.Require().NoError(err) + suite.Equal(expectedResult, result) + suite.driver.AssertExpectations(suite.T()) +} + +func (suite *GetFromDriverTestSuite) TestGetFromDriverMultipleConfigs() { + key := common.AuditLogForwardEndpoint + expectedResult := map[string]any{ + common.AuditLogForwardEndpoint: "syslog://localhost:514", + common.SkipAuditLogDatabase: false, + "other_config": "value", + } + + suite.driver.On("Get", suite.ctx, key).Return(expectedResult, nil) + + result, err := suite.store.GetFromDriver(suite.ctx, key) + + suite.Require().NoError(err) + suite.Equal(expectedResult, result) + suite.Equal("syslog://localhost:514", result[common.AuditLogForwardEndpoint]) + suite.Equal(false, result[common.SkipAuditLogDatabase]) + suite.Equal("value", result["other_config"]) + suite.driver.AssertExpectations(suite.T()) +} + +func (suite *GetFromDriverTestSuite) TestGetFromDriverNilContext() { + key := common.SkipAuditLogDatabase + expectedResult := map[string]any{ + common.SkipAuditLogDatabase: false, + } + + suite.driver.On("Get", mock.Anything, key).Return(expectedResult, nil) + + result, err := suite.store.GetFromDriver(nil, key) + + suite.Require().NoError(err) + suite.Equal(expectedResult, result) + suite.driver.AssertExpectations(suite.T()) +} + +func (suite *GetFromDriverTestSuite) TestGetFromDriverEmptyKey() { + key := "" + expectedResult := map[string]any{} + + suite.driver.On("Get", suite.ctx, key).Return(expectedResult, nil) + + result, err := suite.store.GetFromDriver(suite.ctx, key) + + suite.Require().NoError(err) + suite.Equal(expectedResult, result) + suite.driver.AssertExpectations(suite.T()) +} + +func (suite *GetFromDriverTestSuite) TestGetFromDriverDifferentKeys() { + testCases := []struct { + name string + key string + expectedResult map[string]any + }{ + { + name: "skip_audit_log_database", + key: common.SkipAuditLogDatabase, + expectedResult: map[string]any{ + common.SkipAuditLogDatabase: true, + }, + }, + { + name: "audit_log_forward_endpoint", + key: common.AuditLogForwardEndpoint, + expectedResult: map[string]any{ + common.AuditLogForwardEndpoint: "syslog://remote:514", + }, + }, + { + name: "pull_audit_log_disable", + key: common.PullAuditLogDisable, + expectedResult: map[string]any{ + common.PullAuditLogDisable: false, + }, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.driver.On("Get", suite.ctx, tc.key).Return(tc.expectedResult, nil) + + result, err := suite.store.GetFromDriver(suite.ctx, tc.key) + + suite.Require().NoError(err) + suite.Equal(tc.expectedResult, result) + suite.driver.AssertExpectations(suite.T()) + + // Reset mock for next iteration + suite.driver.ExpectedCalls = nil + }) + } +} + +func TestGetFromDriverTestSuite(t *testing.T) { + suite.Run(t, new(GetFromDriverTestSuite)) +} diff --git a/src/pkg/p2p/preheat/models/policy/policy.go b/src/pkg/p2p/preheat/models/policy/policy.go index 789038be092..f8d23ddc908 100644 --- a/src/pkg/p2p/preheat/models/policy/policy.go +++ b/src/pkg/p2p/preheat/models/policy/policy.go @@ -16,7 +16,6 @@ package policy import ( "encoding/json" - "strconv" "time" beego_orm "github.com/beego/beego/v2/client/orm" @@ -198,24 +197,6 @@ func decodeFilters(filterStr string) ([]*Filter, error) { if err := json.Unmarshal([]byte(filterStr), &filters); err != nil { return nil, err } - - // Convert value type - // TODO: remove switch after UI bug #12579 fixed - for _, f := range filters { - if f.Type == FilterTypeVulnerability { - switch f.Value.(type) { - case string: - sev, err := strconv.ParseInt(f.Value.(string), 10, 32) - if err != nil { - return nil, errors.Wrapf(err, "parse filters") - } - f.Value = (int)(sev) - case float64: - f.Value = (int)(f.Value.(float64)) - } - } - } - return filters, nil } diff --git a/src/pkg/project/models/pro_meta.go b/src/pkg/project/models/pro_meta.go index 25f7e41bee1..6bb82b1b33d 100644 --- a/src/pkg/project/models/pro_meta.go +++ b/src/pkg/project/models/pro_meta.go @@ -25,4 +25,5 @@ const ( ProMetaReuseSysCVEAllowlist = "reuse_sys_cve_allowlist" ProMetaAutoSBOMGen = "auto_sbom_generation" ProMetaProxySpeed = "proxy_speed_kb" + ProMetaMaxUpstreamConn = "max_upstream_conn" ) diff --git a/src/pkg/project/models/project.go b/src/pkg/project/models/project.go index ae8256a7d79..42332697fa6 100644 --- a/src/pkg/project/models/project.go +++ b/src/pkg/project/models/project.go @@ -21,6 +21,7 @@ import ( "strings" "time" + "github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/orm" allowlist "github.com/goharbor/harbor/src/pkg/allowlist/models" ) @@ -169,6 +170,20 @@ func (p *Project) ProxyCacheSpeed() int32 { return int32(speedInt) } +// MaxUpstreamConnection ... +func (p *Project) MaxUpstreamConnection() int { + countVal, exist := p.GetMetadata(ProMetaMaxUpstreamConn) + if !exist { + return 0 + } + cnt, err := strconv.ParseInt(countVal, 10, 32) + if err != nil { + log.Warningf("failed th parse the max_upstream_conn, val:%s error %v", countVal, err) + return 0 + } + return int(cnt) +} + // FilterByPublic returns orm.QuerySeter with public filter func (p *Project) FilterByPublic(_ context.Context, qs orm.QuerySeter, _ string, value any) orm.QuerySeter { subQuery := `SELECT project_id FROM project_metadata WHERE name = 'public' AND value = '%s'` diff --git a/src/pkg/proxy/connection/limit.go b/src/pkg/proxy/connection/limit.go new file mode 100644 index 00000000000..72a20261b9e --- /dev/null +++ b/src/pkg/proxy/connection/limit.go @@ -0,0 +1,79 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connection + +import ( + "context" + "fmt" + + "github.com/go-redis/redis/v8" + + "github.com/goharbor/harbor/src/lib/log" +) + +// ConLimiter is used to limit the number of connections to the upstream service +type ConnLimiter struct { +} + +// Limiter is a global connection limiter instance +var Limiter = &ConnLimiter{} + +// Used to compare and increase connection number in redis +// +// KEYS[1]: key of max_conn_upstream +// ARGV[1]: max connection limit +var increaseWithLimitText = ` +local current = tonumber(redis.call('GET', KEYS[1]) or '0') +local max = tonumber(ARGV[1]) + +if current + 1 <= max then + redis.call('INCRBY', KEYS[1], 1) + redis.call('EXPIRE', KEYS[1], 3600) -- set expire to avoid always lock + return 1 +else + return 0 +end +` + +var acquireScript = redis.NewScript(increaseWithLimitText) + +// Acquire tries to acquire a connection, returns true if successful +func (c *ConnLimiter) Acquire(ctx context.Context, rdb *redis.Client, key string, limit int) bool { + result, err := acquireScript.Run(ctx, rdb, []string{key}, fmt.Sprintf("%v", limit)).Int() + if err != nil { + log.Errorf("failed to get the connection lock in redis, error %v", err) + return false + } + log.Debugf("Acquire script result is %d", result) + return result == 1 +} + +var decreaseText = ` +local val = tonumber(redis.call("GET", KEYS[1]) or "0") +if val > 0 then + redis.call("DECR", KEYS[1]) +end +return 0 +` + +var decreaseScript = redis.NewScript(decreaseText) + +// Release releases a connection in redis +func (c *ConnLimiter) Release(ctx context.Context, rdb *redis.Client, key string) { + _, err := decreaseScript.Run(ctx, rdb, []string{key}).Int() + if err != nil { + log.Infof("release connection failed:%v", err) + } +} diff --git a/src/pkg/proxy/connection/limit_test.go b/src/pkg/proxy/connection/limit_test.go new file mode 100644 index 00000000000..309a9c2b654 --- /dev/null +++ b/src/pkg/proxy/connection/limit_test.go @@ -0,0 +1,59 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connection + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/go-redis/redis/v8" + "github.com/stretchr/testify/assert" +) + +func TestConnLimiter_Acquire_Release(t *testing.T) { + redisAddress := os.Getenv("REDIS_HOST") + redisHost := "localhost" + if len(redisAddress) > 0 { + redisHost = redisAddress + } + + ctx := context.Background() + rdb := redis.NewClient(&redis.Options{ + Addr: fmt.Sprintf("%s:6379", redisHost), // Redis server address + Password: "", // No password set + DB: 0, // Use default DB + }) + key := "test_max_connection_key" + maxConn := 10 + for range 10 { + result := Limiter.Acquire(ctx, rdb, key, maxConn) + assert.True(t, result) + } + // after max connection reached, it should be false + result2 := Limiter.Acquire(ctx, rdb, key, maxConn) + assert.False(t, result2) + + for range 10 { + Limiter.Release(ctx, rdb, key) + } + + // connection in redis should be 0 finally + n, err := rdb.Get(ctx, key).Int() + assert.Nil(t, err) + assert.Equal(t, 0, n) + +} diff --git a/src/pkg/quota/dao/util.go b/src/pkg/quota/dao/util.go index 01d55eae064..e8e2c8c5e19 100644 --- a/src/pkg/quota/dao/util.go +++ b/src/pkg/quota/dao/util.go @@ -111,8 +111,8 @@ func listOrderBy(query *q.Query) string { } prefixes := []string{"hard.", "used."} for _, prefix := range prefixes { - if strings.HasPrefix(sortByItem.Key, prefix) { - resource := strings.TrimPrefix(sortByItem.Key, prefix) + if after, ok := strings.CutPrefix(sortByItem.Key, prefix); ok { + resource := after if types.IsValidResource(types.ResourceName(resource)) { field := fmt.Sprintf("%s->>%s", strings.TrimSuffix(prefix, "."), orm.QuoteLiteral(resource)) orderBy = fmt.Sprintf("(%s) %s", castQuantity(field), order) diff --git a/src/pkg/reg/adapter/tencentcr/artifact_registry_test.go b/src/pkg/reg/adapter/tencentcr/artifact_registry_test.go index a137eb3589b..026a2bcb2db 100644 --- a/src/pkg/reg/adapter/tencentcr/artifact_registry_test.go +++ b/src/pkg/reg/adapter/tencentcr/artifact_registry_test.go @@ -3,6 +3,8 @@ package tencentcr import ( "reflect" "testing" + "fmt" + "strings" tcr "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tcr/v20190924" @@ -22,7 +24,38 @@ func Test_filterToPatterns(t *testing.T) { wantRepoPattern string wantTagsPattern string }{ - // TODO: Add test cases. + { + name: "name and tag filters provided", + args: args{ + filters: []*model.Filter{ + {Type: model.FilterTypeName, Value: "demo/app"}, + {Type: model.FilterTypeTag, Value: "v1.*"}, + }, + }, + wantNamespacePattern: "demo", + wantRepoPattern: "demo/app", + wantTagsPattern: "v1.*", + }, + { + name: "only name filter provided", + args: args{ + filters: []*model.Filter{ + {Type: model.FilterTypeName, Value: "team/project"}, + }, + }, + wantNamespacePattern: "team", + wantRepoPattern: "team/project", + wantTagsPattern: "", + }, + { + name: "empty filters slice", + args: args{ + filters: []*model.Filter{}, + }, + wantNamespacePattern: "", + wantRepoPattern: "", + wantTagsPattern: "", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -130,41 +163,72 @@ func Test_adapter_listCandidateNamespaces(t *testing.T) { } } -func Test_adapter_DeleteManifest(t *testing.T) { - type fields struct { - Adapter *native.Adapter - registryID *string - regionName *string - tcrClient *tcr.Client - pageSize *int64 - client *commonhttp.Client - registry *model.Registry +type mockAdapter struct { + adapter + deleteImageFunc func(namespace, repo, reference string) error +} + +func (m *mockAdapter) deleteImage(namespace, repo, reference string) error { + if m.deleteImageFunc != nil { + return m.deleteImageFunc(namespace, repo, reference) } + return nil +} + +func (m *mockAdapter) DeleteManifest(repository, reference string) error { + parts := strings.Split(repository, "/") + if len(parts) != 2 { + return fmt.Errorf("invalid repository format: %s", repository) + } + namespace, repo := parts[0], parts[1] + return m.deleteImage(namespace, repo, reference) +} + +func Test_adapter_DeleteManifest(t *testing.T) { type args struct { repository string reference string } + tests := []struct { name string - fields fields args args wantErr bool }{ - // TODO: Add test cases. + { + name: "invalid repository format", + args: args{ + repository: "invalidRepo", + reference: "latest", + }, + wantErr: true, + }, + { + name: "valid repository format should not error", + args: args{ + repository: "demo/app", + reference: "v1.0", + }, + wantErr: false, + }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - a := &adapter{ - Adapter: tt.fields.Adapter, - registryID: tt.fields.registryID, - regionName: tt.fields.regionName, - tcrClient: tt.fields.tcrClient, - pageSize: tt.fields.pageSize, - client: tt.fields.client, - registry: tt.fields.registry, + a := &mockAdapter{} + + if tt.name == "valid repository format should not error" { + a.deleteImageFunc = func(namespace, repo, reference string) error { + if namespace != "demo" || repo != "app" || reference != "v1.0" { + t.Errorf("unexpected args: %s/%s:%s", namespace, repo, reference) + } + return nil + } } - if err := a.DeleteManifest(tt.args.repository, tt.args.reference); (err != nil) != tt.wantErr { - t.Errorf("adapter.DeleteManifest() error = %v, wantErr %v", err, tt.wantErr) + + err := a.DeleteManifest(tt.args.repository, tt.args.reference) + if (err != nil) != tt.wantErr { + t.Errorf("DeleteManifest() error = %v, wantErr %v", err, tt.wantErr) } }) } diff --git a/src/pkg/registry/client.go b/src/pkg/registry/client.go index 23bb148270c..3ea4da132d6 100644 --- a/src/pkg/registry/client.go +++ b/src/pkg/registry/client.go @@ -467,7 +467,7 @@ func (c *client) PushBlobChunk(repository, digest string, blobSize int64, chunk resp, err := c.do(req) if err != nil { // if push chunk error, we should query the upload progress for new location and end range. - newLocation, newEnd, err1 := c.getUploadStatus(location) + newLocation, newEnd, err1 := c.getUploadStatus(url) if err1 == nil { return newLocation, newEnd, err } diff --git a/src/pkg/task/dao/execution.go b/src/pkg/task/dao/execution.go index faf5d1b695b..d6f1e25df7a 100644 --- a/src/pkg/task/dao/execution.go +++ b/src/pkg/task/dao/execution.go @@ -412,7 +412,7 @@ func buildInClauseSQLForExtraAttrs(jsonbStrus []jsonbStru) (string, []any) { return "", nil } - var cond string + var cond strings.Builder var args []any sql := "select id from execution where" @@ -423,9 +423,9 @@ func buildInClauseSQLForExtraAttrs(jsonbStrus []jsonbStru) (string, []any) { keys := strings.Split(strings.TrimPrefix(jsonbStr.key, jsonbStr.keyPrefix), ".") if len(keys) == 1 { if i == 0 { - cond += "extra_attrs->>?=?" + cond.WriteString("extra_attrs->>?=?") } else { - cond += " and extra_attrs->>?=?" + cond.WriteString(" and extra_attrs->>?=?") } } if len(keys) >= 2 { @@ -435,9 +435,9 @@ func buildInClauseSQLForExtraAttrs(jsonbStrus []jsonbStru) (string, []any) { } s := strings.Join(elements, "->") if i == 0 { - cond += fmt.Sprintf("extra_attrs->%s->>?=?", s) + cond.WriteString(fmt.Sprintf("extra_attrs->%s->>?=?", s)) } else { - cond += fmt.Sprintf(" and extra_attrs->%s->>?=?", s) + cond.WriteString(fmt.Sprintf(" and extra_attrs->%s->>?=?", s)) } } @@ -447,7 +447,7 @@ func buildInClauseSQLForExtraAttrs(jsonbStrus []jsonbStru) (string, []any) { args = append(args, jsonbStr.value) } - return fmt.Sprintf("%s %s", sql, cond), args + return fmt.Sprintf("%s %s", sql, cond.String()), args } func buildExecStatusOutdateKey(id int64, vendor string) string { diff --git a/src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc.component.html b/src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc.component.html index 5878f296bae..40767407ea5 100644 --- a/src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc.component.html +++ b/src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc.component.html @@ -113,6 +113,24 @@ +
+
+
+ + + + + + +
+
+
+ +
+ + + + {{ + 'PROJECT.PROXY_CACHE_MAX_UPSTREAM_CONN_INPUT_TIP' + | translate + }} + +
+
+
+ +
+ + + + {{ + 'PROJECT.PROXY_CACHE_MAX_UPSTREAM_CONN_INPUT_TIP' + | translate + }} + +
+
diff --git a/src/portal/src/app/base/project/project-config/project-policy-config/project-policy-config.component.ts b/src/portal/src/app/base/project/project-config/project-policy-config/project-policy-config.component.ts index b35e3992dbe..f3bc43d255c 100644 --- a/src/portal/src/app/base/project/project-config/project-policy-config/project-policy-config.component.ts +++ b/src/portal/src/app/base/project/project-config/project-policy-config/project-policy-config.component.ts @@ -58,6 +58,7 @@ export class ProjectPolicy { ProxyCacheEnabled: boolean; RegistryId?: number | null; ProxySpeedKb?: number | null; + MaxUpstreamConn?: number | null; constructor() { this.Public = false; @@ -70,6 +71,7 @@ export class ProjectPolicy { this.ProxyCacheEnabled = false; this.RegistryId = null; this.ProxySpeedKb = -1; + this.MaxUpstreamConn = -1; } initByProject(pro: Project) { @@ -88,6 +90,9 @@ export class ProjectPolicy { this.ProxySpeedKb = pro.metadata.proxy_speed_kb ? pro.metadata.proxy_speed_kb : -1; + this.MaxUpstreamConn = pro.metadata.max_upstream_conn + ? pro.metadata.max_upstream_conn + : -1; } } const PAGE_SIZE: number = 100; @@ -149,6 +154,7 @@ export class ProjectPolicyConfigComponent implements OnInit { ]; // **Added property for bandwidth error message** bandwidthError: string | null = null; + maxUpstreamConnError: string | null = null; registries: Registry[] = []; supportedRegistryTypeQueryString: string = 'type={docker-hub harbor azure-acr aws-ecr google-gcr quay docker-registry github-ghcr jfrog-artifactory}'; @@ -200,8 +206,11 @@ export class ProjectPolicyConfigComponent implements OnInit { (!Number.isInteger(value) && value !== -1) || (value <= 0 && value !== -1) ) { - this.bandwidthError = - 'Please enter -1 or an integer greater than 0.'; + this.translate + .get('PROJECT.SPEED_LIMIT_TIP') + .subscribe((res: string) => { + this.bandwidthError = res; + }); } else { this.bandwidthError = null; } diff --git a/src/portal/src/app/base/project/project-config/project-policy-config/project.ts b/src/portal/src/app/base/project/project-config/project-policy-config/project.ts index 7cc14430292..2f5ab7f7ad9 100644 --- a/src/portal/src/app/base/project/project-config/project-policy-config/project.ts +++ b/src/portal/src/app/base/project/project-config/project-policy-config/project.ts @@ -36,6 +36,7 @@ export class Project { auto_sbom_generation: string | boolean; reuse_sys_cve_allowlist?: string; proxy_speed_kb?: number | null; + max_upstream_conn?: number | null; }; cve_allowlist?: object; constructor() { diff --git a/src/portal/src/app/base/project/project.ts b/src/portal/src/app/base/project/project.ts index ef2e9556ab8..24958b79717 100644 --- a/src/portal/src/app/base/project/project.ts +++ b/src/portal/src/app/base/project/project.ts @@ -36,9 +36,11 @@ export class Project { auto_sbom_generation: string | boolean; retention_id: number; bandwidth: number; + max_upstream_conn: number; }; constructor() { this.metadata = {}; this.metadata.public = false; + this.metadata.max_upstream_conn = -1; } } diff --git a/src/portal/src/i18n/lang/de-de-lang.json b/src/portal/src/i18n/lang/de-de-lang.json index b47eeef650c..68f43c6dfc1 100644 --- a/src/portal/src/i18n/lang/de-de-lang.json +++ b/src/portal/src/i18n/lang/de-de-lang.json @@ -271,7 +271,10 @@ "PROXY_CACHE_TOOLTIP": "Die Aktivierung der Funktion erlaubt es dem Projekt, als Cache für eine andere Registry Instanz zu dienen. Harbor unterstützt die Proxy Funktion nur für DockerHub, Docker Registry, Harbor, Aws ECR, Azure ACR, Alibaba Cloud ACR, Quay, Google GCR, JFrog Artifactory, und Github GHCR", "ENDPOINT": "Endpunkt", "PROXY_CACHE_ENDPOINT": "Proxy Cache Endpunkt", - "NO_PROJECT": "Es konnte kein Projekt gefunden werden" + "NO_PROJECT": "Es konnte kein Projekt gefunden werden", + "CONNECTION_LIMIT": "Max connection to upstream registry", + "PROXY_CACHE_MAX_UPSTREAM_CONN_TIP": "The max connection to the upstream registry for this proxy cache project, if -1, then there is no limit", + "PROXY_CACHE_MAX_UPSTREAM_CONN_INPUT_TIP": "Please enter -1 or an integer greater than 0. " }, "PROJECT_DETAIL": { "SUMMARY": "Zusammenfassung", @@ -1348,6 +1351,7 @@ "MSG_SCHEDULE_RESET": "Speicherbereinigungs-Intervall wurde zurückgesetzt", "PARAMETERS": "Parameter", "DELETE_UNTAGGED": "Erlaube Speicherbereinigung auf Artefakte ohne Tag", + "DELETE_TAG": "Allow garbage collection to remove tag files from backend storage", "EXPLAIN": "Speicherbereinigung (Garbage Collection / GC) ist eine rechenintensive Operation, die die Registry-Leistung beeinflussen kann", "EXPLAIN_TIME_WINDOW": "Artifacts uploaded in the past 2 hours(the default window) are excluded from garbage collection", "DRY_RUN_SUCCESS": "Probelauf erfolgreich gestartet", diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index a6d09d9a10d..1580ef2b0e5 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -271,7 +271,10 @@ "PROXY_CACHE_TOOLTIP": "Enable this to allow this project to act as a pull-through cache for a particular target registry instance. Harbor can only act a proxy for DockerHub, Docker Registry, Harbor, Aws ECR, Azure ACR, Alibaba Cloud ACR, Quay, Google GCR, Github GHCR, and JFrog Artifactory registries.", "ENDPOINT": "Endpoint", "PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint", - "NO_PROJECT": "We couldn't find any projects" + "NO_PROJECT": "We couldn't find any projects", + "CONNECTION_LIMIT": "Max connection to upstream registry", + "PROXY_CACHE_MAX_UPSTREAM_CONN_TIP": "The max connection to the upstream registry for this proxy cache project, if -1, then there is no limit", + "PROXY_CACHE_MAX_UPSTREAM_CONN_INPUT_TIP": "Please enter -1 or an integer greater than 0. " }, "PROJECT_DETAIL": { "SUMMARY": "Summary", @@ -1351,6 +1354,7 @@ "MSG_SCHEDULE_RESET": "Garbage Collection schedule has been reset", "PARAMETERS": "Parameters", "DELETE_UNTAGGED": "Allow garbage collection on untagged artifacts", + "DELETE_TAG": "Allow garbage collection to remove tag files from backend storage", "EXPLAIN": "GC is a compute intensive operation that may impact registry performance", "EXPLAIN_TIME_WINDOW": "Artifacts uploaded in the past 2 hours(the default window) are excluded from garbage collection", "DRY_RUN_SUCCESS": "Triggered dry run successfully", diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index e750889598a..cec5554d9fa 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -272,7 +272,10 @@ "PROXY_CACHE_TOOLTIP": "Habilite esta opción para permitir que este proyecto actúe como caché de extracción para una instancia de registro de destino en particular. Harbor solo puede actuar como proxy para los registros DockerHub, Docker Registry, Harbor, AWS ECR, Azure ACR, Quay, Google GCR, JFrog Artifactory y Github GHCR.", "ENDPOINT": "Endpoint", "PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint", - "NO_PROJECT": "No pudimos encontrar ningún proyecto" + "NO_PROJECT": "No pudimos encontrar ningún proyecto", + "CONNECTION_LIMIT": "Max connection to upstream registry", + "PROXY_CACHE_MAX_UPSTREAM_CONN_TIP": "The max connection to the upstream registry for this proxy cache project, if -1, then there is no limit", + "PROXY_CACHE_MAX_UPSTREAM_CONN_INPUT_TIP": "Please enter -1 or an integer greater than 0. " }, "PROJECT_DETAIL": { "SUMMARY": "Resumen", @@ -1345,6 +1348,7 @@ "MSG_SCHEDULE_RESET": "Programación de Garbage Collection ha sido reiniciada", "PARAMETERS": "Parametros", "DELETE_UNTAGGED": "Permitir garbage collection en artefactos no tageados", + "DELETE_TAG": "Allow garbage collection to remove tag files from backend storage", "EXPLAIN": "GC es una operación que requiere un uso intensivo de recursos informáticos y que puede afectar el rendimiento del registro", "EXPLAIN_TIME_WINDOW": "Los artefactos cargados en las últimas 2 horas (la ventana predeterminada) se excluyen de la recolección de basura", "DRY_RUN_SUCCESS": "Activación de dry run satisfactorio", diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index 2a2aa423309..f529d032920 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -271,7 +271,10 @@ "PROXY_CACHE_TOOLTIP": "Activez cette option pour permettre à ce projet d'agir comme un cache de pull pour un espace de noms particulier dans un registre cible. Harbor ne peut agir en tant que proxy que pour les registres DockerHub et Harbor.", "ENDPOINT": "Endpoint", "PROXY_CACHE_ENDPOINT": "Endpoint du Proxy Cache", - "NO_PROJECT": "Nous n'avons trouvé aucun projet." + "NO_PROJECT": "Nous n'avons trouvé aucun projet.", + "CONNECTION_LIMIT": "Max connection to upstream registry", + "PROXY_CACHE_MAX_UPSTREAM_CONN_TIP": "The max connection to the upstream registry for this proxy cache project, if -1, then there is no limit", + "PROXY_CACHE_MAX_UPSTREAM_CONN_INPUT_TIP": "Please enter -1 or an integer greater than 0. " }, "PROJECT_DETAIL": { "SUMMARY": "Résumé", @@ -1350,6 +1353,7 @@ "MSG_SCHEDULE_RESET": "La planification de la purge a été réinitialisée", "PARAMETERS": "Paramètres", "DELETE_UNTAGGED": "Supprimer les artefacts non tagués", + "DELETE_TAG": "Allow garbage collection to remove tag files from backend storage", "EXPLAIN": "Purger est une opération gourmande en puissance de calcul qui peut impacter les performances du registre", "EXPLAIN_TIME_WINDOW": "Les artefacts téléversés dans les dernières 2 heures (fenêtre de temps par défaut) sont exclues de la purge", "DRY_RUN_SUCCESS": "Exécution à blanc déclenchée avec succès", diff --git a/src/portal/src/i18n/lang/ko-kr-lang.json b/src/portal/src/i18n/lang/ko-kr-lang.json index 24f8d84f94d..f250560d064 100644 --- a/src/portal/src/i18n/lang/ko-kr-lang.json +++ b/src/portal/src/i18n/lang/ko-kr-lang.json @@ -271,7 +271,10 @@ "PROXY_CACHE_TOOLTIP": "이 프로젝트가 특정 대상 레지스트리 인스턴스에 대한 풀스루 캐시 역할을 할 수 있도록 하려면 이 옵션을 활성화합니다. Harbor는 DockerHub, Docker Registry, Harbor, Aws ECR, Azure ACR, Quay, Google GCR, Github GHCR 및 JFrog Artifactory 레지스트리에 대해서만 프록시 역할을 할 수 있습니다.", "ENDPOINT": "엔드포인트", "PROXY_CACHE_ENDPOINT": "프록시 캐시 엔드포인트", - "NO_PROJECT": "프로젝트를 찾을 수 없습니다" + "NO_PROJECT": "프로젝트를 찾을 수 없습니다", + "CONNECTION_LIMIT": "Max connection to upstream registry", + "PROXY_CACHE_MAX_UPSTREAM_CONN_TIP": "The max connection to the upstream registry for this proxy cache project, if -1, then there is no limit", + "PROXY_CACHE_MAX_UPSTREAM_CONN_INPUT_TIP": "Please enter -1 or an integer greater than 0. " }, "PROJECT_DETAIL": { "SUMMARY": "요약", @@ -1343,6 +1346,7 @@ "MSG_SCHEDULE_RESET": "가비지 컬렉션 일정이 초기화됐습니다", "PARAMETERS": "파라미터", "DELETE_UNTAGGED": "태그가 지정되지 않은 아티팩트에 대한 가비지 수집 허용", + "DELETE_TAG": "Allow garbage collection to remove tag files from backend storage", "EXPLAIN": "가비지 컬렉션은 레지스트리 성능에 영향을 미칠 수 있는 커퓨팅 집약적 작업입니다.", "EXPLAIN_TIME_WINDOW": "지난 2시간(기본 기간) 동안 업로드된 아티팩트는 가비지 컬렉션에서 제외됩니다.", "DRY_RUN_SUCCESS": "모의 테스트가 성공적으로 실행됐습니다", diff --git a/src/portal/src/i18n/lang/pt-br-lang.json b/src/portal/src/i18n/lang/pt-br-lang.json index 2f16c75df9c..f1c3fb7e494 100644 --- a/src/portal/src/i18n/lang/pt-br-lang.json +++ b/src/portal/src/i18n/lang/pt-br-lang.json @@ -269,7 +269,10 @@ "PROXY_CACHE": "Cache do Proxy", "PROXY_CACHE_TOOLTIP": "Habilite para fazer deste projeto um cache local de outros repositórios remotos (registries). O Harbor pode servir de cache apenas para outros repositórios Harbor, Docker Hub, AWS ECR, Azure ACR, Alibaba Cloud ACR, Quay, Google GCR, Github GHCR, JFrog Artifactory, e repositórios compatíveis com o protocolo Docker Registry", "ENDPOINT": "Endereço", - "PROXY_CACHE_ENDPOINT": "Endereço do Proxy Cache" + "PROXY_CACHE_ENDPOINT": "Endereço do Proxy Cache", + "CONNECTION_LIMIT": "Max connection to upstream registry", + "PROXY_CACHE_MAX_UPSTREAM_CONN_TIP": "The max connection to the upstream registry for this proxy cache project, if -1, then there is no limit", + "PROXY_CACHE_MAX_UPSTREAM_CONN_INPUT_TIP": "Please enter -1 or an integer greater than 0. " }, "PROJECT_DETAIL": { "SUMMARY": "Resumo", @@ -1345,6 +1348,7 @@ "MSG_SCHEDULE_RESET": "Agendamento da limpeza redefinido", "PARAMETERS": "Parâmetros", "DELETE_UNTAGGED": "Permitir coleta de artefatos sem tags", + "DELETE_TAG": "Allow garbage collection to remove tag files from backend storage", "EXPLAIN": "A limpeza exige recursos computacionais e pode impactar performance.", "EXPLAIN_TIME_WINDOW": "Artifacts uploaded in the past 2 hours(the default window) are excluded from garbage collection", "DRY_RUN_SUCCESS": "Teste executado com sucesso", diff --git a/src/portal/src/i18n/lang/ru-ru-lang.json b/src/portal/src/i18n/lang/ru-ru-lang.json index 4f04faf938c..1e3f454f428 100644 --- a/src/portal/src/i18n/lang/ru-ru-lang.json +++ b/src/portal/src/i18n/lang/ru-ru-lang.json @@ -256,7 +256,10 @@ "PROXY_CACHE_TOOLTIP": "Включите это, чтобы разрешить проекту действовать как прокси-кэш для определенного экземпляра реестра. Harbor может действовать как прокси для DockerHub, Docker Registry, Harbor, Aws ECR, Azure ACR, Quay, Google GCR, Github GHCR и JFrog Artifactory.", "ENDPOINT": "Конечная точка", "PROXY_CACHE_ENDPOINT": "Конечная точка прокси-кэша", - "NO_PROJECT": "Мы не смогли найти никаких проектов" + "NO_PROJECT": "Мы не смогли найти никаких проектов", + "CONNECTION_LIMIT": "Max connection to upstream registry", + "PROXY_CACHE_MAX_UPSTREAM_CONN_TIP": "The max connection to the upstream registry for this proxy cache project, if -1, then there is no limit", + "PROXY_CACHE_MAX_UPSTREAM_CONN_INPUT_TIP": "Please enter -1 or an integer greater than 0. " }, "PROJECT_DETAIL": { "SUMMARY": "Сводка", @@ -1267,6 +1270,7 @@ "MSG_SCHEDULE_RESET": "Расписание для сборки мусора сброшено", "PARAMETERS": "Параметры", "DELETE_UNTAGGED": "Разрешить сборку мусора для непомеченных артефактов", + "DELETE_TAG": "Allow garbage collection to remove tag files from backend storage", "EXPLAIN": "GC — это ресурсоемкая операция, которая может повлиять на производительность реестра", "DRY_RUN_SUCCESS": "Сухой запуск успешно запущен" }, diff --git a/src/portal/src/i18n/lang/tr-tr-lang.json b/src/portal/src/i18n/lang/tr-tr-lang.json index 95eb6b52504..0038c822622 100644 --- a/src/portal/src/i18n/lang/tr-tr-lang.json +++ b/src/portal/src/i18n/lang/tr-tr-lang.json @@ -272,7 +272,10 @@ "PROXY_CACHE_TOOLTIP": "Enable this to allow this project to act as a pull-through cache for a particular target registry instance. Harbor can only act a proxy for DockerHub, Docker Registry, Harbor, Aws ECR, Azure ACR, Alibaba Cloud ACR, Quay, Google GCR, JFrog Artifactory, and Github GHCR registries.", "ENDPOINT": "Endpoint", "PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint", - "NO_PROJECT": "We couldn't find any projects" + "NO_PROJECT": "We couldn't find any projects", + "CONNECTION_LIMIT": "Max connection to upstream registry", + "PROXY_CACHE_MAX_UPSTREAM_CONN_TIP": "The max connection to the upstream registry for this proxy cache project, if -1, then there is no limit", + "PROXY_CACHE_MAX_UPSTREAM_CONN_INPUT_TIP": "Please enter -1 or an integer greater than 0. " }, "PROJECT_DETAIL": { "SUMMARY": "Özet", @@ -1349,6 +1352,7 @@ "MSG_SCHEDULE_RESET": "Çöp Toplama programı sıfırlandı", "PARAMETERS": "Parameters", "DELETE_UNTAGGED": "Allow garbage collection on untagged artifacts", + "DELETE_TAG": "Allow garbage collection to remove tag files from backend storage", "EXPLAIN": "GC is a compute intensive operation that may impact registry performance", "EXPLAIN_TIME_WINDOW": "Artifacts uploaded in the past 2 hours(the default window) are excluded from garbage collection", "DRY_RUN_SUCCESS": "Triggered dry run successfully", diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index 0393f59b9ab..e43f9818846 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -270,7 +270,10 @@ "PROXY_CACHE_TOOLTIP": "开启此项,以使得该项目成为目标仓库的镜像代理.仅支持 DockerHub, Docker Registry, Harbor, Aws ECR, Azure ACR, Alibaba Cloud ACR, Quay, Google GCR, JFrog Artifactory, 和 Github GHCR 类型的仓库", "ENDPOINT": "地址", "PROXY_CACHE_ENDPOINT": "镜像代理地址", - "NO_PROJECT": "未发现任何项目" + "NO_PROJECT": "未发现任何项目", + "CONNECTION_LIMIT": "Max connection to upstream registry", + "PROXY_CACHE_MAX_UPSTREAM_CONN_TIP": "The max connection to the upstream registry for this proxy cache project, if less than 0, then there is no limit", + "PROXY_CACHE_MAX_UPSTREAM_CONN_INPUT_TIP": "请输入-1或者大于0的整数" }, "PROJECT_DETAIL": { "SUMMARY": "概要", @@ -1347,6 +1350,7 @@ "MSG_SCHEDULE_RESET": "垃圾回收定时任务已被重置", "PARAMETERS": "参数", "DELETE_UNTAGGED": "允许回收无 tag 的 artifacts", + "DELETE_TAG": "允许垃圾回收移除后端存储中的Tag文件", "EXPLAIN": "垃圾回收是一个计算密集型操作,可能会影响仓库性能", "EXPLAIN_TIME_WINDOW": "在最近的两小时(默认窗口期)内被推送的 Artifacts 不会被当做垃圾回收的目标", "DRY_RUN_SUCCESS": "触发模拟运行成功", diff --git a/src/portal/src/i18n/lang/zh-tw-lang.json b/src/portal/src/i18n/lang/zh-tw-lang.json index db252fa92da..384094566c6 100644 --- a/src/portal/src/i18n/lang/zh-tw-lang.json +++ b/src/portal/src/i18n/lang/zh-tw-lang.json @@ -6,8 +6,8 @@ "MGMT": "管理", "REG": "Registry", "HARBOR_SWAGGER": "Harbor Swagger", - "THEME_DARK_TEXT": "深色主題", - "THEME_LIGHT_TEXT": "淺色主題" + "THEME_DARK_TEXT": "深色", + "THEME_LIGHT_TEXT": "淺色" }, "SIGN_IN": { "REMEMBER": "記住我", @@ -16,7 +16,7 @@ "HEADER_LINK": "登入", "CORE_SERVICE_NOT_AVAILABLE": "核心服務無法使用。", "OR": "或", - "VIA_LOCAL_DB": "透過本地資料庫登入" + "VIA_LOCAL_DB": "透過本機資料庫登入" }, "SIGN_UP": { "TITLE": "註冊" @@ -28,7 +28,7 @@ "DELETE": "刪除", "LOG_IN": "登入", "LOG_IN_OIDC": "透過 OIDC 提供者登入", - "LOG_IN_OIDC_WITH_PROVIDER_NAME": "LOGIN WITH {{providerName}}", + "LOG_IN_OIDC_WITH_PROVIDER_NAME": "使用 {{providerName}} 登入", "SIGN_UP_LINK": "註冊帳號", "SIGN_UP": "註冊", "CONFIRM": "確認", @@ -47,7 +47,7 @@ "SWITCH": "切換", "REPLICATE": "複製", "ACTIONS": "操作", - "BROWSE": "瀏覽檔案", + "BROWSE": "瀏覽", "UPLOAD": "上傳", "NO_FILE": "未選擇檔案", "ADD": "新增", @@ -61,52 +61,54 @@ "DELETED_FAILURE": "刪除失敗或部分失敗", "SWITCH_SUCCESS": "切換成功", "SWITCH_FAILURE": "切換失敗", - "REPLICATE_SUCCESS": "複製成功", - "REPLICATE_FAILURE": "複製失敗", + "REPLICATE_SUCCESS": "複製已開始", + "REPLICATE_FAILURE": "複製啟動失敗", "STOP_SUCCESS": "停止成功", "STOP_FAILURE": "停止執行失敗", "TIME_OUT": "閘道逾時" }, "TOOLTIP": { - "NAME_FILTER": "篩選資源的名稱。不填或填寫「**」可選取所有資源;「library/**」僅選取「library」下的資源。更多的選取模式請參考使用者手冊。", - "TAG_FILTER": "篩選資源的 tag/version。不填或填寫「**」可選取所有;「1.0*」僅選取以「1.0」開頭的 tag/version。", + "NAME_FILTER": "篩選資源名稱。不填寫、或使用「**」可選取所有資源。「library/**」僅選取「library」下的資源。更多模式請參考使用者手冊。", + "TAG_FILTER": "篩選資源的標籤/版本。不填寫、或使用「**」可選取所有標籤/版本。「1.0*」僅選取以「1.0」開頭的標籤。更多模式請參考使用者手冊。", "LABEL_FILTER": "根據標籤篩選資源。", - "RESOURCE_FILTER": "篩選資源的類型。", - "PUSH_BASED": "將資源由本地 Harbor 推送到遠端儲存庫。", - "PULL_BASED": "將資源由遠端儲存庫拉取到本地 Harbor。", - "DESTINATION_NAMESPACE": "指定目的端命名空間。如果不填,資源會被放到和來源相同的命名空間下。", - "OVERRIDE": "如果存在具有相同名稱的資源,請指定是否覆蓋目標上的資源。", - "EMAIL": "請使用正確的電子郵件地址,例如 name@example.com。", - "USER_NAME": "不能包含特殊字元且長度不能超過 255 個字元。", - "FULL_NAME": "全名長度不能超過 20 個字元。", - "COMMENT": "評論長度不能超過 30 個字元。", - "CURRENT_PWD": "目前密碼必填。", - "PASSWORD": "密碼長度在 8 到 128 之間且需包含至少一個大寫字元、一個小寫字元和一個數字。", - "CONFIRM_PWD": "密碼輸入不一致。", - "SIGN_IN_USERNAME": "使用者名稱必填。", - "SIGN_IN_PWD": "密碼必填。", - "SIGN_UP_MAIL": "電子郵件地址僅用來重設您的密碼。", - "SIGN_UP_REAL_NAME": "全名", - "ITEM_REQUIRED": "此選項必填。", - "SCOPE_REQUIRED": "此選項為必填且必須為 scope 格式。", - "NUMBER_REQUIRED": "此選項為必填且必須為數字。", - "PORT_REQUIRED": "此選項為必填且必須為有效連接埠。", - "CRON_REQUIRED": "此選項為必填且必須為 cron 格式。", + "RESOURCE_FILTER": "篩選資源類型。", + "PUSH_BASED": "將資源從本機 Harbor 推送至遠端 Registry。", + "PULL_BASED": "從遠端 Registry 拉取資源至本機 Harbor。", + "DESTINATION_NAMESPACE": "指定目標命名空間。若留空,資源將被放置在與來源相同的命名空間下。", + "OVERRIDE": "指定如果目標位置已存在同名資源,是否要覆寫。", + "SINGLE_ACTIVE_REPLICATION": "指定是否要等待上一個進行中的執行完成後才繼續,以避免多個相同的複製規則平行執行。", + "EMAIL": "電子郵件應為有效的電子郵件地址,例如:name@example.com。", + "USER_NAME": "不能包含特殊字元,長度上限為 255 個字元。", + "FULL_NAME": "長度上限為 20 個字元。", + "COMMENT": "備註長度應少於 30 個字元。", + "CURRENT_PWD": "必須輸入目前密碼。", + "PASSWORD": "密碼長度應為 8-128 個字元,且至少包含 1 個大寫字母、1 個小寫字母和 1 個數字。", + "CONFIRM_PWD": "密碼不一致。", + "SIGN_IN_USERNAME": "必須輸入使用者名稱。", + "SIGN_IN_PWD": "必須輸入密碼。", + "SIGN_UP_MAIL": "電子郵件僅用於重設您的密碼。", + "SIGN_UP_REAL_NAME": "姓名", + "ITEM_REQUIRED": "此欄位為必填。", + "SCOPE_REQUIRED": "此欄位為必填,且應符合 scope 格式。", + "NUMBER_REQUIRED": "此欄位為必填,且應為數字。", + "PORT_REQUIRED": "此欄位為必填,且應為有效的連接埠。", + "CRON_REQUIRED": "此欄位為必填,且應為 cron 格式。", "EMAIL_EXISTING": "電子郵件地址已存在。", "USER_EXISTING": "使用者名稱已存在。", "RULE_USER_EXISTING": "名稱已存在。", - "EMPTY": "名稱必填", - "NONEMPTY": "不能為空", - "ENDPOINT_FORMAT": "Endpoint 必須以 http:// 或 https:// 開頭。", - "OIDC_ENDPOINT_FORMAT": "Endpoint 必須以 https:// 開頭。", + "EMPTY": "名稱為必填。", + "NONEMPTY": "不得為空。", + "REPO_TOOLTIP": "在此模式下,使用者無法對映像檔進行任何操作。", + "ENDPOINT_FORMAT": "端點必須以 http:// 或 https:// 開頭。", + "OIDC_ENDPOINT_FORMAT": "端點必須以 https:// 開頭。", "OIDC_NAME": "OIDC 提供者的名稱。", - "OIDC_ENDPOINT": "OIDC 伺服器的網址。", - "OIDC_SCOPE": "在身份驗證期間發送到 OIDC 伺服器的 scope。它必須包含「openid」和「offline_access」。如果您使用 Google,請從此欄位中移除「offline_access」。", - "OIDC_VERIFYCERT": "如果您的 OIDC 伺服器是透過自簽憑證託管的,請取消勾選此框。", - "OIDC_AUTOONBOARD": "跳過註冊引導畫面,使用者無法更改其使用者名稱。使用者名稱將由 ID Token 提供。", - "OIDC_USER_CLAIM": "使用者名稱將從 ID Token 中的特定宣告中取得。若未指定特定宣告,則預設會使用 'name' 宣告。", - "NEW_SECRET": "密碼必須超過 8 個字元,並至少包含 1 個大寫字母、1 個小寫字母和 1 個數字。", - "OIDC_LOGOUT": "Logs the user out of their current session with the Identity Provider." + "OIDC_ENDPOINT": "符合 OIDC 標準的伺服器 URL。", + "OIDC_SCOPE": "在認證過程中傳送至 OIDC 伺服器的範圍。它必須包含「openid」和「offline_access」。如果您正在使用 Google,請從此欄位中移除「offline_access」。", + "OIDC_VERIFYCERT": "如果您的 OIDC 伺服器是透過自簽章憑證託管的,請取消勾選此核取方塊。", + "OIDC_AUTOONBOARD": "跳過使用者初次設定畫面,使其無法變更使用者名稱。使用者名稱將由 ID Token 提供。", + "OIDC_USER_CLAIM": "ID Token 中用來擷取使用者名稱的宣告名稱。如果未指定,預設為 'name'。", + "NEW_SECRET": "金鑰長度應為 8-128 個字元,且至少包含 1 個大寫字母、1 個小寫字母和 1 個數字。", + "OIDC_LOGOUT": "將使用者從其目前在身分提供者的工作階段中登出。" }, "PLACEHOLDER": { "CURRENT_PWD": "輸入目前密碼", @@ -119,53 +121,53 @@ "SIGN_IN_PWD": "密碼" }, "PROFILE": { - "TITLE": "使用者資料", - "USER_NAME": "帳號名稱", + "TITLE": "使用者設定", + "USER_NAME": "使用者名稱", "EMAIL": "電子郵件", - "FULL_NAME": "姓名", - "COMMENT": "註解", + "FULL_NAME": "全名", + "COMMENT": "備註", "PASSWORD": "密碼", - "SAVE_SUCCESS": "使用者資料儲存成功。", - "ADMIN_RENAME_BUTTON": "更改帳號名稱", - "ADMIN_RENAME_TIP": "選擇按鈕以將帳號名稱更改為 \"admin@harbor.local\"。此操作無法復原。", - "RENAME_SUCCESS": "帳號名稱更改成功!", - "RENAME_CONFIRM_INFO": "警告,將名稱更改為 admin@harbor.local 是無法復原的。", - "CLI_PASSWORD": "CLI 密碼", - "CLI_PASSWORD_TIP": "CLI 密碼可以用作 Docker 或 Helm 用戶端的密碼。當啟用 OIDC 認證模式時,我們強烈建議使用機器人帳號,因為 CLI 密碼取決於 ID 權杖的有效性,並要求用戶定期登入 UI 以更新權杖。", + "SAVE_SUCCESS": "使用者設定儲存成功。", + "ADMIN_RENAME_BUTTON": "變更使用者名稱", + "ADMIN_RENAME_TIP": "選取此按鈕以將使用者名稱變更為「admin@harbor.local」。此操作無法復原。", + "RENAME_SUCCESS": "重新命名成功!", + "RENAME_CONFIRM_INFO": "警告:將名稱變更為 admin@harbor.local 後將無法復原。", + "CLI_PASSWORD": "CLI 金鑰", + "CLI_PASSWORD_TIP": "CLI 金鑰可作為 Docker 或 Helm 客戶端的密碼使用。啟用 OIDC 認證模式時,強烈建議使用機器人帳號,因為 CLI 金鑰的有效性取決於 ID Token,且需要使用者定期登入 UI 以重新整理權杖。", "COPY_SUCCESS": "複製成功", "COPY_ERROR": "複製失敗", - "ADMIN_CLI_SECRET_BUTTON": "產生密碼", - "ADMIN_CLI_SECRET_RESET_BUTTON": "上傳您自己的密碼", - "NEW_SECRET": "密碼", - "CONFIRM_SECRET": "重複輸入密碼", - "GENERATE_SUCCESS": "CLI 密碼設定成功", - "GENERATE_ERROR": "CLI 密碼設定失敗", - "CONFIRM_TITLE_CLI_GENERATE": "您確定要重新產生密碼嗎?", - "CONFIRM_BODY_CLI_GENERATE": "如果您重新產生 CLI 密碼,舊的 CLI 密碼將被捨棄。" + "ADMIN_CLI_SECRET_BUTTON": "產生金鑰", + "ADMIN_CLI_SECRET_RESET_BUTTON": "上傳您自己的金鑰", + "NEW_SECRET": "新金鑰", + "CONFIRM_SECRET": "確認金鑰", + "GENERATE_SUCCESS": "CLI 金鑰設定成功。", + "GENERATE_ERROR": "CLI 金鑰設定失敗。", + "CONFIRM_TITLE_CLI_GENERATE": "您確定要重新產生金鑰嗎?", + "CONFIRM_BODY_CLI_GENERATE": "如果您重新產生 CLI 金鑰,舊的 CLI 金鑰將會失效。" }, "CHANGE_PWD": { - "TITLE": "修改密碼", + "TITLE": "變更密碼", "CURRENT_PWD": "目前密碼", "NEW_PWD": "新密碼", "CONFIRM_PWD": "確認新密碼", - "SAVE_SUCCESS": "成功更改使用者密碼。", - "PASS_TIPS": "密碼長度需介於 8 到 128 個字元之間,且至少包含一個大寫字母、小寫字母或數字。" + "SAVE_SUCCESS": "使用者密碼變更成功。", + "PASS_TIPS": "密碼長度應為 8-128 個字元,且至少包含 1 個大寫字母、1 個小寫字母和 1 個數字。" }, "CHANGE_PREF": { - "TITLE": "Preferences", - "LANGUAGE": "Language", - "DATE_TIME_FORMAT": "Date/Time Format", - "PULL_CMD_PREFIX": "Pull Command Prefix" + "TITLE": "偏好設定", + "LANGUAGE": "語言", + "DATE_TIME_FORMAT": "日期/時間格式", + "PULL_CMD_PREFIX": "拉取指令前置字串" }, "ACCOUNT_SETTINGS": { "PROFILE": "使用者設定", - "CHANGE_PWD": "修改密碼", - "PREFERENCES": "Preferences", + "PREFERENCES": "偏好設定", + "CHANGE_PWD": "變更密碼", "ABOUT": "關於", "LOGOUT": "登出" }, "GLOBAL_SEARCH": { - "PLACEHOLDER": "搜尋 {{param}} ...", + "PLACEHOLDER": "搜尋 {{param}}...", "PLACEHOLDER_VIC": "搜尋 Registry..." }, "TOP_NAV": { @@ -176,31 +178,31 @@ "PROJECTS": "專案", "SYSTEM_MGMT": { "NAME": "系統管理", - "USER": "使用者管理", - "GROUP": "群組管理", - "REGISTRY": "Registry 管理", - "REPLICATION": "資料複製管理", - "CONFIG": "設定管理", - "VULNERABILITY": "弱點管理", + "USER": "使用者", + "GROUP": "群組", + "REGISTRY": "Registry", + "REPLICATION": "複製", + "CONFIG": "組態設定", + "VULNERABILITY": "弱點", "GARBAGE_COLLECTION": "垃圾回收", "INTERROGATION_SERVICES": "審查服務" }, "LOGS": "日誌", - "AUDIT_LOGS": "Audit Logs", - "LEGACY_LOGS": "Audit Logs (Legacy)", - "TASKS": "任務", + "AUDIT_LOGS": "稽核日誌", + "LEGACY_LOGS": "稽核日誌 (舊版)", + "TASKS": "工作", "API_EXPLORER": "API Explorer", "HARBOR_API_MANAGEMENT": "Harbor API V2.0", "HELM_API_MANAGEMENT": "Harbor API", "DISTRIBUTIONS": { - "NAME": "Distributions", - "INSTANCES": "實例" + "NAME": "發佈", + "INSTANCES": "執行個體" } }, "USER": { - "ADD_ACTION": "建立使用者", - "ENABLE_ADMIN_ACTION": "設定為管理員", - "DISABLE_ADMIN_ACTION": "撤銷管理員", + "ADD_ACTION": "新增使用者", + "ENABLE_ADMIN_ACTION": "設為管理員", + "DISABLE_ADMIN_ACTION": "撤銷管理員權限", "DEL_ACTION": "刪除", "FILTER_PLACEHOLDER": "篩選使用者", "COLUMN_NAME": "名稱", @@ -209,30 +211,30 @@ "COLUMN_REG_NAME": "註冊時間", "IS_ADMIN": "是", "IS_NOT_ADMIN": "否", - "ADD_USER_TITLE": "建立使用者", - "SAVE_SUCCESS": "建立使用者成功。", + "ADD_USER_TITLE": "新增使用者", + "SAVE_SUCCESS": "新使用者建立成功。", "DELETION_TITLE": "確認刪除使用者", "DELETION_SUMMARY": "您確定要刪除使用者 {{param}} 嗎?", - "DELETE_SUCCESS": "成功刪除使用者。", - "OF": "共", - "ITEMS": "筆紀錄", - "RESET_OK": "成功重設使用者密碼", - "EXISTING_PASSWORD": "新密碼不可與舊密碼相同", + "DELETE_SUCCESS": "使用者刪除成功。", + "ITEMS": "個項目", + "OF": "中的", + "RESET_OK": "使用者密碼重設成功。", + "EXISTING_PASSWORD": "新密碼不能與舊密碼相同。", "UNKNOWN": "未知", - "UNKNOWN_TIP": "若值為「未知」,請透過身分驗證提供者系統確認該使用者是否為管理員身分。" + "UNKNOWN_TIP": "如果值為「未知」,請透過身分提供者系統確認該使用者是否具有管理員身分。" }, "PROJECT": { "PROJECTS": "專案", "NAME": "專案名稱", "ROLE": "角色", - "PUBLIC_OR_PRIVATE": "公開或私有", + "PUBLIC_OR_PRIVATE": "存取層級", "REPO_COUNT": "儲存庫數量", - "CHART_COUNT": "Helm Chart 數量", + "CHART_COUNT": "Chart 數量", "CREATION_TIME": "建立時間", - "ACCESS_LEVEL": "存取權限", + "ACCESS_LEVEL": "存取層級", "PUBLIC": "公開", "PRIVATE": "私有", - "MAKE": "建立", + "MAKE": "設為", "NEW_POLICY": "新增複製規則", "DELETE": "刪除", "ALL_PROJECTS": "所有專案", @@ -240,12 +242,12 @@ "PUBLIC_PROJECTS": "公開專案", "PROJECT": "專案", "NEW_PROJECT": "新增專案", - "NAME_TOOLTIP": "專案名稱應由 1~255 個小寫字元、數字和 ._- 組成,並且必須以字母或數字開頭。", - "NAME_IS_REQUIRED": "專案名稱為必填。", + "NAME_TOOLTIP": "專案名稱長度應為 1-255 個字元,可包含小寫字母、數字及 ._-,且必須以字母或數字開頭。", + "NAME_IS_REQUIRED": "必須輸入專案名稱。", "NAME_ALREADY_EXISTS": "專案名稱已存在。", "NAME_IS_ILLEGAL": "專案名稱無效。", "UNKNOWN_ERROR": "建立專案時發生未知錯誤。", - "ITEMS": "筆紀錄", + "ITEMS": "個項目", "DELETION_TITLE": "確認移除專案", "DELETION_SUMMARY": "您確定要刪除專案 {{param}} 嗎?", "FILTER_PLACEHOLDER": "篩選專案", @@ -253,23 +255,26 @@ "CREATED_SUCCESS": "專案建立成功。", "DELETED_SUCCESS": "專案刪除成功。", "TOGGLED_SUCCESS": "專案切換成功。", - "FAILED_TO_DELETE_PROJECT": "由於專案包含儲存庫、複製規則或 helm-charts,無法刪除。", - "INLINE_HELP_PUBLIC": "當專案設為公開時,任何人都可讀取此專案下的儲存庫,使用者不需執行 \"docker login\" 即可拉取此專案下的映像檔。", - "PROXY_CACHE_BANDWIDTH": "Set the maximum network bandwidth to pull image from upstream for proxy-cache. For unlimited bandwidth, please enter -1. ", - "BANDWIDTH": "Bandwidth", - "SPEED_LIMIT_TIP": "Please enter -1 or an integer greater than 0. ", - "OF": "共計", - "COUNT_QUOTA": "數量配額", - "STORAGE_QUOTA": "儲存配額限制", - "COUNT_QUOTA_TIP": "請輸入介於 '1' 和 '100,000,000' 之間的整數,'-1' 表示無限制。", - "STORAGE_QUOTA_TIP": "儲存配額上限只接受整數,上限為 '1024TB'。輸入 '-1' 表示無限配額。", - "QUOTA_UNLIMIT_TIP": "專案所能使用的最大邏輯空間。輸入 '-1' 表示無限制配額。", + "FAILED_TO_DELETE_PROJECT": "專案因包含儲存庫、複製規則或 Helm Chart 而無法刪除。", + "INLINE_HELP_PUBLIC": "當專案設為公開時,任何人對此專案下的儲存庫都具有讀取權限,使用者無需執行「docker login」即可拉取此專案下的映像檔。", + "PROXY_CACHE_BANDWIDTH": "為代理快取設定從上游提取映像檔的最大網路頻寬。若要設定無限頻寬,請輸入 -1。", + "BANDWIDTH": "頻寬", + "SPEED_LIMIT_TIP": "請輸入 -1 或大於 0 的整數。", + "OF": "中的", + "COUNT_QUOTA": "Artifact 數量配額", + "STORAGE_QUOTA": "專案配額限制", + "COUNT_QUOTA_TIP": "請輸入介於 '1' 與 '100,000,000' 之間的整數,輸入 '-1' 代表無限制。", + "STORAGE_QUOTA_TIP": "儲存配額的上限僅接受整數值,最高為 '1024TB'。輸入 '-1' 代表無限配額。", + "QUOTA_UNLIMIT_TIP": "專案可使用的最大邏輯空間。若要設定無限配額,請輸入 '-1'。", "TYPE": "類型", "PROXY_CACHE": "代理快取", - "PROXY_CACHE_TOOLTIP": "啟用此選項可讓此專案作為特定目標 Registry 實例的拉取快取。Harbor 僅可作為 DockerHub、Docker Registry、Harbor、AWS ECR、Azure ACR、Alibaba Cloud ACR、Quay、Google GCR、GitHub GHCR 和 JFrog Artifactory Registry 的代理。", + "PROXY_CACHE_TOOLTIP": "啟用此選項,可讓此專案作為特定目標 Registry 執行個體的通透快取。Harbor 僅能代理 DockerHub、Docker Registry、Harbor、AWS ECR、Azure ACR、阿里雲 ACR、Quay、Google GCR、GitHub GHCR 及 JFrog Artifactory。", "ENDPOINT": "端點", "PROXY_CACHE_ENDPOINT": "代理快取端點", - "NO_PROJECT": "找不到任何專案" + "NO_PROJECT": "找不到任何專案", + "CONNECTION_LIMIT": "上游 Registry 最大連線數", + "PROXY_CACHE_MAX_UPSTREAM_CONN_TIP": "此代理快取專案可與上游 Registry 建立的連線數上限。輸入 -1 代表無限制。", + "PROXY_CACHE_MAX_UPSTREAM_CONN_INPUT_TIP": "請輸入 -1 或大於 0 的整數。" }, "PROJECT_DETAIL": { "SUMMARY": "摘要", @@ -279,55 +284,55 @@ "LOGS": "日誌", "LABELS": "標籤", "PROJECTS": "專案", - "CONFIG": "設定", + "CONFIG": "組態設定", "HELMCHART": "Helm Chart", "ROBOT_ACCOUNTS": "機器人帳號", - "WEBHOOKS": "Webhooks", - "IMMUTABLE_TAG": "不可變標籤", + "WEBHOOKS": "Webhook", + "IMMUTABLE_TAG": "標籤不可變性", "POLICY": "原則" }, "PROJECT_CONFIG": { - "REGISTRY": "專案儲存庫", + "REGISTRY": "專案 Registry", "PUBLIC_TOGGLE": "公開", - "PUBLIC_POLICY": "將專案儲存庫設為公開會讓所有人都能夠存取所有儲存庫。", - "SECURITY": "部署安全", + "PUBLIC_POLICY": "將專案 Registry 設為公開,將使所有儲存庫對所有人開放存取。", + "SECURITY": "部署安全性", "CONTENT_TRUST_TOGGLE": "啟用內容信任", - "CONTENT_TRUST_POLICY": "只允許部署已驗證的映像檔。", - "PREVENT_VULNERABLE_TOGGLE": "阻止有弱點的映像檔執行。", - "PREVENT_VULNERABLE_1": "阻止具有", - "PREVENT_VULNERABLE_2": "或更高危險級別的映像檔部署。", + "CONTENT_TRUST_POLICY": "僅允許部署已驗證的映像檔。", + "PREVENT_VULNERABLE_TOGGLE": "防止執行有弱點的映像檔。", + "PREVENT_VULNERABLE_1": "防止弱點嚴重性為", + "PREVENT_VULNERABLE_2": "及以上的映像檔被部署。", "SCAN": "弱點掃描", "AUTOSCAN_TOGGLE": "推送時自動掃描映像檔", - "AUTOSCAN_POLICY": "當映像檔推送到專案儲存庫時自動掃描。", - "SBOM": "SBOM generation", - "AUTOSBOM_TOGGLE": "Automatically generate SBOM on push", - "AUTOSBOM_POLICY": "Automatically generate SBOM when the images are pushed to the project registry." + "AUTOSCAN_POLICY": "當映像檔被推送至專案 Registry 時自動進行掃描。", + "SBOM": "SBOM 產生", + "AUTOSBOM_TOGGLE": "推送時自動產生 SBOM", + "AUTOSBOM_POLICY": "當映像檔被推送至專案 Registry 時,自動產生 SBOM。" }, "MEMBER": { "NEW_USER": "新增使用者成員", "NEW_MEMBER": "新增成員", "MEMBER": "成員", "NAME": "名稱", - "EMAIL": "電子郵件", "ROLE": "角色", "SYS_ADMIN": "系統管理員", "PROJECT_ADMIN": "專案管理員", - "PROJECT_MAINTAINER": "專案維護者", - "DEVELOPER": "開發人員", + "PROJECT_MAINTAINER": "維護者", + "DEVELOPER": "開發者", "GUEST": "訪客", - "LIMITED_GUEST": "受限制的訪客", + "LIMITED_GUEST": "受限訪客", "DELETE": "刪除", - "ITEMS": "筆紀錄", + "ITEMS": "個項目", "ACTIONS": "操作", "USER": "使用者", "USERS": "使用者", + "EMAIL": "電子郵件", "ADD_USER": "新增使用者", - "NEW_USER_INFO": "將使用者新增為此專案的成員並指定角色", + "NEW_USER_INFO": "將使用者以指定角色新增為此專案的成員。", "NEW_GROUP": "新增群組", - "IMPORT_GROUP": "加入群組成員", - "NEW_GROUP_INFO": "新增現有的使用者群組或從 LDAP/AD 選擇專案成員", - "ADD_GROUP_SELECT": "將現有群組新增至專案成員", - "CREATE_GROUP_SELECT": "從 LDAP 新增群組至專案成員", + "IMPORT_GROUP": "新增群組成員", + "NEW_GROUP_INFO": "將現有使用者群組或從 LDAP/AD 選取的使用者群組新增為專案成員。", + "ADD_GROUP_SELECT": "將現有使用者群組新增為專案成員。", + "CREATE_GROUP_SELECT": "從 LDAP 新增群組為專案成員。", "LDAP_SEARCH_DN": "LDAP 群組 DN", "LDAP_SEARCH_NAME": "名稱", "LDAP_GROUP": "群組", @@ -337,24 +342,24 @@ "MEMBER_TYPE": "成員類型", "GROUP_TYPE": "群組", "USER_TYPE": "使用者", - "USERNAME_IS_REQUIRED": "使用者名稱必填。", + "USERNAME_IS_REQUIRED": "必須輸入使用者名稱。", "USERNAME_DOES_NOT_EXISTS": "使用者名稱不存在。", - "USERNAME_ALREADY_EXISTS": "使用者名稱已存在。", + "USERNAME_ALREADY_EXISTS": "使用者已是此專案的成員。", "UNKNOWN_ERROR": "新增成員時發生未知錯誤。", "FILTER_PLACEHOLDER": "篩選成員", "DELETION_TITLE": "確認刪除專案成員", - "DELETION_SUMMARY": "您確認刪除專案成員 {{param}} 嗎?", + "DELETION_SUMMARY": "您確定要刪除專案成員 {{param}} 嗎?", "ADDED_SUCCESS": "成員新增成功。", "DELETED_SUCCESS": "成員刪除成功。", "SWITCHED_SUCCESS": "成員角色切換成功。", - "OF": "共", + "OF": "中的", "SWITCH_TITLE": "確認切換專案成員", - "SWITCH_SUMMARY": "您確認切換專案成員 {{param}} 嗎?", + "SWITCH_SUMMARY": "您確定要切換專案成員 {{param}} 嗎?", "SET_ROLE": "設定角色", "REMOVE": "移除", - "GROUP_NAME_REQUIRED": "群組名稱必填。", + "GROUP_NAME_REQUIRED": "必須輸入群組名稱。", "NON_EXISTENT_GROUP": "群組名稱不存在。", - "GROUP_ALREADY_ADDED": "群組名稱已被新增至此專案。" + "GROUP_ALREADY_ADDED": "群組已是此專案的成員。" }, "ROBOT_ACCOUNT": { "NAME": "名稱", @@ -362,85 +367,85 @@ "TOKEN": "權杖", "NEW_ROBOT_ACCOUNT": "新增機器人帳號", "ENABLED_STATE": "啟用狀態", - "NUMBER_REQUIRED": "此欄位必須填寫不為 0 的整數。", + "NUMBER_REQUIRED": "此欄位為必填,且應為非零整數。", "DESCRIPTION": "描述", "CREATION": "建立時間", - "EXPIRATION": "過期時間", - "TOKEN_EXPIRATION": "機器人權杖到期時間(天)", + "EXPIRATION": "到期時間", + "TOKEN_EXPIRATION": "機器人權杖到期時間 (天)", "ACTION": "操作", "EDIT": "編輯", - "ITEMS": "筆", - "OF": "共計", + "ITEMS": "個項目", + "OF": "中的", "DISABLE_ACCOUNT": "停用帳號", "ENABLE_ACCOUNT": "啟用帳號", "DELETE": "刪除", "CREAT_ROBOT_ACCOUNT": "建立機器人帳號", "PERMISSIONS_ARTIFACT": "Artifact", - "PERMISSIONS_HELMCHART": "Helm Chart(Chart 儲存庫)", + "PERMISSIONS_HELMCHART": "Helm Chart (Chart Museum)", "PUSH": "推送", "PULL": "拉取", "FILTER_PLACEHOLDER": "篩選機器人帳號", - "ROBOT_NAME": "不能包含特殊字元(~#$%)且長度不能超過 255 個字元。", + "ROBOT_NAME": "不能包含特殊字元 (~#$%),長度上限為 255 個字元。", "ACCOUNT_EXISTING": "機器人帳號已存在。", - "ALERT_TEXT": "這是唯一一次複製此密鑰的機會,您將無法再次複製。", - "CREATED_SUCCESS": "成功建立 '{{param}}'。", - "COPY_SUCCESS": "成功複製 '{{param}}' 的權杖", + "ALERT_TEXT": "這是複製此金鑰的唯一機會,您將無法再次取得。", + "CREATED_SUCCESS": "已成功建立 '{{param}}'。", + "COPY_SUCCESS": "已成功複製 '{{param}}' 的金鑰。", "DELETION_TITLE": "確認移除機器人帳號", - "DELETION_SUMMARY": "您是否要刪除機器人帳號 {{param}}?", - "PULL_IS_MUST": "拉取權限預設已勾選,不可修改。", + "DELETION_SUMMARY": "您確定要刪除機器人帳號 {{param}} 嗎?", + "PULL_IS_MUST": "拉取權限預設為勾選且不能修改。", "EXPORT_TO_FILE": "匯出至檔案", - "EXPIRES_AT": "到期日", - "EXPIRATION_TOOLTIP": "如不設定,將採用系統設定中的過期時間。", - "INVALID_VALUE": "無效的過期日期", - "NEVER_EXPIRED": "永不過期", - "NAME_PREFIX": "機器人名稱前綴", - "NAME_PREFIX_REQUIRED": "機器人名稱前綴為必填項目", - "UPDATE": "Update", - "AUDIT_LOG": "Audit Log", - "PREHEAT_INSTANCE": "Preheat Instance", - "PROJECT": "Project", - "REPLICATION_POLICY": "Replication Policy", - "REPLICATION": "Replication", - "REPLICATION_ADAPTER": "Replication Adapter", + "EXPIRES_AT": "到期於", + "EXPIRATION_TOOLTIP": "若未設定,將使用系統組態中的到期時間。", + "INVALID_VALUE": "到期時間的值無效。", + "NEVER_EXPIRED": "永不到期", + "NAME_PREFIX": "機器人名稱前置字串", + "NAME_PREFIX_REQUIRED": "必須輸入機器人名稱前置字串。", + "UPDATE": "更新", + "AUDIT_LOG": "稽核日誌", + "PREHEAT_INSTANCE": "預熱執行個體", + "PROJECT": "專案", + "REPLICATION_POLICY": "複製原則", + "REPLICATION": "複製", + "REPLICATION_ADAPTER": "複製配接器", "REGISTRY": "Registry", - "SCAN_ALL": "Scan All", - "SYSTEM_VOLUMES": "System Volumes", - "GARBAGE_COLLECTION": "Garbage Collection", - "PURGE_AUDIT": "Purge Audit", - "JOBSERVICE_MONITOR": "Job Service Monitor", - "TAG_RETENTION": "Tag Retention", - "SCANNER": "Scanner", - "LABEL": "Label", - "EXPORT_CVE": "Export CVE", - "SECURITY_HUB": "Security Hub", - "CATALOG": "Catalog", - "METADATA": "Project Metadata", - "REPOSITORY": "Repository", + "SCAN_ALL": "全部掃描", + "SYSTEM_VOLUMES": "系統磁碟區", + "GARBAGE_COLLECTION": "垃圾回收", + "PURGE_AUDIT": "清除稽核記錄", + "JOBSERVICE_MONITOR": "工作服務監控器", + "TAG_RETENTION": "標籤保留", + "SCANNER": "掃描器", + "LABEL": "標籤", + "EXPORT_CVE": "匯出 CVE", + "SECURITY_HUB": "安全中樞", + "CATALOG": "目錄", + "METADATA": "專案中繼資料", + "REPOSITORY": "儲存庫", "ARTIFACT": "Artifact", - "SCAN": "Scan", + "SCAN": "掃描", "SBOM": "SBOM", - "TAG": "Tag", - "ACCESSORY": "Accessory", - "ARTIFACT_ADDITION": "Artifact Addition", - "ARTIFACT_LABEL": "Artifact Label", - "PREHEAT_POLICY": "Preheat Policy", - "IMMUTABLE_TAG": "Immutable Tag", - "LOG": "Log", - "NOTIFICATION_POLICY": "Notification Policy", - "QUOTA": "Quota", - "BACK": "Back", - "NEXT": "Next", - "FINISH": "Finish", - "BASIC_INFO": "Basic Information", - "SELECT_PERMISSIONS": "Select Permissions", - "SELECT_SYSTEM_PERMISSIONS": "Select System Permissions", - "SELECT_PROJECT_PERMISSIONS": "Select Project Permissions", - "SYSTEM_PERMISSIONS": "System Permissions", - "ROBOT": "Robot Account", - "USER": "User", - "LDAPUSER": "LDAP User", - "GROUP": "User Group", - "MEMBER": "Project Member" + "TAG": "標籤", + "ACCESSORY": "附件", + "ARTIFACT_ADDITION": "Artifact 附加項目", + "ARTIFACT_LABEL": "Artifact 標籤", + "PREHEAT_POLICY": "預熱原則", + "IMMUTABLE_TAG": "不可變標籤", + "LOG": "日誌", + "NOTIFICATION_POLICY": "通知原則", + "QUOTA": "配額", + "BACK": "上一步", + "NEXT": "下一步", + "FINISH": "完成", + "BASIC_INFO": "基本資訊", + "SELECT_PERMISSIONS": "選取權限", + "SELECT_SYSTEM_PERMISSIONS": "選取系統權限", + "SELECT_PROJECT_PERMISSIONS": "選取專案權限", + "SYSTEM_PERMISSIONS": "系統權限", + "ROBOT": "機器人帳號", + "USER": "使用者", + "LDAPUSER": "LDAP 使用者", + "GROUP": "使用者群組", + "MEMBER": "專案成員" }, "WEBHOOK": { "EDIT_BUTTON": "編輯", @@ -449,48 +454,48 @@ "TYPE": "Webhook", "STATUS": "狀態", "CREATED": "建立時間", - "ENABLED": "啟用", - "DISABLED": "停用", - "OF": "共計", - "ITEMS": "筆紀錄", - "LAST_TRIGGERED": "最近觸發時間", + "ENABLED": "已啟用", + "DISABLED": "已停用", + "OF": "中的", + "ITEMS": "個項目", + "LAST_TRIGGERED": "上次觸發時間", "EDIT_WEBHOOK": "編輯 Webhook", "ADD_WEBHOOK": "新增 Webhook", - "CREATE_WEBHOOK": "建立 Webhooks", - "EDIT_WEBHOOK_DESC": "指定接收 Webhook 通知的目標", - "CREATE_WEBHOOK_DESC": "為了啟用 webhook,請提供 Endpoint 和憑證以存取 Webhook 伺服器。", - "VERIFY_REMOTE_CERT_TOOLTIP": "勾選此框表示 Webhook 需要驗證遠端網址的憑證。當遠端網址使用自我簽署或不受信任的憑證時,請取消選擇。", - "ENDPOINT_URL": "Endpoint 位址", - "URL_IS_REQUIRED": "Endpoint 位址必填", - "AUTH_HEADER": "驗證標頭", + "CREATE_WEBHOOK": "開始使用 Webhook", + "EDIT_WEBHOOK_DESC": "指定接收 Webhook 通知的端點。", + "CREATE_WEBHOOK_DESC": "若要開始使用 Webhook,請提供一個端點及存取 Webhook 伺服器的憑證。", + "VERIFY_REMOTE_CERT_TOOLTIP": "決定 Webhook 是否應驗證遠端 URL 的憑證。當遠端 URL 使用自簽章或不受信任的憑證時,請取消勾選此核取方塊。", + "ENDPOINT_URL": "端點 URL", + "URL_IS_REQUIRED": "必須輸入端點 URL。", + "AUTH_HEADER": "認證標頭", "VERIFY_REMOTE_CERT": "驗證遠端憑證", - "TEST_ENDPOINT_BUTTON": "測試 Endpoint", + "TEST_ENDPOINT_BUTTON": "測試端點", "CANCEL_BUTTON": "取消", "SAVE_BUTTON": "儲存", - "TEST_ENDPOINT_SUCCESS": "測試連接成功。", - "TEST_ENDPOINT_FAILURE": "測試連接失敗。", + "TEST_ENDPOINT_SUCCESS": "連線測試成功。", + "TEST_ENDPOINT_FAILURE": "Ping 端點失敗。", "ENABLED_WEBHOOK_TITLE": "啟用 Webhook", - "ENABLED_WEBHOOK_SUMMARY": "確認啟用 webhook {{name}}?", + "ENABLED_WEBHOOK_SUMMARY": "您確定要啟用 Webhook {{name}} 嗎?", "DISABLED_WEBHOOK_TITLE": "停用 Webhook", - "DISABLED_WEBHOOK_SUMMARY": "確認停用 webhook {{name}}?", + "DISABLED_WEBHOOK_SUMMARY": "您確定要停用 Webhook {{name}} 嗎?", "DELETE_WEBHOOK_TITLE": "刪除 Webhook", - "DELETE_WEBHOOK_SUMMARY": "確認刪除 webhook(s) {{names}}?", - "WEBHOOKS": "Webhooks", + "DELETE_WEBHOOK_SUMMARY": "您確定要刪除 Webhook {{names}} 嗎?", + "WEBHOOKS": "Webhook", "NEW_WEBHOOK": "新增 Webhook", "ENABLE": "啟用", "DISABLE": "停用", "NAME": "名稱", - "TARGET": "目標位址", + "TARGET": "端點 URL", "EVENT_TYPES": "事件類型", - "DESCRIPTION": "簡介", - "NO_WEBHOOK": "暫無 Webhook 紀錄", - "LAST_TRIGGER": "最新觸發", + "DESCRIPTION": "描述", + "NO_WEBHOOK": "沒有 Webhook", + "LAST_TRIGGER": "上次觸發", "WEBHOOK_NAME": "Webhook 名稱", - "NO_TRIGGER": "暫無觸發記錄", - "NAME_REQUIRED": "名稱必填", + "NO_TRIGGER": "沒有觸發記錄", + "NAME_REQUIRED": "必須輸入名稱。", "NOTIFY_TYPE": "通知類型", "EVENT_TYPE": "事件類型", - "EVENT_TYPE_REQUIRED": "請至少選擇一種事件類型", + "EVENT_TYPE_REQUIRED": "至少需要一種事件類型。", "PAYLOAD_FORMAT": "Payload 格式", "CLOUD_EVENT": "CloudEvents", "PAYLOAD_DATA": "Payload 資料", @@ -502,89 +507,90 @@ "IMPORT_LDAP_GROUP": "匯入 LDAP 群組", "IMPORT_HTTP_GROUP": "新增 HTTP 群組", "IMPORT_OIDC_GROUP": "新增 OIDC 群組", - "ADD": "新增", + "ADD": "新增群組", "EDIT": "編輯", "DELETE": "刪除", "NAME": "名稱", "TYPE": "類型", "DN": "DN", - "PROPERTY": "屬性", "GROUP_DN": "LDAP 群組 DN", + "PROPERTY": "屬性", "REG_TIME": "註冊時間", - "ADD_GROUP_SUCCESS": "新增群組成功", - "EDIT_GROUP_SUCCESS": "編輯群組成功", + "ADD_GROUP_SUCCESS": "群組新增成功。", + "EDIT_GROUP_SUCCESS": "群組編輯成功。", "LDAP_TYPE": "LDAP", "HTTP_TYPE": "HTTP", "OIDC_TYPE": "OIDC", - "OF": "共計", - "ITEMS": "筆紀錄", + "OF": "中的", + "ITEMS": "個項目", "NEW_MEMBER": "新增群組成員", - "NEW_USER_INFO": "將群組以指定角色新增為此專案的成員", + "NEW_USER_INFO": "將群組以指定角色新增為此專案的成員。", "ROLE": "角色", "SYS_ADMIN": "系統管理員", "PROJECT_ADMIN": "專案管理員", - "PROJECT_MAINTAINER": "維護人員", - "DEVELOPER": "開發人員", + "PROJECT_MAINTAINER": "維護者", + "DEVELOPER": "開發者", "GUEST": "訪客", - "LIMITED_GUEST": "受限的訪客", + "LIMITED_GUEST": "受限訪客", "DELETION_TITLE": "確認刪除群組成員", - "DELETION_SUMMARY": "您是否要刪除群組成員 {{param}}?" + "DELETION_SUMMARY": "您確定要刪除群組成員 {{param}} 嗎?" }, "AUDIT_LOG": { "USERNAME": "使用者名稱", "REPOSITORY_NAME": "儲存庫名稱", "TAGS": "標籤", "OPERATION": "操作", - "OPERATION_DESCRIPTION": "Operation Description", + "OPERATION_DESCRIPTION": "操作描述", "OPERATIONS": "操作", - "TIMESTAMP": "時間戳記", + "TIMESTAMP": "時間戳", "ALL_OPERATIONS": "所有操作", "ARTIFACT": "Artifact", - "USER": "User", - "PROJECT": "Project", - "CONFIGURATION": "Configuration", - "PROJECT_MEMBER": "Project Member", - "USER_LOGIN_LOGOUT": "User Login/Logout", + "USER": "使用者", + "PROJECT": "專案", + "CONFIGURATION": "組態設定", + "PROJECT_MEMBER": "專案成員", + "USER_LOGIN_LOGOUT": "使用者登入/登出", "PULL": "拉取", "PUSH": "推送", "CREATE": "建立", "DELETE": "刪除", "OTHERS": "其他", - "ADVANCED": "進階搜尋", - "SIMPLE": "簡易搜尋", - "ITEMS": "筆紀錄", - "RESULT": "Success", + "ADVANCED": "進階", + "SIMPLE": "簡易", + "ITEMS": "個項目", + "RESULT": "成功", "FILTER_PLACEHOLDER": "篩選日誌", "INVALID_DATE": "無效日期。", - "OF": "共計", - "NOT_FOUND": "未發現任何日誌!", + "OF": "中的", + "NOT_FOUND": "找不到任何日誌!", "RESOURCE": "資源", "RESOURCE_TYPE": "資源類型" }, "REPLICATION": { - "PUSH_BASED_ONLY": "Only for the push-based replication", + "PUSH_BASED_ONLY": "僅適用於推送模式的複製", "YES": "是", "SECONDS": "秒", "MINUTES": "分鐘", "HOURS": "小時", "MONTH": "月", - "DAY_MONTH": "一個月的某天", - "DAY_WEEK": "一週的的某天", - "CRON_TITLE": "cron 格式說明 '* * * * * *'。cron 字串是基於 UTC 時間", + "DAY_MONTH": "日 (月)", + "DAY_WEEK": "日 (週)", + "CRON_TITLE": "cron 字串 '* * * * * *' 的格式說明。cron 字串基於 UTC 時間。", "FIELD_NAME": "欄位名稱", - "MANDATORY": "是否必填?", - "ALLOWED_VALUES": "允許的值", + "MANDATORY": "必填?", + "ALLOWED_VALUES": "允許值", "ALLOWED_CHARACTERS": "允許的特殊字元", "TOTAL": "總計", - "OVERRIDE": "覆蓋", + "OVERRIDE": "覆寫", + "SINGLE_ACTIVE_REPLICATION": "單一作用中複製", "ENABLED_RULE": "啟用規則", - "OVERRIDE_INFO": "覆蓋", + "OVERRIDE_INFO": "覆寫", "OPERATION": "操作", "CURRENT": "目前", "FILTER_PLACEHOLDER": "篩選工作", "STOP_TITLE": "確認停止執行", "BOTH": "兩者", - "STOP_SUCCESS": "成功停止執行 {{param}}", + "STOP_SUCCESS": "已成功停止執行 {{param}}。", "STOP_SUMMARY": "您確定要停止執行 {{param}} 嗎?", "TASK_ID": "工作 ID", "RESOURCE_TYPE": "資源類型", @@ -597,17 +603,17 @@ "FAILURE": "失敗", "IN_PROGRESS": "進行中", "REPLICATION_RULE": "複製規則", - "NEW_REPLICATION_RULE": "新複製規則", + "NEW_REPLICATION_RULE": "新增複製規則", "ENDPOINTS": "端點", "FILTER_POLICIES_PLACEHOLDER": "篩選規則", - "FILTER_EXECUTIONS_PLACEHOLDER": "篩選執行", + "FILTER_EXECUTIONS_PLACEHOLDER": "篩選執行項", "DELETION_TITLE": "確認刪除複製規則", "DELETION_SUMMARY": "您確定要刪除複製規則 {{param}} 嗎?", - "REPLICATION_TITLE": "確認規則複製", - "REPLICATION_SUMMARY": "您確定要複製規則 {{param}} 嗎?", - "DELETION_TITLE_FAILURE": "刪除規則確認失敗", - "DELETION_SUMMARY_FAILURE": "有待處理/執行中/重試中的狀態,無法刪除", - "REPLICATE_SUMMARY_FAILURE": "有待處理/執行中的狀態,無法刪除", + "REPLICATION_TITLE": "確認執行複製規則", + "REPLICATION_SUMMARY": "您確定要執行複製規則 {{param}} 嗎?", + "DELETION_TITLE_FAILURE": "刪除規則失敗", + "DELETION_SUMMARY_FAILURE": "因狀態為等待中/執行中/重試中而無法刪除。", + "REPLICATE_SUMMARY_FAILURE": "因狀態為等待中/執行中而無法執行。", "FILTER_TARGETS_PLACEHOLDER": "篩選端點", "DELETION_TITLE_TARGET": "確認刪除端點", "DELETION_SUMMARY_TARGET": "您確定要刪除端點 {{param}} 嗎?", @@ -618,68 +624,68 @@ "TEST_CONNECTION": "測試連線", "TESTING_CONNECTION": "正在測試連線...", "TEST_CONNECTION_SUCCESS": "連線測試成功。", - "TEST_CONNECTION_FAILURE": "連線測試失敗。", + "TEST_CONNECTION_FAILURE": "Ping 端點失敗。", "ID": "ID", "NAME": "名稱", - "NAME_IS_REQUIRED": "名稱是必填的。", + "NAME_IS_REQUIRED": "必須輸入名稱。", "DESCRIPTION": "描述", "ENABLE": "啟用", "DISABLE": "停用", "REPLICATION_MODE": "複製模式", "SRC_REGISTRY": "來源 Registry", - "DESTINATION_NAMESPACE": "目標 Registry:命名空間", - "DESTINATION_NAME_IS_REQUIRED": "目標名稱是必填的。", - "NEW_DESTINATION": "新端點", + "DESTINATION_NAMESPACE": "目標命名空間", + "DESTINATION_NAME_IS_REQUIRED": "必須輸入端點名稱。", + "NEW_DESTINATION": "新增端點", "DESTINATION_URL": "端點 URL", - "DESTINATION_URL_IS_REQUIRED": "端點 URL 是必填的。", + "DESTINATION_URL_IS_REQUIRED": "必須輸入端點 URL。", "DESTINATION_USERNAME": "使用者名稱", "DESTINATION_PASSWORD": "密碼", "ALL_STATUS": "所有狀態", "ENABLED": "已啟用", "DISABLED": "已停用", "LAST_START_TIME": "上次開始時間", - "ACTIVATION": "啟動", - "REPLICATION_EXECUTION": "執行", - "REPLICATION_EXECUTIONS": "執行", + "ACTIVATION": "啟用狀態", + "REPLICATION_EXECUTION": "執行項", + "REPLICATION_EXECUTIONS": "執行項", "STOPJOB": "停止", "ALL": "全部", - "PENDING": "待處理", + "PENDING": "等待中", "RUNNING": "執行中", "ERROR": "錯誤", - "RETRYING": "正在重試", + "RETRYING": "重試中", "STOPPED": "已停止", "FINISHED": "已完成", "CANCELED": "已取消", - "SIMPLE": "簡單", + "SIMPLE": "簡易", "ADVANCED": "進階", "STATUS": "狀態", - "REPLICATION_TRIGGER": "觸發", - "CREATION_TIME": "建立時間", + "REPLICATION_TRIGGER": "觸發器", + "CREATION_TIME": "開始時間", "UPDATE_TIME": "更新時間", "END_TIME": "結束時間", "LOGS": "日誌", - "OF": "筆紀錄", - "ITEMS": "筆紀錄", + "OF": "中的", + "ITEMS": "個項目", "NO_LOGS": "沒有日誌", "TOGGLE_ENABLE_TITLE": "啟用規則", "TOGGLE_DISABLE_TITLE": "停用規則", - "CREATED_SUCCESS": "成功建立複製規則。", - "UPDATED_SUCCESS": "成功更新複製規則。", - "DELETED_SUCCESS": "成功刪除複製規則。", - "DELETED_FAILED": "刪除複製規則失敗。", - "TOGGLED_SUCCESS": "成功切換複製規則狀態。", - "CANNOT_EDIT": "當複製規則啟用時,無法修改。", - "INVALID_DATE": "無效的日期。", - "PLACEHOLDER": "我們找不到任何複製規則!", - "JOB_PLACEHOLDER": "我們找不到任何複製工作!", - "NO_ENDPOINT_INFO": "請先新增端點", - "NO_LABEL_INFO": "請先新增標籤", - "NO_PROJECT_INFO": "此專案不存在", + "CREATED_SUCCESS": "複製規則建立成功。", + "UPDATED_SUCCESS": "複製規則更新成功。", + "DELETED_SUCCESS": "複製規則刪除成功。", + "DELETED_FAILED": "複製規則刪除失敗。", + "TOGGLED_SUCCESS": "複製規則狀態切換成功。", + "CANNOT_EDIT": "複製規則在啟用狀態下無法變更。", + "INVALID_DATE": "無效日期。", + "PLACEHOLDER": "找不到任何複製規則!", + "JOB_PLACEHOLDER": "找不到任何複製工作!", + "NO_ENDPOINT_INFO": "請先新增一個端點。", + "NO_LABEL_INFO": "請先新增一個標籤。", + "NO_PROJECT_INFO": "此專案不存在。", "SOURCE_RESOURCE_FILTER": "來源資源篩選器", "SCHEDULED": "已排程", "MANUAL": "手動", "EVENT_BASED": "事件驅動", - "DAILY": "每天", + "DAILY": "每日", "WEEKLY": "每週", "SETTING": "選項", "TRIGGER": "觸發條件", @@ -688,19 +694,19 @@ "TRIGGER_MODE": "觸發模式", "SOURCE_PROJECT": "來源專案", "REPLICATE": "複製", - "DELETE_REMOTE_IMAGES": "當本地資源被刪除時,同時刪除遠端的資源。", - "DELETE_ENABLED": "預設啟用此規則", + "DELETE_REMOTE_IMAGES": "當本機資源被刪除時,同時刪除遠端資源。", + "DELETE_ENABLED": "啟用此原則", "NEW": "新增", - "NAME_TOOLTIP": "複製規則名稱應由小寫字元、數字和 ._- 組成,且至少有 2 個字元,並且必須以字元或數字開頭。", - "DESTINATION_NAME_TOOLTIP": "目標名稱應由小寫字元、數字和 ._- 組成,且至少有 2 個字元,並且必須以字元或數字開頭。", - "ACKNOWLEDGE": "確認", - "RULE_DISABLED": "此規則已被停用,因為其篩選器中使用的標籤已被刪除。請編輯規則並更新其篩選器以啟用它。", + "NAME_TOOLTIP": "複製規則名稱長度應至少 2 個字元,可包含小寫字母、數字及 ._-,且必須以字母或數字開頭。", + "DESTINATION_NAME_TOOLTIP": "目標名稱長度應至少 2 個字元,可包含小寫字母、數字及 ._-,且必須以字母或數字開頭。", + "ACKNOWLEDGE": "了解", + "RULE_DISABLED": "此規則已被停用,因為其篩選器中使用的標籤已被刪除。請編輯規則並更新其篩選器以重新啟用。", "REPLI_MODE": "複製模式", "SOURCE_REGISTRY": "來源 Registry", "SOURCE_NAMESPACES": "來源命名空間", - "DEST_REGISTRY": "目的 Registry", - "DEST_NAMESPACE": "目的命名空間", - "NAMESPACE_TOOLTIP": "命名空間名稱應由小寫字元、數字和 ._-/ 組成,且至少有 2 個字元,並且必須以字元或數字開頭。", + "DEST_REGISTRY": "目標 Registry", + "DEST_NAMESPACE": "目標命名空間", + "NAMESPACE_TOOLTIP": "命名空間名稱長度應至少 2 個字元,可包含小寫字母、數字及 ._-/,且必須以字母或數字開頭。", "TAG": "標籤", "LABEL": "標籤", "RESOURCE": "資源", @@ -708,33 +714,33 @@ "ENABLE_SUMMARY": "您確定要啟用規則 {{param}} 嗎?", "DISABLE_TITLE": "停用規則", "DISABLE_SUMMARY": "您確定要停用規則 {{param}} 嗎?", - "ENABLE_SUCCESS": "成功啟用規則", - "ENABLE_FAILED": "啟用規則失敗", - "DISABLE_SUCCESS": "成功停用規則", - "DISABLE_FAILED": "停用規則失敗", - "DES_REPO_FLATTENING": "目標儲存庫平整化", + "ENABLE_SUCCESS": "規則啟用成功。", + "ENABLE_FAILED": "規則啟用失敗。", + "DISABLE_SUCCESS": "規則停用成功。", + "DISABLE_FAILED": "規則停用失敗。", + "DES_REPO_FLATTENING": "目標儲存庫扁平化", "NAMESPACE": "命名空間", - "REPO_FLATTENING": "平整化", - "NO_FLATTING": "無平整化", - "FLATTEN_LEVEL_1": "平整化 1 等級", - "FLATTEN_LEVEL_2": "平整化 2 等級", - "FLATTEN_LEVEL_3": "平整化 3 等級", - "FLATTEN_ALL": "平整所有等級", - "FLATTEN_LEVEL_TIP": "在複製映像檔時降低巢狀的儲存庫結構。假設巢狀的儲存庫結構為 'a/b/c/d/img' 並且目標命名空間為 'ns',每個項目的對應結果如下:", - "FLATTEN_LEVEL_TIP_ALL": "'平整所有等級'(在 v2.3 之前使用):'a/b/c/d/img' -> 'ns/img'", - "FLATTEN_LEVEL_TIP_NO": "'無平整化': 'a/b/c/d/img' -> 'ns/a/b/c/d/img", - "FLATTEN_LEVEL_TIP_1": "'平整化 1 等級'(預設):'a/b/c/d/img' -> 'ns/b/c/d/img'", - "FLATTEN_LEVEL_TIP_2": "'平整化 2 等級': 'a/b/c/d/img' -> 'ns/c/d/img'", - "FLATTEN_LEVEL_TIP_3": "'平整化 3 等級': 'a/b/c/d/img' -> 'ns/d/img'", + "REPO_FLATTENING": "扁平化", + "NO_FLATTING": "不扁平化", + "FLATTEN_LEVEL_1": "扁平化 1 層", + "FLATTEN_LEVEL_2": "扁平化 2 層", + "FLATTEN_LEVEL_3": "扁平化 3 層", + "FLATTEN_ALL": "扁平化所有層", + "FLATTEN_LEVEL_TIP": "複製映像檔時減少巢狀儲存庫的結構。假設巢狀儲存庫結構為 'a/b/c/d/img',目標命名空間為 'ns',則各選項對應的結果如下:", + "FLATTEN_LEVEL_TIP_ALL": "「扁平化所有層」(v2.3 之前使用):'a/b/c/d/img' -> 'ns/img'", + "FLATTEN_LEVEL_TIP_NO": "「不扁平化」:'a/b/c/d/img' -> 'ns/a/b/c/d/img'", + "FLATTEN_LEVEL_TIP_1": "「扁平化 1 層」(預設):'a/b/c/d/img' -> 'ns/b/c/d/img'", + "FLATTEN_LEVEL_TIP_2": "「扁平化 2 層」:'a/b/c/d/img' -> 'ns/c/d/img'", + "FLATTEN_LEVEL_TIP_3": "「扁平化 3 層」:'a/b/c/d/img' -> 'ns/d/img'", "BANDWIDTH": "頻寬", - "BANDWIDTH_ERROR_TIP": "請輸入 -1 或大於 0 的整數", - "BANDWIDTH_TOOLTIP": "Set the maximum network bandwidth for each replication worker. Please pay attention to the number of concurrent executions (max. {{max_job_workers}}). For unlimited bandwidth, please enter -1.", + "BANDWIDTH_ERROR_TIP": "請輸入 -1 或大於 0 的整數。", + "BANDWIDTH_TOOLTIP": "為每個複製 Worker 設定最大網路頻寬。請注意平行執行的數量 (最多 {{max_job_workers}} 個)。若要設定無限頻寬,請輸入 -1。", "UNLIMITED": "無限制", - "UNREACHABLE_SOURCE_REGISTRY": "無法連線到來源 Registry,請在編輯此規則之前確保來源 Registry 可用: {{error}}", - "CRON_ERROR_TIP": "cron 字串的第一個欄位必須是 0,第二個欄位不能是 \"*\"", + "UNREACHABLE_SOURCE_REGISTRY": "無法連線至來源 Registry,請在編輯此規則前確認來源 Registry 可用:{{error}}", + "CRON_ERROR_TIP": "cron 字串的第一個欄位必須為 0,且第二個欄位不能為「*」。", "COPY_BY_CHUNK": "分段複製", - "COPY_BY_CHUNK_TIP": "指定是否按分段複製 blob。分段傳輸可能會增加 API 請求的數量。", - "TRIGGER_STOP_SUCCESS": "成功觸發停止執行", + "COPY_BY_CHUNK_TIP": "指定是否要分段複製 blob。分段傳輸可能會增加 API 請求的數量。", + "TRIGGER_STOP_SUCCESS": "已成功觸發停止執行。", "CRON_STR": "Cron 字串" }, "DESTINATION": { @@ -742,62 +748,62 @@ "PROVIDER": "提供者", "ENDPOINT": "端點", "NAME": "端點名稱", - "NAME_IS_REQUIRED": "端點名稱必填。", + "NAME_IS_REQUIRED": "必須輸入端點名稱。", "URL": "端點 URL", - "URL_IS_REQUIRED": "端點 URL 必填。", + "URL_IS_REQUIRED": "必須輸入端點 URL。", "AUTHENTICATION": "認證", "ACCESS_ID": "存取 ID", - "ACCESS_SECRET": "存取密碼", + "ACCESS_SECRET": "存取金鑰", "STATUS": "狀態", "TEST_CONNECTION": "測試連線", "TITLE_EDIT": "編輯端點", - "TITLE_ADD": "新增端點", + "TITLE_ADD": "新增 Registry 端點", "EDIT": "編輯", "DELETE": "刪除", "TESTING_CONNECTION": "正在測試連線...", "TEST_CONNECTION_SUCCESS": "連線測試成功。", - "TEST_CONNECTION_FAILURE": "連線測試失敗。", + "TEST_CONNECTION_FAILURE": "Ping 端點失敗。", "CONFLICT_NAME": "端點名稱已存在。", "INVALID_NAME": "無效的端點名稱。", "FAILED_TO_GET_TARGET": "取得端點失敗。", "CREATION_TIME": "建立時間", - "OF": "共計", - "ITEMS": "筆紀錄", - "CREATED_SUCCESS": "成功建立端點。", - "UPDATED_SUCCESS": "成功更新端點。", - "DELETED_SUCCESS": "成功刪除端點。", - "DELETED_FAILED": "刪除端點失敗。", - "CANNOT_EDIT": "當複製規則啟用時,端點無法修改。", + "OF": "中的", + "ITEMS": "個項目", + "CREATED_SUCCESS": "端點建立成功。", + "UPDATED_SUCCESS": "端點更新成功。", + "DELETED_SUCCESS": "端點刪除成功。", + "DELETED_FAILED": "端點刪除失敗。", + "CANNOT_EDIT": "當複製規則啟用時,無法變更端點。", "FAILED_TO_DELETE_TARGET_IN_USED": "無法刪除正在使用的端點。", - "PLACEHOLDER": "未找到任何端點!" + "PLACEHOLDER": "找不到任何端點!" }, "REPOSITORY": { "COPY_DIGEST_ID": "複製 Digest", "DELETE": "刪除", "NAME": "名稱", "TAGS": "標籤", - "PLATFORM": "OS / ARCH", - "ARTIFACT_TOOTIP": "點選此圖示進入引用的 Artifact 列表", - "ARTIFACTS_COUNT": "Artifact 數量", - "PULL_COUNT": "下載數", - "COPY_SUCCESS": "{{ param }} copied to Clipboard", + "PLATFORM": "作業系統/架構", + "ARTIFACT_TOOTIP": "點選以檢視此 OCI 索引的 Artifact 清單", + "ARTIFACTS_COUNT": "Artifacts", + "PULL_COUNT": "拉取次數", + "COPY_SUCCESS": "{{ param }} 已複製到剪貼簿", "PULL_TIME": "拉取時間", "PUSH_TIME": "推送時間", - "IMMUTABLE": "不可變的", + "IMMUTABLE": "不可變", "MY_REPOSITORY": "我的儲存庫", - "PUBLIC_REPOSITORY": "公共儲存庫", - "DELETION_TITLE_REPO": "刪除映像檔儲存庫確認", - "DELETION_TITLE_REPO_SIGNED": "儲存庫不能被刪除", - "DELETION_SUMMARY_REPO_SIGNED": "映像檔儲存庫'{{repoName}}' 不能被刪除,因為存在以下簽署映像檔.\n{{signedImages}} \n在刪除映像檔儲存庫前需先刪除所有的簽署映像檔", - "DELETION_SUMMARY_REPO": "確認刪除映像檔儲存庫{{repoName}}?", - "DELETION_TITLE_ARTIFACT": "刪除映像檔 Artifact 確認", - "DELETION_SUMMARY_ARTIFACT": "確認刪除映像檔 Artifact {{param}} ? 如果您刪除此 Artifact,則這個 Digest 的所有標籤也將被刪除。", - "DELETION_TITLE_TAG": "刪除 Tag 確認", - "DELETION_SUMMARY_TAG": "確認刪除Tag {{param}} ?", - "DELETION_TITLE_TAG_DENIED": "已簽署的映像檔不能被刪除", - "DELETION_SUMMARY_TAG_DENIED": "要刪除此映像檔 Tag 必須首先從 Notary 中刪除。\n請執行如下Notary 命令刪除:\n", - "TAGS_NO_DELETE": "在唯獨模式下刪除是被禁止的", - "FILTER_FOR_REPOSITORIES": "篩選映像檔儲存庫", + "PUBLIC_REPOSITORY": "公開儲存庫", + "DELETION_TITLE_REPO": "確認刪除儲存庫", + "DELETION_TITLE_REPO_SIGNED": "無法刪除儲存庫", + "DELETION_SUMMARY_REPO_SIGNED": "儲存庫 '{{repoName}}' 無法刪除,因為存在以下已簽署的映像檔。\n{{signedImages}} \n您必須在刪除儲存庫前移除所有已簽署映像檔的簽署!", + "DELETION_SUMMARY_REPO": "您確定要刪除儲存庫 {{repoName}} 嗎?", + "DELETION_TITLE_ARTIFACT": "確認刪除 Artifact", + "DELETION_SUMMARY_ARTIFACT": "您確定要刪除 Artifact {{param}} 嗎?若刪除此 Artifact,所有參考此 digest 的標籤也將被刪除。", + "DELETION_TITLE_TAG": "確認刪除標籤", + "DELETION_SUMMARY_TAG": "您確定要刪除標籤 {{param}} 嗎?", + "DELETION_TITLE_TAG_DENIED": "無法刪除已簽署的標籤", + "DELETION_SUMMARY_TAG_DENIED": "必須先從 Notary 移除此標籤,才能將其刪除。\n請使用此指令從 Notary 刪除:\n", + "TAGS_NO_DELETE": "在唯讀模式下禁止刪除。", + "FILTER_FOR_REPOSITORIES": "篩選儲存庫", "TAG": "標籤", "ARTIFACT": "Artifact", "ARTIFACTS": "Artifacts", @@ -812,32 +818,32 @@ "ARCHITECTURE": "架構", "OS": "作業系統", "SHOW_DETAILS": "顯示詳細資訊", - "REPOSITORIES": "映像檔儲存庫", - "OF": "共計", - "ITEMS": "筆紀錄", - "NO_ITEMS": "沒有記錄", - "POP_REPOS": "受歡迎的映像檔儲存庫", - "DELETED_REPO_SUCCESS": "成功刪除映像檔儲存庫。", - "DELETED_TAG_SUCCESS": "成功刪除映像檔 Tag。", + "REPOSITORIES": "儲存庫", + "OF": "中的", + "ITEMS": "個項目", + "NO_ITEMS": "沒有項目", + "POP_REPOS": "熱門儲存庫", + "DELETED_REPO_SUCCESS": "儲存庫刪除成功。", + "DELETED_TAG_SUCCESS": "標籤刪除成功。", "COPY": "複製", - "NOTARY_IS_UNDETERMINED": "無法確定映像檔 Tag 簽名。", - "PLACEHOLDER": "未發現任何映像檔庫!", - "INFO": "描述資訊", - "NO_INFO": "此映像檔儲存庫沒有描述資訊", + "NOTARY_IS_UNDETERMINED": "無法判斷此標籤的簽署狀態。", + "PLACEHOLDER": "找不到任何儲存庫!", + "INFO": "資訊", + "NO_INFO": "此儲存庫沒有描述。您可以為其新增描述。", "IMAGE": "映像檔", "LABELS": "標籤", - "ADD_LABEL_TO_IMAGE": "新增標籤到此映像檔", + "ADD_LABEL_TO_IMAGE": "為此映像檔新增標籤", + "FILTER_BY_LABEL": "依標籤篩選映像檔", + "FILTER_ARTIFACT_BY_LABEL": "依標籤篩選 Artifact", "ADD_LABELS": "新增標籤", - "STOP": "Stop", + "STOP": "停止", "RETAG": "複製", - "FILTER_BY_LABEL": "篩選標籤", - "FILTER_ARTIFACT_BY_LABEL": "透過標籤篩選 Artifact", "ACTION": "操作", "DEPLOY": "部署", - "ADDITIONAL_INFO": "新增資訊", - "REPO_NAME": "映像檔儲存庫", - "MARKDOWN": "支援使用 Markdown 進行樣式設定", - "LAST_MODIFIED": "最後修改時間" + "ADDITIONAL_INFO": "新增額外資訊", + "REPO_NAME": "儲存庫", + "MARKDOWN": "支援使用 Markdown 設定樣式。", + "LAST_MODIFIED": "上次修改時間" }, "SUMMARY": { "QUOTAS": "配額", @@ -847,20 +853,20 @@ "ARTIFACT_COUNT": "Artifact 數量", "STORAGE_CONSUMPTION": "儲存空間使用量", "ADMIN": "管理員", - "MAINTAINER": "維護人員", + "MAINTAINER": "維護者", "DEVELOPER": "開發者", "GUEST": "訪客", - "LIMITED_GUEST": "受限制的訪客", + "LIMITED_GUEST": "受限訪客", "SEE_ALL": "檢視全部" }, "ALERT": { - "FORM_CHANGE_CONFIRMATION": "部分變更尚未儲存。確認是否取消?" + "FORM_CHANGE_CONFIRMATION": "部分變更尚未儲存。您確定要取消嗎?" }, "RESET_PWD": { "TITLE": "重設密碼", "CAPTION": "輸入您的電子郵件以重設密碼", "EMAIL": "電子郵件", - "SUCCESS": "包含重設密碼連結的郵件已成功寄出。您可以關閉此對話框並檢查您的收件匣。", + "SUCCESS": "包含密碼重設連結的郵件已成功寄出。您可以關閉此對話方塊並檢查您的信箱。", "CAPTION2": "輸入您的新密碼", "RESET_OK": "密碼已成功重設。點選「確定」以使用新密碼登入。" }, @@ -870,7 +876,8 @@ }, "CONFIG": { "SECURITY": "安全性", - "TITLE": "設定", + "HISTORY": "歷史記錄", + "TITLE": "組態設定", "AUTH": "認證", "REPLICATION": "複製", "LABEL": "標籤", @@ -882,81 +889,83 @@ "VULNERABILITY": "弱點", "GC": "垃圾回收", "CONFIRM_TITLE": "確認取消", - "CONFIRM_SUMMARY": "有些變更尚未儲存,您確定要捨棄它們嗎?", - "SAVE_SUCCESS": "設定已成功儲存。", + "CONFIRM_SUMMARY": "有些變更尚未儲存。您確定要捨棄嗎?", + "SAVE_SUCCESS": "組態設定已成功儲存。", "VERIFY_REMOTE_CERT": "驗證遠端憑證", - "TOKEN_EXPIRATION": "權杖到期時間(分鐘)", - "SESSION_TIMEOUT": "工作階段逾時(分鐘)", - "SESSION_TIMEOUT_INFO": "設定 Harbor UI 的工作階段逾時時間。預設值為 60 分鐘。", + "TOKEN_EXPIRATION": "權杖到期時間 (分鐘)", + "SESSION_TIMEOUT": "工作階段逾時 (分鐘)", + "SESSION_TIMEOUT_INFO": "設定 Harbor UI 的工作階段逾時時間。預設為 60 分鐘。", "AUTH_MODE": "認證模式", "PRIMARY_AUTH_MODE": "主要認證模式", - "PRO_CREATION_RESTRICTION": "專案建立限制", - "SELF_REGISTRATION": "允許自助註冊", + "PRO_CREATION_RESTRICTION": "專案建立", + "SELF_REGISTRATION": "允許自行註冊", "AUTH_MODE_DB": "資料庫", "AUTH_MODE_LDAP": "LDAP", "AUTH_MODE_UAA": "UAA", - "AUTH_MODE_HTTP": "Http 認證", + "AUTH_MODE_HTTP": "HTTP 認證", "AUTH_MODE_OIDC": "OIDC", - "SCOPE_BASE": "基礎", - "SCOPE_ONE_LEVEL": "一層", - "SCOPE_SUBTREE": "子樹", - "PRO_CREATION_EVERYONE": "所有人", + "SCOPE_BASE": "Base", + "SCOPE_ONE_LEVEL": "OneLevel", + "SCOPE_SUBTREE": "Subtree", + "PRO_CREATION_EVERYONE": "任何人", "PRO_CREATION_ADMIN": "僅限管理員", "ROOT_CERT": "Registry 根憑證", "ROOT_CERT_LINK": "下載", "REGISTRY_CERTIFICATE": "Registry 憑證", - "NO_CHANGE": "因為沒有變更,所以儲存已中止", - "SKIP_SCANNER_PULL_TIME": "在掃描時保留映像檔「最後拉取時間」", + "NO_CHANGE": "因無任何變更而中止儲存。", + "SKIP_SCANNER_PULL_TIME": "掃描時保留映像檔的「上次拉取時間」", "TOOLTIP": { "SELF_REGISTRATION_ENABLE": "啟用註冊。", "SELF_REGISTRATION_DISABLE": "停用註冊。", - "VERIFY_REMOTE_CERT": "確定映像檔複製是否需要驗證遠端 Harbor Registry的憑證。當遠端 Registry 使用自簽或不受信任的憑證時,請取消勾選此框。", - "AUTH_MODE": "預設的認證模式是資料庫,即憑證儲存在本地資料庫中。如果您希望對使用者的憑證進行 LDAP 伺服器驗證,請將其設定為 LDAP。", - "PRIMARY_AUTH_MODE": "此認證模式將成為使用者登入的預設方式。當使用者選擇透過身份提供者或本地資料庫登入的登入畫面時,將自動將使用者重新導向到此身份提供者。當訪問 URL '/account/sign-in' 時,可以透過資料庫登入。", - "LDAP_SEARCH_DN": "具有搜尋 LDAP/AD 伺服器權限的使用者的 DN。如果您的 LDAP/AD 伺服器不支援匿名搜尋,則應設定此 DN 和 ldap_search_pwd。", - "LDAP_BASE_DN": "在 LDAP/AD 中查詢使用者的基礎 DN。", - "LDAP_UID": "在搜尋中用來選取使用者的屬性。它可以是 uid、cn、email、sAMAccountName 或其他相依於您的 LDAP/AD 的屬性。", + "VERIFY_REMOTE_CERT": "決定映像檔複製是否應驗證遠端 Harbor Registry 的憑證。當遠端 Registry 使用自簽章或不受信任的憑證時,請取消勾選此核取方塊。", + "AUTH_MODE": "預設認證模式為資料庫,即憑證儲存在本機資料庫中。若要根據 LDAP 伺服器驗證使用者憑證,請設為 LDAP。", + "PRIMARY_AUTH_MODE": "此認證模式將成為使用者的預設登入方式。使用者在登入畫面選擇透過身分提供者或本機資料庫登入時,將自動重新導向至此身分提供者。若要透過資料庫登入,需明確存取 URL '/account/sign-in'。", + "LDAP_SEARCH_DN": "具有搜尋 LDAP/AD 伺服器權限的使用者 DN。如果您的 LDAP/AD 伺服器不支援匿名搜尋,您應設定此 DN 及 ldap_search_pwd。", + "LDAP_BASE_DN": "在 LDAP/AD 中查詢使用者的起始 DN。", + "LDAP_UID": "在搜尋中用來比對使用者的屬性。可以是 uid、cn、email、sAMAccountName 或其他屬性,取決於您的 LDAP/AD 設定。", "LDAP_SCOPE": "搜尋使用者的範圍。", - "TOKEN_EXPIRATION": "由權杖服務建立的權杖的到期時間(以分鐘為單位)。預設值為 30 分鐘。", - "ROBOT_NAME_PREFIX": "每個機器人名稱的前綴字串,預設值為 'robot$'", - "ROBOT_TOKEN_EXPIRATION": "機器人帳號的權杖的到期時間(以天為單位),預設值為 30 天。顯示從分鐘轉換成的天數並向下取整數。", - "PRO_CREATION_RESTRICTION": "用於定義哪些使用者有權限建立專案。預設情況下,所有人都可以建立專案。設定為 '僅限管理員',則只有管理員可以建立專案。", + "TOKEN_EXPIRATION": "權杖服務所建立權杖的到期時間 (分鐘)。預設為 30 分鐘。", + "ROBOT_NAME_PREFIX": "每個機器人名稱的前置字串,預設值為 'robot$'", + "ROBOT_TOKEN_EXPIRATION": "機器人帳號權杖的到期時間 (天),預設為 30 天。顯示由分鐘換算而來的天數,並無條件捨去。", + "PRO_CREATION_RESTRICTION": "此旗標定義哪些使用者有權限建立專案。預設情況下,任何人都可以建立專案。設為「僅限管理員」後,只有管理員可以建立專案。", "ROOT_CERT_DOWNLOAD": "下載 Registry 的根憑證。", - "SCANNING_POLICY": "根據不同的需求設定映像檔掃描原則。'無':沒有活動原則;'每日定時':每天在指定時間觸發掃描。", + "SCANNING_POLICY": "根據不同需求設定映像檔掃描原則。「無」:無作用中原則;「每日定時」:每日在指定時間觸發掃描。", "VERIFY_CERT": "驗證來自 LDAP 伺服器的憑證", "REPO_TOOLTIP": "在此模式下,使用者無法對映像檔進行任何操作。", - "WEBHOOK_TOOLTIP": "當執行某些操作(如推送、拉取、刪除、掃描映像檔或 Helm Chart)時,啟用 webhooks 以在指定的端點接收回撥", - "HOURLY_CRON": "每小時開始時執行一次。相當於 0 0 * * * *。", - "WEEKLY_CRON": "每週執行一次,週六/週日凌晨之間。相當於 0 0 0 * * 0。", - "DAILY_CRON": "每天凌晨執行一次。相當於 0 0 0 * * *。", - "SKIP_SCANNER_PULL_TIME_TOOLTIP": "弱點掃描器(例如 Trivy)在掃描映像檔時不會更新映像檔的「最後拉取時間」。" + "WEBHOOK_TOOLTIP": "當執行特定操作 (如推送、拉取、刪除、掃描映像檔或 Chart) 時,啟用 Webhook 以在您指定的端點接收回呼。", + "HOURLY_CRON": "每小時執行一次,於整點開始。相當於 0 0 * * * *。", + "WEEKLY_CRON": "每週執行一次,於週六/週日午夜。相當於 0 0 0 * * 0。", + "DAILY_CRON": "每日執行一次,於午夜。相當於 0 0 0 * * *。", + "SKIP_SCANNER_PULL_TIME_TOOLTIP": "當映像檔被掃描時,弱點掃描器 (例如 Trivy) 不會更新映像檔的「上次拉取時間」。" }, "LDAP": { - "URL": "LDAP 網址", + "URL": "LDAP URL", "SEARCH_DN": "LDAP 搜尋 DN", "SEARCH_PWD": "LDAP 搜尋密碼", "BASE_DN": "LDAP 基礎 DN", "FILTER": "LDAP 篩選器", - "UID": "LDAP 使用者 UID", - "SCOPE": "LDAP 搜尋範圍", - "VERIFY_CERT": "驗證 LDAP 憑證", + "UID": "LDAP UID", + "SCOPE": "LDAP 範圍", + "VERIFY_CERT": "LDAP 驗證憑證", "LDAP_GROUP_BASE_DN": "LDAP 群組基礎 DN", - "LDAP_GROUP_BASE_DN_INFO": "在 LDAP/AD 中查詢群組的基礎 DN。如果您需要啟用 LDAP 群組相關功能,此欄位不能為空。", + "LDAP_GROUP_BASE_DN_INFO": "在 LDAP/AD 中查詢群組的起始 DN。若要啟用 LDAP 群組相關功能,此欄位不得為空。", "LDAP_GROUP_FILTER": "LDAP 群組篩選器", - "LDAP_GROUP_FILTER_INFO": "用於搜尋 LDAP/AD 群組的篩選器。對於 OpenLDAP:objectclass=groupOfNames。對於 Active Directory:objectclass=group。如果您需要 LDAP 群組相關功能,此欄位不能為空。", + "LDAP_GROUP_FILTER_INFO": "搜尋 LDAP/AD 群組的篩選器。OpenLDAP: objectclass=groupOfNames。Active Directory: objectclass=group。若要啟用 LDAP 群組相關功能,此欄位不得為空。", "LDAP_GROUP_GID": "LDAP 群組 GID", - "LDAP_GROUP_GID_INFO": "在搜尋中用來選取使用者的屬性,可以是 uid、cn 或其他屬性,取決於您的 LDAP/AD。Harbor 中的群組預設以此屬性命名。如果您需要啟用 LDAP 群組相關功能,此欄位不能為空。", + "LDAP_GROUP_GID_INFO": "在搜尋中用來比對群組的屬性,可以是 cn 或其他屬性,取決於您的 LDAP/AD。Harbor 中的群組預設以此屬性命名。若要啟用 LDAP 群組相關功能,此欄位不得為空。", "LDAP_GROUP_ADMIN_DN": "LDAP 群組管理員 DN", - "LDAP_GROUP_ADMIN_DN_INFO": "指定 LDAP 群組 DN。此群組中的所有 LDAP 使用者都將具有 Harbor 管理員權限。如果您不需要,此欄可留空。", + "LDAP_GROUP_ADMIN_DN_INFO": "指定一個 LDAP 群組 DN。此群組中的所有 LDAP 使用者都將擁有 Harbor 管理員權限。若不需要,可留空。", "LDAP_GROUP_MEMBERSHIP": "LDAP 群組成員資格", - "LDAP_GROUP_MEMBERSHIP_INFO": "表示 LDAP 群組成員資格的屬性,預設值為 memberof,在某些 LDAP 伺服器中可能為 \"ismemberof\"。如果您需要啟用 LDAP 群組相關功能,此欄位不能為空。", + "LDAP_GROUP_MEMBERSHIP_INFO": "指出 LDAP 群組成員資格的屬性,預設值為 memberof,在某些 LDAP 伺服器中可能為「ismemberof」。若要啟用 LDAP 群組相關功能,此欄位不得為空。", "GROUP_SCOPE": "LDAP 群組搜尋範圍", - "GROUP_SCOPE_INFO": "搜尋群組的範圍,預設選擇子樹。" + "GROUP_SCOPE_INFO": "搜尋群組的範圍,預設選取「子樹」。", + "GROUP_ATTACH_PARALLEL": "平行附加 LDAP 群組", + "GROUP_ATTACH_PARALLEL_INFO": "啟用此選項以平行方式附加群組,避免因群組過多而導致逾時。若停用,LDAP 群組資訊將循序附加。" }, "UAA": { "ENDPOINT": "UAA 端點", "CLIENT_ID": "UAA 用戶端 ID", - "CLIENT_SECRET": "UAA 用戶端密鑰", + "CLIENT_SECRET": "UAA 用戶端金鑰", "VERIFY_CERT": "驗證 UAA 憑證" }, "HTTP_AUTH": { @@ -968,39 +977,39 @@ }, "OIDC": { "OIDC_PROVIDER": "OIDC 提供者名稱", - "OIDC_REDIREC_URL": "請確保在 OIDC 提供者中設定的重新導向 URL 為", + "OIDC_REDIREC_URL": "請確認 OIDC 提供者上的重新導向 URI 已設為", "ENDPOINT": "OIDC 端點", "CLIENT_ID": "OIDC 用戶端 ID", - "CLIENTSECRET": "OIDC 用戶端密鑰", + "CLIENTSECRET": "OIDC 用戶端金鑰", "SCOPE": "OIDC 範圍", "OIDC_VERIFYCERT": "驗證憑證", - "OIDC_AUTOONBOARD": "自動登入", - "USER_CLAIM": "使用者聲明", + "OIDC_AUTOONBOARD": "自動初次設定", + "USER_CLAIM": "使用者名稱宣告", "OIDC_SETNAME": "設定 OIDC 使用者名稱", - "OIDC_SETNAMECONTENT": "在第一次透過第三方(OIDC)進行身份驗證時,您必須建立一個 Harbor 使用者名稱。這將在 Harbor 中用於與專案、角色等關聯。", + "OIDC_SETNAMECONTENT": "首次透過第三方 (OIDC) 進行認證時,您必須建立一個 Harbor 使用者名稱。此名稱將在 Harbor 內部用於關聯專案、角色等。", "OIDC_USERNAME": "使用者名稱", - "GROUP_CLAIM_NAME": "群組聲明名稱", - "GROUP_CLAIM_NAME_INFO": "您在 OIDC 提供者中設定的自定義群組聲明名稱", + "GROUP_CLAIM_NAME": "群組宣告名稱", + "GROUP_CLAIM_NAME_INFO": "您在 OIDC 提供者中設定的自訂群組宣告名稱。", "OIDC_ADMIN_GROUP": "OIDC 管理員群組", - "OIDC_ADMIN_GROUP_INFO": "指定 OIDC 管理員群組名稱。此群組中的所有 OIDC 使用者都將具有 Harbor 管理員權限。如果您不需要,此欄可以留空。", + "OIDC_ADMIN_GROUP_INFO": "指定一個 OIDC 管理員群組名稱。此群組中的所有 OIDC 使用者都將擁有 Harbor 管理員權限。若不需要,可留空。", "OIDC_GROUP_FILTER": "OIDC 群組篩選器", - "OIDC_GROUP_FILTER_INFO": "篩選符合提供的正規表達式的 OIDC 群組。保持空白以選取所有群組。", - "OIDC_LOGOUT": "OIDC Session Logout" + "OIDC_GROUP_FILTER_INFO": "篩選符合所提供正規表示式的 OIDC 群組。留空以符合所有群組。", + "OIDC_LOGOUT": "OIDC 工作階段登出" }, "SCANNING": { - "STOP_SCAN_ALL_SUCCESS": "成功觸發停止全部掃描!", - "TRIGGER_SCAN_ALL_SUCCESS": "成功觸發全部掃描!", - "TRIGGER_SCAN_ALL_FAIL": "觸發全部掃描失敗,錯誤: {{error}}", + "STOP_SCAN_ALL_SUCCESS": "已成功觸發停止全部掃描!", + "TRIGGER_SCAN_ALL_SUCCESS": "已成功觸發全部掃描!", + "TRIGGER_SCAN_ALL_FAIL": "觸發全部掃描失敗,錯誤:{{error}}", "TITLE": "弱點掃描", - "SCAN_ALL": "掃描全部", - "SCHEDULE_TO_SCAN_ALL": "排程掃描全部", + "SCAN_ALL": "全部掃描", + "SCHEDULE_TO_SCAN_ALL": "排程全部掃描", "SCAN_NOW": "立即掃描", "SCAN": "掃描", "NONE_POLICY": "無", "DAILY_POLICY": "每日定時", - "REFRESH_POLICY": "資料庫更新後", + "REFRESH_POLICY": "重新整理時", "DB_REFRESH_TIME": "資料庫更新於", - "DB_NOT_READY": "弱點資料庫可能尚未完全準備好!", + "DB_NOT_READY": "弱點資料庫可能尚未完全就緒!", "NEXT_SCAN": "下次可用時間", "STATUS": { "PENDING": "等待中", @@ -1010,36 +1019,36 @@ "SUCCESS": "成功", "SCHEDULED": "已排程" }, - "MANUAL": "手動觸發", - "SCHEDULED": "定時觸發" + "MANUAL": "手動", + "SCHEDULED": "已排程" }, - "TEST_MAIL_SUCCESS": "已驗證郵件伺服器的連線。", - "TEST_LDAP_SUCCESS": "已驗證 LDAP 伺服器的連線。", - "TEST_MAIL_FAILED": "驗證郵件伺服器失敗,錯誤: {{param}}。", - "TEST_LDAP_FAILED": "驗證 LDAP 伺服器失敗,錯誤: {{param}}。", + "TEST_MAIL_SUCCESS": "郵件伺服器連線已驗證。", + "TEST_LDAP_SUCCESS": "LDAP 伺服器連線已驗證。", + "TEST_MAIL_FAILED": "驗證郵件伺服器失敗,錯誤:{{param}}。", + "TEST_LDAP_FAILED": "驗證 LDAP 伺服器失敗,錯誤:{{param}}。", "LEAVING_CONFIRMATION_TITLE": "確認離開", - "LEAVING_CONFIRMATION_SUMMARY": "有未儲存的變更,確定要離開目前頁面嗎?", - "TEST_OIDC_SUCCESS": "已驗證 OIDC 伺服器的連線。" + "LEAVING_CONFIRMATION_SUMMARY": "變更尚未儲存。您確定要離開目前頁面嗎?", + "TEST_OIDC_SUCCESS": "OIDC 伺服器連線已驗證。" }, "PAGE_NOT_FOUND": { - "MAIN_TITLE": "頁面不存在", - "SUB_TITLE": "正在重新導向到首頁:", - "UNIT": "秒..." + "MAIN_TITLE": "找不到頁面", + "SUB_TITLE": "將在", + "UNIT": "秒後重新導向至主頁..." }, "ABOUT": { "VERSION": "版本", - "BUILD": "建置", - "COPYRIGHT": "Harbor 是一個開源、受信賴的雲原生 Docker Registry 專案,用於儲存、簽署和以及掃描映像檔內容。它擴展了開源的 Docker Distribution,加入了如安全性、身份認證和用戶管理等功能。Harbor 也支援資源監控和不同實例間的同步。將 Registry 靠近部署和執行環境可以提高傳輸效率。", - "COPYRIGHT_SUFIX": "上列出的一項或多項專利保護。", - "TRADEMARK": "VMware 商標及設計都是 VMware,Inc. 在美國和/或其他法律轄區的註冊商標或者商標。此處提到的其他所有商標和名稱分別是其各自公司的商標。", - "END_USER_LICENSE": "終端使用者授權條款", - "OPEN_SOURCE_LICENSE": "開源/第三方授權條款" + "BUILD": "建置版本", + "COPYRIGHT": "Harbor 是一個開源、可信賴的雲原生 Registry 專案,用於儲存、簽署及掃描內容。它透過新增企業使用者所需的功能 (如安全性、身分識別和管理) 來擴展開源的 Docker Distribution。Harbor 支援進階功能,如使用者管理、存取控制、活動監控及執行個體間的複製。將 Registry 部署在靠近建置與執行環境的位置,也能提升映像檔傳輸效率。", + "COPYRIGHT_SUFIX": "。", + "TRADEMARK": "VMware 是 VMware, Inc. 在美國及其他地區的註冊商標或商標。本文提及的所有其他標記和名稱可能是其各自公司的商標。", + "END_USER_LICENSE": "使用者授權合約", + "OPEN_SOURCE_LICENSE": "開源/第三方授權" }, "START_PAGE": { "GETTING_START": "", - "GETTING_START_TITLE": "開始使用" + "GETTING_START_TITLE": "入門指南" }, - "TOP_REPO": "受歡迎的映像檔儲存庫", + "TOP_REPO": "熱門儲存庫", "STATISTICS": { "PRO_ITEM": "專案", "REPO_ITEM": "儲存庫", @@ -1047,8 +1056,8 @@ "INDEX_PUB": "公開", "INDEX_TOTAL": "總計", "STORAGE": "儲存空間", - "LIMIT": "限制", - "STORAGE_USED": "已使用儲存空間" + "LIMIT": "上限", + "STORAGE_USED": "已用儲存空間" }, "SEARCH": { "IN_PROGRESS": "搜尋中...", @@ -1056,39 +1065,39 @@ }, "SBOM": { "CHART": { - "SCANNING_TIME": "Scan completed time:", - "SCANNING_PERCENT": "Scan progress:", - "SCANNING_PERCENT_EXPLAIN": "The scan completion progress is calculated as # of successfully scanned images / total number of images referenced within the image index.", - "TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.", - "TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.", - "TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected" + "SCANNING_TIME": "掃描完成時間:", + "SCANNING_PERCENT": "掃描進度:", + "SCANNING_PERCENT_EXPLAIN": "掃描完成進度是根據成功掃描的映像檔數量除以映像檔索引中參照的總映像檔數量計算得出。", + "TOOLTIPS_TITLE": "{{totalPackages}} 個 {{package}} 中,有 {{totalSbom}} 個具有已知的 {{sbom}}。", + "TOOLTIPS_TITLE_SINGULAR": "{{totalPackages}} 個 {{package}} 中,有 {{totalSbom}} 個具有已知的 {{sbom}}。", + "TOOLTIPS_TITLE_ZERO": "未偵測到可辨識的 SBOM" }, "GRID": { - "PLACEHOLDER": "No scan results found.", - "COLUMN_PACKAGE": "Package", - "COLUMN_PACKAGES": "Packages", - "COLUMN_VERSION": "Current version", - "COLUMN_LICENSE": "License", - "COLUMN_DESCRIPTION": "Description", - "FOOT_ITEMS": "Items", - "FOOT_OF": "of" + "PLACEHOLDER": "找不到任何掃描結果。", + "COLUMN_PACKAGE": "套件", + "COLUMN_PACKAGES": "套件", + "COLUMN_VERSION": "目前版本", + "COLUMN_LICENSE": "授權", + "COLUMN_DESCRIPTION": "描述", + "FOOT_ITEMS": "個項目", + "FOOT_OF": "中的" }, "STATE": { - "OTHER_STATUS": "No SBOM", - "QUEUED": "Queued", - "ERROR": "View Log", - "SCANNING": "Generating", - "STOPPED": "Generation stopped" + "OTHER_STATUS": "沒有 SBOM", + "QUEUED": "已排入佇列", + "ERROR": "檢視日誌", + "SCANNING": "產生中", + "STOPPED": "已停止產生" }, - "NO_SBOM": "No SBOM", + "NO_SBOM": "沒有 SBOM", "PACKAGES": "SBOM", - "COMPLETED": "Completed", - "REPORTED_BY": "Reported by {{scanner}}", - "GENERATE": "Generate SBOM", - "DOWNLOAD": "Download SBOM", - "Details": "SBOM details", - "STOP": "Stop Generate SBOM", - "TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully" + "COMPLETED": "已完成", + "REPORTED_BY": "由 {{scanner}} 回報", + "GENERATE": "產生 SBOM", + "DOWNLOAD": "下載 SBOM", + "Details": "SBOM 詳細資訊", + "STOP": "停止產生 SBOM", + "TRIGGER_STOP_SUCCESS": "已成功觸發停止產生 SBOM。" }, "VULNERABILITY": { "STATE": { @@ -1099,98 +1108,99 @@ "STOPPED": "掃描已停止" }, "GRID": { - "PLACEHOLDER": "沒有找到任何掃描結果!", - "COLUMN_ID": "弱點識別碼", - "COLUMN_SEVERITY": "嚴重程度", + "PLACEHOLDER": "找不到任何掃描結果!", + "COLUMN_ID": "弱點", + "COLUMN_SEVERITY": "嚴重性", + "COLUMN_STATUS": "狀態", "COLUMN_PACKAGE": "套件", "COLUMN_PACKAGES": "套件", "COLUMN_VERSION": "目前版本", - "COLUMN_FIXED": "修復版本", + "COLUMN_FIXED": "已修復版本", "COLUMN_DESCRIPTION": "描述", - "FOOT_ITEMS": "項目", - "FOOT_OF": "總共", - "IN_ALLOW_LIST": "列在 CVE 白名單中", + "FOOT_ITEMS": "個項目", + "FOOT_OF": "中的", + "IN_ALLOW_LIST": "已列於 CVE 允許清單", "CVSS3": "CVSS3" }, "CHART": { "SCANNING_TIME": "掃描完成時間:", "SCANNING_PERCENT": "掃描完成百分比:", - "SCANNING_PERCENT_EXPLAIN": "掃描完成百分比是根據成功掃描的映像檔數量和映像檔索引內的總映像檔數量計算的。", - "TOOLTIPS_TITLE": "{{totalPackages}} 個{{package}}中的{{totalVulnerability}}個已知{{vulnerability}}。", - "TOOLTIPS_TITLE_SINGULAR": "{{totalPackages}} 個{{package}}中的{{totalVulnerability}}個已知{{vulnerability}}。", - "TOOLTIPS_TITLE_ZERO": "沒有發現可識別的弱點" + "SCANNING_PERCENT_EXPLAIN": "掃描完成百分比是根據成功掃描的映像檔數量除以映像檔索引中參照的總映像檔數量計算得出。", + "TOOLTIPS_TITLE": "{{totalPackages}} 個 {{package}} 中,有 {{totalVulnerability}} 個具有已知的 {{vulnerability}}。", + "TOOLTIPS_TITLE_SINGULAR": "{{totalPackages}} 個 {{package}} 中,有 {{totalVulnerability}} 個具有已知的 {{vulnerability}}。", + "TOOLTIPS_TITLE_ZERO": "未偵測到可辨識的弱點" }, "SEVERITY": { - "CRITICAL": "嚴重", + "CRITICAL": "極高", "HIGH": "高", "MEDIUM": "中", "LOW": "低", "NONE": "無" }, - "SINGULAR": "弱點", - "OVERALL_SEVERITY": "弱點嚴重度:", + "SINGULAR": "個弱點", + "OVERALL_SEVERITY": "弱點嚴重性:", "NO_VULNERABILITY": "沒有弱點", - "PLURAL": "弱點", + "PLURAL": "個弱點", "PLACEHOLDER": "篩選弱點", "PACKAGE": "套件", "PACKAGES": "套件", - "SCAN_NOW": "開始掃描", + "SCAN_NOW": "掃描弱點", "SCAN_BY": "由 {{scanner}} 掃描", - "REPORTED_BY": "由 {{scanner}} 報告", - "NO_SCANNER": "無掃描器", - "TRIGGER_STOP_SUCCESS": "成功觸發停止掃描", - "STOP_NOW": "Stop Scan Vulnerability" + "REPORTED_BY": "由 {{scanner}} 回報", + "NO_SCANNER": "沒有掃描器", + "TRIGGER_STOP_SUCCESS": "已成功觸發停止掃描。", + "STOP_NOW": "停止弱點掃描" }, "PUSH_IMAGE": { - "TITLE": "推送命令", + "TITLE": "推送指令", "DOCKER": "Docker", "PODMAN": "Podman", "HELM": "Helm", "CNAB": "CNAB", - "TAG_COMMAND_CHART": "為此專案打包 Helm Chart:", - "PUSH_COMMAND_CHART": "將 Helm Chart 推送至此專案:", - "PUSH_COMMAND_CNAB": "將 CNAB 推送至此專案:", - "TOOLTIP": "推送映像擋至此專案的參考命令。", - "TAG_COMMAND": "為此專案標記映像檔:", - "PUSH_COMMAND": "將映像檔推送至此專案:", - "COPY_ERROR": "複製失敗,請嘗試手動複製參考命令。" + "TAG_COMMAND_CHART": "為此專案打包一個 Chart:", + "PUSH_COMMAND_CHART": "將一個 Chart 推送至此專案:", + "PUSH_COMMAND_CNAB": "將一個 CNAB 推送至此專案:", + "TOOLTIP": "將 Artifact 推送至此專案的參考指令。", + "TAG_COMMAND": "為此專案標記一個映像檔:", + "PUSH_COMMAND": "將一個映像檔推送至此專案:", + "COPY_ERROR": "複製失敗,請嘗試手動複製指令。" }, "ARTIFACT": { - "FILTER_FOR_ARTIFACTS": "篩選 Artifact(s)", - "ADDITIONS": "其他", + "FILTER_FOR_ARTIFACTS": "篩選 Artifact", + "ADDITIONS": "附加物", "ANNOTATION": "註解", - "OVERVIEW": "概覽", + "OVERVIEW": "概觀", "IMAGE": "映像檔", "CHART": "CHART", "CNAB": "CNAB", "WASM": "WASM", - "TAGGED": "包含 Tag", - "UNTAGGED": "不包含 Tag", + "TAGGED": "已標記", + "UNTAGGED": "未標記", "ALL": "全部", - "PLACEHOLDER": "未發現任何 artifacts!", - "SCAN_UNSUPPORTED": "不支援掃描", - "SBOM_UNSUPPORTED": "Unsupported", + "PLACEHOLDER": "找不到任何 Artifact!", + "SCAN_UNSUPPORTED": "不支援", + "SBOM_UNSUPPORTED": "不支援", "SUMMARY": "摘要", - "LICENSE": "許可證", - "FILES": "文件", + "LICENSE": "授權", + "FILES": "檔案", "DEPENDENCIES": "相依性", "VALUES": "值", "NAME": "名稱", "REPO": "儲存庫", - "OF": "共計", + "OF": "中的", "VERSION": "版本", - "NO_README": "此 Helm Chart 未提供 README 文件", - "NO_LICENSE": "此 Model 未提供 LICENSE 文件", - "NO_FILES": "此 Model 未提供文件", - "MODEL_NO_README": "此 Model 未提供 README 文件", - "FILE_TOO_LARGE": "文件太大,無法處理", - "ITEMS": "筆紀錄", + "NO_README": "此 Chart 未提供 readme 檔案。", + "NO_LICENSE": "此模型未提供授權檔案。", + "NO_FILES": "此模型未提供檔案。", + "MODEL_NO_README": "此模型未提供 readme 檔案。", + "FILE_TOO_LARGE": "檔案過大,無法處理。", + "ITEMS": "個項目", "SHOW_KV": "顯示鍵值對", "SHOW_YAML": "顯示 YAML 檔案" }, "TAG": { "CREATION_TIME_PREFIX": "建立於", - "CREATOR_PREFIX": "建立者:", + "CREATOR_PREFIX": "由", "ANONYMITY": "匿名", "IMAGE_DETAILS": "映像檔詳細資訊", "DOCKER_VERSION": "Docker 版本", @@ -1199,10 +1209,10 @@ "OS_VERSION": "作業系統版本", "HAVE": "有", "HAS": "有", - "SCAN_COMPLETION_TIME": "掃描完成時間", + "SCAN_COMPLETION_TIME": "掃描完成於", "IMAGE_VULNERABILITIES": "映像檔弱點", "LEVEL_VULNERABILITIES": "弱點等級", - "PLACEHOLDER": "我們找不到任何標籤!", + "PLACEHOLDER": "找不到任何標籤!", "COPY_ERROR": "複製失敗,請嘗試手動複製。", "FILTER_FOR_TAGS": "篩選標籤", "AUTHOR": "作者", @@ -1213,11 +1223,11 @@ "NAME": "名稱", "PULL_TIME": "拉取時間", "PUSH_TIME": "推送時間", - "OF": "的", - "ITEMS": "項目", + "OF": "中的", + "ITEMS": "個項目", "ADD_TAG": "新增標籤", "REMOVE_TAG": "移除標籤", - "NAME_ALREADY_EXISTS": "儲存庫下已存在此標籤" + "NAME_ALREADY_EXISTS": "標籤已存在於此儲存庫下。" }, "LABEL": { "LABEL": "標籤", @@ -1229,10 +1239,10 @@ "LABEL_NAME": "標籤名稱", "COLOR": "顏色", "FILTER_LABEL_PLACEHOLDER": "篩選標籤", - "NO_LABELS": "無標籤", + "NO_LABELS": "沒有標籤", "DELETION_TITLE_TARGET": "確認刪除標籤", - "DELETION_SUMMARY_TARGET": "您是否要刪除 {{param}} ?", - "PLACEHOLDER": "未找到任何標籤!", + "DELETION_SUMMARY_TARGET": "您確定要刪除 {{param}} 嗎?", + "PLACEHOLDER": "找不到任何標籤!", "NAME_ALREADY_EXISTS": "標籤名稱已存在。" }, "QUOTA": { @@ -1242,35 +1252,35 @@ "STORAGE": "儲存空間", "EDIT": "編輯", "DELETE": "刪除", - "OF": "共計", - "PROJECT_QUOTA_DEFAULT_ARTIFACT": "每個專案的預設 artifact 數量", - "PROJECT_QUOTA_DEFAULT_DISK": "每個專案的預設儲存配額", - "EDIT_PROJECT_QUOTAS": "修改專案配額", - "EDIT_DEFAULT_PROJECT_QUOTAS": "修改專案預設配額", - "SET_QUOTAS": "為專案「{{params}}」設定配額", - "SET_DEFAULT_QUOTAS": "建立新專案時設定預設配額", - "COUNT_QUOTA": "artifact 數量", - "COUNT_DEFAULT_QUOTA": "預設 artifact 數量", - "STORAGE_QUOTA": "專案儲存配額限制", - "STORAGE_DEFAULT_QUOTA": "預設儲存空間使用量", - "SAVE_SUCCESS": "配額編輯成功", + "OF": "中的", + "PROJECT_QUOTA_DEFAULT_ARTIFACT": "每個專案的預設 Artifact 數量", + "PROJECT_QUOTA_DEFAULT_DISK": "每個專案的預設配額空間", + "EDIT_PROJECT_QUOTAS": "編輯專案配額", + "EDIT_DEFAULT_PROJECT_QUOTAS": "編輯預設專案配額", + "SET_QUOTAS": "為專案 '{{params}}' 設定配額", + "SET_DEFAULT_QUOTAS": "設定新專案的預設配額", + "COUNT_QUOTA": "Artifact 數量", + "COUNT_DEFAULT_QUOTA": "預設 Artifact 數量", + "STORAGE_QUOTA": "專案配額限制", + "STORAGE_DEFAULT_QUOTA": "預設儲存空間耗用量", + "SAVE_SUCCESS": "配額編輯成功。", "UNLIMITED": "無限制", - "INVALID_INPUT": "輸入無效", - "PLACEHOLDER": "我們找不到任何專案配額", - "FILTER_PLACEHOLDER": "根據名稱搜尋(完全符合)", - "QUOTA_USED": "已使用配額" + "INVALID_INPUT": "無效輸入", + "PLACEHOLDER": "找不到任何專案配額", + "FILTER_PLACEHOLDER": "依名稱搜尋 (完全符合)", + "QUOTA_USED": "已用配額" }, "WEEKLY": { - "MONDAY": "週一", - "TUESDAY": "週二", - "WEDNESDAY": "週三", - "THURSDAY": "週四", - "FRIDAY": "週五", - "SATURDAY": "週六", - "SUNDAY": "週日" + "MONDAY": "星期一", + "TUESDAY": "星期二", + "WEDNESDAY": "星期三", + "THURSDAY": "星期四", + "FRIDAY": "星期五", + "SATURDAY": "星期六", + "SUNDAY": "星期日" }, "OPERATION": { - "LOCAL_EVENT": "本地事件", + "LOCAL_EVENT": "本機事件", "ALL": "全部", "RUNNING": "執行中", "FAILED": "失敗", @@ -1281,11 +1291,11 @@ "DELETE_USER": "刪除使用者", "DELETE_ROBOT": "刪除機器人", "DELETE_REGISTRY": "刪除 Registry", - "DELETE_REPLICATION": "刪除複製項目", + "DELETE_REPLICATION": "刪除複製", "DELETE_MEMBER": "刪除使用者成員", "DELETE_GROUP": "刪除群組成員", - "DELETE_CHART_VERSION": "刪除 Helm Chart 版本", - "DELETE_CHART": "刪除 Helm Chart", + "DELETE_CHART_VERSION": "刪除 Chart 版本", + "DELETE_CHART": "刪除 Chart", "SWITCH_ROLE": "切換角色", "ADD_GROUP": "新增群組成員", "ADD_USER": "新增使用者成員", @@ -1294,99 +1304,101 @@ "DAY_AGO": "天前", "HOUR_AGO": "小時前", "MINUTE_AGO": "分鐘前", - "SECOND_AGO": "不到一分鐘前", - "EVENT_LOG": "事件紀錄" + "SECOND_AGO": "不到 1 分鐘", + "EVENT_LOG": "事件日誌" }, - "UNKNOWN_ERROR": "發生未知錯誤,請稍後再試。", - "UNAUTHORIZED_ERROR": "工作階段無效或者已經過期,請重新登入以繼續。", - "REPO_READ_ONLY": "Harbor 被設定為唯獨模式,在此模式下,不能刪除儲存庫、Tag 及推送映像檔。", - "FORBIDDEN_ERROR": "目前操作被禁止,請確認您有有效的權限。", - "GENERAL_ERROR": "呼叫後台服務時出現錯誤: {{param}} 。", - "BAD_REQUEST_ERROR": "錯誤請求,操作無法完成。", - "NOT_FOUND_ERROR": "對像不存在,請求無法完成。", - "CONFLICT_ERROR": "請求包含衝突,操作無法完成。", - "PRECONDITION_FAILED": "驗證前置條件失敗,無法執行操作。", - "SERVER_ERROR": "伺服器出現內部錯誤,請求無法完成。", + "UNKNOWN_ERROR": "發生未知錯誤。請稍後再試。", + "UNAUTHORIZED_ERROR": "您的工作階段無效或已過期。您需要重新登入以繼續操作。", + "REPO_READ_ONLY": "Harbor 已設為唯讀模式。在唯讀模式下,刪除儲存庫、Artifact、標籤及推送映像檔等操作將被停用。", + "FORBIDDEN_ERROR": "您沒有執行此操作的適當權限。", + "GENERAL_ERROR": "執行服務呼叫時發生錯誤:{{param}}。", + "BAD_REQUEST_ERROR": "因請求不正確,我們無法執行您的操作。", + "NOT_FOUND_ERROR": "因物件不存在,您的請求無法完成。", + "CONFLICT_ERROR": "因您的提交存在衝突,我們無法執行您的操作。", + "PRECONDITION_FAILED": "因先決條件失敗,我們無法執行您的操作。", + "SERVER_ERROR": "因發生內部伺服器錯誤,我們無法執行您的操作。", "INCORRECT_OLD_PWD": "舊密碼不正確。", - "UNKNOWN": "未知", + "UNKNOWN": "不適用", "STATUS": "狀態", - "START_TIME": "建立時間", + "START_TIME": "開始時間", + "CREATION_TIME": "建立時間", "UPDATE_TIME": "更新時間", "LOGS": "日誌", - "PENDING": "未開始", + "PENDING": "等待中", "FINISHED": "已完成", "STOPPED": "已停止", "RUNNING": "執行中", "ERROR": "錯誤", "SCHEDULE": { "NONE": "無", - "DAILY": "每天", + "DAILY": "每日", "WEEKLY": "每週", "HOURLY": "每小時", - "CUSTOM": "自定義", + "CUSTOM": "自訂", "MANUAL": "手動", "SCHEDULE": "已排程", - "CRON": "定時", + "CRON": "cron", "ON": "於", "AT": "在", - "NOSCHEDULE": "取得排程時出現錯誤" + "NOSCHEDULE": "取得排程時發生錯誤" }, "GC": { - "CURRENT_SCHEDULE": "目前的垃圾回收排程", - "GC_NOW": "立即執行垃圾回收", - "JOB_HISTORY": "垃圾回收歷史任務", - "JOB_ID": "任務 ID", + "CURRENT_SCHEDULE": "排程進行垃圾回收", + "GC_NOW": "立即進行垃圾回收", + "JOB_HISTORY": "垃圾回收歷史", + "JOB_ID": "工作 ID", "TRIGGER_TYPE": "觸發類型", - "LATEST_JOBS": "最新的 {{param}} 個任務", - "MSG_SUCCESS": "垃圾回收成功", - "MSG_SCHEDULE_SET": "已設定垃圾回收排程", - "MSG_SCHEDULE_RESET": "已重設垃圾回收排程", + "LATEST_JOBS": "最新的 {{param}} 個工作", + "MSG_SUCCESS": "垃圾回收成功。", + "MSG_SCHEDULE_SET": "垃圾回收排程已設定。", + "MSG_SCHEDULE_RESET": "垃圾回收排程已重設。", "PARAMETERS": "參數", - "DELETE_UNTAGGED": "允許對未標籤的檔案進行垃圾回收", - "EXPLAIN": "垃圾回收是一個密集計算的操作,可能影響 Registry 的效能", - "EXPLAIN_TIME_WINDOW": "過去 2 小時(預設窗口)內上傳的檔案將不包括在垃圾回收中", - "DRY_RUN_SUCCESS": "成功觸發模擬執行", - "DELETE_DETAIL": "{{blob}} 個 blob 和 {{manifest}} 個 manifest 已刪除,釋放了 {{size}} 的空間", - "DELETE_DETAIL_DRY_RUN": "{{blob}} 個 blob 和 {{manifest}} 個 manifest 可以刪除,可以釋放 {{size}} 的空間", - "WORKERS_TOOLTIP": "設定可同時執行垃圾回收任務的工作數量,預設值是 1。" + "DELETE_UNTAGGED": "允許對未標記的 Artifact 進行垃圾回收", + "DELETE_TAG": "允許垃圾回收從後端儲存中移除標籤檔案", + "EXPLAIN": "垃圾回收是一項計算密集型操作,可能會影響 Registry 的效能。", + "EXPLAIN_TIME_WINDOW": "過去 2 小時 (預設時間) 內上傳的 Artifact 將被排除在垃圾回收之外。", + "DRY_RUN_SUCCESS": "已成功觸發模擬執行。", + "DELETE_DETAIL": "已刪除 {{blob}} 個 blob 和 {{manifest}} 個 manifest,釋放了 {{size}} 空間。", + "DELETE_DETAIL_DRY_RUN": "可刪除 {{blob}} 個 blob 和 {{manifest}} 個 manifest,可釋放 {{size}} 空間。", + "WORKERS_TOOLTIP": "設定可平行執行垃圾回收任務的 Worker 數量,預設值為 1。" }, "RETAG": { - "MSG_SUCCESS": "Artifact 複製成功", - "TIP_REPO": "儲存庫名稱被分解成路徑元素。每個元素必須至少包括一個小寫字母、字母數字字元,可選用句點、破折號或底線分隔。嚴格來說,它必須符合正規表達式 [a-z0-9]+(?:[._-][a-z0-9]+)*。如果儲存庫名稱有兩個或更多路徑元素,它們必須用正斜槓('/')分隔。儲存庫名稱的總長度(包括斜槓)必須少於 256 個字元。", - "TIP_TAG": "標籤(Tag)是套用到儲存庫中 Docker 映像檔的一個標籤,用來區別儲存庫內的不同映像檔。標籤必須符合正規表達式:`[\\w][\\w.-]{0,127}`。" + "MSG_SUCCESS": "Artifact 複製成功。", + "TIP_REPO": "儲存庫名稱由路徑元件組成。儲存庫名稱的元件必須至少包含一個小寫英數字元,可選擇性地以句點、破折號或底線分隔。更嚴格地說,它必須符合正規表示式 [a-z0-9]+(?:[._-][a-z0-9]+)*。如果儲存庫名稱有兩個或更多路徑元件,它們必須以正斜線 ('/') 分隔。儲存庫名稱的總長度 (含斜線) 必須小於 256 個字元。", + "TIP_TAG": "標籤是應用於儲存庫中 Docker 映像檔的標示。標籤用於區分儲存庫中的不同映像檔。它需要符合正規表示式:(`[\\w][\\w.-]{0,127}`)" }, "CVE_ALLOWLIST": { "DEPLOYMENT_SECURITY": "部署安全性", - "CVE_ALLOWLIST": "CVE 白名單", - "SYS_ALLOWLIST_EXPLAIN": "在計算映像檔的的安全性弱點時,在系統的 CVE 白名單中的弱點將會被忽略。", - "ADD_SYS": "可新增一條或多條 CVE ID 至系統的 CVE 白名單中", - "WARNING_SYS": "系統的 CVE 白名單已過期. 請延長有效期以使白名單生效", - "WARNING_PRO": "該專案的 CVE 白名單已過期. 請延長有效期以使白名單生效", + "CVE_ALLOWLIST": "CVE 允許清單", + "SYS_ALLOWLIST_EXPLAIN": "系統允許清單允許在計算映像檔弱點時忽略此清單中的弱點。", + "ADD_SYS": "將 CVE ID 新增至系統允許清單", + "WARNING_SYS": "系統 CVE 允許清單已過期。您可以透過延長到期日來啟用允許清單。", + "WARNING_PRO": "專案 CVE 允許清單已過期。您可以透過延長到期日來啟用允許清單。", "ADD": "新增", "ENTER": "輸入 CVE ID", - "HELP": "CVE ID之間請用英文逗號隔開或者換行", + "HELP": "分隔符號:逗號或換行字元", "NONE": "無", - "EXPIRES_AT": "有效期至", + "EXPIRES_AT": "到期於", "NEVER_EXPIRES": "永不過期", - "PRO_ALLOWLIST_EXPLAIN": "專案白名單允許此清單中的弱點在推送和拉取映像檔時被忽略。", - "PRO_OR_SYS": "您可以使用在系統層級設定的預設白名單,或點選「專案白名單」以建立新白名單。", - "MERGE_INTO": "您可以點選「複製系統白名單」項將系統白名單合併至該專案白名單中,並可為該專案白名單新增個別的 CVE IDs", - "SYS_ALLOWLIST": "系統白名單", - "PRO_ALLOWLIST": "專案白名單", - "ADD_SYSTEM": "複製系統白名單" + "PRO_ALLOWLIST_EXPLAIN": "專案允許清單允許在推送和拉取映像檔時忽略此專案中此清單內的弱點。", + "PRO_OR_SYS": "您可以使用在系統層級設定的預設允許清單,或點選「專案允許清單」以建立新的允許清單。", + "MERGE_INTO": "在點選「從系統複製」之前新增個別的 CVE ID,以同時新增系統允許清單。", + "SYS_ALLOWLIST": "系統允許清單", + "PRO_ALLOWLIST": "專案允許清單", + "ADD_SYSTEM": "從系統複製" }, "TAG_RETENTION": { "TAG_RETENTION": "標籤保留", "RETENTION_RULES": "保留規則", - "RULE_NAME_1": "最近{{number}}天的映像檔", - "RULE_NAME_2": "最近活躍的{{number}}個映像檔", - "RULE_NAME_3": "最近推送的{{number}}個映像檔", - "RULE_NAME_4": "最近拉取的{{number}}個映像檔", - "RULE_NAME_5": "全部映像檔", + "RULE_NAME_1": "保留最近 {{number}} 天的 Artifact", + "RULE_NAME_2": "保留最近 {{number}} 個活躍的 Artifact", + "RULE_NAME_3": "保留最近推送的 {{number}} 個 Artifact", + "RULE_NAME_4": "保留最近拉取的 {{number}} 個 Artifact", + "RULE_NAME_5": "一律保留", "ADD_RULE": "新增規則", - "ADD_RULE_HELP_1": "點選新增按鈕可新增規則", - "ADD_RULE_HELP_2": "Tag 保留原則每天執行一次.", - "RETENTION_RUNS": "執行保留原則", + "ADD_RULE_HELP_1": "點選「新增規則」按鈕以新增規則。", + "ADD_RULE_HELP_2": "標籤保留原則每日執行一次。", + "RETENTION_RUNS": "保留執行", "RUN_NOW": "立即執行", "WHAT_IF_RUN": "模擬執行", "ABORT": "中止", @@ -1401,141 +1413,141 @@ "DISABLE": "停用", "ENABLE": "啟用", "DELETE": "刪除", - "ADD_TITLE": "新增Tag 保留規則", - "ADD_SUBTITLE": "為目前專案指定 tag 保留規則。所有 tag 保留規則獨立計算並且適用於所有符合條件的儲存庫。", - "BY_WHAT": "以映像檔或天數為條件", - "RULE_TEMPLATE_1": "最近#天的映像檔", - "RULE_TEMPLATE_2": "最近活躍的#個映像檔", - "RULE_TEMPLATE_3": "最近推送的#個映像檔", - "RULE_TEMPLATE_4": "最近拉取的#個映像檔", - "RULE_TEMPLATE_5": "全部", + "ADD_TITLE": "新增標籤保留規則", + "ADD_SUBTITLE": "為此專案指定標籤保留規則。所有標籤保留規則都將獨立計算,且每個規則可應用於選定的儲存庫清單。", + "BY_WHAT": "依 Artifact 數量或天數", + "RULE_TEMPLATE_1": "保留最近 # 天的 Artifact", + "RULE_TEMPLATE_2": "保留最近 # 個活躍的 Artifact", + "RULE_TEMPLATE_3": "保留最近推送的 # 個 Artifact", + "RULE_TEMPLATE_4": "保留最近拉取的 # 個 Artifact", + "RULE_TEMPLATE_5": "一律保留", "ACTION_RETAIN": "保留", - "UNIT_DAY": "天數", - "UNIT_COUNT": "個數", - "NUMBER": "數量", - "IN_REPOSITORIES": "套用到儲存庫", - "REP_SEPARATOR": "使用逗號分隔repos,repo*和**", + "UNIT_DAY": "天", + "UNIT_COUNT": "個", + "NUMBER": "數字", + "IN_REPOSITORIES": "對於儲存庫", + "REP_SEPARATOR": "輸入多個以逗號分隔的儲存庫,例如 repo、repo* 或 **", "TAGS": "標籤", - "INCLUDE_UNTAGGED": "無標記的", - "UNTAGGED": "無標籤", + "UNTAGGED": "未標記的", + "INCLUDE_UNTAGGED": "未標記的 Artifact", "MATCHES_TAGS": "符合標籤", - "MATCHES_EXCEPT_TAGS": "排除標籤", - "TAG_SEPARATOR": "輸入多個逗號分隔的 Tags,Tag*或**。可透過勾選將未加Tag 的映像檔作為此原則的一部分。", + "MATCHES_EXCEPT_TAGS": "符合但排除標籤", + "TAG_SEPARATOR": "輸入多個以逗號分隔的標籤,例如 tag、tag* 或 **。可選擇性地透過勾選上方核取方塊,在套用「包含」或「排除」選取器時包含所有未標記的 Artifact。", "LABELS": "標籤", "MATCHES_LABELS": "符合標籤", - "MATCHES_EXCEPT_LABELS": "排除標籤", - "REP_LABELS": "使用逗號分割標籤", - "RETENTION_RUN": "執行保留原則", - "RETENTION_RUN_EXPLAIN": "執行保留原則將對該專案中的映像檔產生反向影響,受影響的映像檔 tags 將會被刪除。您可選擇取消或者使用模擬執行,或者點選執行以繼續。", - "RETENTION_RUN_ABORTED": "中止執行保留原則", - "RETENTION_RUN_ABORTED_EXPLAIN": "已中止執行保留原則,已刪除的映像檔不可恢復。您可執行另一個執行命令以便繼續刪除映像檔。如需模擬執行,請點選模擬執行按鈕。", + "MATCHES_EXCEPT_LABELS": "符合但排除標籤", + "REP_LABELS": "輸入多個以逗號分隔的標籤", + "RETENTION_RUN": "執行保留", + "RETENTION_RUN_EXPLAIN": "執行保留原則可能對此專案中的 Artifact 產生不利影響,且受影響的 Artifact 標籤將被刪除。按「取消」並使用「模擬執行」來模擬此原則的效果。否則按「執行」以繼續。", + "RETention_RUN_ABORTED": "保留執行已中止", + "RETENTION_RUN_ABORTED_EXPLAIN": "此保留執行已中止。已刪除的 Artifact 無法復原。您可以啟動另一個執行以繼續刪除 Artifact。若要模擬執行,您可以使用「模擬執行」。", "LOADING": "載入中...", - "NO_EXECUTION": "暫無記錄!", - "NO_HISTORY": "暫無記錄!", - "DELETION": "刪除記錄", - "EDIT_TITLE": "編輯Tag保留規則", + "NO_EXECUTION": "找不到任何執行項!", + "NO_HISTORY": "找不到任何歷史記錄!", + "DELETION": "刪除項", + "EDIT_TITLE": "編輯標籤保留規則", "LOG": "日誌", "EXCLUDES": "排除", "MATCHES": "符合", - "REPO": "儲存庫", + "REPO": "個儲存庫", "EXC": "排除", "MAT": "符合", "AND": "且", - "WITH": "有", - "WITHOUT": "沒有", - "LOWER_LABELS": "標籤", - "WITH_CONDITION": "基於條件", - "LOWER_TAGS": " 標籤", - "TRIGGER": "定時執行", - "RETAINED": "保留數量", - "TOTAL": "總數", - "NONE": "空", - "RULE_NAME_6": "最近{{number}}天被拉取過的映像檔", - "RULE_NAME_7": "最近{{number}}天被推送過的映像檔", - "RULE_TEMPLATE_6": "最近#天被拉取過的映像檔", - "RULE_TEMPLATE_7": "最近#天被推送過的映像檔", - "SCHEDULE": "定時任務", - "SCHEDULE_WARNING": "執行保留原則將會刪除受影響的映像檔,且無法回復。請在制定定時任務前仔細檢查所有保留規則。", - "EXISTING_RULE": "規則已存在", - "ILLEGAL_RULE": "規則無效", + "WITH": "帶有", + "WITHOUT": "不帶", + "LOWER_LABELS": "個標籤", + "WITH_CONDITION": "帶有", + "LOWER_TAGS": "個標籤", + "TRIGGER": "排程", + "RETAINED": "已保留", + "TOTAL": "總計", + "NONE": "無", + "RULE_NAME_6": "保留最近 {{number}} 天內拉取的 Artifact", + "RULE_NAME_7": "保留最近 {{number}} 天內推送的 Artifact", + "RULE_TEMPLATE_6": "保留最近 # 天內拉取的 Artifact", + "RULE_TEMPLATE_7": "保留最近 # 天內推送的 Artifact", + "SCHEDULE": "排程", + "SCHEDULE_WARNING": "執行保留原則將導致從 Harbor 專案中永久刪除 Artifact。請在排程前仔細檢查所有原則。", + "EXISTING_RULE": "既有規則", + "ILLEGAL_RULE": "非法規則", "INVALID_RULE": "無效規則", - "COUNT_LARGE": "參數“個數”太大", - "DAYS_LARGE": "參數“天數”太大", + "COUNT_LARGE": "參數「數量」過大", + "DAYS_LARGE": "參數「天數」過大", "EXECUTION_TYPE": "執行類型", "ACTION": "操作", - "YES": "Yes", - "NO": "No" + "YES": "是", + "NO": "否" }, "IMMUTABLE_TAG": { - "IMMUTABLE_RULES": "不可變更的標籤規則", + "IMMUTABLE_RULES": "不可變性規則", "ADD_RULE": "新增規則", - "ADD_RULE_HELP_1": "點選新增規則按鈕以加入規則。", + "ADD_RULE_HELP_1": "點選「新增規則」按鈕以新增規則。", "EDIT": "編輯", "DISABLE": "停用", "ENABLE": "啟用", "DELETE": "刪除", - "ADD_TITLE": "新增標籤不可變更規則", - "ADD_SUBTITLE": "為此專案指定標籤不可變更規則。注意:所有標籤不可變更規則都將首先獨立計算,然後合併以得到最終的不可變更標籤集合。", - "IN_REPOSITORIES": "套用至儲存庫", - "REP_SEPARATOR": "輸入多個逗號分隔的儲存庫,例如 repos,repo* 或 **。", + "ADD_TITLE": "新增標籤不可變性規則", + "ADD_SUBTITLE": "為此專案指定標籤不可變性規則。注意:所有標籤不可變性規則將先獨立計算,然後取聯集以得到最終的不可變標籤集合。", + "IN_REPOSITORIES": "對於儲存庫", + "REP_SEPARATOR": "輸入多個以逗號分隔的儲存庫,例如 repo、repo* 或 **", "TAGS": "標籤", - "TAG_SEPARATOR": "輸入多個逗號分隔的標籤,例如 tags,tag* 或 **。", - "EDIT_TITLE": "編輯標籤不可變更規則", + "TAG_SEPARATOR": "輸入多個以逗號分隔的標籤,例如 tag、tag* 或 **。", + "EDIT_TITLE": "編輯標籤不可變性規則", "EXC": "排除", "MAT": "符合", "AND": "且", - "WITH": "含有", - "WITHOUT": "不含", - "LOWER_LABELS": "標籤", - "LOWER_TAGS": "標籤", + "WITH": "帶有", + "WITHOUT": "不帶", + "LOWER_LABELS": "個標籤", + "LOWER_TAGS": "個標籤", "NONE": "無", "EXISTING_RULE": "既有規則", "ACTION": "操作" }, "SCANNER": { - "DELETION_SUMMARY": "您是否要刪除掃描器 {{param}}?", - "SKIP_CERT_VERIFY": "當遠端 Registry 使用自我簽署或不受信任的憑證時,請勾選此框以跳過憑證驗證。", + "DELETION_SUMMARY": "您確定要刪除掃描器 {{param}} 嗎?", + "SKIP_CERT_VERIFY": "當遠端 Registry 使用自簽章或不受信任的憑證時,勾選此方塊以跳過憑證驗證。", "NAME": "名稱", "NAME_EXISTS": "名稱已存在", - "NAME_REQUIRED": "名稱必填", - "NAME_REX": "名稱必須由小寫字母、數字和 ._- 組成,至少2個字元,並且必須以字母或數字開頭。", + "NAME_REQUIRED": "必須輸入名稱", + "NAME_REX": "名稱長度應至少 2 個字元,可包含小寫字母、數字及 ._-,且必須以字母或數字開頭。", "DESCRIPTION": "描述", "SBOM": "SBOM", - "VULNERABILITY": "Vulnerability", - "SUPPORTED": "Supported", - "NOT_SUPPORTED": "Not Supported", + "VULNERABILITY": "弱點", + "SUPPORTED": "支援", + "NOT_SUPPORTED": "不支援", "ENDPOINT": "端點", "ENDPOINT_EXISTS": "端點 URL 已存在", - "ENDPOINT_REQUIRED": "端點 URL 必填", - "ILLEGAL_ENDPOINT": "端點 URL 無效", - "AUTH": "認證", + "ENDPOINT_REQUIRED": "必須輸入端點 URL", + "ILLEGAL_ENDPOINT": "端點 URL 非法", + "AUTH": "授權", "NONE": "無", - "BASIC": "Basic", + "BASIC": "基本", "BEARER": "Bearer", "API_KEY": "API 金鑰", "USERNAME": "使用者名稱", - "USERNAME_REQUIRED": "使用者名稱必填", + "USERNAME_REQUIRED": "必須輸入使用者名稱", "PASSWORD": "密碼", - "PASSWORD_REQUIRED": "密碼必填", + "PASSWORD_REQUIRED": "必須輸入密碼", "TOKEN": "權杖", - "TOKEN_REQUIRED": "權杖必填", - "API_KEY_REQUIRED": "API 金鑰必填", + "TOKEN_REQUIRED": "必須輸入權杖", + "API_KEY_REQUIRED": "必須輸入 API 金鑰", "SKIP": "跳過憑證驗證", "ADD_SCANNER": "新增掃描器", "EDIT_SCANNER": "編輯掃描器", - "TEST_CONNECTION": "測試連接", + "TEST_CONNECTION": "測試連線", "ADD_SUCCESS": "新增成功", "TEST_PASS": "測試通過", - "TEST_FAILED": "Ping: 目標位址 {{name}}:{{url}} 無法連接", + "TEST_FAILED": "Ping:註冊 {{name}}:{{url}} 無法連線", "UPDATE_SUCCESS": "更新成功", "SCANNER_COLON": "掃描器:", "NAME_COLON": "名稱:", "VENDOR_COLON": "供應商:", "VERSION_COLON": "版本:", - "CAPABILITIES": "功能:", - "CONSUMES_MIME_TYPES_COLON": "可處理的 MIME 類型:", - "PRODUCTS_MIME_TYPES_COLON": "產出的 MIME 類型:", - "CAPABILITIES_TYPE": "Type:", + "CAPABILITIES": "能力:", + "CONSUMES_MIME_TYPES_COLON": "消耗的 MIME 類型:", + "PRODUCTS_MIME_TYPES_COLON": "產生的 MIME 類型:", + "CAPABILITIES_TYPE": "類型:", "PROPERTIES": "屬性", "NEW_SCANNER": "新增掃描器", "SET_AS_DEFAULT": "設為預設", @@ -1548,82 +1560,82 @@ "SCANNERS": "掃描器", "SCANNER": "掃描器", "EDIT": "編輯", - "NOT_AVAILABLE": "不可用", - "ADAPTER": "轉接器", + "NOT_AVAILABLE": "無法使用", + "ADAPTER": "配接器", "VENDOR": "供應商", "VERSION": "版本", - "SELECT_SCANNER": "選擇掃描器", + "SELECT_SCANNER": "選取掃描器", "ENABLED": "已啟用", "ENABLE": "啟用", "DISABLE": "停用", "DELETE_SUCCESS": "刪除成功", "TOTAL": "總計", "FIXABLE": "可修復", - "DURATION": "掃描用時:", + "DURATION": "持續時間:", "OPTIONS": "選項", - "USE_INNER": "使用內部儲存庫位址", - "USE_INNER_TIP": "勾選此選項,掃描器將被強制使用內部儲存庫位址來存取相關內容。", - "VULNERABILITY_SEVERITY": "弱點嚴重程度:", + "USE_INNER": "使用內部 Registry 位址", + "USE_INNER_TIP": "若勾選此選項,掃描器將被強制使用內部 Registry 位址來存取相關內容。", + "VULNERABILITY_SEVERITY": "弱點嚴重性:", "CONFIRM_DELETION": "確認刪除掃描器", - "NO_PROJECT_SCANNER": "無掃描器", - "SET_UNHEALTHY_SCANNER": "所選擇的掃描器:{{name}} 是不健康的", - "SCANNED_BY": "由此掃描:", + "NO_PROJECT_SCANNER": "沒有掃描器", + "SET_UNHEALTHY_SCANNER": "選取的掃描器:{{name}} 不健康", + "SCANNED_BY": "掃描者:", "IMAGE_SCANNERS": "映像檔掃描器", - "ALL_SCANNERS": "全部掃描器", - "HELP_INFO_1": "預設掃描器已安裝。如需其他掃描器的安裝說明,請參閱", - "HELP_INFO_2": "說明文件。", - "NO_DEFAULT_SCANNER": "未設定預設掃描器" + "ALL_SCANNERS": "所有掃描器", + "HELP_INFO_1": "預設掃描器已安裝。若要安裝其他掃描器,請參考", + "HELP_INFO_2": "文件。", + "NO_DEFAULT_SCANNER": "沒有預設掃描器" }, "DISTRIBUTION": { - "FILTER_INSTANCE_PLACEHOLDER": "篩選實例", + "FILTER_INSTANCE_PLACEHOLDER": "篩選執行個體", "FILTER_HISTORIES_PLACEHOLDER": "篩選歷史記錄", - "ADD_ACTION": "新增實例", - "PREHEAT_ACTION": "預載", + "ADD_ACTION": "新增執行個體", + "PREHEAT_ACTION": "預熱", "EDIT_ACTION": "編輯", "ENABLE_ACTION": "啟用", "DISABLE_ACTION": "停用", "DELETE_ACTION": "刪除", - "NOT_FOUND": "找不到任何實例!", + "NOT_FOUND": "找不到任何執行個體!", "NAME": "名稱", "ENDPOINT": "端點", "STATUS": "狀態", "ENABLED": "已啟用", "SETUP_TIMESTAMP": "設定時間戳", "PROVIDER": "提供者", - "DELETION_TITLE": "確認刪除實例", - "DELETION_SUMMARY": "您是否要刪除實例 {{param}}?", - "ENABLE_TITLE": "啟用實例", - "ENABLE_SUMMARY": "您是否要啟用實例 {{param}}?", - "DISABLE_TITLE": "停用實例", - "DISABLE_SUMMARY": "您是否要停用實例 {{param}}?", + "DELETION_TITLE": "確認刪除執行個體", + "DELETION_SUMMARY": "您確定要刪除執行個體 {{param}} 嗎?", + "ENABLE_TITLE": "啟用執行個體", + "ENABLE_SUMMARY": "您確定要啟用執行個體 {{param}} 嗎?", + "DISABLE_TITLE": "停用執行個體", + "DISABLE_SUMMARY": "您確定要停用執行個體 {{param}} 嗎?", "IMAGE": "映像檔", "START_TIME": "開始時間", "FINISH_TIME": "完成時間", - "INSTANCE": "實例", + "INSTANCE": "執行個體", "HISTORIES": "歷史記錄", - "CREATE_SUCCESS": "成功建立實例", - "CREATE_FAILED": "建立實例失敗", - "DELETED_SUCCESS": "成功刪除實例", - "DELETED_FAILED": "刪除實例失敗", - "ENABLE_SUCCESS": "成功啟用實例", - "ENABLE_FAILED": "啟用實例失敗", - "DISABLE_SUCCESS": "成功停用實例", - "DISABLE_FAILED": "停用實例失敗", - "UPDATE_SUCCESS": "成功更新實例", - "UPDATE_FAILED": "更新實例失敗", - "REQUEST_PREHEAT_SUCCESS": "預載請求成功", - "REQUEST_PREHEAT_FAILED": "預載請求失敗", + "CREATE_SUCCESS": "執行個體建立成功。", + "CREATE_FAILED": "執行個體建立失敗。", + "DELETED_SUCCESS": "執行個體刪除成功。", + "DELETED_FAILED": "執行個體刪除失敗。", + "ENABLE_SUCCESS": "執行個體啟用成功。", + "ENABLE_FAILED": "執行個體啟用失敗。", + "DISABLE_SUCCESS": "執行個體停用成功。", + "DISABLE_FAILED": "執行個體停用失敗。", + "UPDATE_SUCCESS": "執行個體更新成功。", + "UPDATE_FAILED": "執行個體更新失敗。", + "REQUEST_PREHEAT_SUCCESS": "預熱請求成功。", + "REQUEST_PREHEAT_FAILED": "預熱請求失敗。", "DESCRIPTION": "描述", - "AUTH_MODE": "驗證模式", + "AUTH_MODE": "認證模式", "USERNAME": "使用者名稱", "PASSWORD": "密碼", "TOKEN": "權杖", - "SETUP_NEW_INSTANCE": "設定新實例", - "EDIT_INSTANCE": "編輯實例", + "SETUP_NEW_INSTANCE": "設定新執行個體", + "EDIT_INSTANCE": "編輯執行個體", "SETUP": { - "NAME_PLACEHOLDER": "輸入實例名稱", - "DESCRIPTION_PLACEHOLDER": "輸入實例描述", - "ENDPOINT_PLACEHOLDER": "輸入實例端點", + "NAME_PLACEHOLDER": "輸入執行個體名稱", + "DESCRIPTION_PLACEHOLDER": "輸入執行個體描述", + "ENDPOINT_PLACEHOLDER": "輸入執行個體端點", "USERNAME_PLACEHOLDER": "輸入使用者名稱", "PASSWORD_PLACEHOLDER": "輸入密碼", "TOKEN_PLACEHOLDER": "輸入權杖" @@ -1632,24 +1644,24 @@ "SOURCE": "來源", "VERSION": "版本", "SET_AS_DEFAULT": "設為預設", - "DELETE_INSTANCE": "刪除實例", - "ENABLE_INSTANCE": "啟用實例", - "DISABLE_INSTANCE": "停用實例", + "DELETE_INSTANCE": "刪除執行個體", + "ENABLE_INSTANCE": "啟用執行個體", + "DISABLE_INSTANCE": "停用執行個體", "SET_DEFAULT_SUCCESS": "設為預設成功", "SET_DEFAULT_FAILED": "設為預設失敗", - "UPDATE_INSTANCE": "更新實例", - "CREATE_INSTANCE": "建立實例" + "UPDATE_INSTANCE": "更新執行個體", + "CREATE_INSTANCE": "建立執行個體" }, "P2P_PROVIDER": { "DRAGONFLY": { "SCOPE": "範圍", - "SCOPE_SINGLE_SEED_PEER": "單一種子節點", - "SCOPE_ALL_SEED_PEERS": "所有種子節點", - "SCOPE_ALL_PEERS": "所有節點", - "CLUSTER_IDS": "叢集 IDs", - "CLUSTER_IDS_SEPARATOR": "用逗號分隔的叢集ID, 範例 1,2,3 (若為空則預熱所有叢集)" + "SCOPE_SINGLE_SEED_PEER": "單一種子對等點", + "SCOPE_ALL_SEED_PEERS": "所有種子對等點", + "SCOPE_ALL_PEERS": "所有對等點", + "CLUSTER_IDS": "叢集 ID", + "CLUSTER_IDS_SEPARATOR": "輸入多個以逗號分隔的叢集 ID,例如 1,2,3 (若留空,則預熱所有叢集)" }, - "P2P_PROVIDER": "P2P 預載", + "P2P_PROVIDER": "P2P 預熱", "POLICIES": "原則", "NEW_POLICY": "新增原則", "NAME": "名稱", @@ -1659,68 +1671,68 @@ "TRIGGER": "觸發器", "CREATED": "建立時間", "DESCRIPTION": "描述", - "NO_POLICY": "無原則", - "ENABLED_POLICY_SUMMARY": "您是否要啟用原則 {{name}}?", - "DISABLED_POLICY_SUMMARY": "您是否要停用原則 {{name}}?", + "NO_POLICY": "沒有原則", + "ENABLED_POLICY_SUMMARY": "您確定要啟用原則 {{name}} 嗎?", + "DISABLED_POLICY_SUMMARY": "您確定要停用原則 {{name}} 嗎?", "ENABLED_POLICY_TITLE": "啟用原則", "DISABLED_POLICY_TITLE": "停用原則", - "DELETE_POLICY_SUMMARY": "您是否要刪除原則 {{names}}?", + "DELETE_POLICY_SUMMARY": "您確定要刪除原則 {{names}} 嗎?", "EDIT_POLICY": "編輯 P2P 提供者原則", "ADD_POLICY": "建立 P2P 提供者原則", - "NAME_REQUIRED": "名稱為必填選項", - "PROVIDER_REQUIRED": "提供者為必填選項", - "ADDED_SUCCESS": "新增原則成功", - "UPDATED_SUCCESS": "更新原則成功", + "NAME_REQUIRED": "必須輸入名稱。", + "PROVIDER_REQUIRED": "必須輸入提供者。", + "ADDED_SUCCESS": "原則新增成功。", + "UPDATED_SUCCESS": "原則更新成功。", "EXECUTE": "執行", - "EXECUTE_SUCCESSFULLY": "成功執行", - "EXECUTE_TITLE": "確認原則執行", - "EXECUTE_SUMMARY": "您是否要執行原則 {{param}}?", + "EXECUTE_SUCCESSFULLY": "執行成功。", + "EXECUTE_TITLE": "確認執行原則", + "EXECUTE_SUMMARY": "您確定要執行原則 {{param}} 嗎?", "STOP_TITLE": "確認停止執行", - "STOP_SUMMARY": "您是否要停止執行原則 {{param}}?", - "STOP_SUCCESSFULLY": "成功停止執行", + "STOP_SUMMARY": "您確定要停止執行原則 {{param}} 嗎?", + "STOP_SUCCESSFULLY": "停止執行成功。", "STATUS_MSG": "狀態訊息", - "JOB_PLACEHOLDER": "找不到任何執行作業", - "PROVIDER_TYPE": "提供者類型", + "JOB_PLACEHOLDER": "找不到任何執行項", + "PROVIDER_TYPE": "供應商", "ID": "執行 ID", - "NO_PROVIDER": "請先新增一個提供者", + "NO_PROVIDER": "請先新增一個提供者。", "ARTIFACT": "Artifact", "DIGEST": "Digest", "TYPE": "類型", - "TASKS": "任務", - "TASKS_PLACEHOLDER": "找不到任何任務", - "SEVERITY_WARNING": "此處的弱點設定與相關專案設定衝突,將覆蓋此處的設定", + "TASKS": "工作", + "TASKS_PLACEHOLDER": "找不到任何工作", + "SEVERITY_WARNING": "此處的弱點設定與相關的專案組態衝突,專案組態將覆寫此處的設定。", "REPOS": "儲存庫", "TAGS": "標籤", - "CRITERIA": "標準", + "CRITERIA": "準則", "ONLY_SIGNED": "僅限已簽署的映像檔", - "PREHEAT_ON_PUSH": "推送時預載", - "START_TEXT": "無弱點嚴重性", + "PREHEAT_ON_PUSH": "推送時預熱", + "START_TEXT": "無弱點嚴重性為", "EDN_TEXT": "及以上", "LABELS": "標籤", "SCHEDULE": "排程", "TEST_FAILED": "測試失敗", "MANUAL": "手動", - "SCHEDULED": "定時", - "EVENT_BASED": "事件基礎", - "EVENT_BASED_EXPLAIN_LINE1": "以下事件發生時,將評估原則:", - "EVENT_BASED_EXPLAIN_LINE2": "已推送 Artifact", - "EVENT_BASED_EXPLAIN_LINE3": "已標記 Artifact", - "EVENT_BASED_EXPLAIN_LINE4": "已掃描 Artifact", - "REPO_REQUIRED": "儲存庫為必填選項", - "TAG_REQUIRED": "標籤為必填選項", - "DELETE_SUCCESSFULLY": "成功刪除原則", - "UPDATED_SUCCESSFULLY": "成功更新原則", - "EXECUTIONS": "執行次數", - "TAG_SEPARATOR": "輸入多個逗號分隔的標籤,標籤*,或 **", - "CONTENT_WARNING": "此處的內容信任設定與相關專案設定衝突,將覆蓋此處的設定", - "PREHEAT_EXPLAIN": "預載將映像檔遷移到 P2P 網絡", - "CRITERIA_EXPLAIN": "如 '部署安全性' 部分所指定的設定標籤", - "SKIP_CERT_VERIFY": "若遠端提供者使用自簽署或不受信任的憑證,請勾選此框以跳過憑證驗證", - "NAME_TOOLTIP": "原則名稱由一個或多個組合構成,每個組合包括大寫字母、小寫字母或數字;組合之間用點、底線或連字符分隔。", - "NEED_HELP": "請向您的系統管理員尋求協助以新增一個提供者" + "SCHEDULED": "已排程", + "EVENT_BASED": "事件驅動", + "EVENT_BASED_EXPLAIN_LINE1": "當發生以下事件時,將評估此原則:", + "EVENT_BASED_EXPLAIN_LINE2": "Artifact 已推送", + "EVENT_BASED_EXPLAIN_LINE3": "Artifact 已標記", + "EVENT_BASED_EXPLAIN_LINE4": "Artifact 已掃描", + "REPO_REQUIRED": "必須輸入儲存庫。", + "TAG_REQUIRED": "必須輸入標籤。", + "DELETE_SUCCESSFULLY": "原則刪除成功。", + "UPDATED_SUCCESSFULLY": "原則更新成功。", + "EXECUTIONS": "執行項", + "TAG_SEPARATOR": "輸入多個以逗號分隔的標籤,例如 tag、tag* 或 **", + "CONTENT_WARNING": "此處的內容信任設定與相關的專案組態衝突,專案組態將覆寫此處的設定。", + "PREHEAT_EXPLAIN": "預熱會將映像檔遷移至 P2P 網路。", + "CRITERIA_EXPLAIN": "如「組態設定」分頁下的「部署安全性」區段中所指定。", + "SKIP_CERT_VERIFY": "當遠端提供者使用自簽章或不受信任的憑證時,勾選此方塊以跳過憑證驗證。", + "NAME_TOOLTIP": "原則名稱由一或多個大寫字母、小寫字母或數字的群組組成;群組之間以點、底線或連字號分隔。", + "NEED_HELP": "請要求您的系統管理員先新增一個提供者。" }, "PAGINATION": { - "PAGE_SIZE": "每頁顯示數量" + "PAGE_SIZE": "每頁大小" }, "SYSTEM_ROBOT": { "READ": "讀取", @@ -1728,90 +1740,90 @@ "ARTIFACT": "Artifact", "ADD_ROBOT": "新增機器人", "UPDATE_ROBOT": "更新機器人", - "UPDATE_ROBOT_SUCCESSFULLY": "機器人更新成功", - "PLACEHOLDER": "輸入新的密鑰", - "SECRET": "密鑰應包含至少 1 個大寫字母、1 個小寫字母和 1 個數字,長度為 8-128 個字元。", - "REFRESH_SECRET": "更新密鑰", - "REFRESH_SECRET_SUCCESS": "密鑰更新成功", + "UPDATE_ROBOT_SUCCESSFULLY": "機器人更新成功。", + "PLACEHOLDER": "輸入新金鑰", + "SECRET": "金鑰長度應為 8-128 個字元,且至少包含 1 個大寫字母、1 個小寫字母和 1 個數字。", + "REFRESH_SECRET": "重新整理金鑰", + "REFRESH_SECRET_SUCCESS": "金鑰重新整理成功。", "DELETE_ROBOT": "刪除機器人", - "DELETE_ROBOT_SUCCESS": "成功刪除機器人", + "DELETE_ROBOT_SUCCESS": "機器人刪除成功。", "ENABLE_TITLE": "啟用機器人", - "ENABLE_SUMMARY": "您是否要啟用機器人 {{param}}?", + "ENABLE_SUMMARY": "您確定要啟用機器人 {{param}} 嗎?", "DISABLE_TITLE": "停用機器人", - "DISABLE_SUMMARY": "您是否要停用機器人 {{param}}?", - "ENABLE_ROBOT_SUCCESSFULLY": "成功啟用機器人", - "DISABLE_ROBOT_SUCCESSFULLY": "成功停用機器人", + "DISABLE_SUMMARY": "您確定要停用機器人 {{param}} 嗎?", + "ENABLE_ROBOT_SUCCESSFULLY": "機器人啟用成功。", + "DISABLE_ROBOT_SUCCESSFULLY": "機器人停用成功。", "ROBOT_ACCOUNT": "機器人帳號", "PROJECTS": "專案", "ALL_PROJECTS": "所有專案", "PERMISSIONS": "權限", - "REFRESH_SECRET_SUMMARY": "自動更新機器人帳號的密鑰,或者選擇開啟詳細資訊手動指定新的密鑰", - "TOKEN": "密鑰", - "NEW_TOKEN": "新密鑰", - "REFRESH": "更新", + "REFRESH_SECRET_SUMMARY": "自動重新整理機器人帳號金鑰,或可選地開啟詳細資訊以手動指定新金鑰。", + "TOKEN": "權杖", + "NEW_TOKEN": "新權杖", + "REFRESH": "重新整理", "PROJECTS_MODAL_TITLE": "機器人帳號的專案", - "PROJECTS_MODAL_SUMMARY": "這些是此機器人帳號涵蓋的專案。", + "PROJECTS_MODAL_SUMMARY": "這些是此機器人帳號所涵蓋的專案。", "CREATE_ROBOT": "建立系統機器人帳號", - "CREATE_ROBOT_SUMMARY": "Create a system Robot Account that will cover permissions for the system as well as for specific projects", + "CREATE_ROBOT_SUMMARY": "建立一個系統機器人帳號,其權限將涵蓋系統及特定專案。", "EDIT_ROBOT": "編輯系統機器人帳號", - "EDIT_ROBOT_SUMMARY": "Edit a system Robot Account that will cover permissions for the system as well as for specific projects", + "EDIT_ROBOT_SUMMARY": "編輯一個系統機器人帳號,其權限將涵蓋系統及特定專案。", "EXPIRATION_TIME": "到期時間", - "EXPIRATION_TIME_EXPLAIN": "機器人帳號的權杖到期時間(以建立時間為起點的天數)。要使其永不過期,請輸入「-1」。", - "EXPIRATION_DEFAULT": "天(預設)", + "EXPIRATION_TIME_EXPLAIN": "機器人帳號權杖的到期時間 (以天為單位,從建立時間起算)。若要永不到期,請輸入「-1」。", + "EXPIRATION_DEFAULT": "天 (預設)", "EXPIRATION_DAYS": "指定天數", "EXPIRATION_NEVER": "永不", - "EXPIRATION_REQUIRED": "需要有效的到期時間", + "EXPIRATION_REQUIRED": "必須輸入有效的到期時間。", "COVER_ALL": "涵蓋所有專案", - "COVER_ALL_EXPLAIN": "選擇以套用到所有現有和未來的專案", - "COVER_ALL_SUMMARY": "選擇了所有現有和未來的專案。", - "RESET_PERMISSION": "RESET ALL PROJECT PERMISSIONS", + "COVER_ALL_EXPLAIN": "勾選以應用於所有現有及未來的專案。", + "COVER_ALL_SUMMARY": "已選取所有目前及未來的專案。", + "RESET_PERMISSION": "重設所有專案權限", "PERMISSION_COLUMN": "權限", "EXPIRES_AT": "到期於", - "VIEW_SECRET": "更新密鑰", + "VIEW_SECRET": "重新整理金鑰", "LEGACY": "舊版", - "CREATE_PROJECT_ROBOT": "建立專案機器人帳號", - "CREATE_PROJECT_ROBOT_SUMMARY": "為此專案建立一個機器人帳號", + "CREATE_PROJECT_ROBOT": "建立機器人帳號", + "CREATE_PROJECT_ROBOT_SUMMARY": "為此專案建立一個機器人帳號。", "EDIT_PROJECT_ROBOT": "編輯機器人帳號", - "EDIT_PROJECT_ROBOT_SUMMARY": "編輯此專案的機器人帳號", - "NOT_FOUND": "我們找不到任何機器人!", + "EDIT_PROJECT_ROBOT_SUMMARY": "編輯此專案的機器人帳號。", + "NOT_FOUND": "找不到任何機器人!", "SELECT_ALL": "全選", "UNSELECT_ALL": "取消全選", "ROBOT_ACCOUNT_NAV": "機器人帳號", - "COVERED_PROJECTS": "涵蓋的專案", - "CONFIRM_SECRET": "確認密鑰", - "SECRET_AGAIN": "再次輸入密鑰", - "INCONSISTENT": "兩個密鑰不一致", - "NAME_TOOLTIP": "機器人名稱應該是 1~255 個字元長,包含小寫字母、數字和 ._-,並且必須以字母或數字開頭。", - "ENABLE_NEW_SECRET": "啟用此選項以手動指定新的密鑰", + "COVERED_PROJECTS": "個專案", + "CONFIRM_SECRET": "確認金鑰", + "SECRET_AGAIN": "再次輸入金鑰", + "INCONSISTENT": "兩個金鑰不一致。", + "NAME_TOOLTIP": "機器人名稱長度應為 1-255 個字元,可包含小寫字母、數字及 ._-,且必須以字母或數字開頭。", + "ENABLE_NEW_SECRET": "啟用此選項以手動指定新金鑰。", "DELETE": "刪除", "ARTIFACT_LABEL": "Artifact 標籤", "SCAN": "掃描", "SCANNER_PULL": "掃描器拉取", - "SEARCH_BY_NAME": "按名稱搜尋(無前綴)", - "FINAL_PROJECT_NAME_TIP": "最終的專案機器人名稱由前綴、專案名稱、加號和目前輸入的值所組成", - "FINAL_SYSTEM_NAME_TIP": "最終的系統機器人名稱由前綴和目前輸入的值組所組成", + "SEARCH_BY_NAME": "依名稱搜尋 (不含前置字串)", + "FINAL_PROJECT_NAME_TIP": "最終的專案機器人名稱由前置字串、專案名稱、一個加號及目前的輸入值組成。", + "FINAL_SYSTEM_NAME_TIP": "最終的系統機器人名稱由前置字串及目前的輸入值組成。", "PUSH_AND_PULL": "推送", - "PUSH_PERMISSION_TOOLTIP": "推送權限不能單獨使用,必須與拉取權限一起使用", + "PUSH_PERMISSION_TOOLTIP": "推送權限不能單獨作用,必須與拉取權限一起使用。", "STOP": "停止", - "LIST": "列表", + "LIST": "清單", "REPOSITORY": "儲存庫", "EXPIRES_IN": "到期於", "EXPIRED": "已過期", - "SELECT_ALL_PROJECT": "SELECT ALL PROJECTS", - "UNSELECT_ALL_PROJECT": "UNSELECT ALL PROJECTS" + "SELECT_ALL_PROJECT": "選取所有專案", + "UNSELECT_ALL_PROJECT": "取消選取所有專案" }, "ACCESSORY": { "DELETION_TITLE_ACCESSORY": "確認刪除附件", - "DELETION_SUMMARY_ACCESSORY": "您是否要刪除 artifact {{param}} 的所有附件?", - "DELETION_SUMMARY_ONE_ACCESSORY": "您是否要刪除附件 {{param}} ?", + "DELETION_SUMMARY_ACCESSORY": "您確定要刪除 Artifact {{param}} 的所有附件嗎?", + "DELETION_SUMMARY_ONE_ACCESSORY": "您確定要刪除附件 {{param}} 嗎?", "DELETE_ACCESSORY": "刪除附件", - "DELETED_SUCCESS": "附件刪除成功", - "DELETED_FAILED": "刪除附件失敗", + "DELETED_SUCCESS": "附件刪除成功。", + "DELETED_FAILED": "附件刪除失敗。", "CO_SIGNED": "由 Cosign 簽署", "NOTARY_SIGNED": "由 Notary 簽署", "ACCESSORY": "附件", "ACCESSORIES": "附件", - "SUBJECT_ARTIFACT": "目標 artifact", + "SUBJECT_ARTIFACT": "主體 Artifact", "CO_SIGN": "Cosign", "NOTARY": "Notation", "PLACEHOLDER": "找不到任何附件!" @@ -1819,34 +1831,34 @@ "CLEARANCES": { "CLEARANCES": "清理", "AUDIT_LOG": "日誌輪替", - "LAST_COMPLETED": "上次完成", - "NEXT_SCHEDULED_TIME": "下次預定時間", - "SCHEDULE_TO_PURGE": "安排清除", - "KEEP_IN": "保留記錄", - "KEEP_IN_TOOLTIP": "保留此區間內的記錄", - "KEEP_IN_ERROR": "此選項的值必須為整數,且時間值必須大於 0 且小於 10000 天", + "LAST_COMPLETED": "上次完成時間", + "NEXT_SCHEDULED_TIME": "下次排程時間", + "SCHEDULE_TO_PURGE": "排程清除", + "KEEP_IN": "保留紀錄於", + "KEEP_IN_TOOLTIP": "保留此時間間隔內的紀錄", + "KEEP_IN_ERROR": "此項目的值必須為整數,且時間值必須大於 0 且小於 10000 天。", "DAYS": "天", "HOURS": "小時", "INCLUDED_OPERATIONS": "包含的操作", "INCLUDED_OPERATION_TOOLTIP": "移除所選操作的稽核日誌", - "INCLUDED_OPERATION_ERROR": "請至少選擇一個操作", - "INCLUDED_EVENT_TYPES": "Event types to purge", - "INCLUDED_EVENT_TYPE_TOOLTIP": "Remove audit logs for the selected event types, checkbox \"Other events\" include all events not listed here ", - "INCLUDED_EVENT_TYPE_ERROR": "Please select at least one event type", + "INCLUDED_OPERATION_ERROR": "請至少選取一項操作", + "INCLUDED_EVENT_TYPES": "要清除的事件類型", + "INCLUDED_EVENT_TYPE_TOOLTIP": "移除所選事件類型的稽核日誌,勾選「其他事件」將包含此處未列出的所有事件。", + "INCLUDED_EVENT_TYPE_ERROR": "請至少選取一種事件類型", "PURGE_NOW": "立即清除", - "PURGE_NOW_SUCCESS": "成功觸發清除", - "PURGE_SCHEDULE_RESET": "清除排程已重設", + "PURGE_NOW_SUCCESS": "已成功觸發清除。", + "PURGE_SCHEDULE_RESET": "清除排程已重設。", "PURGE_HISTORY": "清除歷史", "FORWARD_ENDPOINT": "稽核日誌轉發 Syslog 端點", - "FORWARD_ENDPOINT_TOOLTIP": "將稽核日誌轉發至 syslog 端點,例如: harbor-log:10514", - "ENABLE_AUDIT_LOG_EVENT_TYPE": "Enable Audit Log Event Type", - "ENABLE_AUDIT_LOG_EVENT_TYPE_TOOLTIP": "The comma-separated name of the audit log event to be enabled.", - "AUDIT_LOG_EVENT_TYPE_EMPTY": "No audit log event type exists.", + "FORWARD_ENDPOINT_TOOLTIP": "將稽核日誌轉發至 syslog 端點,例如:harbor-log:10514", + "ENABLE_AUDIT_LOG_EVENT_TYPE": "啟用稽核日誌事件類型", + "ENABLE_AUDIT_LOG_EVENT_TYPE_TOOLTIP": "以逗號分隔的要啟用的稽核日誌事件名稱。", + "AUDIT_LOG_EVENT_TYPE_EMPTY": "不存在任何稽核日誌事件類型。", "SKIP_DATABASE": "跳過稽核日誌資料庫", - "SKIP_DATABASE_TOOLTIP": "跳過在資料庫中記錄稽核日誌,僅在設定稽核日誌轉發端點時可用", - "STOP_GC_SUCCESS": "成功觸發停止清理垃圾操作", - "STOP_PURGE_SUCCESS": "成功觸發停止清除操作", - "NO_GC_RECORDS": "找不到任何清理垃圾歷史記錄!", + "SKIP_DATABASE_TOOLTIP": "跳過在資料庫中記錄稽核日誌,僅在設定稽核日誌轉發端點時可用。", + "STOP_GC_SUCCESS": "已成功觸發停止垃圾回收操作。", + "STOP_PURGE_SUCCESS": "已成功觸發停止清除操作。", + "NO_GC_RECORDS": "找不到任何垃圾回收歷史記錄!", "NO_PURGE_RECORDS": "找不到任何清除歷史記錄!" }, "CVE_EXPORT": { @@ -1858,87 +1870,87 @@ "CVE_IDS": "CVE ID", "EXPORT_BUTTON": "匯出", "JOB_NAME": "工作名稱", - "JOB_NAME_REQUIRED": "工作名稱為必填選項", - "JOB_NAME_EXISTING": "工作名稱已存在", - "TRIGGER_EXPORT_SUCCESS": "成功觸發 CVE 匯出!" + "JOB_NAME_REQUIRED": "必須輸入工作名稱。", + "JOB_NAME_EXISTING": "工作名稱已存在。", + "TRIGGER_EXPORT_SUCCESS": "已成功觸發匯出 CVE!" }, "JOB_SERVICE_DASHBOARD": { - "SCHEDULE_PAUSED": "已排程(暫停)", - "SCHEDULE_BEEN_PAUSED": "{{param}} 已被暫停", - "PENDING_JOBS": "等待中的工作佇列", + "SCHEDULE_PAUSED": "已排程 (已暫停)", + "SCHEDULE_BEEN_PAUSED": "{{param}} 已暫停", + "PENDING_JOBS": "佇列中的等待工作", "OTHERS": "其他", "STOP_ALL": "全部停止", - "CONFIRM_STOP_ALL": "確認停止全部", - "CONFIRM_STOP_ALL_CONTENT": "您確定要停止所有的工作佇列嗎?", - "STOP_ALL_SUCCESS": "成功停止所有的工作佇列", + "CONFIRM_STOP_ALL": "確認全部停止", + "CONFIRM_STOP_ALL_CONTENT": "您確定要停止所有工作佇列嗎?", + "STOP_ALL_SUCCESS": "已成功停止所有工作佇列。", "STOP_BTN": "停止", "PAUSE_BTN": "暫停", - "RESUME_BTN": "恢復", + "RESUME_BTN": "繼續", "JOB_TYPE": "工作類型", "PENDING_COUNT": "等待數量", "LATENCY": "延遲", "PAUSED": "已暫停", - "NO_JOB_QUEUE": "我們找不到任何工作佇列", + "NO_JOB_QUEUE": "找不到任何工作佇列。", "CONFIRM_STOPPING_JOBS": "確認停止工作", "CONFIRM_STOPPING_JOBS_CONTENT": "您確定要停止工作 {{param}} 嗎?", "CONFIRM_PAUSING_JOBS": "確認暫停工作", "CONFIRM_PAUSING_JOBS_CONTENT": "您確定要暫停工作 {{param}} 嗎?", - "CONFIRM_RESUMING_JOBS": "確認恢復工作", - "CONFIRM_RESUMING_JOBS_CONTENT": "您確定要恢復工作 {{param}} 嗎?", - "STOP_SUCCESS": "成功停止工作", - "PAUSE_SUCCESS": "成功暫停工作", - "RESUME_SUCCESS": "成功恢復工作", + "CONFIRM_RESUMING_JOBS": "確認繼續工作", + "CONFIRM_RESUMING_JOBS_CONTENT": "您確定要繼續工作 {{param}} 嗎?", + "STOP_SUCCESS": "工作停止成功。", + "PAUSE_SUCCESS": "工作暫停成功。", + "RESUME_SUCCESS": "工作繼續成功。", "SCHEDULES": "排程", - "RUNNING_STATUS": "正在執行", - "RESUME_ALL_BTN_TEXT": "全部恢復", + "RUNNING_STATUS": "執行中", + "RESUME_ALL_BTN_TEXT": "全部繼續", "PAUSE_ALL_BTN_TEXT": "全部暫停", - "CONFIRM_PAUSING_ALL": "確認暫停全部", - "CONFIRM_PAUSING_ALL_CONTENT": "您確定要暫停所有的工作排程嗎?", - "CONFIRM_RESUMING_ALL": "確認恢復全部", - "CONFIRM_RESUMING_ALL_CONTENT": "您確定要恢復所有的工作排程嗎?", - "PAUSE_ALL_SUCCESS": "成功暫停所有的排程", - "RESUME_ALL_SUCCESS": "成功恢復所有的排程", + "CONFIRM_PAUSING_ALL": "確認全部暫停", + "CONFIRM_PAUSING_ALL_CONTENT": "您確定要暫停所有工作排程嗎?", + "CONFIRM_RESUMING_ALL": "確認全部繼續", + "CONFIRM_RESUMING_ALL_CONTENT": "您確定要繼續所有工作排程嗎?", + "PAUSE_ALL_SUCCESS": "所有排程已成功暫停。", + "RESUME_ALL_SUCCESS": "所有排程已成功繼續。", "VENDOR_TYPE": "供應商類型", "VENDOR_ID": "供應商 ID", - "NO_SCHEDULE": "我們找不到任何排程", + "NO_SCHEDULE": "找不到任何排程。", "WORKERS": "Worker", "FREE_ALL": "全部釋放", - "CONFIRM_FREE_ALL": "確認釋放全部", - "CONFIRM_FREE_ALL_CONTENT": "您確定要釋放所有的 Worker 嗎?", + "CONFIRM_FREE_ALL": "確認全部釋放", + "CONFIRM_FREE_ALL_CONTENT": "您確定要釋放所有 Worker 嗎?", "CONFIRM_FREE_WORKERS": "確認釋放 Worker", "CONFIRM_FREE_WORKERS_CONTENT": "您確定要釋放 Worker {{param}} 嗎?", - "FREE_WORKER_SUCCESS": "成功釋放 Worker", - "FREE_ALL_SUCCESS": "成功釋放所有的 Worker", - "WORKER_POOL": "Worker Pool", - "WORKER_POOL_ID": "Worker Pool ID", - "PID": "Pid", + "FREE_WORKER_SUCCESS": "Worker 釋放成功。", + "FREE_ALL_SUCCESS": "所有 Worker 已成功釋放。", + "WORKER_POOL": "Worker 池", + "WORKER_POOL_ID": "Worker 池 ID", + "PID": "PID", "START_AT": "開始於", "HEARTBEAT_AT": "最後回報時間", - "CONCURRENCY": "並行性", - "NO_WORKER_POOL": "我們找不到任何 Worker Pool", + "CONCURRENCY": "並行度", + "NO_WORKER_POOL": "找不到任何 Worker 池。", "FREE": "釋放", "WORKER_ID": "Worker ID", "JOB_ID": "工作 ID", "CHECK_IN_AT": "簽到於", - "NO_WORKER": "我們找不到任何 Worker", + "NO_WORKER": "找不到任何 Worker。", "JOB_QUEUE": "工作佇列", "JOB_SERVICE_DASHBOARD": "工作服務儀表板", "OPERATION_STOP_ALL_QUEUES": "停止所有工作佇列", "OPERATION_STOP_SPECIFIED_QUEUES": "停止指定的工作佇列", "OPERATION_PAUSE_SPECIFIED_QUEUES": "暫停指定的工作佇列", - "OPERATION_RESUME_SPECIFIED_QUEUES": "恢復指定的工作佇列", + "OPERATION_RESUME_SPECIFIED_QUEUES": "繼續指定的工作佇列", "OPERATION_PAUSE_SCHEDULE": "暫停所有排程", - "OPERATION_RESUME_SCHEDULE": "恢復所有排程", + "OPERATION_RESUME_SCHEDULE": "繼續所有排程", "OPERATION_FREE_ALL": "釋放所有 Worker", "OPERATION_FREE_SPECIFIED_WORKERS": "釋放指定的 Worker", - "QUEUE_STOP_BTN_INFO": "停止 — 停止並移除選定佇列中的所有工作。", + "QUEUE_STOP_BTN_INFO": "停止 — 停止並移除所選佇列中的所有工作。", "QUEUE_PAUSE_BTN_INFO": "暫停 — 暫停此類型工作佇列中的工作執行。佇列暫停時,仍可將工作加入佇列。", - "QUEUE_RESUME_BTN_INFO": "恢復 — 恢復此類型工作佇列中的工作執行。", + "QUEUE_RESUME_BTN_INFO": "繼續 — 繼續此類型工作佇列中的工作執行。", "SCHEDULE_PAUSE_BTN_INFO": "暫停 — 暫停所有排程的執行。", - "SCHEDULE_RESUME_BTN_INFO": "恢復 — 恢復所有排程的執行。", - "WORKER_FREE_BTN_INFO": "停止目前正在執行的工作以釋放 Worker", + "SCHEDULE_RESUME_BTN_INFO": "繼續 — 繼續所有排程的執行。", + "WORKER_FREE_BTN_INFO": "停止目前執行的工作以釋放 Worker。", "CRON": "Cron", - "WAITING_TOO_LONG_1": "有些工作已經等待執行超過 24 小時。請檢查工作服務", + "WAITING_TOO_LONG_1": "某些工作等待執行已超過 24 小時。請檢查工作服務", "WAITING_TOO_LONG_2": "儀表板。", "WAITING_TOO_LONG_3": "更多詳細資訊,請參考", "WAITING_TOO_LONG_4": "Wiki。" @@ -1951,7 +1963,7 @@ "EXPAND": "展開", "COLLAPSE": "收合", "MORE": "更多", - "SELECT": "選擇", + "SELECT": "選取", "SELECT_ALL": "全選", "PREVIOUS": "上一個", "NEXT": "下一個", @@ -1965,33 +1977,47 @@ "SHOW_COLUMNS": "顯示欄位", "SORT_COLUMNS": "排序欄位", "FIRST_PAGE": "第一頁", - "LAST_PAGE": "最後一頁", + "LAST_PAGE": "最末頁", "NEXT_PAGE": "下一頁", "PREVIOUS_PAGE": "上一頁", - "CURRENT_PAGE": "目前頁面", + "CURRENT_PAGE": "目前頁碼", "TOTAL_PAGE": "總頁數", "FILTER_ITEMS": "篩選項目", "MIN_VALUE": "最小值", "MAX_VALUE": "最大值", - "MODAL_CONTENT_START": "模態框內容開始", - "MODAL_CONTENT_END": "模態框內容結束", + "MODAL_CONTENT_START": "互動視窗內容開始", + "MODAL_CONTENT_END": "互動視窗內容結束", "SHOW_COLUMNS_MENU_DESCRIPTION": "顯示或隱藏欄位選單", - "ALL_COLUMNS_SELECTED": "已選擇所有欄位", - "SIGNPOST_TOGGLE": "標誌牌切換", - "SIGNPOST_CLOSE": "標誌牌關閉", - "LOADING": "載入中", + "ALL_COLUMNS_SELECTED": "已選取所有欄位", + "SIGNPOST_TOGGLE": "提示切換", + "SIGNPOST_CLOSE": "關閉提示", + "LOADING": "載入中...", "DATE_PICKER_DIALOG_LABEL": "選擇日期", "DATE_PICKER_TOGGLE_CHOOSE_DATE_LABEL": "選擇日期", "DATE_PICKER_TOGGLE_CHANGE_DATE_LABEL": "變更日期,{SELECTED_DATE}", "DATE_PICKER_PREVIOUS_MONTH": "上個月", "DATE_PICKER_CURRENT_MONTH": "本月", "DATE_PICKER_NEXT_MONTH": "下個月", - "DATE_PICKER_PREVIOUS_DECADE": "上個十年", - "DATE_PICKER_NEXT_DECADE": "下個十年", - "DATE_PICKER_CURRENT_DECADE": "本十年", - "DATE_PICKER_SELECT_MONTH_TEXT": "選擇月份,目前月份為 {CALENDAR_MONTH}", - "DATE_PICKER_SELECT_YEAR_TEXT": "選擇年份,目前年份為 {CALENDAR_YEAR}", - "DATE_PICKER_SELECTED_LABEL": "{FULL_DATE} - 已選擇" + "DATE_PICKER_PREVIOUS_DECADE": "前十年", + "DATE_PICKER_NEXT_DECADE": "後十年", + "DATE_PICKER_CURRENT_DECADE": "目前這個十年", + "DATE_PICKER_SELECT_YEAR_BUTTON_LABEL": "選擇年份", + "DATE_PICKER_SELECT_MONTH_BUTTON_LABEL": "選擇月份", + "DATE_PICKER_SELECT_DAY_BUTTON_LABEL": "選擇日期", + "DATE_PICKER_MONTH_WIDE": "月份", + "DATE_PICKER_MONTH_NARROW": "月", + "DATE_PICKER_DAY_WIDE": "日期", + "DATE_PICKER_DAY_NARROW": "日", + "DATE_PICKER_YEAR": "年份", + "DATAGRID_FOOTER_ROWS_PER_PAGE": "每頁列數", + "DATAGRID_FOOTER_TOTAL_ITEMS": "總項目數", + "DATAGRID_FOOTER_SELECTED_ITEMS": "已選項目", + "DATAGRID_FOOTER_FIRST_PAGE": "第一頁", + "DATAGRID_FOOTER_LAST_PAGE": "最末頁", + "DATAGrid_FOOTER_NEXT_PAGE": "下一頁", + "DATAGRID_FOOTER_PREVIOUS_PAGE": "上一頁", + "DATAGRID_FOOTER_PAGE_INPUT_LABEL": "頁碼", + "DATAGRID_FOOTER_PAGE_SIZE_INPUT_LABEL": "每頁大小" }, "BANNER_MESSAGE": { "BANNER_MESSAGE": "橫幅訊息", @@ -2006,14 +2032,14 @@ "ENTER_MESSAGE": "在此輸入您的訊息" }, "SECURITY_HUB": { - "SECURITY_HUB": "安全中心", - "ARTIFACTS": "Artifact(s)", + "SECURITY_HUB": "安全中樞", + "ARTIFACTS": "Artifacts", "SCANNED": "已掃描", "NOT_SCANNED": "未掃描", - "TOTAL_VUL": "總計弱點", - "TOTAL_AND_FIXABLE": "總共 {{totalNum}} 個,其中 {{fixableNum}} 個可修復", - "TOP_5_ARTIFACT": "前五名危險的 Artifacts", - "TOP_5_CVE": "前五名危險的 CVE", + "TOTAL_VUL": "弱點總數", + "TOTAL_AND_FIXABLE": "總計 {{totalNum}} 個,其中 {{fixableNum}} 個可修復", + "TOP_5_ARTIFACT": "最危險的前 5 個 Artifact", + "TOP_5_CVE": "最危險的前 5 個 CVE", "CVE_ID": "CVE ID", "VUL": "弱點", "CVE": "CVE", @@ -2023,8 +2049,8 @@ "SEARCH": "搜尋", "REPO_NAME": "儲存庫名稱", "TOOLTIP": "除了 CVSS3 之外,所有的篩選條件都只支援完全符合", - "NO_VUL": "我們找不到任何弱點", - "INVALID_VALUE": "The CVSS3 score should range between 0 and 10", - "PAGE_TITLE_TOOLTIP": "The comprehensive artifact count comprises the cumulative total of individual artifacts, including artifact accessories, as well as child artifacts associated with the image index and CNAB artifacts" + "NO_VUL": "找不到任何弱點", + "INVALID_VALUE": "CVSS3 分數應介於 0 到 10", + "PAGE_TITLE_TOOLTIP": "完整 Artifact 數量包含所有單一 Artifact 的總和,且包括 Artifact 附件,以及與 OCI 索引及 CNAB Artifact 相關聯的子 Artifact" } } diff --git a/src/server/middleware/repoproxy/proxy.go b/src/server/middleware/repoproxy/proxy.go index 75abdcae26a..a3524add3f6 100644 --- a/src/server/middleware/repoproxy/proxy.go +++ b/src/server/middleware/repoproxy/proxy.go @@ -19,6 +19,7 @@ import ( "fmt" "io" "net/http" + "os" "strings" "time" @@ -33,18 +34,21 @@ import ( httpLib "github.com/goharbor/harbor/src/lib/http" "github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/lib/redis" proModels "github.com/goharbor/harbor/src/pkg/project/models" + "github.com/goharbor/harbor/src/pkg/proxy/connection" "github.com/goharbor/harbor/src/pkg/reg/model" "github.com/goharbor/harbor/src/server/middleware" ) const ( - contentLength = "Content-Length" - contentType = "Content-Type" - dockerContentDigest = "Docker-Content-Digest" - etag = "Etag" - ensureTagInterval = 10 * time.Second - ensureTagMaxRetry = 60 + contentLength = "Content-Length" + contentType = "Content-Type" + dockerContentDigest = "Docker-Content-Digest" + etag = "Etag" + ensureTagInterval = 10 * time.Second + ensureTagMaxRetry = 60 + upstreamRegistryLimitOnProject = "UPSTREAM_REGISTRY_LIMIT_ON_PROJECT" // if UPSTREAM_REGISTRY_LIMIT_ON_PROJECT is true, the upstream registry connection is based on project level, by default it is artifact level ) var tooManyRequestsError = errors.New("too many requests to upstream registry").WithCode(errors.RateLimitCode) @@ -99,6 +103,22 @@ func handleBlob(w http.ResponseWriter, r *http.Request, next http.Handler) error next.ServeHTTP(w, r) return nil } + + if p.MaxUpstreamConnection() > 0 { + client, err := redis.GetHarborClient() + if err != nil { + return errors.NewErrs(err) + } + key := upstreamRegistryConnectionKey(art) + log.Debugf("handle blob, upstream registry connection limit key: %s", key) + if !connection.Limiter.Acquire(ctx, client, key, p.MaxUpstreamConnection()) { + log.Infof("current connection exceed max connections to upstream registry") + // send http code 429 to client + return tooManyRequestsError + } + defer connection.Limiter.Release(context.Background(), client, key) // use background context in defer to avoid been canceled + } + size, reader, err := proxyCtl.ProxyBlob(ctx, p, art) if err != nil { return err @@ -173,6 +193,15 @@ func defaultBlobURL(projectName string, name string, digest string) string { return fmt.Sprintf("/v2/%s/library/%s/blobs/%s", projectName, name, digest) } +// upstreamRegistryConnectionKey get upstream registry connection key +func upstreamRegistryConnectionKey(art lib.ArtifactInfo) string { + limitOnProject := os.Getenv(upstreamRegistryLimitOnProject) + if strings.EqualFold("true", limitOnProject) { + return fmt.Sprintf("{upstream_registry_connection}:%s", art.ProjectName) + } + return fmt.Sprintf("{upstream_registry_connection}:%s:%s", art.Repository, art.Digest) +} + func handleManifest(w http.ResponseWriter, r *http.Request, next http.Handler) error { ctx := r.Context() art, p, proxyCtl, err := preCheck(ctx, true) @@ -219,6 +248,20 @@ func handleManifest(w http.ResponseWriter, r *http.Request, next http.Handler) e next.ServeHTTP(w, r) return nil } + if p.MaxUpstreamConnection() > 0 { + client, err := redis.GetHarborClient() + if err != nil { + return errors.NewErrs(err) + } + key := upstreamRegistryConnectionKey(art) + log.Debugf("handle manifest key %v", key) + if !connection.Limiter.Acquire(ctx, client, key, p.MaxUpstreamConnection()) { + log.Infof("current connection exceed max connections to upstream registry") + // send http code 429 to client + return tooManyRequestsError + } + defer connection.Limiter.Release(context.Background(), client, key) // use background context in defer to avoid been canceled + } log.Debugf("the tag is %v, digest is %v", art.Tag, art.Digest) if r.Method == http.MethodHead { diff --git a/src/server/middleware/util/util.go b/src/server/middleware/util/util.go index 3da05752650..7a1ef29eccc 100644 --- a/src/server/middleware/util/util.go +++ b/src/server/middleware/util/util.go @@ -41,8 +41,8 @@ func ParseProjectName(r *http.Request) string { } for _, prefix := range prefixes { - if strings.HasPrefix(path, prefix) { - parts := strings.Split(strings.TrimPrefix(path, prefix), "/") + if after, ok := strings.CutPrefix(path, prefix); ok { + parts := strings.Split(after, "/") if len(parts) > 0 { projectName = parts[0] break diff --git a/src/server/registry/manifest.go b/src/server/registry/manifest.go index 6cf02486a88..513e62a5729 100644 --- a/src/server/registry/manifest.go +++ b/src/server/registry/manifest.go @@ -15,7 +15,9 @@ package registry import ( + "bytes" "fmt" + "io" "net/http" "strings" @@ -182,7 +184,26 @@ func putManifest(w http.ResponseWriter, req *http.Request) { } buffer := lib.NewResponseBuffer(w) + // proxy the req to the backend docker registry + // If reference is a tag (not a digest), replace it with the computed digest + // before proxying to the backend. This prevents tags from being stored in the + // backend registry storage, while Harbor maintains the tag-to-digest mapping in the database. + if _, err := digest.Parse(reference); err != nil { + // reference is a tag, not a digest + data, err := io.ReadAll(req.Body) + if err != nil { + lib_http.SendError(w, err) + return + } + + dgst := digest.FromBytes(data) + req = req.Clone(req.Context()) + req.URL.Path = strings.TrimSuffix(req.URL.Path, reference) + dgst.String() + req.URL.RawPath = req.URL.EscapedPath() + req.Body = io.NopCloser(bytes.NewReader(data)) + req.ContentLength = int64(len(data)) + } proxy.ServeHTTP(buffer, req) if !buffer.Success() { if _, err := buffer.Flush(); err != nil { @@ -198,15 +219,14 @@ func putManifest(w http.ResponseWriter, req *http.Request) { var tags []string dgt := reference // the reference is tag, get the digest from the response header - if _, err = digest.Parse(reference); err != nil { + if _, err := digest.Parse(reference); err != nil { dgt = buffer.Header().Get("Docker-Content-Digest") tags = append(tags, reference) } - _, _, err = artifact.Ctl.Ensure(req.Context(), repo, dgt, &artifact.ArtOption{ + if _, _, err := artifact.Ctl.Ensure(req.Context(), repo, dgt, &artifact.ArtOption{ Tags: tags, - }) - if err != nil { + }); err != nil { lib_http.SendError(w, err) return } diff --git a/src/server/registry/manifest_test.go b/src/server/registry/manifest_test.go index 39af2f66087..bf228746bb3 100644 --- a/src/server/registry/manifest_test.go +++ b/src/server/registry/manifest_test.go @@ -15,12 +15,16 @@ package registry import ( + "bytes" "context" + "io" "net/http" "net/http/httptest" + "strings" "testing" beegocontext "github.com/beego/beego/v2/server/web/context" + "github.com/opencontainers/go-digest" "github.com/stretchr/testify/suite" "github.com/goharbor/harbor/src/controller/artifact" @@ -71,18 +75,14 @@ func (m *manifestTestSuite) TearDownSuite() { } func (m *manifestTestSuite) TestGetManifest() { - // doesn't exist req := httptest.NewRequest(http.MethodGet, "/v2/library/hello-world/manifests/latest", nil) w := &httptest.ResponseRecorder{} - mock.OnAnything(m.artCtl, "GetByReference").Return(nil, errors.New(nil).WithCode(errors.NotFoundCode)) getManifest(w, req) m.Equal(http.StatusNotFound, w.Code) - // reset the mock m.SetupTest() - // exist proxy = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { if req.Method == http.MethodGet && req.URL.Path == "/v2/library/hello-world/manifests/sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180" { w.WriteHeader(http.StatusOK) @@ -91,7 +91,6 @@ func (m *manifestTestSuite) TestGetManifest() { w.WriteHeader(http.StatusNotFound) }) - // as we cannot set the beego input in the context, here the request doesn't carry reference part req = httptest.NewRequest(http.MethodGet, "/v2/library/hello-world/manifests/", nil) w = &httptest.ResponseRecorder{} @@ -101,14 +100,12 @@ func (m *manifestTestSuite) TestGetManifest() { getManifest(w, req) m.Equal(http.StatusOK, w.Code) - // if etag match, return 304 req = httptest.NewRequest(http.MethodGet, "/v2/library/hello-world/manifests/", nil) w = &httptest.ResponseRecorder{} req.Header.Set("If-None-Match", "sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180") getManifest(w, req) m.Equal(http.StatusNotModified, w.Code) - // should get from cache if enable cache. config.DefaultMgr().Set(req.Context(), "cache_enabled", true) defer config.DefaultMgr().Set(req.Context(), "cache_enabled", false) req = httptest.NewRequest(http.MethodGet, "/v2/library/hello-world/manifests/", nil) @@ -121,21 +118,14 @@ func (m *manifestTestSuite) TestGetManifest() { } func (m *manifestTestSuite) TestDeleteManifest() { - // doesn't exist req := httptest.NewRequest(http.MethodDelete, "/v2/library/hello-world/manifests/latest", nil) w := &httptest.ResponseRecorder{} - mock.OnAnything(m.artCtl, "GetByReference").Return(nil, errors.New(nil).WithCode(errors.NotFoundCode)) deleteManifest(w, req) m.Equal(http.StatusBadRequest, w.Code) - // reset the mock - m.SetupTest() - - // reset the mock m.SetupTest() - // exist proxy = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { if req.Method == http.MethodPut && req.URL.Path == "/v2/library/hello-world/manifests/latest" { w.WriteHeader(http.StatusInternalServerError) @@ -154,10 +144,8 @@ func (m *manifestTestSuite) TestDeleteManifest() { deleteManifest(w, req) m.Equal(http.StatusAccepted, w.Code) - // should get from cache if enable cache. config.DefaultMgr().Set(req.Context(), "cache_enabled", true) defer config.DefaultMgr().Set(req.Context(), "cache_enabled", false) - // should delete cache when manifest be deleted. req = httptest.NewRequest(http.MethodDelete, "/v2/library/hello-world/manifests/sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180", nil) input = &beegocontext.BeegoInput{} input.SetParam(":reference", "sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180") @@ -170,33 +158,44 @@ func (m *manifestTestSuite) TestDeleteManifest() { } func (m *manifestTestSuite) TestPutManifest() { + manifestContent := []byte(`{"schemaVersion":2}`) + expectedDigest := digest.FromBytes(manifestContent).String() + // the backend registry response with 500 proxy = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - if req.Method == http.MethodPut && req.URL.Path == "/v2/library/hello-world/manifests/latest" { + if req.Method == http.MethodPut && strings.Contains(req.URL.Path, "/v2/library/hello-world/manifests/") { w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusNotFound) }) - req := httptest.NewRequest(http.MethodPut, "/v2/library/hello-world/manifests/latest", nil) + req := httptest.NewRequest(http.MethodPut, "/v2/library/hello-world/manifests/latest", bytes.NewReader(manifestContent)) + input := &beegocontext.BeegoInput{} + input.SetParam(":splat", "library/hello-world") + input.SetParam(":reference", "latest") + *req = *(req.WithContext(context.WithValue(req.Context(), router.ContextKeyInput{}, input))) w := &httptest.ResponseRecorder{} mock.OnAnything(m.repoCtl, "Ensure").Return(false, int64(1), nil) putManifest(w, req) m.Equal(http.StatusInternalServerError, w.Code) - // reset the mock m.SetupTest() - // // the backend registry serves the request successfully + // the backend registry serves the request successfully proxy = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - if req.Method == http.MethodPut && req.URL.Path == "/v2/library/hello-world/manifests/latest" { - w.Header().Set("Docker-Content-Digest", "sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180") + // After our changes, the URL path will contain the digest, not the tag + if req.Method == http.MethodPut && strings.Contains(req.URL.Path, "/v2/library/hello-world/manifests/") { + w.Header().Set("Docker-Content-Digest", expectedDigest) w.WriteHeader(http.StatusCreated) return } w.WriteHeader(http.StatusNotFound) }) - req = httptest.NewRequest(http.MethodPut, "/v2/library/hello-world/manifests/latest", nil) + req = httptest.NewRequest(http.MethodPut, "/v2/library/hello-world/manifests/latest", bytes.NewReader(manifestContent)) + input = &beegocontext.BeegoInput{} + input.SetParam(":splat", "library/hello-world") + input.SetParam(":reference", "latest") + *req = *(req.WithContext(context.WithValue(req.Context(), router.ContextKeyInput{}, input))) w = &httptest.ResponseRecorder{} mock.OnAnything(m.repoCtl, "Ensure").Return(false, int64(1), nil) mock.OnAnything(m.artCtl, "Ensure").Return(true, int64(1), nil) @@ -204,6 +203,124 @@ func (m *manifestTestSuite) TestPutManifest() { m.Equal(http.StatusCreated, w.Code) } +func (m *manifestTestSuite) TestPutManifestWithTagToDigestReplacement() { + manifestContent := []byte(`{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 1234, + "digest": "sha256:abcd1234" + } + }`) + expectedDigest := digest.FromBytes(manifestContent).String() + + var proxyRequest *http.Request + proxy = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + proxyRequest = req + if !strings.Contains(req.URL.Path, expectedDigest) { + m.T().Errorf("Expected URL path to contain digest %s, got %s", expectedDigest, req.URL.Path) + } + body, err := io.ReadAll(req.Body) + if err != nil { + m.T().Errorf("Failed to read body: %v", err) + } + if len(body) == 0 { + m.T().Error("Request body is empty - body restoration failed") + } + if !bytes.Equal(body, manifestContent) { + m.T().Errorf("Body content mismatch. Expected %s, got %s", string(manifestContent), string(body)) + } + if req.ContentLength != int64(len(manifestContent)) { + m.T().Errorf("ContentLength mismatch. Expected %d, got %d", len(manifestContent), req.ContentLength) + } + w.Header().Set("Docker-Content-Digest", expectedDigest) + w.WriteHeader(http.StatusCreated) + }) + + req := httptest.NewRequest(http.MethodPut, "/v2/library/hello-world/manifests/latest", bytes.NewReader(manifestContent)) + req.Header.Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json") + input := &beegocontext.BeegoInput{} + input.SetParam(":splat", "library/hello-world") + input.SetParam(":reference", "latest") + *req = *(req.WithContext(context.WithValue(req.Context(), router.ContextKeyInput{}, input))) + + w := &httptest.ResponseRecorder{} + mock.OnAnything(m.repoCtl, "Ensure").Return(false, int64(1), nil) + mock.OnAnything(m.artCtl, "Ensure").Return(true, int64(1), nil) + putManifest(w, req) + m.Equal(http.StatusCreated, w.Code) + m.NotNil(proxyRequest, "Request was not captured by proxy") + m.Contains(proxyRequest.URL.Path, expectedDigest, "URL should contain digest") +} + +func (m *manifestTestSuite) TestPutManifestWithDigest() { + manifestContent := []byte(`{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 1234, + "digest": "sha256:abcd1234" + } + }`) + providedDigest := "sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180" + + var proxyRequest *http.Request + proxy = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + proxyRequest = req + if !strings.Contains(req.URL.Path, providedDigest) { + m.T().Errorf("Expected URL path to contain original digest %s, got %s", providedDigest, req.URL.Path) + } + body, err := io.ReadAll(req.Body) + if err != nil { + m.T().Errorf("Failed to read body: %v", err) + } + if len(body) == 0 { + m.T().Error("Request body is empty") + } + w.Header().Set("Docker-Content-Digest", providedDigest) + w.WriteHeader(http.StatusCreated) + }) + + req := httptest.NewRequest(http.MethodPut, "/v2/library/hello-world/manifests/"+providedDigest, bytes.NewReader(manifestContent)) + req.Header.Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json") + input := &beegocontext.BeegoInput{} + input.SetParam(":splat", "library/hello-world") + input.SetParam(":reference", providedDigest) + *req = *(req.WithContext(context.WithValue(req.Context(), router.ContextKeyInput{}, input))) + + w := &httptest.ResponseRecorder{} + mock.OnAnything(m.repoCtl, "Ensure").Return(false, int64(1), nil) + mock.OnAnything(m.artCtl, "Ensure").Return(true, int64(1), nil) + putManifest(w, req) + m.Equal(http.StatusCreated, w.Code) + m.NotNil(proxyRequest, "Request was not captured by proxy") + m.Contains(proxyRequest.URL.Path, providedDigest, "URL should contain original digest") +} + +func (m *manifestTestSuite) TestPutManifestEmptyBody() { + proxy = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + body, _ := io.ReadAll(req.Body) + dgst := digest.FromBytes(body).String() + w.Header().Set("Docker-Content-Digest", dgst) + w.WriteHeader(http.StatusCreated) + }) + + req := httptest.NewRequest(http.MethodPut, "/v2/library/empty/manifests/latest", bytes.NewReader([]byte{})) + input := &beegocontext.BeegoInput{} + input.SetParam(":splat", "library/empty") + input.SetParam(":reference", "latest") + *req = *(req.WithContext(context.WithValue(req.Context(), router.ContextKeyInput{}, input))) + + w := &httptest.ResponseRecorder{} + mock.OnAnything(m.repoCtl, "Ensure").Return(false, int64(1), nil) + mock.OnAnything(m.artCtl, "Ensure").Return(true, int64(1), nil) + + putManifest(w, req) + m.Equal(http.StatusCreated, w.Code) +} + func TestManifestTestSuite(t *testing.T) { suite.Run(t, &manifestTestSuite{}) } diff --git a/src/server/v2.0/handler/gc.go b/src/server/v2.0/handler/gc.go index 03ca356dada..924077597b2 100644 --- a/src/server/v2.0/handler/gc.go +++ b/src/server/v2.0/handler/gc.go @@ -100,6 +100,9 @@ func (g *gcAPI) kick(ctx context.Context, scheType string, cron string, paramete if deleteUntagged, ok := parameters["delete_untagged"].(bool); ok { policy.DeleteUntagged = deleteUntagged } + if deleteTag, ok := parameters["delete_tag"].(bool); ok { + policy.DeleteTag = deleteTag + } if workers, ok := parameters["workers"].(json.Number); ok { wInt, err := workers.Int64() if err != nil { @@ -124,6 +127,9 @@ func (g *gcAPI) kick(ctx context.Context, scheType string, cron string, paramete if deleteUntagged, ok := parameters["delete_untagged"].(bool); ok { policy.DeleteUntagged = deleteUntagged } + if deleteTag, ok := parameters["delete_tag"].(bool); ok { + policy.DeleteTag = deleteTag + } if workers, ok := parameters["workers"].(json.Number); ok { wInt, err := workers.Int64() if err != nil { diff --git a/src/server/v2.0/handler/project.go b/src/server/v2.0/handler/project.go index 23d48357549..de75072b8c7 100644 --- a/src/server/v2.0/handler/project.go +++ b/src/server/v2.0/handler/project.go @@ -163,9 +163,10 @@ func (a *projectAPI) CreateProject(ctx context.Context, params operation.CreateP } } - // ignore metadata.proxy_speed_kb for non-proxy-cache project + // ignore metadata.proxy_speed_kb and metadata.max_upstream_conn for non-proxy-cache project if req.RegistryID == nil { req.Metadata.ProxySpeedKb = nil + req.Metadata.MaxUpstreamConn = nil } // ignore enable_content_trust metadata for proxy cache project @@ -566,9 +567,10 @@ func (a *projectAPI) UpdateProject(ctx context.Context, params operation.UpdateP } } - // ignore metadata.proxy_speed_kb for non-proxy-cache project + // ignore metadata.proxy_speed_kb and metadata.max_upstream_conn for non-proxy-cache project if params.Project.Metadata != nil && !p.IsProxy() { params.Project.Metadata.ProxySpeedKb = nil + params.Project.Metadata.MaxUpstreamConn = nil } // ignore enable_content_trust metadata for proxy cache project @@ -818,6 +820,12 @@ func (a *projectAPI) validateProjectReq(ctx context.Context, req *models.Project return errors.BadRequestError(nil).WithMessagef("metadata.proxy_speed_kb should by an int32, but got: '%s', err: %s", *ps, err) } } + + if cnt := req.Metadata.MaxUpstreamConn; cnt != nil { + if _, err := strconv.ParseInt(*cnt, 10, 32); err != nil { + return errors.BadRequestError(nil).WithMessagef("metadata.max_upstream_conn should be an int, but got '%s', err: %s", *cnt, err) + } + } } if req.StorageLimit != nil { diff --git a/src/server/v2.0/handler/project_metadata.go b/src/server/v2.0/handler/project_metadata.go index b58c59ffa66..61933af25bf 100644 --- a/src/server/v2.0/handler/project_metadata.go +++ b/src/server/v2.0/handler/project_metadata.go @@ -161,6 +161,12 @@ func (p *projectMetadataAPI) validate(metas map[string]string) (map[string]strin return nil, errors.New(nil).WithCode(errors.BadRequestCode).WithMessagef("invalid value: %s", value) } metas[proModels.ProMetaProxySpeed] = strconv.FormatInt(v, 10) + case proModels.ProMetaMaxUpstreamConn: + v, err := strconv.ParseInt(value, 10, 32) + if err != nil { + return nil, errors.New(nil).WithCode(errors.BadRequestCode).WithMessagef("invalid value: %s", value) + } + metas[proModels.ProMetaMaxUpstreamConn] = strconv.FormatInt(v, 10) default: return nil, errors.New(nil).WithCode(errors.BadRequestCode).WithMessagef("invalid key: %s", key) } diff --git a/src/server/v2.0/handler/project_metadata_test.go b/src/server/v2.0/handler/project_metadata_test.go new file mode 100644 index 00000000000..46a8f398aeb --- /dev/null +++ b/src/server/v2.0/handler/project_metadata_test.go @@ -0,0 +1,76 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handler + +import ( + "testing" + + proModels "github.com/goharbor/harbor/src/pkg/project/models" + "github.com/stretchr/testify/assert" +) + +func TestValidate(t *testing.T) { + api := &projectMetadataAPI{} + + tests := []struct { + name string + metas map[string]string + expectErr bool + }{ + { + name: "Invalid max upstream conn value", + metas: map[string]string{proModels.ProMetaMaxUpstreamConn: "invalid"}, + expectErr: true, + }, + { + name: "max upstream conn value 0", + metas: map[string]string{proModels.ProMetaMaxUpstreamConn: "0"}, + expectErr: false, + }, + { + name: "max upstream conn value -1", + metas: map[string]string{proModels.ProMetaMaxUpstreamConn: "-1"}, + expectErr: false, + }, + { + name: "normal max upstream conn value", + metas: map[string]string{proModels.ProMetaMaxUpstreamConn: "30"}, + expectErr: false, + }, + { + name: "Unsupported key", + metas: map[string]string{"unsupported_key": "value"}, + expectErr: true, + }, + { + name: "Empty map", + metas: map[string]string{}, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := api.validate(tt.metas) + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.NotNil(t, result) + } + }) + } +} diff --git a/src/server/v2.0/handler/robot.go b/src/server/v2.0/handler/robot.go index 221a0725779..a49d52cc7b4 100644 --- a/src/server/v2.0/handler/robot.go +++ b/src/server/v2.0/handler/robot.go @@ -31,10 +31,8 @@ import ( "github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/controller/robot" "github.com/goharbor/harbor/src/lib" - "github.com/goharbor/harbor/src/lib/config" "github.com/goharbor/harbor/src/lib/errors" "github.com/goharbor/harbor/src/lib/log" - "github.com/goharbor/harbor/src/lib/q" "github.com/goharbor/harbor/src/pkg/permission/types" pkg "github.com/goharbor/harbor/src/pkg/robot/model" "github.com/goharbor/harbor/src/server/v2.0/handler/model" @@ -87,6 +85,12 @@ func (rAPI *robotAPI) CreateRobot(ctx context.Context, params operation.CreateRo case *local.SecurityContext: creatorRef = int64(s.User().UserID) case *robotSc.SecurityContext: + if s.User() == nil { + return rAPI.SendError(ctx, errors.New(nil).WithMessage("invalid security context: empty robot account")) + } + if !isValidPermissionScope(params.Robot.Permissions, s.User().Permissions) { + return rAPI.SendError(ctx, errors.New(nil).WithMessagef("permission scope is invalid. It must be equal to or more restrictive than the creator robot's permissions: %s", s.User().Name).WithCode(errors.DENIED)) + } creatorRef = s.User().ID default: return rAPI.SendError(ctx, errors.New(nil).WithMessage("invalid security context")) @@ -102,25 +106,6 @@ func (rAPI *robotAPI) CreateRobot(ctx context.Context, params operation.CreateRo return rAPI.SendError(ctx, err) } - if _, ok := sc.(*robotSc.SecurityContext); ok { - creatorRobots, err := rAPI.robotCtl.List(ctx, q.New(q.KeyWords{ - "name": strings.TrimPrefix(sc.GetUsername(), config.RobotPrefix(ctx)), - "project_id": r.ProjectID, - }), &robot.Option{ - WithPermission: true, - }) - if err != nil { - return rAPI.SendError(ctx, err) - } - if len(creatorRobots) == 0 { - return rAPI.SendError(ctx, errors.DeniedError(nil)) - } - - if !isValidPermissionScope(params.Robot.Permissions, creatorRobots[0].Permissions) { - return rAPI.SendError(ctx, errors.New(nil).WithMessagef("permission scope is invalid. It must be equal to or more restrictive than the creator robot's permissions: %s", creatorRobots[0].Name).WithCode(errors.DENIED)) - } - } - rid, pwd, err := rAPI.robotCtl.Create(ctx, r) if err != nil { return rAPI.SendError(ctx, err) diff --git a/src/server/v2.0/handler/user.go b/src/server/v2.0/handler/user.go index 05b1e51dbed..63233d59186 100644 --- a/src/server/v2.0/handler/user.go +++ b/src/server/v2.0/handler/user.go @@ -302,8 +302,8 @@ func (u *usersAPI) UpdateUserPassword(ctx context.Context, params operation.Upda if err := u.requireModifiable(ctx, uid); err != nil { return u.SendError(ctx, err) } - sctx, _ := security.FromContext(ctx) - if matchUserID(sctx, uid) { + if matchUserID(ctx, uid) { + sctx, _ := security.FromContext(ctx) ok, err := u.ctl.VerifyPassword(ctx, sctx.GetUsername(), params.Password.OldPassword) if err != nil { log.G(ctx).Errorf("Failed to verify password for user: %s, error: %v", sctx.GetUsername(), err) @@ -324,7 +324,7 @@ func (u *usersAPI) UpdateUserPassword(ctx context.Context, params operation.Upda } ok, err := u.ctl.VerifyPassword(ctx, user.Username, newPwd) if err != nil { - log.G(ctx).Errorf("Failed to verify password for user: %s, error: %v", sctx.GetUsername(), err) + log.G(ctx).Errorf("Failed to verify password for user: %s, error: %v", user.Username, err) return u.SendError(ctx, errors.UnknownError(nil).WithMessage("Failed to verify password")) } if ok { @@ -358,14 +358,10 @@ func (u *usersAPI) requireForCLISecret(ctx context.Context, id int) error { if a != common.OIDCAuth { return errors.PreconditionFailedError(nil).WithMessagef("unable to update CLI secret under authmode: %s", a) } - sctx, ok := security.FromContext(ctx) - if !ok || !sctx.IsAuthenticated() { - return errors.UnauthorizedError(nil) - } - if !matchUserID(sctx, id) && !sctx.Can(ctx, rbac.ActionUpdate, rbac.ResourceUser) { - return errors.ForbiddenError(nil).WithMessagef("Not authorized to update the CLI secret for user: %d", id) + if matchUserID(ctx, id) { + return nil } - return nil + return u.RequireSystemAccess(ctx, rbac.ActionUpdate, rbac.ResourceUser) } func (u *usersAPI) requireCreatable(ctx context.Context) error { @@ -393,25 +389,17 @@ func (u *usersAPI) requireCreatable(ctx context.Context) error { } func (u *usersAPI) requireReadable(ctx context.Context, id int) error { - sctx, ok := security.FromContext(ctx) - if !ok || !sctx.IsAuthenticated() { - return errors.UnauthorizedError(nil) - } - if !matchUserID(sctx, id) && !sctx.Can(ctx, rbac.ActionRead, rbac.ResourceUser) { - return errors.ForbiddenError(nil).WithMessagef("Not authorized to read user: %d", id) + if matchUserID(ctx, id) { + return nil } - return nil + return u.RequireSystemAccess(ctx, rbac.ActionRead, rbac.ResourceUser) } func (u *usersAPI) requireDeletable(ctx context.Context, id int) error { - sctx, ok := security.FromContext(ctx) - if !ok || !sctx.IsAuthenticated() { - return errors.UnauthorizedError(nil) - } - if !sctx.Can(ctx, rbac.ActionDelete, rbac.ResourceUser) { - return errors.ForbiddenError(nil).WithMessage("Not authorized to delete users") + if err := u.RequireSystemAccess(ctx, rbac.ActionDelete, rbac.ResourceUser); err != nil { + return err } - if matchUserID(sctx, id) || id == 1 { + if matchUserID(ctx, id) || id == 1 { return errors.ForbiddenError(nil).WithMessagef("User with ID %d cannot be deleted", id) } return nil @@ -422,27 +410,22 @@ func (u *usersAPI) requireModifiable(ctx context.Context, id int) error { if err != nil { return err } - sctx, ok := security.FromContext(ctx) - if !ok || !sctx.IsAuthenticated() { - return errors.UnauthorizedError(nil) + if a == common.DBAuth { + // In db auth, admin can update anyone's info, and regular user can update his own + if matchUserID(ctx, id) { + return nil + } + return u.RequireSystemAccess(ctx, rbac.ActionUpdate, rbac.ResourceUser) } - if !modifiable(ctx, a, id) { + // In none db auth, only the local admin's password can be updated. + if id != 1 { return errors.ForbiddenError(nil).WithMessagef("User with ID %d can't be updated", id) } - return nil + return u.RequireSystemAccess(ctx, rbac.ActionUpdate, rbac.ResourceUser) } -func modifiable(ctx context.Context, authMode string, id int) bool { +func matchUserID(ctx context.Context, id int) bool { sctx, _ := security.FromContext(ctx) - if authMode == common.DBAuth { - // In db auth, admin can update anyone's info, and regular user can update his own - return sctx.Can(ctx, rbac.ActionUpdate, rbac.ResourceUser) || matchUserID(sctx, id) - } - // In none db auth, only the local admin's password can be updated. - return id == 1 && sctx.Can(ctx, rbac.ActionUpdate, rbac.ResourceUser) -} - -func matchUserID(sctx security.Context, id int) bool { if localSCtx, ok := sctx.(*local.SecurityContext); ok { return localSCtx.User().UserID == id } diff --git a/src/server/v2.0/handler/user_test.go b/src/server/v2.0/handler/user_test.go index b13fadb57bc..a6f7b37b05b 100644 --- a/src/server/v2.0/handler/user_test.go +++ b/src/server/v2.0/handler/user_test.go @@ -62,9 +62,9 @@ func (uts *UserTestSuite) SetupSuite() { }, }, } + uts.Suite.SetupSuite() uts.Security.On("IsAuthenticated").Return(true) - } func (uts *UserTestSuite) TestUpdateUserPassword() { @@ -76,6 +76,7 @@ func (uts *UserTestSuite) TestUpdateUserPassword() { { url := "/users/2/password" uts.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(false).Times(1) + uts.Security.On("GetUsername").Return("testuser") res, err := uts.Suite.PutJSON(url, &body) uts.NoError(err) uts.Equal(403, res.StatusCode) @@ -83,6 +84,7 @@ func (uts *UserTestSuite) TestUpdateUserPassword() { { url := "/users/1/password" uts.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(1) + uts.Security.On("GetUsername").Return("admin") uts.uCtl.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(uts.user, nil).Times(1) uts.uCtl.On("VerifyPassword", mock.Anything, "admin", "Passw0rd").Return(true, nil).Times(1) diff --git a/src/testing/lib/config/manager.go b/src/testing/lib/config/manager.go index 276c80f0735..31038431b4d 100644 --- a/src/testing/lib/config/manager.go +++ b/src/testing/lib/config/manager.go @@ -76,6 +76,36 @@ func (_m *Manager) GetDatabaseCfg() *models.Database { return r0 } +// GetItemFromDriver provides a mock function with given fields: ctx, key +func (_m *Manager) GetItemFromDriver(ctx context.Context, key string) (map[string]interface{}, error) { + ret := _m.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for GetItemFromDriver") + } + + var r0 map[string]interface{} + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (map[string]interface{}, error)); ok { + return rf(ctx, key) + } + if rf, ok := ret.Get(0).(func(context.Context, string) map[string]interface{}); ok { + r0 = rf(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]interface{}) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, key) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetUserCfgs provides a mock function with given fields: ctx func (_m *Manager) GetUserCfgs(ctx context.Context) map[string]interface{} { ret := _m.Called(ctx) diff --git a/tests/ci/ut_install.sh b/tests/ci/ut_install.sh index a207780b388..25192197ace 100755 --- a/tests/ci/ut_install.sh +++ b/tests/ci/ut_install.sh @@ -4,7 +4,7 @@ set -x set -e sudo apt-get update && sudo apt-get install -y libldap2-dev -sudo go env -w GO111MODULE=auto +go env -w GO111MODULE=auto pwd # cd ./src # go get github.com/docker/distribution@latest diff --git a/tests/resources/TestCaseBody.robot b/tests/resources/TestCaseBody.robot index 4d3adf74a2e..5541bc467ac 100644 --- a/tests/resources/TestCaseBody.robot +++ b/tests/resources/TestCaseBody.robot @@ -16,10 +16,16 @@ Documentation This resource wrap test case body Library ../apitests/python/testutils.py Library ../apitests/python/library/repository.py +Library String *** Variables *** *** Keywords *** +Remove Port + [Arguments] ${address} + ${result}= Replace String ${address} :9443 ${EMPTY} + [Return] ${result} + Body Of Manage project publicity Init Chrome Driver ${d}= Get Current Date result_format=%m%s @@ -578,7 +584,8 @@ Verify Webhook By Tag Retention Finished Event Verify Webhook By Replication Status Changed Event [Arguments] ${project_name} ${webhook_name} ${project_dest_name} ${replication_rule_name} ${user} ${harbor_handle} ${webhook_handle} ${payload_format}=Default &{replication_finished_property}= Create Dictionary - Run Keyword If '${payload_format}' == 'Default' Set To Dictionary ${replication_finished_property} type=REPLICATION operator=${user} registry_type=harbor harbor_hostname=${ip} + ${cleaned_ip}= Remove Port ${ip} + Run Keyword If '${payload_format}' == 'Default' Set To Dictionary ${replication_finished_property} type=REPLICATION operator=${user} registry_type=harbor harbor_hostname=${cleaned_ip} ... ELSE Set To Dictionary ${replication_finished_property} specversion=1.0 type=harbor.replication.status.changed datacontenttype=application/json operator=${user} trigger_type=MANUAL namespace=${project_name} Switch Window ${webhook_handle} Delete All Requests diff --git a/tests/robot-cases/Group1-Nightly/P2P_Preheat.robot b/tests/robot-cases/Group1-Nightly/P2P_Preheat.robot index a5f66dfe2e0..fe7db21f37d 100644 --- a/tests/robot-cases/Group1-Nightly/P2P_Preheat.robot +++ b/tests/robot-cases/Group1-Nightly/P2P_Preheat.robot @@ -79,32 +79,3 @@ Test Case - P2P Preheat By Manual Delete A P2P Preheat Policy ${policy_name} Delete A Distribution ${dist_name} ${DISTRIBUTION_ENDPOINT} Close Browser - -Test Case - P2P Preheat By Event - [Tags] p2p_preheat_by_event need_distribution_endpoint - ${d}= Get Current Date result_format=%m%s - ${project_name}= Set Variable project_p2p${d} - ${dist_name}= Set Variable distribution${d} - ${policy_name}= Set Variable policy${d} - ${image1}= Set Variable busybox - ${image2}= Set Variable hello-world - ${tag1}= Set Variable latest - ${tag2}= Set Variable stable - ${label}= Set Variable p2p_preheat - Init Chrome Driver - Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} - Create An New Distribution Dragonfly ${dist_name} ${DISTRIBUTION_ENDPOINT} ${DRAGONFLY_AUTH_TOKEN} - Create An New Project And Go Into Project ${project_name} - Push Image With Tag ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} ${project_name} ${image1} ${tag1} ${tag1} - Push Image With Tag ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} ${project_name} ${image1} ${tag2} ${tag2} - Create An New P2P Preheat Policy ${policy_name} ${dist_name} ** ** Event based - Retry Double Keywords When Error Select P2P Preheat Policy ${policy_name} Wait Until Element Is Visible ${p2p_execution_header} - # Artifact is pushed event - Retry Action Keyword Verify Artifact Is Pushed Event ${project_name} ${policy_name} ${image2} ${tag1} - # Artifact is scanned event - Retry Action Keyword Verify Artifact Is Scanned Event ${project_name} ${policy_name} ${image1} ${tag1} - # Artifact is labeled event - Retry Action Keyword Verify Artifact Is Labeled Event ${project_name} ${policy_name} ${image1} ${tag2} ${label} - Delete A P2P Preheat Policy ${policy_name} - Delete A Distribution ${dist_name} ${DISTRIBUTION_ENDPOINT} - Close Browser \ No newline at end of file diff --git a/tests/robot-cases/Group1-Nightly/Replication.robot b/tests/robot-cases/Group1-Nightly/Replication.robot index 0b404173685..e1626caabe6 100644 --- a/tests/robot-cases/Group1-Nightly/Replication.robot +++ b/tests/robot-cases/Group1-Nightly/Replication.robot @@ -266,23 +266,6 @@ Test Case - Replication Of Pull Images from AWS-ECR To Self Image Should Be Replicated To Project project${d} hello-world Close Browser -Test Case - Replication Of Pull Images from Google-GCR To Self - Init Chrome Driver - ${d}= Get Current Date result_format=%m%s - #login source - Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} - Create An New Project And Go Into Project project${d} - Switch To Registries - Create A New Endpoint google-gcr e${d} asia.gcr.io ${null} ${gcr_ac_key} Y - Switch To Replication Manage - Create A Rule With Existing Endpoint rule${d} pull eminent-nation-87317/* image e${d} project${d} - Filter Replication Rule rule${d} - Select Rule And Replicate rule${d} - Check Latest Replication Job Status Succeeded - Image Should Be Replicated To Project project${d} httpd - Image Should Be Replicated To Project project${d} tomcat - Close Browser - Test Case - Replication Of Push Images to DockerHub Triggered By Event Body Of Replication Of Push Images to Registry Triggered By Event docker-hub https://hub.docker.com/ ${DOCKER_USER} ${DOCKER_PWD} ${DOCKER_USER}