feat(sync): move stream from global to per upstream

Signed-off-by: Vishwas Rajashekar <dev@vrajashkr.com>
This commit is contained in:
Vishwas Rajashekar
2026-05-19 00:55:07 +05:30
parent 63b7654d50
commit 14cd52e993
12 changed files with 198 additions and 28 deletions
+27
View File
@@ -1565,6 +1565,33 @@ func validateSync(config *config.Config, logger zlog.Logger) error {
// can't check with IsSyncEnabled(), because it can't test invalid sync configs
if extensionsConfig != nil && extensionsConfig.Sync != nil && len(extensionsConfig.Sync.Registries) > 0 {
for regID, regCfg := range extensionsConfig.Sync.Registries {
// check streaming sync configuration
if regCfg.IsStreamEnabled() {
if !regCfg.OnDemand {
msg := "streaming sync requires onDemand to be enabled"
logger.Error().Err(zerr.ErrBadConfig).Int("id", regID).Interface("extensions.sync.registries[id]",
extensionsConfig.Sync.Registries[regID]).Msg(msg)
return fmt.Errorf("%w: %s", zerr.ErrBadConfig, msg)
}
if regCfg.MaxRetries != nil {
msg := "maxRetries cannot be used when streaming sync is enabled"
logger.Error().Err(zerr.ErrBadConfig).Int("id", regID).Interface("extensions.sync.registries[id]",
extensionsConfig.Sync.Registries[regID]).Msg(msg)
return fmt.Errorf("%w: %s", zerr.ErrBadConfig, msg)
}
if regCfg.RetryDelay != nil {
msg := "retryDelay cannot be used when streaming sync is enabled"
logger.Error().Err(zerr.ErrBadConfig).Int("id", regID).Interface("extensions.sync.registries[id]",
extensionsConfig.Sync.Registries[regID]).Msg(msg)
return fmt.Errorf("%w: %s", zerr.ErrBadConfig, msg)
}
}
// check retry options are configured for sync
if regCfg.MaxRetries != nil && regCfg.RetryDelay == nil {
msg := "retryDelay is required when using maxRetries"
+98
View File
@@ -3446,3 +3446,101 @@ func TestBearerASMConfigValidation(t *testing.T) {
})
})
}
func TestValidateStreamingSync(t *testing.T) {
Convey("Test streaming sync config validation", t, func() {
Convey("Valid streaming sync with onDemand enabled", func() {
content := `{"storage":{"rootDirectory":"/tmp/zot"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
"extensions":{"sync": {"registries": [{"urls":["localhost:9999"],
"onDemand": true, "stream": true}]}}}`
cfg := config.New()
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
err := cli.LoadConfiguration(cfg, tmpfile)
So(err, ShouldBeNil)
})
Convey("Reject streaming sync when onDemand is false", func() {
content := `{"storage":{"rootDirectory":"/tmp/zot"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
"extensions":{"sync": {"registries": [{"urls":["localhost:9999"],
"onDemand": false, "stream": true}]}}}`
cfg := config.New()
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
err := cli.LoadConfiguration(cfg, tmpfile)
So(err, ShouldNotBeNil)
So(err, ShouldWrap, zerr.ErrBadConfig)
So(err.Error(), ShouldContainSubstring, "streaming sync requires onDemand to be enabled")
})
Convey("Reject streaming sync when maxRetries is set", func() {
content := `{"storage":{"rootDirectory":"/tmp/zot"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
"extensions":{"sync": {"registries": [{"urls":["localhost:9999"],
"onDemand": true, "stream": true, "maxRetries": 3, "retryDelay": "10s"}]}}}`
cfg := config.New()
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
err := cli.LoadConfiguration(cfg, tmpfile)
So(err, ShouldNotBeNil)
So(err, ShouldWrap, zerr.ErrBadConfig)
So(err.Error(), ShouldContainSubstring, "maxRetries cannot be used when streaming sync is enabled")
})
Convey("Reject streaming sync when retryDelay is set without maxRetries", func() {
content := `{"storage":{"rootDirectory":"/tmp/zot"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
"extensions":{"sync": {"registries": [{"urls":["localhost:9999"],
"onDemand": true, "stream": true, "retryDelay": "10s"}]}}}`
cfg := config.New()
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
err := cli.LoadConfiguration(cfg, tmpfile)
So(err, ShouldNotBeNil)
So(err, ShouldWrap, zerr.ErrBadConfig)
So(err.Error(), ShouldContainSubstring, "retryDelay cannot be used when streaming sync is enabled")
})
Convey("Non-streaming sync allows maxRetries with retryDelay", func() {
content := `{"storage":{"rootDirectory":"/tmp/zot"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
"extensions":{"sync": {"registries": [{"urls":["localhost:9999"],
"onDemand": true, "maxRetries": 3, "retryDelay": "10s"}]}}}`
cfg := config.New()
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
err := cli.LoadConfiguration(cfg, tmpfile)
So(err, ShouldBeNil)
})
Convey("Streaming registry and non-streaming registry can coexist", func() {
content := `{"storage":{"rootDirectory":"/tmp/zot"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
"extensions":{"sync": {"registries": [
{"urls":["localhost:9999"], "onDemand": true, "stream": true},
{"urls":["localhost:9998"], "onDemand": true, "maxRetries": 3, "retryDelay": "10s"}
]}}}`
cfg := config.New()
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
err := cli.LoadConfiguration(cfg, tmpfile)
So(err, ShouldBeNil)
})
Convey("Streaming upstream and periodic sync upstreams can coexist", func() {
content := `{"storage":{"rootDirectory":"/tmp/zot"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
"extensions":{"sync": {"registries": [
{"urls":["localhost:9999"], "onDemand": true, "stream": true},
{"urls":["localhost:9998"], "onDemand": false, "pollInterval": "12h"}
]}}}`
cfg := config.New()
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
err := cli.LoadConfiguration(cfg, tmpfile)
So(err, ShouldBeNil)
})
})
}