Merge branch 'main' into e2e-teardown

This commit is contained in:
Prasanth Baskar 2025-09-08 05:51:56 +05:30 committed by GitHub
commit 32e780bfab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
75 changed files with 1511 additions and 288 deletions

View File

@ -8,6 +8,9 @@
* Add date here... Add signature here...
- Add your reason here...
* Aug 12 2025 <yan-yw.wang@broadcom.com>
- Refresh base image
* Oct 24 2024 <yan-yw.wang@broadcom.com>
- Refresh base image

View File

@ -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:
@ -46,7 +55,7 @@ jobs:
with:
go-version: 1.23.2
id: go
- uses: actions/checkout@v3
- uses: actions/checkout@v5
with:
path: src/github.com/goharbor/harbor
- name: setup env
@ -162,7 +171,7 @@ jobs:
with:
go-version: 1.23.2
id: go
- uses: actions/checkout@v3
- uses: actions/checkout@v5
with:
path: src/github.com/goharbor/harbor
- name: setup env
@ -203,7 +212,12 @@ jobs:
df -h
bash ./tests/showtime.sh ./tests/ci/api_run.sh DB $IP
df -h
- name: upload_logs
uses: actions/upload-artifact@v4
with:
name: db-api-harbor-logs.tar.gz
path: /home/runner/work/harbor/harbor/src/github.com/goharbor/harbor/integration_logs.tar.gz
retention-days: 5
APITEST_DB_PROXY_CACHE:
env:
APITEST_DB: true
@ -217,7 +231,7 @@ jobs:
with:
go-version: 1.23.2
id: go
- uses: actions/checkout@v3
- uses: actions/checkout@v5
with:
path: src/github.com/goharbor/harbor
- name: setup env
@ -258,7 +272,12 @@ jobs:
df -h
bash ./tests/showtime.sh ./tests/ci/api_run.sh PROXY_CACHE $IP
df -h
- name: upload_logs
uses: actions/upload-artifact@v4
with:
name: proxy-api-harbor-logs.tar.gz
path: /home/runner/work/harbor/harbor/src/github.com/goharbor/harbor/integration_logs.tar.gz
retention-days: 5
APITEST_LDAP:
env:
APITEST_LDAP: true
@ -272,7 +291,7 @@ jobs:
with:
go-version: 1.23.2
id: go
- uses: actions/checkout@v3
- uses: actions/checkout@v5
with:
path: src/github.com/goharbor/harbor
- name: setup env
@ -311,7 +330,12 @@ jobs:
cd src/github.com/goharbor/harbor
bash ./tests/showtime.sh ./tests/ci/api_run.sh LDAP $IP
df -h
- name: upload_logs
uses: actions/upload-artifact@v4
with:
name: ldap-api-harbor-logs.tar.gz
path: /home/runner/work/harbor/harbor/src/github.com/goharbor/harbor/integration_logs.tar.gz
retention-days: 5
OFFLINE:
env:
OFFLINE: true
@ -325,7 +349,7 @@ jobs:
with:
go-version: 1.23.2
id: go
- uses: actions/checkout@v3
- uses: actions/checkout@v5
with:
path: src/github.com/goharbor/harbor
- name: setup env
@ -375,7 +399,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: '18'
- uses: actions/checkout@v3
- uses: actions/checkout@v5
with:
path: src/github.com/goharbor/harbor
- name: script

View File

@ -31,13 +31,13 @@ jobs:
with:
docker_version: 20.10
docker_channel: stable
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- uses: jitterbit/get-changed-files@v1
id: changed-files
with:
format: space-delimited
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v3
- uses: actions/checkout@v5
with:
path: src/github.com/goharbor/harbor
- name: Build Base Image
@ -85,7 +85,7 @@ jobs:
if [ -z "$BUILD_BASE" ] || [ "$BUILD_BASE" != "true" ]; then
echo "Do not need to build base images!"
else
build_base_params=" BUILD_BASE=true PUSHBASEIMAGE=true REGISTRYUSER=\"${{ secrets.DOCKER_HUB_USERNAME }}\" REGISTRYPASSWORD=\"${{ secrets.DOCKER_HUB_PASSWORD }}\""
build_base_params=" BUILD_BASE=true PULL_BASE_FROM_DOCKERHUB=true PUSHBASEIMAGE=true REGISTRYUSER=\"${{ secrets.DOCKER_HUB_USERNAME }}\" REGISTRYPASSWORD=\"${{ secrets.DOCKER_HUB_PASSWORD }}\""
fi
sudo make package_offline GOBUILDTAGS="include_oss include_gcs" BASEIMAGETAG=${Harbor_Build_Base_Tag} VERSIONTAG=${Harbor_Assets_Version} PKGVERSIONTAG=${Harbor_Package_Version} TRIVYFLAG=true EXPORTERFLAG=true HTTPPROXY= ${build_base_params}
sudo make package_online GOBUILDTAGS="include_oss include_gcs" BASEIMAGETAG=${Harbor_Build_Base_Tag} VERSIONTAG=${Harbor_Assets_Version} PKGVERSIONTAG=${Harbor_Package_Version} TRIVYFLAG=true EXPORTERFLAG=true HTTPPROXY= ${build_base_params}

View File

@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v5
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.

View File

@ -28,7 +28,7 @@ jobs:
with:
go-version: 1.23.2
id: go
- uses: actions/checkout@v3
- uses: actions/checkout@v5
with:
path: src/github.com/goharbor/harbor
- name: before_install

View File

@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v5
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:

View File

@ -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'
@ -11,7 +12,9 @@ on:
- '!tests/ci/**'
- '!tests/resources/**'
- '!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'
@ -21,6 +24,7 @@ on:
- '!tests/ci/**'
- '!tests/resources/**'
- '!tests/robot-cases/**'
- '!tests/robot-cases/Group1-Nightly/**'
jobs:
UTTEST:

View File

@ -9,7 +9,7 @@ jobs:
release:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Setup env
run: |
echo "CUR_TAG=${{ github.ref_name }}" >> $GITHUB_ENV

View File

