diff --git a/errors/errors.go b/errors/errors.go index ee69022d..456017a8 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -55,4 +55,5 @@ var ( ErrSyncSignature = errors.New("sync: couldn't get upstream notary/cosign signatures") ErrImageLintAnnotations = errors.New("routes: lint checks failed") ErrParsingAuthHeader = errors.New("auth: failed parsing authorization header") + ErrBadType = errors.New("core: invalid type") ) diff --git a/pkg/api/controller.go b/pkg/api/controller.go index 25777068..92332ac7 100644 --- a/pkg/api/controller.go +++ b/pkg/api/controller.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "runtime" + "strconv" "strings" goSync "sync" "syscall" @@ -41,6 +42,8 @@ type Controller struct { Server *http.Server Metrics monitoring.MetricServer wgShutDown *goSync.WaitGroup // use it to gracefully shutdown goroutines + // runtime params + chosenPort int // kernel-chosen port } func NewController(config *config.Config) *Controller { @@ -103,6 +106,10 @@ func DumpRuntimeParams(log log.Logger) { evt.Msg("runtime params") } +func (c *Controller) GetPort() int { + return c.chosenPort +} + func (c *Controller) Run(reloadCtx context.Context) error { // print the current configuration, but strip secrets c.Log.Info().Interface("params", c.Config.Sanitize()).Msg("configuration settings") @@ -171,6 +178,25 @@ func (c *Controller) Run(reloadCtx context.Context) error { return err } + if c.Config.HTTP.Port == "0" || c.Config.HTTP.Port == "" { + chosenAddr, ok := listener.Addr().(*net.TCPAddr) + if !ok { + c.Log.Error().Str("port", c.Config.HTTP.Port).Msg("invalid addr type") + + return errors.ErrBadType + } + + c.chosenPort = chosenAddr.Port + + c.Log.Info().Int("port", chosenAddr.Port).IPAddr("address", chosenAddr.IP).Msg( + "port is unspecified, listening on kernel chosen port", + ) + } else { + chosenPort, _ := strconv.ParseInt(c.Config.HTTP.Port, 10, 64) + + c.chosenPort = int(chosenPort) + } + if c.Config.HTTP.TLS != nil && c.Config.HTTP.TLS.Key != "" && c.Config.HTTP.TLS.Cert != "" { server.TLSConfig = &tls.Config{ CipherSuites: []uint16{ diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index f9034ad7..eba208c6 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -143,6 +143,55 @@ func TestRunAlreadyRunningServer(t *testing.T) { }) } +func TestAutoPortSelection(t *testing.T) { + Convey("Run server with specifying a port", t, func() { + conf := config.New() + conf.HTTP.Port = "0" + + logFile, err := os.CreateTemp("", "zot-log*.txt") + So(err, ShouldBeNil) + conf.Log.Output = logFile.Name() + defer os.Remove(logFile.Name()) // clean up + + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = t.TempDir() + + go startServer(ctlr) + time.Sleep(1000 * time.Millisecond) + defer stopServer(ctlr) + + file, err := os.Open(logFile.Name()) + So(err, ShouldBeNil) + defer file.Close() + + scanner := bufio.NewScanner(file) + + var contents bytes.Buffer + start := time.Now() + + for scanner.Scan() { + if time.Since(start) < time.Second*30 { + t.Logf("Exhausted: Controller did not print the expected log within 30 seconds") + } + text := scanner.Text() + contents.WriteString(text) + if strings.Contains(text, "Port unspecified") { + break + } + t.Logf(scanner.Text()) + } + So(scanner.Err(), ShouldBeNil) + So(contents.String(), ShouldContainSubstring, + "port is unspecified, listening on kernel chosen port", + ) + So(contents.String(), ShouldContainSubstring, "\"address\":\"127.0.0.1\"") + So(contents.String(), ShouldContainSubstring, "\"port\":") + + So(ctlr.GetPort(), ShouldBeGreaterThan, 0) + So(ctlr.GetPort(), ShouldBeLessThan, 65536) + }) +} + func TestObjectStorageController(t *testing.T) { skipIt(t) Convey("Negative make a new object storage controller", t, func() { diff --git a/pkg/cli/root.go b/pkg/cli/root.go index 19f8948c..31f663eb 100644 --- a/pkg/cli/root.go +++ b/pkg/cli/root.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "net/http" + "strconv" "strings" "time" @@ -232,6 +233,10 @@ func validateStorageConfig(cfg *config.Config) error { } func validateConfiguration(config *config.Config) error { + if err := validateHTTP(config); err != nil { + return err + } + if err := validateGC(config); err != nil { return err } @@ -514,6 +519,21 @@ func validateLDAP(config *config.Config) error { return nil } +func validateHTTP(config *config.Config) error { + if config.HTTP.Port != "" { + port, err := strconv.ParseInt(config.HTTP.Port, 10, 64) + if err != nil || (port < 0 || port > 65535) { + log.Error().Str("port", config.HTTP.Port).Msg("invalid port") + + return errors.ErrBadConfig + } + + fmt.Printf("HTTP port %d\n", port) + } + + return nil +} + func validateGC(config *config.Config) error { // enforce GC params if config.Storage.GCDelay < 0 { diff --git a/pkg/cli/root_test.go b/pkg/cli/root_test.go index 64ad47e9..b362cb4b 100644 --- a/pkg/cli/root_test.go +++ b/pkg/cli/root_test.go @@ -540,6 +540,43 @@ func TestLoadConfig(t *testing.T) { err = cli.LoadConfiguration(config, tmpfile.Name()) So(err, ShouldBeNil) }) + + Convey("Test HTTP port", t, func() { + config := config.New() + tmpfile, err := os.CreateTemp("", "zot-test*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpfile.Name()) + + content := []byte(`{"storage":{"rootDirectory":"/tmp/zot", + "subPaths": {"/a": {"rootDirectory": "/zot-a","dedupe":"true"}, + "/b": {"rootDirectory": "/zot-a","dedupe":"true"}}}, + "http":{"address":"127.0.0.1","port":"8080","realm":"zot", + "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`) + err = os.WriteFile(tmpfile.Name(), content, 0o0600) + So(err, ShouldBeNil) + err = cli.LoadConfiguration(config, tmpfile.Name()) + So(err, ShouldBeNil) + + content = []byte(`{"storage":{"rootDirectory":"/tmp/zot", + "subPaths": {"/a": {"rootDirectory": "/zot-a","dedupe":"true"}, + "/b": {"rootDirectory": "/zot-a","dedupe":"true"}}}, + "http":{"address":"127.0.0.1","port":"-1","realm":"zot", + "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`) + err = os.WriteFile(tmpfile.Name(), content, 0o0600) + So(err, ShouldBeNil) + err = cli.LoadConfiguration(config, tmpfile.Name()) + So(err, ShouldNotBeNil) + + content = []byte(`{"storage":{"rootDirectory":"/tmp/zot", + "subPaths": {"/a": {"rootDirectory": "/zot-a","dedupe":"true"}, + "/b": {"rootDirectory": "/zot-a","dedupe":"true"}}}, + "http":{"address":"127.0.0.1","port":"65536","realm":"zot", + "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`) + err = os.WriteFile(tmpfile.Name(), content, 0o0600) + So(err, ShouldBeNil) + err = cli.LoadConfiguration(config, tmpfile.Name()) + So(err, ShouldNotBeNil) + }) } func TestGC(t *testing.T) {