mirror of
https://github.com/project-zot/zot.git
synced 2024-12-16 21:56:37 -05:00
feat(sync): sync can include self url in registry.URLs (#1562)
sync now ignores self referencing urls, this will help in clustering mode where we can have the same config for multiple zots closes #1335 Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
This commit is contained in:
parent
cda6916b45
commit
1d01b644ea
6 changed files with 338 additions and 8 deletions
2
Makefile
2
Makefile
|
@ -386,12 +386,14 @@ test-bats-sync: BUILD_LABELS=sync
|
||||||
test-bats-sync: binary binary-minimal bench check-skopeo $(BATS) $(NOTATION) $(COSIGN)
|
test-bats-sync: binary binary-minimal bench check-skopeo $(BATS) $(NOTATION) $(COSIGN)
|
||||||
$(BATS) --trace --print-output-on-failure test/blackbox/sync.bats
|
$(BATS) --trace --print-output-on-failure test/blackbox/sync.bats
|
||||||
$(BATS) --trace --print-output-on-failure test/blackbox/sync_docker.bats
|
$(BATS) --trace --print-output-on-failure test/blackbox/sync_docker.bats
|
||||||
|
$(BATS) --trace --print-output-on-failure test/blackbox/sync_replica_cluster.bats
|
||||||
|
|
||||||
.PHONY: test-bats-sync-verbose
|
.PHONY: test-bats-sync-verbose
|
||||||
test-bats-sync-verbose: BUILD_LABELS=sync
|
test-bats-sync-verbose: BUILD_LABELS=sync
|
||||||
test-bats-sync-verbose: binary binary-minimal bench check-skopeo $(BATS) $(NOTATION) $(COSIGN)
|
test-bats-sync-verbose: binary binary-minimal bench check-skopeo $(BATS) $(NOTATION) $(COSIGN)
|
||||||
$(BATS) --trace -t -x -p --verbose-run --print-output-on-failure --show-output-of-passing-tests test/blackbox/sync.bats
|
$(BATS) --trace -t -x -p --verbose-run --print-output-on-failure --show-output-of-passing-tests test/blackbox/sync.bats
|
||||||
$(BATS) --trace -t -x -p --verbose-run --print-output-on-failure --show-output-of-passing-tests test/blackbox/sync_docker.bats
|
$(BATS) --trace -t -x -p --verbose-run --print-output-on-failure --show-output-of-passing-tests test/blackbox/sync_docker.bats
|
||||||
|
$(BATS) --trace -t -x -p --verbose-run --print-output-on-failure --show-output-of-passing-tests test/blackbox/sync_replica_cluster.bats
|
||||||
|
|
||||||
.PHONY: test-bats-cve
|
.PHONY: test-bats-cve
|
||||||
test-bats-cve: BUILD_LABELS=search
|
test-bats-cve: BUILD_LABELS=search
|
||||||
|
|
|
@ -108,4 +108,5 @@ var (
|
||||||
ErrInvalidTruststoreName = errors.New("signatures: invalid truststore name")
|
ErrInvalidTruststoreName = errors.New("signatures: invalid truststore name")
|
||||||
ErrInvalidCertificateContent = errors.New("signatures: invalid certificate content")
|
ErrInvalidCertificateContent = errors.New("signatures: invalid certificate content")
|
||||||
ErrInvalidStateCookie = errors.New("auth: state cookie not present or differs from original state")
|
ErrInvalidStateCookie = errors.New("auth: state cookie not present or differs from original state")
|
||||||
|
ErrSyncNoURLsLeft = errors.New("sync: no valid registry urls left after filtering local ones")
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,7 +4,13 @@
|
||||||
package extensions
|
package extensions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
zerr "zotregistry.io/zot/errors"
|
||||||
"zotregistry.io/zot/pkg/api/config"
|
"zotregistry.io/zot/pkg/api/config"
|
||||||
|
syncconf "zotregistry.io/zot/pkg/extensions/config/sync"
|
||||||
"zotregistry.io/zot/pkg/extensions/sync"
|
"zotregistry.io/zot/pkg/extensions/sync"
|
||||||
"zotregistry.io/zot/pkg/log"
|
"zotregistry.io/zot/pkg/log"
|
||||||
"zotregistry.io/zot/pkg/meta/repodb"
|
"zotregistry.io/zot/pkg/meta/repodb"
|
||||||
|
@ -19,6 +25,19 @@ func EnableSyncExtension(config *config.Config, repoDB repodb.RepoDB,
|
||||||
onDemand := sync.NewOnDemand(log)
|
onDemand := sync.NewOnDemand(log)
|
||||||
|
|
||||||
for _, registryConfig := range config.Extensions.Sync.Registries {
|
for _, registryConfig := range config.Extensions.Sync.Registries {
|
||||||
|
registryConfig := registryConfig
|
||||||
|
if len(registryConfig.URLs) > 1 {
|
||||||
|
if err := removeSelfURLs(config, ®istryConfig, log); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(registryConfig.URLs) == 0 {
|
||||||
|
log.Error().Err(zerr.ErrSyncNoURLsLeft).Msg("unable to start sync extension")
|
||||||
|
|
||||||
|
return nil, zerr.ErrSyncNoURLsLeft
|
||||||
|
}
|
||||||
|
|
||||||
isPeriodical := len(registryConfig.Content) != 0 && registryConfig.PollInterval != 0
|
isPeriodical := len(registryConfig.Content) != 0 && registryConfig.PollInterval != 0
|
||||||
isOnDemand := registryConfig.OnDemand
|
isOnDemand := registryConfig.OnDemand
|
||||||
|
|
||||||
|
@ -49,3 +68,106 @@ func EnableSyncExtension(config *config.Config, repoDB repodb.RepoDB,
|
||||||
|
|
||||||
return nil, nil //nolint: nilnil
|
return nil, nil //nolint: nilnil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getLocalIPs() ([]string, error) {
|
||||||
|
var localIPs []string
|
||||||
|
|
||||||
|
ifaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, i := range ifaces {
|
||||||
|
addrs, err := i.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
return localIPs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if localIP, ok := addr.(*net.IPNet); ok {
|
||||||
|
localIPs = append(localIPs, localIP.IP.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return localIPs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIPFromHostName(host string) ([]string, error) {
|
||||||
|
addrs, err := net.LookupIP(host)
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ips := make([]string, 0, len(addrs))
|
||||||
|
|
||||||
|
for _, ip := range addrs {
|
||||||
|
ips = append(ips, ip.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return ips, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeSelfURLs(config *config.Config, registryConfig *syncconf.RegistryConfig, log log.Logger) error {
|
||||||
|
// get IP from config
|
||||||
|
port := config.HTTP.Port
|
||||||
|
selfAddress := net.JoinHostPort(config.HTTP.Address, port)
|
||||||
|
|
||||||
|
// get all local IPs from interfaces
|
||||||
|
localIPs, err := getLocalIPs()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := len(registryConfig.URLs) - 1; idx >= 0; idx-- {
|
||||||
|
registryURL := registryConfig.URLs[idx]
|
||||||
|
|
||||||
|
url, err := url.Parse(registryURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Str("url", registryURL).Msg("failed to parse sync registry url, removing it")
|
||||||
|
|
||||||
|
registryConfig.URLs = append(registryConfig.URLs[:idx], registryConfig.URLs[idx+1:]...)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// check self address
|
||||||
|
if strings.Contains(registryURL, selfAddress) {
|
||||||
|
log.Info().Str("url", registryURL).Msg("removing local registry url")
|
||||||
|
|
||||||
|
registryConfig.URLs = append(registryConfig.URLs[:idx], registryConfig.URLs[idx+1:]...)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// check dns
|
||||||
|
ips, err := getIPFromHostName(url.Hostname())
|
||||||
|
if err != nil {
|
||||||
|
// will not remove, maybe it will get resolved later after multiple retries
|
||||||
|
log.Warn().Str("url", registryURL).Msg("failed to lookup sync registry url's hostname")
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var removed bool
|
||||||
|
|
||||||
|
for _, localIP := range localIPs {
|
||||||
|
// if ip resolved from hostname/dns is equal with any local ip
|
||||||
|
for _, ip := range ips {
|
||||||
|
if net.JoinHostPort(ip, url.Port()) == net.JoinHostPort(localIP, port) {
|
||||||
|
registryConfig.URLs = append(registryConfig.URLs[:idx], registryConfig.URLs[idx+1:]...)
|
||||||
|
|
||||||
|
removed = true
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if removed {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -125,7 +125,7 @@ func (service *BaseService) SetNextAvailableClient() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !service.client.IsAvailable() {
|
if !service.client.IsAvailable() {
|
||||||
|
|
|
@ -802,6 +802,14 @@ func TestOnDemand(t *testing.T) {
|
||||||
regex := ".*"
|
regex := ".*"
|
||||||
semver := true
|
semver := true
|
||||||
|
|
||||||
|
destPort := test.GetFreePort()
|
||||||
|
destConfig := config.New()
|
||||||
|
|
||||||
|
destBaseURL := test.GetBaseURL(destPort)
|
||||||
|
|
||||||
|
hostname, err := os.Hostname()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
syncRegistryConfig := syncconf.RegistryConfig{
|
syncRegistryConfig := syncconf.RegistryConfig{
|
||||||
Content: []syncconf.Content{
|
Content: []syncconf.Content{
|
||||||
{
|
{
|
||||||
|
@ -812,7 +820,11 @@ func TestOnDemand(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
URLs: []string{srcBaseURL},
|
// include self url, should be ignored
|
||||||
|
URLs: []string{
|
||||||
|
fmt.Sprintf("http://%s", hostname), destBaseURL,
|
||||||
|
srcBaseURL, fmt.Sprintf("http://localhost:%s", destPort),
|
||||||
|
},
|
||||||
TLSVerify: &tlsVerify,
|
TLSVerify: &tlsVerify,
|
||||||
CertDir: "",
|
CertDir: "",
|
||||||
OnDemand: true,
|
OnDemand: true,
|
||||||
|
@ -824,11 +836,6 @@ func TestOnDemand(t *testing.T) {
|
||||||
Registries: []syncconf.RegistryConfig{syncRegistryConfig},
|
Registries: []syncconf.RegistryConfig{syncRegistryConfig},
|
||||||
}
|
}
|
||||||
|
|
||||||
destPort := test.GetFreePort()
|
|
||||||
destConfig := config.New()
|
|
||||||
|
|
||||||
destBaseURL := test.GetBaseURL(destPort)
|
|
||||||
|
|
||||||
destConfig.HTTP.Port = destPort
|
destConfig.HTTP.Port = destPort
|
||||||
|
|
||||||
destDir := t.TempDir()
|
destDir := t.TempDir()
|
||||||
|
@ -3384,7 +3391,7 @@ func TestMultipleURLs(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
URLs: []string{"badURL", "http://invalid.invalid/invalid/", srcBaseURL},
|
URLs: []string{"badURL", "@!#!$#@%", "http://invalid.invalid/invalid/", srcBaseURL},
|
||||||
PollInterval: updateDuration,
|
PollInterval: updateDuration,
|
||||||
TLSVerify: &tlsVerify,
|
TLSVerify: &tlsVerify,
|
||||||
CertDir: "",
|
CertDir: "",
|
||||||
|
@ -3438,6 +3445,49 @@ func TestMultipleURLs(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNoURLsLeftInConfig(t *testing.T) {
|
||||||
|
Convey("Verify sync feature", t, func() {
|
||||||
|
updateDuration, _ := time.ParseDuration("30m")
|
||||||
|
|
||||||
|
regex := ".*"
|
||||||
|
semver := true
|
||||||
|
var tlsVerify bool
|
||||||
|
|
||||||
|
syncRegistryConfig := syncconf.RegistryConfig{
|
||||||
|
Content: []syncconf.Content{
|
||||||
|
{
|
||||||
|
Prefix: testImage,
|
||||||
|
Tags: &syncconf.Tags{
|
||||||
|
Regex: ®ex,
|
||||||
|
Semver: &semver,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
URLs: []string{"@!#!$#@%", "@!#!$#@%"},
|
||||||
|
PollInterval: updateDuration,
|
||||||
|
TLSVerify: &tlsVerify,
|
||||||
|
CertDir: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultVal := true
|
||||||
|
syncConfig := &syncconf.Config{
|
||||||
|
Enable: &defaultVal,
|
||||||
|
Registries: []syncconf.RegistryConfig{syncRegistryConfig},
|
||||||
|
}
|
||||||
|
|
||||||
|
dctlr, destBaseURL, _, destClient := makeDownstreamServer(t, false, syncConfig)
|
||||||
|
|
||||||
|
dcm := test.NewControllerManager(dctlr)
|
||||||
|
dcm.StartAndWait(dctlr.Config.HTTP.Port)
|
||||||
|
defer dcm.StopServer()
|
||||||
|
|
||||||
|
resp, err := destClient.R().Get(destBaseURL + "/v2/" + testImage + "/tags/list")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(resp, ShouldNotBeNil)
|
||||||
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestPeriodicallySignaturesErr(t *testing.T) {
|
func TestPeriodicallySignaturesErr(t *testing.T) {
|
||||||
Convey("Verify sync periodically signatures errors", t, func() {
|
Convey("Verify sync periodically signatures errors", t, func() {
|
||||||
updateDuration, _ := time.ParseDuration("30m")
|
updateDuration, _ := time.ParseDuration("30m")
|
||||||
|
|
155
test/blackbox/sync_replica_cluster.bats
Normal file
155
test/blackbox/sync_replica_cluster.bats
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
load helpers_sync
|
||||||
|
|
||||||
|
function setup_file() {
|
||||||
|
# Verify prerequisites are available
|
||||||
|
if ! verify_prerequisites; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download test data to folder common for the entire suite, not just this file
|
||||||
|
skopeo --insecure-policy copy --format=oci docker://ghcr.io/project-zot/golang:1.20 oci:${TEST_DATA_DIR}/golang:1.20
|
||||||
|
# Setup zot server
|
||||||
|
local zot_sync_one_root_dir=${BATS_FILE_TMPDIR}/zot-one
|
||||||
|
local zot_sync_two_root_dir=${BATS_FILE_TMPDIR}/zot-two
|
||||||
|
|
||||||
|
local zot_sync_one_config_file=${BATS_FILE_TMPDIR}/zot_sync_one_config.json
|
||||||
|
local zot_sync_two_config_file=${BATS_FILE_TMPDIR}/zot_sync_two_config.json
|
||||||
|
|
||||||
|
mkdir -p ${zot_sync_one_root_dir}
|
||||||
|
mkdir -p ${zot_sync_two_root_dir}
|
||||||
|
|
||||||
|
cat >${zot_sync_one_config_file} <<EOF
|
||||||
|
{
|
||||||
|
"distSpecVersion": "1.1.0",
|
||||||
|
"storage": {
|
||||||
|
"rootDirectory": "${zot_sync_one_root_dir}"
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"address": "0.0.0.0",
|
||||||
|
"port": "8081"
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"level": "debug"
|
||||||
|
},
|
||||||
|
"extensions": {
|
||||||
|
"sync": {
|
||||||
|
"registries": [
|
||||||
|
{
|
||||||
|
"urls": [
|
||||||
|
"http://localhost:8081",
|
||||||
|
"http://localhost:8082"
|
||||||
|
],
|
||||||
|
"onDemand": false,
|
||||||
|
"tlsVerify": false,
|
||||||
|
"PollInterval": "1s",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"prefix": "**"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat >${zot_sync_two_config_file} <<EOF
|
||||||
|
{
|
||||||
|
"distSpecVersion": "1.1.0",
|
||||||
|
"storage": {
|
||||||
|
"rootDirectory": "${zot_sync_two_root_dir}"
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"address": "0.0.0.0",
|
||||||
|
"port": "8082"
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"level": "debug"
|
||||||
|
},
|
||||||
|
"extensions": {
|
||||||
|
"sync": {
|
||||||
|
"registries": [
|
||||||
|
{
|
||||||
|
"urls": [
|
||||||
|
"http://localhost:8081",
|
||||||
|
"http://localhost:8082"
|
||||||
|
],
|
||||||
|
"onDemand": false,
|
||||||
|
"tlsVerify": false,
|
||||||
|
"PollInterval": "1s",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"prefix": "**"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
git -C ${BATS_FILE_TMPDIR} clone https://github.com/project-zot/helm-charts.git
|
||||||
|
|
||||||
|
setup_zot_file_level ${zot_sync_one_config_file}
|
||||||
|
wait_zot_reachable "http://127.0.0.1:8081/v2/_catalog"
|
||||||
|
|
||||||
|
setup_zot_file_level ${zot_sync_two_config_file}
|
||||||
|
wait_zot_reachable "http://127.0.0.1:8082/v2/_catalog"
|
||||||
|
}
|
||||||
|
|
||||||
|
function teardown_file() {
|
||||||
|
local zot_sync_one_root_dir=${BATS_FILE_TMPDIR}/zot-per
|
||||||
|
local zot_sync_two_root_dir=${BATS_FILE_TMPDIR}/zot-ondemand
|
||||||
|
teardown_zot_file_level
|
||||||
|
rm -rf ${zot_sync_one_root_dir}
|
||||||
|
rm -rf ${zot_sync_two_root_dir}
|
||||||
|
}
|
||||||
|
|
||||||
|
# sync image
|
||||||
|
@test "push one image to zot one, zot two should sync it" {
|
||||||
|
run skopeo --insecure-policy copy --dest-tls-verify=false \
|
||||||
|
oci:${TEST_DATA_DIR}/golang:1.20 \
|
||||||
|
docker://127.0.0.1:8081/golang:1.20
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
run curl http://127.0.0.1:8081/v2/_catalog
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ $(echo "${lines[-1]}" | jq '.repositories[]') = '"golang"' ]
|
||||||
|
run curl http://127.0.0.1:8081/v2/golang/tags/list
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ $(echo "${lines[-1]}" | jq '.tags[]') = '"1.20"' ]
|
||||||
|
|
||||||
|
run sleep 30s
|
||||||
|
|
||||||
|
run curl http://127.0.0.1:8082/v2/_catalog
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ $(echo "${lines[-1]}" | jq '.repositories[]') = '"golang"' ]
|
||||||
|
|
||||||
|
run curl http://127.0.0.1:8082/v2/golang/tags/list
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ $(echo "${lines[-1]}" | jq '.tags[]') = '"1.20"' ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "push one image to zot-two, zot-one should sync it" {
|
||||||
|
run skopeo --insecure-policy copy --dest-tls-verify=false \
|
||||||
|
oci:${TEST_DATA_DIR}/golang:1.20 \
|
||||||
|
docker://127.0.0.1:8082/anothergolang:1.20
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
run curl http://127.0.0.1:8082/v2/_catalog
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ $(echo "${lines[-1]}" | jq '.repositories[0]') = '"anothergolang"' ]
|
||||||
|
run curl http://127.0.0.1:8082/v2/anothergolang/tags/list
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ $(echo "${lines[-1]}" | jq '.tags[]') = '"1.20"' ]
|
||||||
|
|
||||||
|
run sleep 30s
|
||||||
|
|
||||||
|
run curl http://127.0.0.1:8081/v2/_catalog
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ $(echo "${lines[-1]}" | jq '.repositories[0]') = '"anothergolang"' ]
|
||||||
|
|
||||||
|
run curl http://127.0.0.1:8081/v2/anothergolang/tags/list
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ $(echo "${lines[-1]}" | jq '.tags[]') = '"1.20"' ]
|
||||||
|
}
|
Loading…
Reference in a new issue