@ -168,7 +168,7 @@ Harbor backend is written in [Go](http://golang.org/). If you don't have a Harbo
| 2.11 | 1.22.3 |
| 2.12 | 1.23.2 |
| 2.13 | 1.23.8 |
| 2.14 | 1.24.3 |
| 2.14 | 1.24.6 |
Ensure your GOPATH and PATH have been configured in accordance with the Go environment instructions.

View File

@ -111,8 +111,8 @@ PREPARE_VERSION_NAME=versions
#versions
REGISTRYVERSION=v2.8.3-patch-redis
TRIVYVERSION=v0.61.0
TRIVYADAPTERVERSION=v0.33.0-rc.2
TRIVYVERSION=v0.65.0
TRIVYADAPTERVERSION=v0.34.0-rc.1
NODEBUILDIMAGE=node:16.18.0
# version of registry for pulling the source code
@ -151,7 +151,7 @@ GOINSTALL=$(GOCMD) install
GOTEST=$(GOCMD) test
GODEP=$(GOTEST) -i
GOFMT=gofmt -w
GOBUILDIMAGE=golang:1.24.3
GOBUILDIMAGE=golang:1.24.6
GOBUILDPATHINCONTAINER=/harbor
# go build
@ -395,11 +395,6 @@ build:
echo Do not push base images since no base images built. ; \
exit 1; \
fi
# PULL_BASE_FROM_DOCKERHUB should be true if BUILD_BASE is not true
@if [ "$(BUILD_BASE)" != "true" ] && [ "$(PULL_BASE_FROM_DOCKERHUB)" = "false" ] ; then \
echo Should pull base images from registry in docker configuration since no base images built. ; \
exit 1; \
fi
make -f $(MAKEFILEPATH_PHOTON)/Makefile $(BUILDTARGET) -e DEVFLAG=$(DEVFLAG) -e GOBUILDIMAGE=$(GOBUILDIMAGE) -e NODEBUILDIMAGE=$(NODEBUILDIMAGE) \
-e REGISTRYVERSION=$(REGISTRYVERSION) -e REGISTRY_SRC_TAG=$(REGISTRY_SRC_TAG) -e DISTRIBUTION_SRC=$(DISTRIBUTION_SRC)\
-e TRIVYVERSION=$(TRIVYVERSION) -e TRIVYADAPTERVERSION=$(TRIVYADAPTERVERSION) \

View File

@ -1 +1 @@
v2.14.0
v2.15.0

View File

@ -3031,6 +3031,8 @@ paths:
type: string
'401':
$ref: '#/responses/401'
'409':
$ref: '#/responses/409'
'500':
$ref: '#/responses/500'
'/usergroups/{group_id}':
@ -7460,6 +7462,12 @@ definitions:
type: boolean
description: Whether to enable copy by chunk.
x-isnullable: true
single_active_replication:
type: boolean
description: |-
Whether to skip execution until the previous active execution finishes,
avoiding the execution of the same replication rules multiple times in parallel.
x-isnullable: true # make this field optional to keep backward compatibility
ReplicationTrigger:
type: object
properties:

View File

@ -176,7 +176,7 @@ log:
# port: 5140
#This attribute is for migrator to detect the version of the .cfg file, DO NOT MODIFY!
_version: 2.13.0
_version: 2.14.0
# Uncomment external_database if using external database.
# external_database:

View File

@ -7,3 +7,5 @@ ALTER SEQUENCE permission_policy_id_seq AS BIGINT;
ALTER TABLE role_permission ALTER COLUMN permission_policy_id TYPE BIGINT;
ALTER TABLE vulnerability_record ADD COLUMN IF NOT EXISTS status text;
ALTER TABLE replication_policy ADD COLUMN IF NOT EXISTS single_active_replication boolean;

View File

@ -10,7 +10,7 @@ from migrations import accept_versions
@click.command()
@click.option('-i', '--input', 'input_', required=True, help="The path of original config file")
@click.option('-o', '--output', default='', help="the path of output config file")
@click.option('-t', '--target', default='2.13.0', help="target version of input path")
@click.option('-t', '--target', default='2.14.0', help="target version of input path")
def migrate(input_, output, target):
"""
migrate command will migrate config file style to specific version

View File

@ -2,4 +2,4 @@ import os
MIGRATION_BASE_DIR = os.path.dirname(__file__)
accept_versions = {'1.9.0', '1.10.0', '2.0.0', '2.1.0', '2.2.0', '2.3.0', '2.4.0', '2.5.0', '2.6.0', '2.7.0', '2.8.0', '2.9.0','2.10.0', '2.11.0', '2.12.0', '2.13.0'}
accept_versions = {'1.9.0', '1.10.0', '2.0.0', '2.1.0', '2.2.0', '2.3.0', '2.4.0', '2.5.0', '2.6.0', '2.7.0', '2.8.0', '2.9.0','2.10.0', '2.11.0', '2.12.0', '2.13.0', '2.14.0'}

View File

@ -0,0 +1,21 @@
import os
from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape
from utils.migration import read_conf
revision = '2.14.0'
down_revisions = ['2.13.0']
def migrate(input_cfg, output_cfg):
current_dir = os.path.dirname(__file__)
tpl = Environment(
loader=FileSystemLoader(current_dir),
undefined=StrictUndefined,
trim_blocks=True,
lstrip_blocks=True,
autoescape = select_autoescape()
).get_template('harbor.yml.jinja')
config_dict = read_conf(input_cfg)
with open(output_cfg, 'w') as f:
f.write(tpl.render(**config_dict))

View File

@ -0,0 +1,775 @@
# Configuration file of Harbor
# The IP address or hostname to access admin UI and registry service.
# DO NOT use localhost or 127.0.0.1, because Harbor needs to be accessed by external clients.
hostname: {{ hostname }}
# http related config
{% if http is defined %}
http:
# port for http, default is 80. If https enabled, this port will redirect to https port
port: {{ http.port }}
{% else %}
# http:
# # port for http, default is 80. If https enabled, this port will redirect to https port
# port: 80
{% endif %}
{% if https is defined %}
# https related config
https:
# https port for harbor, default is 443
port: {{ https.port }}
# The path of cert and key files for nginx
certificate: {{ https.certificate }}
private_key: {{ https.private_key }}
# enable strong ssl ciphers (default: false)
{% if strong_ssl_ciphers is defined %}
strong_ssl_ciphers: {{ strong_ssl_ciphers | lower }}
{% else %}
strong_ssl_ciphers: false
{% endif %}
{% else %}
# https related config
# https:
# # https port for harbor, default is 443
# port: 443
# # The path of cert and key files for nginx
# certificate: /your/certificate/path
# private_key: /your/private/key/path
# enable strong ssl ciphers (default: false)
# strong_ssl_ciphers: false
{% endif %}
# # Harbor will set ipv4 enabled only by default if this block is not configured
# # Otherwise, please uncomment this block to configure your own ip_family stacks
{% if ip_family is defined %}
ip_family:
# ipv6Enabled set to true if ipv6 is enabled in docker network, currently it affected the nginx related component
{% if ip_family.ipv6 is defined %}
ipv6:
enabled: {{ ip_family.ipv6.enabled | lower }}
{% else %}
ipv6:
enabled: false
{% endif %}
# ipv4Enabled set to true by default, currently it affected the nginx related component
{% if ip_family.ipv4 is defined %}
ipv4:
enabled: {{ ip_family.ipv4.enabled | lower }}
{% else %}
ipv4:
enabled: true
{% endif %}
{% else %}
# ip_family:
# # ipv6Enabled set to true if ipv6 is enabled in docker network, currently it affected the nginx related component
# ipv6:
# enabled: false
# # ipv4Enabled set to true by default, currently it affected the nginx related component
# ipv4:
# enabled: true
{% endif %}
{% if internal_tls is defined %}
# Uncomment following will enable tls communication between all harbor components
internal_tls:
# set enabled to true means internal tls is enabled
enabled: {{ internal_tls.enabled | lower }}
{% if internal_tls.dir is defined %}
# put your cert and key files on dir
dir: {{ internal_tls.dir }}
{% endif %}
{% else %}
# internal_tls:
# # set enabled to true means internal tls is enabled
# enabled: true
# # put your cert and key files on dir
# dir: /etc/harbor/tls/internal
{% endif %}
# Uncomment external_url if you want to enable external proxy
# And when it enabled the hostname will no longer used
{% if external_url is defined %}
external_url: {{ external_url }}
{% else %}
# external_url: https://reg.mydomain.com:8433
{% endif %}
# The initial password of Harbor admin
# It only works in first time to install harbor
# Remember Change the admin password from UI after launching Harbor.
{% if harbor_admin_password is defined %}
harbor_admin_password: {{ harbor_admin_password }}
{% else %}
harbor_admin_password: Harbor12345
{% endif %}
# Harbor DB configuration
database:
{% if database is defined %}
# The password for the root user of Harbor DB. Change this before any production use.
password: {{ database.password}}
# The maximum number of connections in the idle connection pool. If it <=0, no idle connections are retained.
max_idle_conns: {{ database.max_idle_conns }}
# The maximum number of open connections to the database. If it <= 0, then there is no limit on the number of open connections.
# Note: the default number of connections is 1024 for postgres of harbor.
max_open_conns: {{ database.max_open_conns }}
# The maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If it <= 0, connections are not closed due to a connection's age.
# The value is a duration string. A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
{% if database.conn_max_lifetime is defined %}
conn_max_lifetime: {{ database.conn_max_lifetime }}
{% else %}
conn_max_lifetime: 5m
{% endif %}
# The maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If it <= 0, connections are not closed due to a connection's idle time.
# The value is a duration string. A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
{% if database.conn_max_idle_time is defined %}
conn_max_idle_time: {{ database.conn_max_idle_time }}
{% else %}
conn_max_idle_time: 0
{% endif %}
{% else %}
# The password for the root user of Harbor DB. Change this before any production use.
password: root123
# The maximum number of connections in the idle connection pool. If it <=0, no idle connections are retained.
max_idle_conns: 100
# The maximum number of open connections to the database. If it <= 0, then there is no limit on the number of open connections.
# Note: the default number of connections is 1024 for postgres of harbor.
max_open_conns: 900
# The maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If it <= 0, connections are not closed due to a connection's age.
# The value is a duration string. A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
conn_max_lifetime: 5m
# The maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If it <= 0, connections are not closed due to a connection's idle time.
# The value is a duration string. A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
conn_max_idle_time: 0
{% endif %}
{% if data_volume is defined %}
# The default data volume
data_volume: {{ data_volume }}
{% else %}
# The default data volume
data_volume: /data
{% endif %}
# Harbor Storage settings by default is using /data dir on local filesystem
# Uncomment storage_service setting If you want to using external storage
{% if storage_service is defined %}
storage_service:
{% for key, value in storage_service.items() %}
{% if key == 'ca_bundle' %}
# # ca_bundle is the path to the custom root ca certificate, which will be injected into the truststore
# # of registry's and chart repository's containers. This is usually needed when the user hosts a internal storage with self signed certificate.
ca_bundle: {{ value if value is not none else '' }}
{% elif key == 'redirect' %}
# # set disable to true when you want to disable registry redirect
redirect:
{% if storage_service.redirect.disabled is defined %}
disable: {{ storage_service.redirect.disabled | lower}}
{% else %}
disable: {{ storage_service.redirect.disable | lower}}
{% endif %}
{% else %}
# # storage backend, default is filesystem, options include filesystem, azure, gcs, s3, swift and oss
# # for more info about this configuration please refer https://distribution.github.io/distribution/about/configuration/
# # and https://distribution.github.io/distribution/storage-drivers/
{{ key }}:
{% for k, v in value.items() %}
{{ k }}: {{ v if v is not none else '' }}
{% endfor %}
{% endif %}
{% endfor %}
{% else %}
# storage_service:
# # ca_bundle is the path to the custom root ca certificate, which will be injected into the truststore
# # of registry's and chart repository's containers. This is usually needed when the user hosts a internal storage with self signed certificate.
# ca_bundle:
# # storage backend, default is filesystem, options include filesystem, azure, gcs, s3, swift and oss
# # for more info about this configuration please refer https://distribution.github.io/distribution/about/configuration/
# # and https://distribution.github.io/distribution/storage-drivers/
# filesystem:
# maxthreads: 100
# # set disable to true when you want to disable registry redirect
# redirect:
# disable: false
{% endif %}
# Trivy configuration
#
# Trivy DB contains vulnerability information from NVD, Red Hat, and many other upstream vulnerability databases.
# It is downloaded by Trivy from the GitHub release page https://github.com/aquasecurity/trivy-db/releases and cached
# in the local file system. In addition, the database contains the update timestamp so Trivy can detect whether it
# should download a newer version from the Internet or use the cached one. Currently, the database is updated every
# 12 hours and published as a new release to GitHub.
{% if trivy is defined %}
trivy:
# ignoreUnfixed The flag to display only fixed vulnerabilities
{% if trivy.ignore_unfixed is defined %}
ignore_unfixed: {{ trivy.ignore_unfixed | lower }}
{% else %}
ignore_unfixed: false
{% endif %}
# skipUpdate The flag to enable or disable Trivy DB downloads from GitHub
#
# You might want to enable this flag in test or CI/CD environments to avoid GitHub rate limiting issues.
# If the flag is enabled you have to download the `trivy-offline.tar.gz` archive manually, extract `trivy.db` and
# `metadata.json` files and mount them in the `/home/scanner/.cache/trivy/db` path.
{% if trivy.skip_update is defined %}
skip_update: {{ trivy.skip_update | lower }}
{% else %}
skip_update: false
{% endif %}
{% if trivy.skip_java_db_update is defined %}
# skipJavaDBUpdate If the flag is enabled you have to manually download the `trivy-java.db` file and mount it in the
# `/home/scanner/.cache/trivy/java-db/trivy-java.db` path
skip_java_db_update: {{ trivy.skip_java_db_update | lower }}
{% else %}
skip_java_db_update: false
{% endif %}
#
{% if trivy.offline_scan is defined %}
offline_scan: {{ trivy.offline_scan | lower }}
{% else %}
offline_scan: false
{% endif %}
#
# Comma-separated list of what security issues to detect. Possible values are `vuln`, `config` and `secret`. Defaults to `vuln`.
{% if trivy.security_check is defined %}
security_check: {{ trivy.security_check }}
{% else %}
security_check: vuln
{% endif %}
#
# insecure The flag to skip verifying registry certificate
{% if trivy.insecure is defined %}
insecure: {{ trivy.insecure | lower }}
{% else %}
insecure: false
{% endif %}
#
{% if trivy.timeout is defined %}
# timeout The duration to wait for scan completion.
# There is upper bound of 30 minutes defined in scan job. So if this `timeout` is larger than 30m0s, it will also timeout at 30m0s.
timeout: {{ trivy.timeout}}
{% else %}
timeout: 5m0s
{% endif %}
#
# github_token The GitHub access token to download Trivy DB
#
# Anonymous downloads from GitHub are subject to the limit of 60 requests per hour. Normally such rate limit is enough
# for production operations. If, for any reason, it's not enough, you could increase the rate limit to 5000
# requests per hour by specifying the GitHub access token. For more details on GitHub rate limiting please consult
# https://developer.github.com/v3/#rate-limiting
#
# You can create a GitHub token by following the instructions in
# https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line
#
{% if trivy.github_token is defined %}
github_token: {{ trivy.github_token }}
{% else %}
# github_token: xxx
{% endif %}
{% else %}
# trivy:
# # ignoreUnfixed The flag to display only fixed vulnerabilities
# ignore_unfixed: false
# # skipUpdate The flag to enable or disable Trivy DB downloads from GitHub
# #
# # You might want to enable this flag in test or CI/CD environments to avoid GitHub rate limiting issues.
# # If the flag is enabled you have to download the `trivy-offline.tar.gz` archive manually, extract `trivy.db` and
# # `metadata.json` files and mount them in the `/home/scanner/.cache/trivy/db` path.
# skip_update: false
# #
# # skipJavaDBUpdate If the flag is enabled you have to manually download the `trivy-java.db` file and mount it in the
# # `/home/scanner/.cache/trivy/java-db/trivy-java.db` path
# skip_java_db_update: false
# #
# #The offline_scan option prevents Trivy from sending API requests to identify dependencies.
# # Scanning JAR files and pom.xml may require Internet access for better detection, but this option tries to avoid it.
# # For example, the offline mode will not try to resolve transitive dependencies in pom.xml when the dependency doesn't
# # exist in the local repositories. It means a number of detected vulnerabilities might be fewer in offline mode.
# # It would work if all the dependencies are in local.
# # This option doesnt affect DB download. You need to specify "skip-update" as well as "offline-scan" in an air-gapped environment.
# offline_scan: false
# #
# # insecure The flag to skip verifying registry certificate
# insecure: false
# # github_token The GitHub access token to download Trivy DB
# #
# # Anonymous downloads from GitHub are subject to the limit of 60 requests per hour. Normally such rate limit is enough
# # for production operations. If, for any reason, it's not enough, you could increase the rate limit to 5000
# # requests per hour by specifying the GitHub access token. For more details on GitHub rate limiting please consult
# # https://developer.github.com/v3/#rate-limiting
# #
# # timeout The duration to wait for scan completion.
# # There is upper bound of 30 minutes defined in scan job. So if this `timeout` is larger than 30m0s, it will also timeout at 30m0s.
# timeout: 5m0s
# #
# # You can create a GitHub token by following the instructions in
# # https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line
# #
# # github_token: xxx
{% endif %}
jobservice:
# Maximum number of job workers in job service
{% if jobservice is defined %}
max_job_workers: {{ jobservice.max_job_workers }}
# Maximum hours of task duration in job service, default 24
{% if jobservice.max_job_duration_hours is defined %}
max_job_duration_hours: {{ jobservice.max_job_duration_hours }}
{% else %}
max_job_duration_hours: 24
{% endif %}
# The jobLoggers backend name, only support "STD_OUTPUT", "FILE" and/or "DB"
{% if jobservice.job_loggers is defined %}
job_loggers:
{% for job_logger in jobservice.job_loggers %}
- {{job_logger}}
{% endfor %}
{% else %}
job_loggers:
- STD_OUTPUT
- FILE
# - DB
{% endif %}
# The jobLogger sweeper duration (ignored if `jobLogger` is `stdout`)
{% if jobservice.logger_sweeper_duration is defined %}
logger_sweeper_duration: {{ jobservice.logger_sweeper_duration }}
{% else %}
logger_sweeper_duration: 1
{% endif %}
{% else %}
max_job_workers: 10
max_job_duration_hours: 24
# The jobLoggers backend name, only support "STD_OUTPUT", "FILE" and/or "DB"
job_loggers:
- STD_OUTPUT
- FILE
# - DB
# The jobLogger sweeper duration (ignored if `jobLogger` is `stdout`)
logger_sweeper_duration: 1
{% endif %}
notification:
# Maximum retry count for webhook job
{% if notification is defined %}
webhook_job_max_retry: {{ notification.webhook_job_max_retry}}
# HTTP client timeout for webhook job
{% if notification.webhook_job_http_client_timeout is defined %}
webhook_job_http_client_timeout: {{ notification.webhook_job_http_client_timeout }}
{% else %}
webhook_job_http_client_timeout: 3 #seconds
{% endif %}
{% else %}
webhook_job_max_retry: 3
# HTTP client timeout for webhook job
webhook_job_http_client_timeout: 3 #seconds
{% endif %}
# Log configurations
log:
# options are debug, info, warning, error, fatal
{% if log is defined %}
level: {{ log.level }}
# configs for logs in local storage
local:
# Log files are rotated log_rotate_count times before being removed. If count is 0, old versions are removed rather than rotated.
rotate_count: {{ log.local.rotate_count }}
# Log files are rotated only if they grow bigger than log_rotate_size bytes. If size is followed by k, the size is assumed to be in kilobytes.
# If the M is used, the size is in megabytes, and if G is used, the size is in gigabytes. So size 100, size 100k, size 100M and size 100G
# are all valid.
rotate_size: {{ log.local.rotate_size }}
# The directory on your host that store log
location: {{ log.local.location }}
{% if log.external_endpoint is defined %}
external_endpoint:
# protocol used to transmit log to external endpoint, options is tcp or udp
protocol: {{ log.external_endpoint.protocol }}
# The host of external endpoint
host: {{ log.external_endpoint.host }}
# Port of external endpoint
port: {{ log.external_endpoint.port }}
{% else %}
# Uncomment following lines to enable external syslog endpoint.
# external_endpoint:
# # protocol used to transmit log to external endpoint, options is tcp or udp
# protocol: tcp
# # The host of external endpoint
# host: localhost
# # Port of external endpoint
# port: 5140
{% endif %}
{% else %}
level: info
# configs for logs in local storage
local:
# Log files are rotated log_rotate_count times before being removed. If count is 0, old versions are removed rather than rotated.
rotate_count: 50
# Log files are rotated only if they grow bigger than log_rotate_size bytes. If size is followed by k, the size is assumed to be in kilobytes.
# If the M is used, the size is in megabytes, and if G is used, the size is in gigabytes. So size 100, size 100k, size 100M and size 100G
# are all valid.
rotate_size: 200M
# The directory on your host that store log
location: /var/log/harbor
# Uncomment following lines to enable external syslog endpoint.
# external_endpoint:
# # protocol used to transmit log to external endpoint, options is tcp or udp
# protocol: tcp
# # The host of external endpoint
# host: localhost
# # Port of external endpoint
# port: 5140
{% endif %}
#This attribute is for migrator to detect the version of the .cfg file, DO NOT MODIFY!
_version: 2.14.0
{% if external_database is defined %}
# Uncomment external_database if using external database.
external_database:
harbor:
host: {{ external_database.harbor.host }}
port: {{ external_database.harbor.port }}
db_name: {{ external_database.harbor.db_name }}
username: {{ external_database.harbor.username }}
password: {{ external_database.harbor.password }}
ssl_mode: {{ external_database.harbor.ssl_mode }}
max_idle_conns: {{ external_database.harbor.max_idle_conns}}
max_open_conns: {{ external_database.harbor.max_open_conns}}
{% else %}
# Uncomment external_database if using external database.
# external_database:
# harbor:
# host: harbor_db_host
# port: harbor_db_port
# db_name: harbor_db_name
# username: harbor_db_username
# password: harbor_db_password
# ssl_mode: disable
# max_idle_conns: 2
# max_open_conns: 0
{% endif %}
{% if redis is defined %}
redis:
# # db_index 0 is for core, it's unchangeable
{% if redis.registry_db_index is defined %}
registry_db_index: {{ redis.registry_db_index }}
{% else %}
# # registry_db_index: 1
{% endif %}
{% if redis.jobservice_db_index is defined %}
jobservice_db_index: {{ redis.jobservice_db_index }}
{% else %}
# # jobservice_db_index: 2
{% endif %}
{% if redis.trivy_db_index is defined %}
trivy_db_index: {{ redis.trivy_db_index }}
{% else %}
# # trivy_db_index: 5
{% endif %}
{% if redis.harbor_db_index is defined %}
harbor_db_index: {{ redis.harbor_db_index }}
{% else %}
# # it's optional, the db for harbor business misc, by default is 0, uncomment it if you want to change it.
# # harbor_db_index: 6
{% endif %}
{% if redis.cache_layer_db_index is defined %}
cache_layer_db_index: {{ redis.cache_layer_db_index }}
{% else %}
# # it's optional, the db for harbor cache layer, by default is 0, uncomment it if you want to change it.
# # cache_layer_db_index: 7
{% endif %}
{% else %}
# Uncomment redis if need to customize redis db
# redis:
# # db_index 0 is for core, it's unchangeable
# # registry_db_index: 1
# # jobservice_db_index: 2
# # trivy_db_index: 5
# # it's optional, the db for harbor business misc, by default is 0, uncomment it if you want to change it.
# # harbor_db_index: 6
# # it's optional, the db for harbor cache layer, by default is 0, uncomment it if you want to change it.
# # cache_layer_db_index: 7
{% endif %}
{% if external_redis is defined %}
external_redis:
# support redis, redis+sentinel
# host for redis: <host_redis>:<port_redis>
# host for redis+sentinel:
# <host_sentinel1>:<port_sentinel1>,<host_sentinel2>:<port_sentinel2>,<host_sentinel3>:<port_sentinel3>
host: {{ external_redis.host }}
password: {{ external_redis.password }}
# Redis AUTH command was extended in Redis 6, it is possible to use it in the two-arguments AUTH <username> <password> form.
{% if external_redis.username is defined %}
username: {{ external_redis.username }}
{% else %}
# username:
{% endif %}
# sentinel_master_set must be set to support redis+sentinel
#sentinel_master_set:
{% if external_redis.tlsOptions is defined %}
# # tls configuration for redis connection
# # only server-authentication is supported
# # mtls for redis connection is not supported
# # tls connection will be disable by default
tlsOptions:
enable: {{ external_redis.tlsOptions.enable }}
# if it is a self-signed ca, please set the ca path specifically.
{% if external_redis.tlsOptions.rootCA is defined %}
rootCA: {{ external_redis.tlsOptions.rootCA }}
{% else %}
# rootCA:
{% endif %}
{% else %}
# # tls configuration for redis connection
# # only server-authentication is supported
# # mtls for redis connection is not supported
# # tls connection will be disable by default
# tlsOptions:
# enable: false
# # if it is a self-signed ca, please set the ca path specifically.
# rootCA:
{% endif %}
# db_index 0 is for core, it's unchangeable
registry_db_index: {{ external_redis.registry_db_index }}
jobservice_db_index: {{ external_redis.jobservice_db_index }}
trivy_db_index: 5
idle_timeout_seconds: 30
{% if external_redis.harbor_db_index is defined %}
harbor_db_index: {{ redis.harbor_db_index }}
{% else %}
# # it's optional, the db for harbor business misc, by default is 0, uncomment it if you want to change it.
# # harbor_db_index: 6
{% endif %}
{% if external_redis.cache_layer_db_index is defined %}
cache_layer_db_index: {{ redis.cache_layer_db_index }}
{% else %}
# # it's optional, the db for harbor cache layer, by default is 0, uncomment it if you want to change it.
# # cache_layer_db_index: 7
{% endif %}
{% else %}
# Uncomments external_redis if using external Redis server
# external_redis:
# # support redis, redis+sentinel
# # host for redis: <host_redis>:<port_redis>
# # host for redis+sentinel:
# # <host_sentinel1>:<port_sentinel1>,<host_sentinel2>:<port_sentinel2>,<host_sentinel3>:<port_sentinel3>
# host: redis:6379
# password:
# # Redis AUTH command was extended in Redis 6, it is possible to use it in the two-arguments AUTH <username> <password> form.
# # username:
# # sentinel_master_set must be set to support redis+sentinel
# #sentinel_master_set:
# # tls configuration for redis connection
# # only server-authentication is supported
# # mtls for redis connection is not supported
# # tls connection will be disable by default
# tlsOptions:
# enable: false
# # if it is a self-signed ca, please set the ca path specifically.
# rootCA:
# # db_index 0 is for core, it's unchangeable
# registry_db_index: 1
# jobservice_db_index: 2
# trivy_db_index: 5
# idle_timeout_seconds: 30
# # it's optional, the db for harbor business misc, by default is 0, uncomment it if you want to change it.
# # harbor_db_index: 6
# # it's optional, the db for harbor cache layer, by default is 0, uncomment it if you want to change it.
# # cache_layer_db_index: 7
{% endif %}
{% if uaa is defined %}
# Uncomment uaa for trusting the certificate of uaa instance that is hosted via self-signed cert.
uaa:
ca_file: {{ uaa.ca_file }}
{% else %}
# Uncomment uaa for trusting the certificate of uaa instance that is hosted via self-signed cert.
# uaa:
# ca_file: /path/to/ca
{% endif %}
# Global proxy
# Config http proxy for components, e.g. http://my.proxy.com:3128
# Components doesn't need to connect to each others via http proxy.
# Remove component from `components` array if want disable proxy
# for it. If you want use proxy for replication, MUST enable proxy
# for core and jobservice, and set `http_proxy` and `https_proxy`.
# Add domain to the `no_proxy` field, when you want disable proxy
# for some special registry.
{% if proxy is defined %}
proxy:
http_proxy: {{ proxy.http_proxy or ''}}
https_proxy: {{ proxy.https_proxy or ''}}
no_proxy: {{ proxy.no_proxy or ''}}
{% if proxy.components is defined %}
components:
{% for component in proxy.components %}
{% if component != 'clair' %}
- {{component}}
{% endif %}
{% endfor %}
{% endif %}
{% else %}
proxy:
http_proxy:
https_proxy:
no_proxy:
components:
- core
- jobservice
- trivy
{% endif %}
{% if metric is defined %}
metric:
enabled: {{ metric.enabled }}
port: {{ metric.port }}
path: {{ metric.path }}
{% else %}
# metric:
# enabled: false
# port: 9090
# path: /metrics
{% endif %}
# Trace related config
# only can enable one trace provider(jaeger or otel) at the same time,
# and when using jaeger as provider, can only enable it with agent mode or collector mode.
# if using jaeger collector mode, uncomment endpoint and uncomment username, password if needed
# if using jaeger agetn mode uncomment agent_host and agent_port
{% if trace is defined %}
trace:
enabled: {{ trace.enabled | lower}}
sample_rate: {{ trace.sample_rate }}
# # namespace used to differentiate different harbor services
{% if trace.namespace is defined %}
namespace: {{ trace.namespace }}
{% else %}
# namespace:
{% endif %}
# # attributes is a key value dict contains user defined attributes used to initialize trace provider
{% if trace.attributes is defined%}
attributes:
{% for name, value in trace.attributes.items() %}
{{name}}: {{value}}
{% endfor %}
{% else %}
# attributes:
# application: harbor
{% endif %}
{% if trace.jaeger is defined%}
jaeger:
endpoint: {{trace.jaeger.endpoint or '' }}
username: {{trace.jaeger.username or ''}}
password: {{trace.jaeger.password or ''}}
agent_host: {{trace.jaeger.agent_host or ''}}
agent_port: {{trace.jaeger.agent_port or ''}}
{% else %}
# jaeger:
# endpoint:
# username:
# password:
# agent_host:
# agent_port:
{% endif %}
{% if trace. otel is defined %}
otel:
endpoint: {{trace.otel.endpoint or '' }}
url_path: {{trace.otel.url_path or '' }}
compression: {{trace.otel.compression | lower }}
insecure: {{trace.otel.insecure | lower }}
timeout: {{trace.otel.timeout or '' }}
{% else %}
# otel:
# endpoint: hostname:4318
# url_path: /v1/traces
# compression: false
# insecure: true
# # timeout is in seconds
# timeout: 10
{% endif%}
{% else %}
# trace:
# enabled: true
# # set sample_rate to 1 if you wanna sampling 100% of trace data; set 0.5 if you wanna sampling 50% of trace data, and so forth
# sample_rate: 1
# # # namespace used to differentiate different harbor services
# # namespace:
# # # attributes is a key value dict contains user defined attributes used to initialize trace provider
# # attributes:
# # application: harbor
# # jaeger:
# # endpoint: http://hostname:14268/api/traces
# # username:
# # password:
# # agent_host: hostname
# # agent_port: 6831
# # otel:
# # endpoint: hostname:4318
# # url_path: /v1/traces
# # compression: false
# # insecure: true
# # # timeout is in seconds
# # timeout: 10
{% endif %}
# enable purge _upload directories
{% if upload_purging is defined %}
upload_purging:
enabled: {{ upload_purging.enabled | lower}}
age: {{ upload_purging.age }}
interval: {{ upload_purging.interval }}
dryrun: {{ upload_purging.dryrun | lower}}
{% else %}
upload_purging:
enabled: true
# remove files in _upload directories which exist for a period of time, default is one week.
age: 168h
# the interval of the purge operations
interval: 24h
dryrun: false
{% endif %}
# Cache layer related config
{% if cache is defined %}
cache:
enabled: {{ cache.enabled | lower}}
expire_hours: {{ cache.expire_hours }}
{% else %}
cache:
enabled: false
expire_hours: 24
{% endif %}
# Harbor core configurations
# Uncomment to enable the following harbor core related configuration items.
{% if core is defined %}
core:
# The provider for updating project quota(usage), there are 2 options, redis or db,
# by default is implemented by db but you can switch the updation via redis which
# can improve the performance of high concurrent pushing to the same project,
# and reduce the database connections spike and occupies.
# By redis will bring up some delay for quota usage updation for display, so only
# suggest switch provider to redis if you were ran into the db connections spike aroud
# the scenario of high concurrent pushing to same project, no improvment for other scenes.
quota_update_provider: {{ core.quota_update_provider }}
{% else %}
# core:
# # The provider for updating project quota(usage), there are 2 options, redis or db,
# # by default is implemented by db but you can switch the updation via redis which
# # can improve the performance of high concurrent pushing to the same project,
# # and reduce the database connections spike and occupies.
# # By redis will bring up some delay for quota usage updation for display, so only
# # suggest switch provider to redis if you were ran into the db connections spike around
# # the scenario of high concurrent pushing to same project, no improvement for other scenes.
# quota_update_provider: redis # Or db
{% endif %}

View File

@ -4,7 +4,6 @@ _REDIS_URL_CORE={{redis_url_core}}
{% if redis_url_harbor %}
_REDIS_URL_HARBOR={{redis_url_harbor}}
{% endif %}
SYNC_QUOTA=true
_REDIS_URL_REG={{redis_url_reg}}
LOG_LEVEL={{log_level}}
@ -40,7 +39,8 @@ REGISTRY_CREDENTIAL_USERNAME={{registry_username}}
REGISTRY_CREDENTIAL_PASSWORD={{registry_password}}
CSRF_KEY={{csrf_key}}
ROBOT_SCANNER_NAME_PREFIX={{scan_robot_prefix}}
PERMITTED_REGISTRY_TYPES_FOR_PROXY_CACHE=docker-hub,harbor,azure-acr,ali-acr,aws-ecr,google-gcr,quay,docker-registry,github-ghcr,jfrog-artifactory
PERMITTED_REGISTRY_TYPES_FOR_PROXY_CACHE=docker-hub,harbor,azure-acr,ali-acr,aws-ecr,google-gcr,docker-registry,github-ghcr,jfrog-artifactory
REPLICATION_ADAPTER_WHITELIST=ali-acr,aws-ecr,azure-acr,docker-hub,docker-registry,github-ghcr,google-gcr,harbor,huawei-SWR,jfrog-artifactory,tencent-tcr,volcengine-cr
HTTP_PROXY={{core_http_proxy}}
HTTPS_PROXY={{core_https_proxy}}

View File

@ -252,4 +252,7 @@ const (
// Global Leeway used for token validation
JwtLeeway = 60 * time.Second
// The replication adapter whitelist
ReplicationAdapterWhiteList = "REPLICATION_ADAPTER_WHITELIST"
)

View File

@ -162,6 +162,7 @@ var (
{Resource: rbac.ResourceRobot, Action: rbac.ActionRead},
{Resource: rbac.ResourceRobot, Action: rbac.ActionList},
{Resource: rbac.ResourceNotificationPolicy, Action: rbac.ActionRead},
{Resource: rbac.ResourceNotificationPolicy, Action: rbac.ActionList},
{Resource: rbac.ResourceScan, Action: rbac.ActionCreate},

View File

@ -72,7 +72,7 @@ func (s *SecurityContext) IsSolutionUser() bool {
// Can returns true only when requesting pull/push operation against the specific project
func (s *SecurityContext) Can(ctx context.Context, action types.Action, resource types.Resource) bool {
if !(action == rbac.ActionPull || action == rbac.ActionPush) {
if !(action == rbac.ActionPull || action == rbac.ActionPush || action == rbac.ActionDelete) {
log.Debugf("unauthorized for action %s", action)
return false
}

View File

@ -63,8 +63,8 @@ func (p *proxyCacheSecretTestSuite) TestIsSolutionUser() {
}
func (p *proxyCacheSecretTestSuite) TestCan() {
// the action isn't pull/push
action := rbac.ActionDelete
// the action isn't pull/push/delete
action := rbac.ActionUpdate
resource := project.NewNamespace(1).Resource(rbac.ResourceRepository)
p.False(p.sc.Can(context.TODO(), action, resource))

View File

@ -17,6 +17,8 @@ package parser
import (
"context"
"fmt"
"io"
"path/filepath"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
@ -40,6 +42,11 @@ const (
// defaultFileSizeLimit is the default file size limit.
defaultFileSizeLimit = 1024 * 1024 * 4 // 4MB
// formatTar is the format of tar file.
formatTar = ".tar"
// formatRaw is the format of raw file.
formatRaw = ".raw"
)
// newBase creates a new base parser.
@ -70,10 +77,23 @@ func (b *base) Parse(_ context.Context, artifact *artifact.Artifact, layer *ocis
}
defer stream.Close()
content, err := untar(stream)
content, err := decodeContent(layer.MediaType, stream)
if err != nil {
return "", nil, fmt.Errorf("failed to untar the content: %w", err)
return "", nil, fmt.Errorf("failed to decode content: %w", err)
}
return contentTypeTextPlain, content, nil
}
func decodeContent(mediaType string, reader io.Reader) ([]byte, error) {
format := filepath.Ext(mediaType)
switch format {
case formatTar:
return untar(reader)
case formatRaw:
return io.ReadAll(reader)
default:
return nil, fmt.Errorf("unsupported format: %s", format)
}
}

View File

@ -63,9 +63,10 @@ func TestBaseParse(t *testing.T) {
expectedError: "failed to pull blob from registry: registry error",
},
{
name: "successful parse",
name: "successful parse (tar format)",
artifact: &artifact.Artifact{RepositoryName: "test/repo"},
layer: &v1.Descriptor{
MediaType: "vnd.foo.bar.tar",
Digest: "sha256:1234",
},
mockSetup: func(m *mock.Client) {
@ -82,6 +83,34 @@ func TestBaseParse(t *testing.T) {
},
expectedType: contentTypeTextPlain,
},
{
name: "successful parse (raw format)",
artifact: &artifact.Artifact{RepositoryName: "test/repo"},
layer: &v1.Descriptor{
MediaType: "vnd.foo.bar.raw",
Digest: "sha256:1234",
},
mockSetup: func(m *mock.Client) {
var buf bytes.Buffer
buf.Write([]byte("test content"))
m.On("PullBlob", "test/repo", "sha256:1234").Return(int64(0), io.NopCloser(bytes.NewReader(buf.Bytes())), nil)
},
expectedType: contentTypeTextPlain,
},
{
name: "error parse (unsupported format)",
artifact: &artifact.Artifact{RepositoryName: "test/repo"},
layer: &v1.Descriptor{
MediaType: "vnd.foo.bar.unknown",
Digest: "sha256:1234",
},
mockSetup: func(m *mock.Client) {
var buf bytes.Buffer
buf.Write([]byte("test content"))
m.On("PullBlob", "test/repo", "sha256:1234").Return(int64(0), io.NopCloser(bytes.NewReader(buf.Bytes())), nil)
},
expectedError: "failed to decode content: unsupported format: .unknown",
},
}
for _, tt := range tests {

View File

@ -17,6 +17,7 @@ package parser
import (
"context"
"fmt"
"slices"
modelspec "github.com/CloudNativeAI/model-spec/specs-go/v1"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
@ -47,7 +48,10 @@ func (l *license) Parse(ctx context.Context, artifact *artifact.Artifact, manife
// lookup the license file layer
var layer *ocispec.Descriptor
for _, desc := range manifest.Layers {
if desc.MediaType == modelspec.MediaTypeModelDoc {
if slices.Contains([]string{
modelspec.MediaTypeModelDoc,
modelspec.MediaTypeModelDocRaw,
}, desc.MediaType) {
if desc.Annotations != nil {
filepath := desc.Annotations[modelspec.AnnotationFilepath]
if filepath == "LICENSE" || filepath == "LICENSE.txt" {

View File

@ -83,6 +83,29 @@ func TestLicenseParser(t *testing.T) {
expectedType: contentTypeTextPlain,
expectedOutput: []byte("MIT License"),
},
{
name: "LICENSE parse success (raw)",
manifest: &ocispec.Manifest{
Layers: []ocispec.Descriptor{
{
MediaType: modelspec.MediaTypeModelDocRaw,
Annotations: map[string]string{
modelspec.AnnotationFilepath: "LICENSE",
},
Digest: "sha256:abc123",
},
},
},
setupMockReg: func(mc *mockregistry.Client) {
var buf bytes.Buffer
buf.Write([]byte("MIT License"))
mc.On("PullBlob", mock.Anything, "sha256:abc123").
Return(int64(buf.Len()), io.NopCloser(bytes.NewReader(buf.Bytes())), nil)
},
expectedType: contentTypeTextPlain,
expectedOutput: []byte("MIT License"),
},
{
name: "LICENSE.txt parse success",
manifest: &ocispec.Manifest{

View File

@ -17,6 +17,7 @@ package parser
import (
"context"
"fmt"
"slices"
modelspec "github.com/CloudNativeAI/model-spec/specs-go/v1"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
@ -47,7 +48,10 @@ func (r *readme) Parse(ctx context.Context, artifact *artifact.Artifact, manifes
// lookup the readme file layer.
var layer *ocispec.Descriptor
for _, desc := range manifest.Layers {
if desc.MediaType == modelspec.MediaTypeModelDoc {
if slices.Contains([]string{
modelspec.MediaTypeModelDoc,
modelspec.MediaTypeModelDocRaw,
}, desc.MediaType) {
if desc.Annotations != nil {
filepath := desc.Annotations[modelspec.AnnotationFilepath]
if filepath == "README" || filepath == "README.md" {

View File

@ -113,6 +113,29 @@ func TestReadmeParser(t *testing.T) {
expectedType: contentTypeMarkdown,
expectedOutput: []byte("# Test README"),
},
{
name: "README parse success (raw)",
manifest: &ocispec.Manifest{
Layers: []ocispec.Descriptor{
{
MediaType: modelspec.MediaTypeModelDocRaw,
Annotations: map[string]string{
modelspec.AnnotationFilepath: "README",
},
Digest: "sha256:def456",
},
},
},
setupMockReg: func(mc *mockregistry.Client) {
var buf bytes.Buffer
buf.Write([]byte("# Test README"))
mc.On("PullBlob", mock.Anything, "sha256:def456").
Return(int64(buf.Len()), io.NopCloser(bytes.NewReader(buf.Bytes())), nil)
},
expectedType: contentTypeMarkdown,
expectedOutput: []byte("# Test README"),
},
{
name: "registry error",
manifest: &ocispec.Manifest{

View File

@ -149,6 +149,22 @@ type ManifestList struct {
ContentType string
}
// getManifestDigestInLocal get the artifact digest in local
func (c *controller) getManifestDigestInLocal(ctx context.Context, art lib.ArtifactInfo) (string, error) {
// Get the manifest from local registry
a, err := c.local.GetManifest(ctx, art)
if err != nil {
return "", err
}
if a == nil {
return "", errors.NotFoundError(fmt.Errorf("manifest %v not found in local registry", art.Repository))
}
if len(a.Digest) == 0 {
return "", errors.NotFoundError(fmt.Errorf("manifest %v not found in local registry", art.Repository))
}
return a.Digest, nil
}
// UseLocalManifest check if these manifest could be found in local registry,
// the return error should be nil when it is not found in local and need to delegate to remote registry
// the return error should be NotFoundError when it is not found in remote registry
@ -172,6 +188,16 @@ func (c *controller) UseLocalManifest(ctx context.Context, art lib.ArtifactInfo,
return false, nil, err
}
if !exist || desc == nil {
dig, err := c.getManifestDigestInLocal(ctx, art)
if err != nil {
// skip to delete when error, use debug level log to avoid too many logs when the manifest is removed from upstream
log.Debugf("failed to get manifest digest in local, error: %v, skip to delete it, art %+v", err, art)
} else {
go func() {
c.local.DeleteManifest(art.Repository, dig)
log.Infof("delete manifest %s with digest %s", art.Repository, dig)
}()
}
return false, nil, errors.NotFoundError(fmt.Errorf("repo %v, tag %v not found", art.Repository, art.Tag))
}

View File

@ -17,9 +17,12 @@ package registry
import (
"context"
"math/rand"
"strings"
"time"
"github.com/goharbor/harbor/src/common"
"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"
@ -205,12 +208,49 @@ func (c *controller) GetInfo(ctx context.Context, id int64) (*model.RegistryInfo
return info, nil
}
func getWhitelistedAdapters(ctx context.Context) map[string]struct{} {
adapterWhitelistRaw := config.GetCfgManager(ctx).Get(ctx, common.ReplicationAdapterWhiteList).GetString()
if adapterWhitelistRaw == "" {
return nil
}
adapterWhitelist := make(map[string]struct{})
for _, adapter := range strings.Split(adapterWhitelistRaw, ",") {
adapter = strings.TrimSpace(adapter)
if adapter != "" {
adapterWhitelist[adapter] = struct{}{}
}
}
return adapterWhitelist
}
func (c *controller) ListRegistryProviderTypes(ctx context.Context) ([]string, error) {
return c.regMgr.ListRegistryProviderTypes(ctx)
allAdapters, err := c.regMgr.ListRegistryProviderTypes(ctx)
if err != nil {
return []string{}, err
}
whitelistedAdapters := getWhitelistedAdapters(ctx)
var filtered []string
for _, t := range allAdapters {
if _, ok := whitelistedAdapters[t]; ok {
filtered = append(filtered, t)
}
}
return filtered, nil
}
func (c *controller) ListRegistryProviderInfos(ctx context.Context) (map[string]*model.AdapterPattern, error) {
return c.regMgr.ListRegistryProviderInfos(ctx)
allAdaptersInfo, err := c.regMgr.ListRegistryProviderInfos(ctx)
if err != nil {
return nil, err
}
whitelistedAdapters := getWhitelistedAdapters(ctx)
filtered := make(map[string]*model.AdapterPattern)
for k, v := range allAdaptersInfo {
if _, ok := whitelistedAdapters[k]; ok {
filtered[k] = v
}
}
return filtered, nil
}
func (c *controller) StartRegularHealthCheck(ctx context.Context, closing, done chan struct{}) {

View File

@ -15,10 +15,14 @@
package registry
import (
"context"
"testing"
"github.com/stretchr/testify/suite"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/lib/config"
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
"github.com/goharbor/harbor/src/pkg/reg/model"
"github.com/goharbor/harbor/src/testing/mock"
testingproject "github.com/goharbor/harbor/src/testing/pkg/project"
@ -174,6 +178,64 @@ func (r *registryTestSuite) TestDelete() {
r.proMgr.AssertExpectations(r.T())
}
func (r *registryTestSuite) TestGetWhitelistedAdapters() {
tests := []struct {
name string
input string
expected map[string]struct{}
}{
{
name: "adapter empty",
input: "",
expected: nil,
},
{
name: "adapters with spaces",
input: "dockerhub, aws, gcr ",
expected: map[string]struct{}{
"dockerhub": {},
"aws": {},
"gcr": {},
},
},
{
name: "adapters with empty entries",
input: "harbor, , quay,",
expected: map[string]struct{}{
"harbor": {},
"quay": {},
},
},
{
name: "adapters all",
input: "ali-acr,aws-ecr,azure-acr,docker-hub,google-gcr,harbor,huawei-SWR,jfrog-artifactory,tencent-tcr,volcengine-cr",
expected: map[string]struct{}{
"ali-acr": {},
"aws-ecr": {},
"azure-acr": {},
"docker-hub": {},
"google-gcr": {},
"harbor": {},
"huawei-SWR": {},
"jfrog-artifactory": {},
"tencent-tcr": {},
"volcengine-cr": {},
},
},
}
for _, tt := range tests {
r.Run(tt.name, func() {
conf := map[string]any{
common.ReplicationAdapterWhiteList: tt.input,
}
config.InitWithSettings(conf)
result := getWhitelistedAdapters(context.TODO())
r.Equal(tt.expected, result)
})
}
}
func TestRegistryTestSuite(t *testing.T) {
suite.Run(t, &registryTestSuite{})
}

View File

@ -109,10 +109,37 @@ func (c *controller) Start(ctx context.Context, policy *replicationmodel.Policy,
if op := operator.FromContext(ctx); op != "" {
extra["operator"] = op
}
var count int64
// If running executions are found, skip the current execution and mark it as error.
if policy.SingleActiveReplication {
var err error
count, err = c.execMgr.Count(ctx, &q.Query{
Keywords: map[string]any{
"VendorType": job.ReplicationVendorType,
"VendorID": policy.ID,
"Status": job.RunningStatus.String(),
},
})
if err != nil {
return 0, fmt.Errorf("failed to count running executions for policy ID: %d: %v", policy.ID, err)
}
}
id, err := c.execMgr.Create(ctx, job.ReplicationVendorType, policy.ID, trigger, extra)
if err != nil {
return 0, err
}
if policy.SingleActiveReplication {
if count > 0 {
if err = c.execMgr.MarkError(ctx, id, "Execution skipped: active replication still in progress."); err != nil {
return 0, err
}
return id, nil
}
}
// start the replication flow in background
// as the process runs inside a goroutine, the transaction in the outer ctx
// may be submitted already when the process starts, so create an new context

View File

@ -101,6 +101,38 @@ func (r *replicationTestSuite) TestStart() {
r.execMgr.AssertExpectations(r.T())
r.flowCtl.AssertExpectations(r.T())
r.ormCreator.AssertExpectations(r.T())
r.SetupTest()
// run replication flow with SingleActiveReplication, flow should not start
r.execMgr.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil)
r.execMgr.On("MarkError", mock.Anything, mock.Anything, mock.Anything).Return(nil)
r.execMgr.On("Count", mock.Anything, mock.Anything).Return(int64(1), nil) // Simulate an existing running execution
id, err = r.ctl.Start(context.Background(), &repctlmodel.Policy{Enabled: true, SingleActiveReplication: true}, nil, task.ExecutionTriggerManual)
r.Require().Nil(err)
r.Equal(int64(1), id)
time.Sleep(1 * time.Second) // wait the functions called in the goroutine
r.flowCtl.AssertNumberOfCalls(r.T(), "Start", 0)
r.execMgr.AssertNumberOfCalls(r.T(), "MarkError", 1) // Ensure execution marked as final status error
r.execMgr.AssertExpectations(r.T())
r.flowCtl.AssertExpectations(r.T())
r.ormCreator.AssertExpectations(r.T())
r.SetupTest()
// no error when running the replication flow with SingleActiveReplication
r.execMgr.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil)
r.execMgr.On("Get", mock.Anything, mock.Anything).Return(&task.Execution{}, nil)
r.flowCtl.On("Start", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
r.ormCreator.On("Create").Return(nil)
r.execMgr.On("Count", mock.Anything, mock.Anything).Return(int64(0), nil) // Simulate no running execution
id, err = r.ctl.Start(context.Background(), &repctlmodel.Policy{Enabled: true, SingleActiveReplication: true}, nil, task.ExecutionTriggerManual)
r.Require().Nil(err)
r.Equal(int64(1), id)
time.Sleep(1 * time.Second) // wait the functions called in the goroutine
r.execMgr.AssertExpectations(r.T())
r.flowCtl.AssertExpectations(r.T())
r.ormCreator.AssertExpectations(r.T())
}
func (r *replicationTestSuite) TestStop() {

View File

@ -47,6 +47,7 @@ type Policy struct {
UpdateTime time.Time `json:"update_time"`
Speed int32 `json:"speed"`
CopyByChunk bool `json:"copy_by_chunk"`
SingleActiveReplication bool `json:"single_active_replication"`
}
// IsScheduledTrigger returns true when the policy is scheduled trigger and enabled
@ -141,6 +142,7 @@ func (p *Policy) From(policy *replicationmodel.Policy) error {
p.UpdateTime = policy.UpdateTime
p.Speed = policy.Speed
p.CopyByChunk = policy.CopyByChunk
p.SingleActiveReplication = policy.SingleActiveReplication
if policy.SrcRegistryID > 0 {
p.SrcRegistry = &model.Registry{
@ -186,6 +188,7 @@ func (p *Policy) To() (*replicationmodel.Policy, error) {
UpdateTime: p.UpdateTime,
Speed: p.Speed,
CopyByChunk: p.CopyByChunk,
SingleActiveReplication: p.SingleActiveReplication,
}
if p.SrcRegistry != nil {
policy.SrcRegistryID = p.SrcRegistry.ID

View File

@ -597,7 +597,9 @@ func (bc *basicController) GetReport(ctx context.Context, artifact *ar.Artifact,
return nil, err
}
if !scannable {
// When the scanner is unhealthy, the artifact will be recognized as "not scannable", in this case we will not return the error
// but return the scanner report with the best effort.
if !scannable && r.Health == sc.StatusHealthy {
return nil, errors.NotFoundError(nil).WithMessagef("report not found for %s@%s", artifact.RepositoryName, artifact.Digest)
}

View File

@ -36,8 +36,8 @@ import (
const (
proScannerMetaKey = "projectScanner"
statusUnhealthy = "unhealthy"
statusHealthy = "healthy"
StatusUnhealthy = "unhealthy"
StatusHealthy = "healthy"
// RetrieveCapFailMsg the message indicate failed to retrieve the scanner capabilities
RetrieveCapFailMsg = "failed to retrieve scanner capabilities, error %v"
)
@ -287,9 +287,9 @@ func (bc *basicController) GetRegistrationByProject(ctx context.Context, project
if err != nil {
// Not blocked, just logged it
log.Error(errors.Wrap(err, "api controller: get project scanner"))
registration.Health = statusUnhealthy
registration.Health = StatusUnhealthy
} else {
registration.Health = statusHealthy
registration.Health = StatusHealthy
// Fill in some metadata
registration.Adapter = meta.Scanner.Name
registration.Vendor = meta.Scanner.Vendor

View File

@ -1,20 +1,20 @@
module github.com/goharbor/harbor/src
go 1.24.3
go 1.24.6
require (
github.com/CloudNativeAI/model-spec v0.0.3
github.com/CloudNativeAI/model-spec v0.0.5
github.com/FZambia/sentinel v1.1.0
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/beego/beego/v2 v2.3.6
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.15.2
github.com/cloudevents/sdk-go/v2 v2.16.1
github.com/coreos/go-oidc/v3 v3.12.0
github.com/dghubble/sling v1.4.2
github.com/docker/distribution v2.8.3+incompatible
@ -26,7 +26,7 @@ require (
github.com/go-openapi/runtime v0.28.0
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/strfmt v0.23.0
github.com/go-openapi/swag v0.23.0
github.com/go-openapi/swag v0.23.1
github.com/go-openapi/validate v0.24.0 // indirect
github.com/go-redis/redis/v8 v8.11.4
github.com/gocarina/gocsv v0.0.0-20210516172204-ca9e8a8ddea8
@ -50,13 +50,13 @@ require (
github.com/opencontainers/image-spec v1.1.1
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.22.0
github.com/prometheus/client_model v0.6.1
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/tencentcloud/tencentcloud-sdk-go v3.0.233+incompatible
github.com/vmihailenco/msgpack/v5 v5.4.1
github.com/volcengine/volcengine-go-sdk v1.1.19
github.com/volcengine/volcengine-go-sdk v1.1.29
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.59.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0
go.opentelemetry.io/otel v1.35.0
@ -66,19 +66,19 @@ require (
go.opentelemetry.io/otel/trace v1.35.0
go.pinniped.dev v0.37.0
go.uber.org/ratelimit v0.3.1
golang.org/x/crypto v0.37.0
golang.org/x/net v0.38.0
golang.org/x/crypto v0.40.0
golang.org/x/net v0.41.0
golang.org/x/oauth2 v0.28.0
golang.org/x/sync v0.14.0
golang.org/x/text v0.24.0
golang.org/x/time v0.11.0
golang.org/x/sync v0.16.0
golang.org/x/text v0.27.0
golang.org/x/time v0.12.0
gopkg.in/h2non/gock.v1 v1.1.2
gopkg.in/yaml.v2 v2.4.0
helm.sh/helm/v3 v3.18.2
k8s.io/api v0.33.1
k8s.io/apimachinery v0.33.1
k8s.io/client-go v0.33.1
sigs.k8s.io/yaml v1.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
)
require (
@ -138,7 +138,7 @@ require (
github.com/klauspost/compress v1.18.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@ -161,7 +161,7 @@ require (
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/pflag v1.0.7 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
@ -177,15 +177,16 @@ require (
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.33.0 // indirect
golang.org/x/term v0.31.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/term v0.33.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
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/grpc v1.69.4 // indirect
google.golang.org/protobuf v1.36.5 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

View File

@ -40,8 +40,8 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzS
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/CloudNativeAI/model-spec v0.0.3 h1:5mvgFQ+3pyupzxYjtV5XAeg9zRe6+46pLPBFNHlOrqE=
github.com/CloudNativeAI/model-spec v0.0.3/go.mod h1:3U/4zubBfbUkW59ATSg41HnkYyKrKUcKFH/cVdoPQnk=
github.com/CloudNativeAI/model-spec v0.0.5 h1:gVwvpiVJgGZynvIi+xy0Hp/zh+g1EuWa6x6Czm4P0uA=
github.com/CloudNativeAI/model-spec v0.0.5/go.mod h1:3U/4zubBfbUkW59ATSg41HnkYyKrKUcKFH/cVdoPQnk=
github.com/FZambia/sentinel v1.1.0 h1:qrCBfxc8SvJihYNjBWgwUI93ZCvFe/PJIPTHKmlp8a8=
github.com/FZambia/sentinel v1.1.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
@ -68,8 +68,8 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:W
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/beego/beego/v2 v2.3.6 h1:VorPq190QdbA6ZSCM4K901gBkjcpN3csi7ReDk63ClI=
github.com/beego/beego/v2 v2.3.6/go.mod h1:5cqHsOHJIxkq44tBpRvtDe59GuVRVv/9/tyVDxd5ce4=
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=
github.com/beego/i18n v0.0.0-20140604031826-e87155e8f0c0/go.mod h1:KLeFCpAMq2+50NkXC8iiJxLLiiTfTqrGtKEVm+2fk7s=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
@ -89,8 +89,8 @@ github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudevents/sdk-go/v2 v2.15.2 h1:54+I5xQEnI73RBhWHxbI1XJcqOFOVJN85vb41+8mHUc=
github.com/cloudevents/sdk-go/v2 v2.15.2/go.mod h1:lL7kSWAE/V8VI4Wh0jbL2v/jvqsm6tjmaQBSvxcv4uE=
github.com/cloudevents/sdk-go/v2 v2.16.1 h1:G91iUdqvl88BZ1GYYr9vScTj5zzXSyEuqbfE63gbu9Q=
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/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
@ -185,8 +185,8 @@ github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9Z
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
@ -393,8 +393,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
@ -402,8 +402,8 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU=
github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@ -472,8 +472,8 @@ github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
@ -522,8 +522,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -564,8 +564,8 @@ 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.19 h1:+jLVMqDtdtiAt8QGBk6AMiEg22Br5SZGu2FSHUrIcU0=
github.com/volcengine/volcengine-go-sdk v1.1.19/go.mod h1:EyKoi6t6eZxoPNGr2GdFCZti2Skd7MO3eUzx7TtSvNo=
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/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=
@ -621,6 +621,10 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
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=
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=
@ -638,8 +642,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
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.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
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/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=
@ -666,8 +670,8 @@ 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.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
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/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=
@ -693,8 +697,8 @@ 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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
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/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=
@ -710,8 +714,8 @@ 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.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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=
@ -746,8 +750,8 @@ 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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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/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=
@ -758,8 +762,8 @@ 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.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
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/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=
@ -772,10 +776,10 @@ 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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
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/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,8 +802,8 @@ 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.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
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=
@ -839,8 +843,8 @@ 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.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -874,17 +878,17 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
helm.sh/helm/v3 v3.18.2 h1:mPQP/HHYjNEDAztAK50dD6uxTCNV1zSVU38WwSVdw9M=
helm.sh/helm/v3 v3.18.2/go.mod h1:43QHS1W97RcoFJRk36ZBhHdTfykqBlJdsWp3yhzdq8w=
helm.sh/helm/v3 v3.18.5 h1:Cc3Z5vd6kDrZq9wO9KxKLNEickiTho6/H/dBNRVSos4=
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.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw=
k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw=
k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4=
k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA=
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/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=
@ -899,5 +903,6 @@ 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 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
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=

View File

@ -203,5 +203,7 @@ var (
{Name: common.BeegoMaxMemoryBytes, Scope: SystemScope, Group: BasicGroup, EnvKey: "BEEGO_MAX_MEMORY_BYTES", DefaultValue: fmt.Sprintf("%d", common.DefaultBeegoMaxMemoryBytes), ItemType: &Int64Type{}, Editable: false, Description: `The bytes for limiting the beego max memory, default is 128GB`},
{Name: common.BeegoMaxUploadSizeBytes, Scope: SystemScope, Group: BasicGroup, EnvKey: "BEEGO_MAX_UPLOAD_SIZE_BYTES", DefaultValue: fmt.Sprintf("%d", common.DefaultBeegoMaxUploadSizeBytes), ItemType: &Int64Type{}, Editable: false, Description: `The bytes for limiting the beego max upload size, default it 128GB`},
{Name: common.ReplicationAdapterWhiteList, Scope: SystemScope, Group: BasicGroup, EnvKey: "REPLICATION_ADAPTER_WHITELIST", DefaultValue: "", ItemType: &StringType{}, Editable: false},
}
)

View File

@ -161,17 +161,36 @@ func setFilters(ctx context.Context, qs orm.QuerySeter, query *q.Query, meta *me
log.Warningf("The separator '%s' is not valid in the query parameter '%s__%s'. Please use the correct field name.", orm.ExprSep, keyPieces[0], keyPieces[1])
continue
}
k := keyPieces[0]
mk, filterable := meta.Filterable(k)
fieldKey := keyPieces[0]
mk, filterable := meta.Filterable(fieldKey)
if !filterable {
// This is a workaround for the unsuitable usage of query, the keyword format for field and method should be consistent
// e.g. "ArtifactDigest" or the snake case format "artifact_digest" should be used instead:
// https://github.com/goharbor/harbor/blob/v2.2.0/src/controller/blob/controller.go#L233
mk, filterable = meta.Filterable(snakeCase(k))
mk, filterable = meta.Filterable(snakeCase(fieldKey))
if mk == nil || !filterable {
continue
}
}
// only accept the below operators
if len(keyPieces) == 2 {
operator := orm.ExprSep + keyPieces[1]
allowedOperators := map[string]struct{}{
"__icontains": {},
"__in": {},
"__gte": {},
"__lte": {},
"__gt": {},
"__lt": {},
"__exact": {},
}
if _, ok := allowedOperators[operator]; !ok {
log.Warningf("the operator '%s' in query parameter '%s' is not supported, the query will be skipped.", operator, key)
continue
}
}
// filter function defined, use it directly
if mk.FilterFunc != nil {
qs = mk.FilterFunc(ctx, qs, key, value)

View File

@ -43,6 +43,7 @@ type Policy struct {
UpdateTime time.Time `orm:"column(update_time);auto_now"`
Speed int32 `orm:"column(speed_kb)"`
CopyByChunk bool `orm:"column(copy_by_chunk)"`
SingleActiveReplication bool `orm:"column(single_active_replication)"`
}
// TableName set table name for ORM

View File

@ -159,7 +159,7 @@ func (d *dao) NonEmptyRepos(ctx context.Context) ([]*model.RepoRecord, error) {
return nil, err
}
sql := `select * from repository where repository_id in (select distinct repository_id from tag)`
sql := `select * from repository where exists (select 1 from tag where tag.repository_id = repository.repository_id)`
_, err = ormer.Raw(sql).QueryRows(&repos)
if err != nil {
return repos, err

View File

@ -825,6 +825,39 @@
'REPLICATION.ENABLED_RULE' | translate
}}</label>
</div>
<div
class="clr-checkbox-wrapper"
[hidden]="!isNotEventBased()">
<input
type="checkbox"
class="clr-checkbox"
[checked]="true"
id="singleActiveReplication"
formControlName="single_active_replication" />
<label
for="singleActiveReplication"
class="clr-control-label single-active"
>{{
'REPLICATION.SINGLE_ACTIVE_REPLICATION'
| translate
}}
<clr-tooltip class="override-tooltip">
<clr-icon
clrTooltipTrigger
shape="info-circle"
size="24"></clr-icon>
<clr-tooltip-content
clrPosition="top-left"
clrSize="md"
*clrIfOpen>
<span>{{
'TOOLTIP.SINGLE_ACTIVE_REPLICATION'
| translate
}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
</div>
</div>
</div>
</form>

View File

@ -246,6 +246,10 @@ clr-modal {
width: 8.6rem;
}
.single-active {
width: 16rem;
}
.des-tooltip {
margin-left: 0.5rem;
}

View File

@ -334,6 +334,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
override: true,
speed: -1,
copy_by_chunk: false,
single_active_replication: false,
});
}
@ -367,6 +368,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
dest_namespace_replace_count: Flatten_Level.FLATTEN_LEVEl_1,
speed: -1,
copy_by_chunk: false,
single_active_replication: false,
});
this.isPushMode = true;
this.selectedUnit = BandwidthUnit.KB;
@ -410,6 +412,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
override: rule.override,
speed: speed,
copy_by_chunk: rule.copy_by_chunk,
single_active_replication: rule.single_active_replication,
});
let filtersArray = this.getFilterArray(rule);
this.noSelectedEndpoint = false;
@ -510,6 +513,9 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
}
onSubmit() {
if (this.ruleForm.value.trigger.type === 'event_based') {
this.ruleForm.get('single_active_replication').setValue(false);
}
if (this.ruleForm.value.trigger.type !== 'scheduled') {
this.ruleForm
.get('trigger')

View File

@ -138,7 +138,7 @@
}}</a>
</clr-dg-cell>
<clr-dg-cell>
{{ getStatusStr(j.status) }}
{{ getStatusStr(j.status, j.status_text) }}
<clr-tooltip>
<clr-icon
*ngIf="j.status_text"

View File

@ -644,7 +644,11 @@ export class ReplicationComponent implements OnInit, OnDestroy {
}
}
getStatusStr(status: string): string {
getStatusStr(status: string, status_text: string): string {
// If status is Failed and status_text has 'Execution skipped', it means the replication task is skipped.
if (status === 'Failed' && status_text.startsWith('Execution skipped'))
return 'Skipped';
if (STATUS_MAP && STATUS_MAP[status]) {
return STATUS_MAP[status];
}

View File

@ -196,7 +196,7 @@ export class PasswordSettingComponent implements AfterViewChecked {
msg.includes('old_password_is_not_correct')
) {
this.inlineAlert.showInlineError(
'INCONRRECT_OLD_PWD'
'INCORRECT_OLD_PWD'
);
} else {
this.inlineAlert.showInlineError(error);

View File

@ -123,7 +123,7 @@
}}</label>
</clr-checkbox-wrapper>
<clr-control-helper class="config-subtext">
{{ 'PROJECT_CONFIG.CONTENT_TRUST_POLCIY' | translate }}
{{ 'PROJECT_CONFIG.CONTENT_TRUST_POLICY' | translate }}
</clr-control-helper>
</clr-checkbox-container>
<clr-checkbox-container

View File

@ -76,6 +76,7 @@
"PULL_BASED": "Lade die Ressourcen von der entfernten Registry auf den lokalen Harbor runter.",
"DESTINATION_NAMESPACE": "Spezifizieren des Ziel-Namespace. Wenn das Feld leer ist, werden die Ressourcen unter dem gleichen Namespace abgelegt wie in der Quelle.",
"OVERRIDE": "Spezifizieren, ob die Ressourcen am Ziel überschrieben werden sollen, falls eine Ressource mit gleichem Namen existiert.",
"SINGLE_ACTIVE_REPLICATION": "Specify whether to skip execution until the previous active execution finishes, avoiding the execution of the same replication rules multiple times in parallel.",
"EMAIL": "E-Mail sollte eine gültige E-Mail-Adresse wie name@example.com sein.",
"USER_NAME": "Darf keine Sonderzeichen enthalten und sollte kürzer als 255 Zeichen sein.",
"FULL_NAME": "Maximale Länge soll 20 Zeichen sein.",
@ -293,7 +294,7 @@
"PUBLIC_POLICY": "Ein Projekt öffentlich einzustellen, macht die Repositories für alle zugreifbar.",
"SECURITY": "Deployment Sicherheit",
"CONTENT_TRUST_TOGGLE": "Aktiviere Content Trust",
"CONTENT_TRUST_POLCIY": "Erlaube ausschließlich verifizierte Images.",
"CONTENT_TRUST_POLICY": "Erlaube ausschließlich verifizierte Images.",
"PREVENT_VULNERABLE_TOGGLE": "Verhindere den Download von Images mit Schwachstellen.",
"PREVENT_VULNERABLE_1": "Verhindere den Download von Images mit Schwachstellen des Schweregrads ",
"PREVENT_VULNERABLE_2": "und darüber.",
@ -578,6 +579,7 @@
"ALLOWED_CHARACTERS": "Erlaubte Sonderzeichen",
"TOTAL": "Gesamt",
"OVERRIDE": "Überschreiben",
"SINGLE_ACTIVE_REPLICATION": "Single active replication",
"ENABLED_RULE": "Aktiviere Regel",
"OVERRIDE_INFO": "Überschreiben",
"OPERATION": "Operation",
@ -1309,7 +1311,7 @@
"CONFLICT_ERROR": "Der Befehl kann nicht ausgeführt werden, da es Konflikte gibt.",
"PRECONDITION_FAILED": "Der Befehl kann wegen eines Überprüfungsfehlers nicht ausgeführt werden.",
"SERVER_ERROR": "Der Befehl kann wegen einem internen Serverfehler nicht ausgeführt werden.",
"INCONRRECT_OLD_PWD": "Das alte Passwort ist nicht korrekt.",
"INCORRECT_OLD_PWD": "Das alte Passwort ist nicht korrekt.",
"UNKNOWN": "n/a",
"STATUS": "Status",
"START_TIME": "Startzeit",

View File

@ -76,6 +76,7 @@
"PULL_BASED": "Pull the resources from the remote registry to the local Harbor.",
"DESTINATION_NAMESPACE": "Specify the destination namespace. If empty, the resources will be put under the same namespace as the source.",
"OVERRIDE": "Specify whether to override the resources at the destination if a resource with the same name exists.",
"SINGLE_ACTIVE_REPLICATION": "Specify whether to skip execution until the previous active execution finishes, avoiding the execution of the same replication rules multiple times in parallel.",
"EMAIL": "Email should be a valid email address like name@example.com.",
"USER_NAME": "Cannot contain special characters and maximum length should be 255 characters.",
"FULL_NAME": "Maximum length should be 20 characters.",
@ -293,7 +294,7 @@
"PUBLIC_POLICY": "Making a project registry public will make all repositories accessible to everyone.",
"SECURITY": "Deployment security",
"CONTENT_TRUST_TOGGLE": "Enable content trust",
"CONTENT_TRUST_POLCIY": "Allow only verified images to be deployed.",
"CONTENT_TRUST_POLICY": "Allow only verified images to be deployed.",
"PREVENT_VULNERABLE_TOGGLE": "Prevent vulnerable images from running.",
"PREVENT_VULNERABLE_1": "Prevent images with vulnerability severity of",
"PREVENT_VULNERABLE_2": "and above from being deployed.",
@ -578,6 +579,7 @@
"ALLOWED_CHARACTERS": "Allowed special characters",
"TOTAL": "Total",
"OVERRIDE": "Override",
"SINGLE_ACTIVE_REPLICATION": "Single active replication",
"ENABLED_RULE": "Enable rule",
"OVERRIDE_INFO": "Override",
"OPERATION": "Operation",
@ -800,7 +802,7 @@
"TAGS_NO_DELETE": "Delete is prohibited in read only mode.",
"FILTER_FOR_REPOSITORIES": "Filter Repositories",
"TAG": "Tag",
"ARTIFACT": "Aarifact",
"ARTIFACT": "Artifact",
"ARTIFACTS": "Artifacts",
"SIZE": "Size",
"VULNERABILITY": "Vulnerabilities",
@ -1312,7 +1314,7 @@
"CONFLICT_ERROR": "We are unable to perform your action because your submission has conflicts.",
"PRECONDITION_FAILED": "We are unable to perform your action because of a precondition failure.",
"SERVER_ERROR": "We are unable to perform your action because internal server errors have occurred.",
"INCONRRECT_OLD_PWD": "The old password is incorrect.",
"INCORRECT_OLD_PWD": "The old password is incorrect.",
"UNKNOWN": "n/a",
"STATUS": "Status",
"START_TIME": "Start Time",

View File

@ -76,6 +76,7 @@
"PULL_BASED": "Pull de recursos del remote registry al local Harbor.",
"DESTINATION_NAMESPACE": "Especificar el namespace de destino. Si esta vacio, los recursos se colocan en el mismo namespace del recurso.",
"OVERRIDE": "Especifique si desea anular los recursos en el destino si existe un recurso con el mismo nombre.",
"SINGLE_ACTIVE_REPLICATION": "Specify whether to skip execution until the previous active execution finishes, avoiding the execution of the same replication rules multiple times in parallel.",
"EMAIL": "El email debe ser una dirección válida como nombre@ejemplo.com.",
"USER_NAME": "Debe tener una longitud máxima de 255 caracteres y no puede contener caracteres especiales.",
"FULL_NAME": "La longitud máxima debería ser de 20 caracteres.",
@ -294,7 +295,7 @@
"PUBLIC_POLICY": "Hacer público un registro de proyecto hará que todos los repositorios sean accesibles para todos.",
"SECURITY": "Seguridad Despliegues",
"CONTENT_TRUST_TOGGLE": "Habilitar la confianza de contenido",
"CONTENT_TRUST_POLCIY": "Solo permita la implementación de imágenes verificadas.",
"CONTENT_TRUST_POLICY": "Solo permita la implementación de imágenes verificadas.",
"PREVENT_VULNERABLE_TOGGLE": "Evitar que se ejecuten imágenes vulnerables.",
"PREVENT_VULNERABLE_1": "Impedir imágenes con la gravedad de la vulnerabilidad de",
"PREVENT_VULNERABLE_2": "y más arriba de ser desplegado.",
@ -578,6 +579,7 @@
"ALLOWED_CHARACTERS": "Caracteres Especiales Permitidos",
"TOTAL": "Total",
"OVERRIDE": "Sobreescribir",
"SINGLE_ACTIVE_REPLICATION": "Single active replication",
"ENABLED_RULE": "Activar regla",
"OVERRIDE_INFO": "Sobreescribir",
"CURRENT": "Actual",
@ -1306,7 +1308,7 @@
"CONFLICT_ERROR": "No hemos podido llevar a cabo la acción debido a un conflicto.",
"PRECONDITION_FAILED": "No hemos podido llevar a cabo la acción debido a un error de precondición.",
"SERVER_ERROR": "No hemos podido llevar a cabo la acción debido a un error interno.",
"INCONRRECT_OLD_PWD": "La contraseña antigua no es correcta.",
"INCORRECT_OLD_PWD": "La contraseña antigua no es correcta.",
"UNKNOWN": "n/a",
"STATUS": "Estatus",
"START_TIME": "Tiempo Inicio",

View File

@ -76,6 +76,7 @@
"PULL_BASED": "Pull les ressources du registre distant vers le Harbor local.",
"DESTINATION_NAMESPACE": "Spécifier l'espace de nom de destination. Si vide, les ressources seront placées sous le même espace de nom que la source.",
"OVERRIDE": "Spécifier s'il faut remplacer les ressources dans la destination si une ressource avec le même nom existe.",
"SINGLE_ACTIVE_REPLICATION": "Specify whether to skip execution until the previous active execution finishes, avoiding the execution of the same replication rules multiple times in parallel.",
"EMAIL": "L'e-mail doit être une adresse e-mail valide comme name@example.com.",
"USER_NAME": "Ne peut pas contenir de caractères spéciaux et la longueur maximale est de 255 caractères.",
"FULL_NAME": "La longueur maximale est de 20 caractères.",
@ -293,7 +294,7 @@
"PUBLIC_POLICY": "Rendre public un registre de projets rendra tous les dépôts accessibles à tous.",
"SECURITY": "Sécurité de déploiement",
"CONTENT_TRUST_TOGGLE": "Activer la confiance du contenu",
"CONTENT_TRUST_POLCIY": "Autoriser uniquement le déploiement d'images vérifiées.",
"CONTENT_TRUST_POLICY": "Autoriser uniquement le déploiement d'images vérifiées.",
"PREVENT_VULNERABLE_TOGGLE": "Empêche les images vulnérables de fonctionner.",
"PREVENT_VULNERABLE_1": "Empêcher les images présentant une vulnérabilité grave de",
"PREVENT_VULNERABLE_2": "et au-dessus d'être déployées.",
@ -578,6 +579,7 @@
"ALLOWED_CHARACTERS": "Caractères spéciaux autorisés",
"TOTAL": "Total",
"OVERRIDE": "Surcharger",
"SINGLE_ACTIVE_REPLICATION": "Single active replication",
"ENABLED_RULE": "Activer la règle",
"OVERRIDE_INFO": "Surcharger",
"OPERATION": "Opération",
@ -1311,7 +1313,7 @@
"CONFLICT_ERROR": "Nous ne sommes pas en mesure d'exécuter votre action parce que votre soumission a des conflits.",
"PRECONDITION_FAILED": "Nous ne pouvons pas exécuter votre action en raison d'un échec de conditions préalables.",
"SERVER_ERROR": "Nous ne sommes pas en mesure d'exécuter votre action parce que des erreurs internes de serveur se sont produites.",
"INCONRRECT_OLD_PWD": "L'ancien mot de passe est incorrect.",
"INCORRECT_OLD_PWD": "L'ancien mot de passe est incorrect.",
"UNKNOWN": "n. d.",
"STATUS": "Statut",
"START_TIME": "Date/Heure de début",

View File

@ -76,6 +76,7 @@
"PULL_BASED": "원격 레지스트리의 리소스를 로컬 'Harbor'로 가져옵니다.",
"DESTINATION_NAMESPACE": "대상 네임스페이스를 지정합니다. 비어 있으면 리소스는 소스와 동일한 네임스페이스에 배치됩니다.",
"OVERRIDE": "동일한 이름의 리소스가 있는 경우 대상의 리소스를 재정의할지 여부를 지정합니다.",
"SINGLE_ACTIVE_REPLICATION": "Specify whether to skip execution until the previous active execution finishes, avoiding the execution of the same replication rules multiple times in parallel.",
"EMAIL": "이메일은 name@example.com과 같은 유효한 이메일 주소여야 합니다.",
"USER_NAME": "특수 문자를 포함할 수 없으며 최대 길이는 255자입니다.",
"FULL_NAME": "최대 길이는 20자입니다.",
@ -293,7 +294,7 @@
"PUBLIC_POLICY": "프로젝트 레지스트리를 공개하면 모든 사람이 모든 저장소에 액세스할 수 있습니다.",
"SECURITY": "배포 보안",
"CONTENT_TRUST_TOGGLE": "콘텐츠 신뢰 활성화",
"CONTENT_TRUST_POLCIY": "확인된 이미지만 배포되도록 허용합니다.",
"CONTENT_TRUST_POLICY": "확인된 이미지만 배포되도록 허용합니다.",
"PREVENT_VULNERABLE_TOGGLE": "취약한 이미지가 실행되지 않도록 방지합니다.",
"PREVENT_VULNERABLE_1": "Prevent images with vulnerability severity of",
"PREVENT_VULNERABLE_2": "and above from being deployed.",
@ -575,6 +576,7 @@
"ALLOWED_CHARACTERS": "허용되는 특수 문자",
"TOTAL": "총",
"OVERRIDE": "Override",
"SINGLE_ACTIVE_REPLICATION": "Single active replication",
"ENABLED_RULE": "규칙 활성화",
"OVERRIDE_INFO": "Override",
"OPERATION": "작업",
@ -1304,7 +1306,7 @@
"CONFLICT_ERROR": "귀하의 제출 내용이 충돌하기 때문에 귀하의 작업을 수행할 수 없습니다.",
"PRECONDITION_FAILED": "전제 조건 요류로 인해 작업을 수행할 수 없습니다.",
"SERVER_ERROR": "내부 서버 오류가 발생하여 귀하의 작업을 수행할 수 없습니다.",
"INCONRRECT_OLD_PWD": "이전 비밀번호가 올바르지 않습니다.",
"INCORRECT_OLD_PWD": "이전 비밀번호가 올바르지 않습니다.",
"UNKNOWN": "n/a",
"STATUS": "상태",
"START_TIME": "시작 시간",

View File

@ -76,6 +76,7 @@
"PULL_BASED": "Trazer recursos do repositório remoto para o Harbor local.",
"DESTINATION_NAMESPACE": "Especificar o namespace de destino. Se vazio, os recursos serão colocados no mesmo namespace que a fonte.",
"OVERRIDE": "Sobrescrever recursos no destino se já existir com o mesmo nome.",
"SINGLE_ACTIVE_REPLICATION": "Specify whether to skip execution until the previous active execution finishes, avoiding the execution of the same replication rules multiple times in parallel.",
"EMAIL": "Deve ser um endereço de e-mail válido como nome@exemplo.com.",
"USER_NAME": "Não pode conter caracteres especiais. Tamanho máximo de 255 caracteres.",
"FULL_NAME": "Tamanho máximo de 20 caracteres.",
@ -291,7 +292,7 @@
"PUBLIC_POLICY": "Tornar o repositório do projeto público tornará todos os artefatos acessíveis sem autenticação.",
"SECURITY": "Segurança de implantação",
"CONTENT_TRUST_TOGGLE": "Habilitar regras de confiança",
"CONTENT_TRUST_POLCIY": "Permitir implantação apenas de imagens verificadas.",
"CONTENT_TRUST_POLICY": "Permitir implantação apenas de imagens verificadas.",
"PREVENT_VULNERABLE_TOGGLE": "Prevenir execução de imagens vulneráveis.",
"PREVENT_VULNERABLE_1": "Prevenir imagens com vulnerabilidades de severidade",
"PREVENT_VULNERABLE_2": "e acima de serem utilizadas.",
@ -576,6 +577,7 @@
"ALLOWED_CHARACTERS": "Símbolos permitidos",
"TOTAL": "Total",
"OVERRIDE": "Sobrescrever",
"SINGLE_ACTIVE_REPLICATION": "Single active replication",
"ENABLED_RULE": "Habiltar regra",
"OVERRIDE_INFO": "Sobrescrever",
"CURRENT": "atual",
@ -1302,7 +1304,7 @@
"CONFLICT_ERROR": "Não foi possível executar suas ações pois existem conflitos de envio.",
"PRECONDITION_FAILED": "Não foi possível executar suas ações pois ocorreram falhas de pré-condição.",
"SERVER_ERROR": "Não foi possível executar suas ações pois ocorreram erros internos.",
"INCONRRECT_OLD_PWD": "A senha antiga está incorreta.",
"INCORRECT_OLD_PWD": "A senha antiga está incorreta.",
"UNKNOWN": "n/a",
"STATUS": "Status",
"START_TIME": "Início",

View File

@ -279,7 +279,7 @@
"PUBLIC_POLICY": "Сделать реестр проекта публичным сделает все репозитории доступными для всех.",
"SECURITY": "Безопасность развертывания",
"CONTENT_TRUST_TOGGLE": "Включить доверие к контенту",
"CONTENT_TRUST_POLCIY": "Разрешить развертывание только проверенных образов.",
"CONTENT_TRUST_POLICY": "Разрешить развертывание только проверенных образов.",
"PREVENT_VULNERABLE_TOGGLE": "Предотвращать запуск уязвимых образов.",
"PREVENT_VULNERABLE_1": "Предотвращать развертывание образов с уязвимостью серьезности",
"PREVENT_VULNERABLE_2": "и выше.",
@ -1230,7 +1230,7 @@
"CONFLICT_ERROR": "Мы не можем выполнить ваш запрос из-за конфликта.",
"PRECONDITION_FAILED": "Мы не можем выполнить ваш запрос из-за невыполнения предварительного условия.",
"SERVER_ERROR": "Мы не можем выполнить ваш запрос из-за внутренних ошибок сервера.",
"INCONRRECT_OLD_PWD": "Старый пароль неверный.",
"INCORRECT_OLD_PWD": "Старый пароль неверный.",
"UNKNOWN": "н/д",
"STATUS": "Статус",
"START_TIME": "Время начала",

View File

@ -76,6 +76,7 @@
"PULL_BASED": "Kaynakları uzak kayıt defterinden yerel Harbora çekin.",
"DESTINATION_NAMESPACE": "Hedef ad alanını belirtin. Boşsa, kaynaklar, kaynak ile aynı ad alanına yerleştirilir.",
"OVERRIDE": "Aynı adı taşıyan bir kaynak varsa, hedefteki kaynakları geçersiz kılmayacağınızı belirtin.",
"SINGLE_ACTIVE_REPLICATION": "Specify whether to skip execution until the previous active execution finishes, avoiding the execution of the same replication rules multiple times in parallel.",
"EMAIL": "E-posta, ad@example.com gibi geçerli bir e-posta adresi olmalıdır.",
"USER_NAME": "Özel karakterler içeremez ve maksimum uzunluk 255 karakter olmalıdır.",
"FULL_NAME": "Maksimum uzunluk 20 karakter olmalıdır.",
@ -294,7 +295,7 @@
"PUBLIC_POLICY": "Bir proje sicilini halka açmak, bütün depoları herkesin erişebileceği hale getirecektir.",
"SECURITY": "Dağıtım güvenliği",
"CONTENT_TRUST_TOGGLE": "İçerik güvenini etkinleştir",
"CONTENT_TRUST_POLCIY": "Yalnızca doğrulanmış imajların yüklenilmesine izin verin.",
"CONTENT_TRUST_POLICY": "Yalnızca doğrulanmış imajların yüklenilmesine izin verin.",
"PREVENT_VULNERABLE_TOGGLE": "Güvenlik açığı bulunan görüntülerin çalışmasını önleyin.",
"PREVENT_VULNERABLE_1": "Güvenlik açığı ciddiyeti olan imajları önleyin",
"PREVENT_VULNERABLE_2": "ve yukarıda yüklenilmekte.",
@ -579,6 +580,7 @@
"ALLOWED_CHARACTERS": "İzin verilen özel karakterler",
"TOTAL": "Toplam",
"OVERRIDE": "Geçersiz Kıl",
"SINGLE_ACTIVE_REPLICATION": "Single active replication",
"ENABLED_RULE": "Kuralı etkinleştir",
"OVERRIDE_INFO": "Geçersiz Kıl",
"OPERATION": "Operasyon",
@ -801,7 +803,7 @@
"TAGS_NO_DELETE": "Salt okunur modda silmek yasaktır.",
"FILTER_FOR_REPOSITORIES": "Depoları Filtrele",
"TAG": "Etiket",
"ARTIFACT": "Aarifact",
"ARTIFACT": "Artifact",
"ARTIFACTS": "Artifacts",
"SIZE": "Boyut",
"VULNERABILITY": "Güvenlik Açığı",
@ -1310,7 +1312,7 @@
"CONFLICT_ERROR": "Gönderiminizde çakışmalar olduğundan eyleminizi gerçekleştiremiyoruz.",
"PRECONDITION_FAILED": "Önkoşul arızası nedeniyle işleminizi gerçekleştiremiyoruz.",
"SERVER_ERROR": "Dahili sunucu hataları oluştuğundan işleminizi gerçekleştiremiyoruz.",
"INCONRRECT_OLD_PWD": "Eski şifre yanlış.",
"INCORRECT_OLD_PWD": "Eski şifre yanlış.",
"UNKNOWN": "n/a",
"STATUS": "Durum",
"START_TIME": "Başlama Zamanı",

View File

@ -76,6 +76,7 @@
"PULL_BASED": "把资源由远端仓库拉取到本地Harbor。",
"DESTINATION_NAMESPACE": "指定目标名称空间。如果不填,资源会被放到和源相同的名称空间下。",
"OVERRIDE": "如果存在具有相同名称的资源,请指定是否覆盖目标上的资源。",
"SINGLE_ACTIVE_REPLICATION": "Specify whether to skip execution until the previous active execution finishes, avoiding the execution of the same replication rules multiple times in parallel.",
"EMAIL": "请使用正确的邮箱地址比如name@example.com。",
"USER_NAME": "不能包含特殊字符且长度不能超过255。",
"FULL_NAME": "长度不能超过20。",
@ -292,7 +293,7 @@
"PUBLIC_POLICY": "所有人都可访问公开的项目仓库。",
"SECURITY": "部署安全",
"CONTENT_TRUST_TOGGLE": "内容信任",
"CONTENT_TRUST_POLCIY": "仅允许部署通过认证的镜像。",
"CONTENT_TRUST_POLICY": "仅允许部署通过认证的镜像。",
"PREVENT_VULNERABLE_TOGGLE": "阻止潜在漏洞镜像",
"PREVENT_VULNERABLE_1": "阻止危害级别",
"PREVENT_VULNERABLE_2": "以上的镜像运行。",
@ -576,6 +577,7 @@
"ALLOWED_CHARACTERS": "允许的特殊字符",
"TOTAL": "总数",
"OVERRIDE": "覆盖",
"SINGLE_ACTIVE_REPLICATION": "Single active replication",
"ENABLED_RULE": "启用规则",
"OVERRIDE_INFO": "覆盖",
"CURRENT": "当前仓库",
@ -799,7 +801,7 @@
"TAGS_NO_DELETE": "在只读模式下删除是被禁止的",
"FILTER_FOR_REPOSITORIES": "过滤镜像仓库",
"TAG": "Tag",
"ARTIFACT": "Aarifact",
"ARTIFACT": "Artifact",
"ARTIFACTS": "Artifacts",
"SIZE": "大小",
"VULNERABILITY": "漏洞",
@ -1308,7 +1310,7 @@
"CONFLICT_ERROR": "请求包含冲突, 操作无法完成。",
"PRECONDITION_FAILED": "验证前置条件失败, 无法执行操作。",
"SERVER_ERROR": "服务器出现内部错误,请求无法完成。",
"INCONRRECT_OLD_PWD": "旧密码不正确。",
"INCORRECT_OLD_PWD": "旧密码不正确。",
"UNKNOWN": "未知",
"STATUS": "状态",
"START_TIME": "创建时间",

View File

@ -292,7 +292,7 @@
"PUBLIC_POLICY": "將專案儲存庫設為公開會讓所有人都能夠存取所有儲存庫。",
"SECURITY": "部署安全",
"CONTENT_TRUST_TOGGLE": "啟用內容信任",
"CONTENT_TRUST_POLCIY": "只允許部署已驗證的映像檔。",
"CONTENT_TRUST_POLICY": "只允許部署已驗證的映像檔。",
"PREVENT_VULNERABLE_TOGGLE": "阻止有弱點的映像檔執行。",
"PREVENT_VULNERABLE_1": "阻止具有",
"PREVENT_VULNERABLE_2": "或更高危險級別的映像檔部署。",
@ -1307,7 +1307,7 @@
"CONFLICT_ERROR": "請求包含衝突,操作無法完成。",
"PRECONDITION_FAILED": "驗證前置條件失敗,無法執行操作。",
"SERVER_ERROR": "伺服器出現內部錯誤,請求無法完成。",
"INCONRRECT_OLD_PWD": "舊密碼不正確。",
"INCORRECT_OLD_PWD": "舊密碼不正確。",
"UNKNOWN": "未知",
"STATUS": "狀態",
"START_TIME": "建立時間",

View File

@ -77,33 +77,29 @@ func Middleware() func(http.Handler) http.Handler {
logger.Debugf("artifact %s@%s is pulling by the scanner/cosign, skip the checking", info.Repository, info.Digest)
return nil
}
allowlist := proj.CVEAllowlist.CVESet()
projectSeverity := vuln.ParseSeverityVersion3(proj.Severity())
vulnerable, err := scanController.GetVulnerable(ctx, art, allowlist, proj.CVEAllowlist.IsExpired())
if errors.IsNotFoundErr(err) {
// When the scanner is disconnected the artifact will be considered not scannable.
// We'll try to check the existing scan report even when it's not scannable, and only if there is no report, we will skip checking the vulnerability.
checker := scanChecker()
scannable, err := checker.IsScannable(ctx, art)
if err != nil {
logger.Errorf("check the scannable status of the artifact %s@%s failed, error: %v", art.RepositoryName, art.Digest, err)
return err
}
if !scannable {
// the artifact is not scannable, skip the checking
logger.Debugf("artifact %s@%s is not scannable, skip the checking", art.RepositoryName, art.Digest)
logger.Debugf("artifact %s@%s does not have a scan report, and it is not scannable, skip the checking", art.RepositoryName, art.Digest)
return nil
}
allowlist := proj.CVEAllowlist.CVESet()
projectSeverity := vuln.ParseSeverityVersion3(proj.Severity())
vulnerable, err := scanController.GetVulnerable(ctx, art, allowlist, proj.CVEAllowlist.IsExpired())
if err != nil {
if errors.IsNotFoundErr(err) {
// No report yet?
// If the artifact is scannable but there's no report, it's a violation.
msg := fmt.Sprintf(`current image without vulnerability scanning cannot be pulled due to configured policy in 'Prevent images with vulnerability severity of "%s" or higher from running.' `+
`To continue with pull, please contact your project administrator for help.`, projectSeverity)
return errors.New(nil).WithCode(errors.PROJECTPOLICYVIOLATION).WithMessage(msg)
}
} else if err != nil {
logger.Errorf("get vulnerability summary of the artifact %s@%s failed, error: %v", art.RepositoryName, art.Digest, err)
return err
}

View File

@ -184,6 +184,7 @@ func (suite *MiddlewareTestSuite) TestNonScannerPulling() {
mock.OnAnything(securityCtx, "Name").Return("local")
mock.OnAnything(securityCtx, "Can").Return(true, nil)
mock.OnAnything(suite.checker, "IsScannable").Return(false, nil)
mock.OnAnything(suite.scanController, "GetVulnerable").Return(nil, errors.NotFoundError(nil))
req := suite.makeRequest()
req = req.WithContext(security.NewContext(req.Context(), securityCtx))
@ -213,6 +214,7 @@ func (suite *MiddlewareTestSuite) TestCheckIsScannableFailed() {
mock.OnAnything(suite.artifactController, "GetByReference").Return(suite.artifact, nil)
mock.OnAnything(suite.projectController, "Get").Return(suite.project, nil)
mock.OnAnything(suite.checker, "IsScannable").Return(false, fmt.Errorf("error"))
mock.OnAnything(suite.scanController, "GetVulnerable").Return(nil, errors.NotFoundError(nil))
mock.OnAnything(suite.accessMgr, "List").Return([]accessorymodel.Accessory{}, nil)
req := suite.makeRequest()
@ -222,11 +224,29 @@ func (suite *MiddlewareTestSuite) TestCheckIsScannableFailed() {
suite.Equal(rr.Code, http.StatusInternalServerError)
}
func (suite *MiddlewareTestSuite) TestArtifactIsNotScannable() {
func (suite *MiddlewareTestSuite) TestArtifactIsNotScannableButScanned() {
mock.OnAnything(suite.artifactController, "GetByReference").Return(suite.artifact, nil)
mock.OnAnything(suite.projectController, "Get").Return(suite.project, nil)
mock.OnAnything(suite.checker, "IsScannable").Return(false, nil)
mock.OnAnything(suite.accessMgr, "List").Return([]accessorymodel.Accessory{}, nil)
mock.OnAnything(suite.scanController, "GetVulnerable").Return(&scan.Vulnerable{
ScanStatus: "Success",
CVEBypassed: []string{"cve-2020"},
}, nil)
req := suite.makeRequest()
rr := httptest.NewRecorder()
Middleware()(suite.next).ServeHTTP(rr, req)
suite.Equal(rr.Code, http.StatusOK)
}
func (suite *MiddlewareTestSuite) TestArtifactIsNotScannableNotScanned() {
mock.OnAnything(suite.artifactController, "GetByReference").Return(suite.artifact, nil)
mock.OnAnything(suite.projectController, "Get").Return(suite.project, nil)
mock.OnAnything(suite.checker, "IsScannable").Return(false, nil)
mock.OnAnything(suite.accessMgr, "List").Return([]accessorymodel.Accessory{}, nil)
mock.OnAnything(suite.scanController, "GetVulnerable").Return(nil, errors.NotFoundError(nil))
req := suite.makeRequest()
rr := httptest.NewRecorder()

View File

@ -49,17 +49,17 @@ type auditlogAPI struct {
projectCtl project.Controller
}
func (a *auditlogAPI) ListAuditLogs(ctx context.Context, params auditlog.ListAuditLogsParams) middleware.Responder {
func (a *auditlogAPI) checkPermissionAndBuildQuery(ctx context.Context, qs *string, sort *string, page *int64, pageSize *int64) (*q.Query, error) {
secCtx, ok := security.FromContext(ctx)
if !ok {
return a.SendError(ctx, errors.UnauthorizedError(errors.New("security context not found")))
return nil, errors.UnauthorizedError(errors.New("security context not found"))
}
if !secCtx.IsAuthenticated() {
return a.SendError(ctx, errors.UnauthorizedError(nil).WithMessage(secCtx.GetUsername()))
return nil, errors.UnauthorizedError(nil).WithMessage(secCtx.GetUsername())
}
query, err := a.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
query, err := a.BuildQuery(ctx, qs, sort, page, pageSize)
if err != nil {
return a.SendError(ctx, err)
return nil, err
}
if err := a.RequireSystemAccess(ctx, rbac.ActionList, rbac.ResourceAuditLog); err != nil {
@ -73,11 +73,16 @@ func (a *auditlogAPI) ListAuditLogs(ctx context.Context, params auditlog.ListAud
projects, err := a.projectCtl.List(ctx, q.New(q.KeyWords{"member": member}), project.Metadata(false))
if err != nil {
return a.SendError(ctx, fmt.Errorf(
"failed to get projects of user %s: %v", secCtx.GetUsername(), err))
return nil, errors.Errorf(
"failed to get projects of user %s: %v", secCtx.GetUsername(), err)
}
for _, project := range projects {
if a.HasProjectPermission(ctx, project.ProjectID, rbac.ActionList, rbac.ResourceLog) {
hasPerm, err := a.HasProjectPermission(ctx, project.ProjectID, rbac.ActionList, rbac.ResourceLog)
if err != nil {
return nil, fmt.Errorf(
"failed to get project permission of user %s: %v", secCtx.GetUsername(), err)
}
if hasPerm {
ol.Values = append(ol.Values, project.ProjectID)
}
}
@ -88,6 +93,14 @@ func (a *auditlogAPI) ListAuditLogs(ctx context.Context, params auditlog.ListAud
}
query.Keywords["ProjectID"] = ol
}
return query, nil
}
func (a *auditlogAPI) ListAuditLogs(ctx context.Context, params auditlog.ListAuditLogsParams) middleware.Responder {
query, err := a.checkPermissionAndBuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil {
return a.SendError(ctx, err)
}
total, err := a.auditMgr.Count(ctx, query)
if err != nil {
@ -114,46 +127,13 @@ func (a *auditlogAPI) ListAuditLogs(ctx context.Context, params auditlog.ListAud
WithLink(a.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
WithPayload(auditLogs)
}
func (a *auditlogAPI) ListAuditLogExts(ctx context.Context, params auditlog.ListAuditLogExtsParams) middleware.Responder {
secCtx, ok := security.FromContext(ctx)
if !ok {
return a.SendError(ctx, errors.UnauthorizedError(errors.New("security context not found")))
}
if !secCtx.IsAuthenticated() {
return a.SendError(ctx, errors.UnauthorizedError(nil).WithMessage(secCtx.GetUsername()))
}
query, err := a.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
query, err := a.checkPermissionAndBuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil {
return a.SendError(ctx, err)
}
if err := a.RequireSystemAccess(ctx, rbac.ActionList, rbac.ResourceAuditLog); err != nil {
ol := &q.OrList{}
if sc, ok := secCtx.(*local.SecurityContext); ok && sc.IsAuthenticated() {
user := sc.User()
member := &project.MemberQuery{
UserID: user.UserID,
GroupIDs: user.GroupIDs,
}
projects, err := a.projectCtl.List(ctx, q.New(q.KeyWords{"member": member}), project.Metadata(false))
if err != nil {
return a.SendError(ctx, fmt.Errorf(
"failed to get projects of user %s: %v", secCtx.GetUsername(), err))
}
for _, project := range projects {
if a.HasProjectPermission(ctx, project.ProjectID, rbac.ActionList, rbac.ResourceLog) {
ol.Values = append(ol.Values, project.ProjectID)
}
}
}
// make sure no project will be selected with the query
if len(ol.Values) == 0 {
ol.Values = append(ol.Values, -1)
}
query.Keywords["ProjectID"] = ol
}
total, err := a.auditextMgr.Count(ctx, query)
if err != nil {
return a.SendError(ctx, err)

View File

@ -74,11 +74,11 @@ func (b *BaseAPI) HasPermission(ctx context.Context, action rbac.Action, resourc
return s.Can(ctx, action, resource)
}
// HasProjectPermission returns true when the request has action permission on project subresource
func (b *BaseAPI) HasProjectPermission(ctx context.Context, projectIDOrName any, action rbac.Action, subresource ...rbac.Resource) bool {
// HasProjectPermission returns true, nil when the request has action permission on project subresource, and return false, error when the request does not have permission or an error occurs.
func (b *BaseAPI) HasProjectPermission(ctx context.Context, projectIDOrName any, action rbac.Action, subresource ...rbac.Resource) (bool, error) {
projectID, projectName, err := utils.ParseProjectIDOrName(projectIDOrName)
if err != nil {
return false
return false, err
}
if projectName != "" {
@ -88,25 +88,29 @@ func (b *BaseAPI) HasProjectPermission(ctx context.Context, projectIDOrName any,
if errors.IsNotFoundErr(err) {
p = &project.Project{}
} else {
return false
return false, err
}
}
if p == nil {
log.Warningf("project %s not found", projectName)
return false
return false, nil
}
projectID = p.ProjectID
}
resource := rbac_project.NewNamespace(projectID).Resource(subresource...)
return b.HasPermission(ctx, action, resource)
return b.HasPermission(ctx, action, resource), nil
}
// RequireProjectAccess checks the permission against the resources according to the context
// An error will be returned if it doesn't meet the requirement
func (b *BaseAPI) RequireProjectAccess(ctx context.Context, projectIDOrName any, action rbac.Action, subresource ...rbac.Resource) error {
if b.HasProjectPermission(ctx, projectIDOrName, action, subresource...) {
has, err := b.HasProjectPermission(ctx, projectIDOrName, action, subresource...)
if err != nil {
return err
}
if has {
return nil
}
secCtx, err := b.GetSecurityContext(ctx)

View File

@ -387,12 +387,18 @@ func (a *projectAPI) GetProjectSummary(ctx context.Context, params operation.Get
}
var fetchSummaries []func(context.Context, *project.Project, *models.ProjectSummary)
if hasPerm := a.HasProjectPermission(ctx, p.ProjectID, rbac.ActionRead, rbac.ResourceQuota); hasPerm {
hasPerm, err := a.HasProjectPermission(ctx, p.ProjectID, rbac.ActionRead, rbac.ResourceQuota)
if err != nil {
return a.SendError(ctx, err)
}
if hasPerm {
fetchSummaries = append(fetchSummaries, getProjectQuotaSummary)
}
if hasPerm := a.HasProjectPermission(ctx, p.ProjectID, rbac.ActionList, rbac.ResourceMember); hasPerm {
hasPerm, err = a.HasProjectPermission(ctx, p.ProjectID, rbac.ActionList, rbac.ResourceMember)
if err != nil {
return a.SendError(ctx, err)
}
if hasPerm {
fetchSummaries = append(fetchSummaries, a.getProjectMemberSummary)
}

View File

@ -113,6 +113,14 @@ func (r *replicationAPI) CreateReplicationPolicy(ctx context.Context, params ope
policy.CopyByChunk = *params.Policy.CopyByChunk
}
if params.Policy.SingleActiveReplication != nil {
// Validate and assign SingleActiveReplication only for non-event_based triggers
if params.Policy.Trigger != nil && params.Policy.Trigger.Type == model.TriggerTypeEventBased && *params.Policy.SingleActiveReplication {
return r.SendError(ctx, fmt.Errorf("single active replication is not allowed for event_based triggers"))
}
policy.SingleActiveReplication = *params.Policy.SingleActiveReplication
}
id, err := r.ctl.CreatePolicy(ctx, policy)
if err != nil {
return r.SendError(ctx, err)
@ -181,6 +189,14 @@ func (r *replicationAPI) UpdateReplicationPolicy(ctx context.Context, params ope
policy.CopyByChunk = *params.Policy.CopyByChunk
}
if params.Policy.SingleActiveReplication != nil {
// Validate and assign SingleActiveReplication only for non-event_based triggers
if params.Policy.Trigger != nil && params.Policy.Trigger.Type == model.TriggerTypeEventBased && *params.Policy.SingleActiveReplication {
return r.SendError(ctx, fmt.Errorf("single active replication is not allowed for event_based triggers"))
}
policy.SingleActiveReplication = *params.Policy.SingleActiveReplication
}
if err := r.ctl.UpdatePolicy(ctx, policy); err != nil {
return r.SendError(ctx, err)
}
@ -446,6 +462,7 @@ func convertReplicationPolicy(policy *repctlmodel.Policy) *models.ReplicationPol
Speed: &policy.Speed,
UpdateTime: strfmt.DateTime(policy.UpdateTime),
CopyByChunk: &policy.CopyByChunk,
SingleActiveReplication: &policy.SingleActiveReplication,
}
if policy.SrcRegistry != nil {
p.SrcRegistry = convertRegistry(policy.SrcRegistry)

View File

@ -151,7 +151,8 @@ replication_policy_payload = {
"deletion": False,
"override": True,
"speed": -1,
"copy_by_chunk": False
"copy_by_chunk": False,
"single_active_replication": False
}
create_replication_policy = Permission("{}/replication/policies".format(harbor_base_url), "POST", 201, replication_policy_payload, "id", id_from_header=True)
list_replication_policy = Permission("{}/replication/policies".format(harbor_base_url), "GET", 200, replication_policy_payload)
@ -204,7 +205,8 @@ if "replication" in resources or "all" == resources:
"deletion": False,
"override": True,
"speed": -1,
"copy_by_chunk": False
"copy_by_chunk": False,
"single_active_replication": False
}
response = requests.post("{}/replication/policies".format(harbor_base_url), data=json.dumps(replication_policy_payload), verify=False, auth=(admin_user_name, admin_password), headers={"Content-Type": "application/json"})
replication_policy_id = int(response.headers["Location"].split("/")[-1])

View File

@ -39,19 +39,9 @@ else
rc=999
fi
rc=$?
## --------------------------------------------- Upload Harbor CI Logs -------------------------------------------
#timestamp=$(date +%s)
#GIT_COMMIT=$(git rev-parse --short "$GITHUB_SHA")
#outfile="integration_logs_$timestamp$GIT_COMMIT.tar.gz"
#sudo tar -zcvf $outfile output.xml log.html /var/log/harbor/*
#if [ -f "$outfile" ]; then
# uploader $outfile $harbor_logs_bucket
# echo "----------------------------------------------"
# echo "Download test logs:"
# echo "https://storage.googleapis.com/harbor-ci-logs/$outfile"
# echo "----------------------------------------------"
#else
# echo "No log output file to upload"
#fi
## --------------------------------------------- Package Harbor CI Logs -------------------------------------------
outfile="integration_logs.tar.gz"
sudo tar -zcvf $outfile output.xml log.html /var/log/harbor/*
pwd
ls -lh $outfile
exit $rc

View File

@ -3,7 +3,25 @@ set -x
set -e
function s3_to_https() {
local s3_url="$1"
if [[ "$s3_url" =~ ^s3://([^/]+)/(.+)$ ]]; then
local bucket="${BASH_REMATCH[1]}"
local path="${BASH_REMATCH[2]}"
# current s3 bucket is create in this region
local region="us-west-1"
echo "https://${bucket}.s3.${region}.amazonaws.com/${path}"
else
echo "Invalid S3 URL: $s3_url" >&2
return 1
fi
}
function uploader {
converted_url=$(s3_to_https "s3://$2/$1")
echo "download url $converted_url"
aws s3 cp $1 s3://$2/$1
}

View File

@ -3,5 +3,5 @@ set -x
set -e
sudo make package_online GOBUILDTAGS="include_oss include_gcs" VERSIONTAG=dev-gitaction PKGVERSIONTAG=dev-gitaction UIVERSIONTAG=dev-gitaction GOBUILDIMAGE=golang:1.24.3 COMPILETAG=compile_golangimage TRIVYFLAG=true EXPORTERFLAG=true HTTPPROXY= PULL_BASE_FROM_DOCKERHUB=false
sudo make package_offline GOBUILDTAGS="include_oss include_gcs" VERSIONTAG=dev-gitaction PKGVERSIONTAG=dev-gitaction UIVERSIONTAG=dev-gitaction GOBUILDIMAGE=golang:1.24.3 COMPILETAG=compile_golangimage TRIVYFLAG=true EXPORTERFLAG=true HTTPPROXY= PULL_BASE_FROM_DOCKERHUB=false
sudo make package_online GOBUILDTAGS="include_oss include_gcs" VERSIONTAG=dev-gitaction PKGVERSIONTAG=dev-gitaction UIVERSIONTAG=dev-gitaction GOBUILDIMAGE=golang:1.24.6 COMPILETAG=compile_golangimage TRIVYFLAG=true EXPORTERFLAG=true HTTPPROXY= PULL_BASE_FROM_DOCKERHUB=false
sudo make package_offline GOBUILDTAGS="include_oss include_gcs" VERSIONTAG=dev-gitaction PKGVERSIONTAG=dev-gitaction UIVERSIONTAG=dev-gitaction GOBUILDIMAGE=golang:1.24.6 COMPILETAG=compile_golangimage TRIVYFLAG=true EXPORTERFLAG=true HTTPPROXY= PULL_BASE_FROM_DOCKERHUB=false

View File

@ -96,7 +96,7 @@ Check The Search By One Condition
Select From List By Value ${vulnerabilities_filter_select} package
Retry Text Input ${vulnerabilities_filter_input} ${package}
Retry Button Click ${security_hub_search_btn}
Retry Wait Element Count //div[@class='datagrid']//clr-dg-cell[7][@title='${package}'] 15
Retry Wait Element Count //div[@class='datagrid']//clr-dg-cell[8][@title='${package}'] 15
# Check the search by tag
Select From List By Value ${vulnerabilities_filter_select} tag
Retry Text Input ${vulnerabilities_filter_input} ${tag}
@ -189,7 +189,6 @@ Check The Search By All Condition
Retry Text Input ${cvss3_to_input} ${cvss_score_v3_to}
# severity
Retry Element Click ${add_search_criteria_icon}
Retry Wait Element ${add_search_criteria_icon_disabled}
Retry Wait Element ${remove_search_criteria_icon}
${severity_select}= Format String {}{} ${vulnerabilities_filter_select} [8]
${severity_input}= Format String {}{} (//form[contains(@class,'clr-form')]//select) [9]
@ -198,7 +197,7 @@ Check The Search By All Condition
# search
Retry Button Click ${security_hub_search_btn}
Retry Wait Element Count ${vulnerabilities_datagrid_row} 1
${target_row_xpath}= Set Variable //div[@class='datagrid'][..//clr-dg-cell[2][@title='${repository_name}'] and ..//clr-dg-cell[3][@title='${digest}'] and ..//clr-dg-cell[1]//a[text()='${cve_id}'] and ..//clr-dg-cell[7][@title='${package}'] and ..//clr-dg-cell[4][text()='${tag}'] and ..//clr-dg-cell[5][text()>=${cvss_score_v3_from} and text()<=${cvss_score_v3_to}] and ..//clr-dg-cell[6]//span[text()='${severity}']]
${target_row_xpath}= Set Variable //div[@class='datagrid'][..//clr-dg-cell[2][@title='${repository_name}'] and ..//clr-dg-cell[3][@title='${digest}'] and ..//clr-dg-cell[1]//a[text()='${cve_id}'] and ..//clr-dg-cell[8][@title='${package}'] and ..//clr-dg-cell[4][text()='${tag}'] and ..//clr-dg-cell[5][text()>=${cvss_score_v3_from} and text()<=${cvss_score_v3_to}] and ..//clr-dg-cell[6]//span[text()='${severity}']]
Log ${target_row_xpath}
Retry Wait Element ${target_row_xpath}
FOR ${index} IN RANGE 7

View File

@ -124,7 +124,7 @@ Check Listed In CVE Allowlist
Go Into Artifact ${tag}
Scroll Element Into View //clr-dg-row[contains(.,'${cve_id}')]
${text}= Get Text //clr-dg-row[contains(.,'${cve_id}')]//clr-dg-cell[7]
${text}= Get Text //clr-dg-row[contains(.,'${cve_id}')]//clr-dg-cell[8]
Capture Page Screenshot
Log All is_in_allow_list:${text}

View File

@ -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

View File

@ -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}
@ -293,18 +276,6 @@ Test Case - Replication Of Push Images to DockerHub Triggered By Event
Test Case - Replication Of Push Images to AWS-ECR Triggered By Event
Body Of Replication Of Push Images to Registry Triggered By Event aws-ecr us-east-2 ${ecr_ac_id} ${ecr_ac_key} harbor-nightly-replication
Test Case - Replication Of Pull Images from Gitlab To Self
&{image1_with_tag}= Create Dictionary image=photon tag=1.0
&{image2_with_tag}= Create Dictionary image=alpine tag=latest
${image1}= Get From Dictionary ${image1_with_tag} image
${image2}= Get From Dictionary ${image2_with_tag} image
@{target_images}= Create List '&{image1_with_tag}' '&{image2_with_tag}'
#harbor424542/harbor-ci is the project created in gitlab by user stonezdj, change it when the gitlab user changed
Body Of Replication Of Pull Images from Registry To Self gitlab https://registry.gitlab.com ${gitlab_id} ${gitlab_key} harbor424542/harbor-ci/{${image1},${image2}} ${null} N Flatten All Levels @{target_images}
Test Case - Replication Of Push Images to Gitlab Triggered By Event
Body Of Replication Of Push Images to Registry Triggered By Event gitlab https://registry.gitlab.com ${gitlab_id} ${gitlab_key} harbor424542/harbor-ci
Test Case - Replication Of Pull Manifest List and CNAB from Harbor To Self
&{image1_with_tag}= Create Dictionary image=busybox tag=1.32.0 total_artifact_count=9 archive_count=0
&{image2_with_tag}= Create Dictionary image=index tag=index_tag total_artifact_count=2 archive_count=0

View File

@ -102,12 +102,3 @@ Test Case - Open CVE Details Page
Go Into Artifact ${sha256}
Retry Double Keywords When Error Click Link New Tab And Switch //hbr-artifact-vulnerabilities//clr-dg-row//a[contains(.,'${cve}')] Retry Wait Element //h1[contains(.,'${cve}')]
Close Browser
Test Case - Open Image Scanners Documentation Page
[Tags] image_scanners_documentation_page
Init Chrome Driver
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
Switch To Scanners Page
Retry Element Click ${view_scanner_icon_xpath}
Retry Double Keywords When Error Click Link New Tab And Switch ${view_scanner_doc_xpath} Retry Wait Until Page Contains Vulnerability Scanning with Pluggable Scanners
Close Browser