From 2a8a217f30433685e7ed1316bd0ed2d71d873a84 Mon Sep 17 00:00:00 2001 From: Yury Fedorov Date: Sat, 19 Feb 2022 02:13:41 +0200 Subject: [PATCH] Support explicit port mappings in remote mode When gnomock runs in server mode, it is impossible to control the port mappings created by the presets. In this commit I add an ability to override preset-specific named ports by any other values in order to control which ports are allocated by the remote gnomock instance. This feature is a bit fragile, because the user is required to study the preset before attempting to override its built-in values. Custom named ports must expose the ports with the same names and internal ports for the override to work. Basically, only the "host port" value should be changed. This commit fixes https://github.com/orlangure/gnomock/issues/441. --- gnomock.go | 19 ++++++++++++--- internal/gnomockd/gnomockd_test.go | 38 +++++++++++++++++++++++++++++- options.go | 24 +++++++++++++++++++ preset_test.go | 20 ++++++++++++++++ swagger/swagger.yaml | 2 ++ 5 files changed, 99 insertions(+), 4 deletions(-) diff --git a/gnomock.go b/gnomock.go index bde34117..2ba75281 100644 --- a/gnomock.go +++ b/gnomock.go @@ -78,7 +78,7 @@ type g struct { // include tag, which is set to "latest" by default. Optional configuration is // available through Option functions. The returned container must be stopped // when no longer needed using its Stop() method. -func StartCustom(image string, ports NamedPorts, opts ...Option) (c *Container, err error) { +func StartCustom(image string, ports NamedPorts, opts ...Option) (*Container, error) { config, image := buildConfig(opts...), buildImage(image) g, err := newG(config.Debug) @@ -88,9 +88,24 @@ func StartCustom(image string, ports NamedPorts, opts ...Option) (c *Container, defer func() { _ = g.log.Sync() }() + if config.CustomNamedPorts != nil { + ports = config.CustomNamedPorts + } + g.log.Infow("starting", "image", image, "ports", ports) g.log.Infow("using config", "image", image, "ports", ports, "config", config) + c, err := newContainer(g, image, ports, config) + if err != nil { + return c, err + } + + g.log.Infow("container is ready to use", "id", c.ID, "ports", c.Ports) + + return c, nil +} + +func newContainer(g *g, image string, ports NamedPorts, config *Options) (c *Container, err error) { ctx, cancel := context.WithTimeout(config.ctx, config.Timeout) defer cancel() @@ -127,8 +142,6 @@ func StartCustom(image string, ports NamedPorts, opts ...Option) (c *Container, return c, fmt.Errorf("can't init container: %w", err) } - g.log.Infow("container is ready to use", "id", c.ID, "ports", c.Ports) - return c, nil } diff --git a/internal/gnomockd/gnomockd_test.go b/internal/gnomockd/gnomockd_test.go index 6be484e9..ca399de0 100644 --- a/internal/gnomockd/gnomockd_test.go +++ b/internal/gnomockd/gnomockd_test.go @@ -2,10 +2,13 @@ package gnomockd_test import ( "bytes" + "encoding/json" + "io" "net/http" "net/http/httptest" "testing" + "github.com/orlangure/gnomock" "github.com/orlangure/gnomock/internal/gnomockd" _ "github.com/orlangure/gnomock/preset/mongo" // this is only to prevent error 404 "github.com/stretchr/testify/require" @@ -73,7 +76,7 @@ func TestGnomockd(t *testing.T) { t.Parallel() h := gnomockd.Handler() - buf := bytes.NewBuffer([]byte(`{"id":"invalid"}`)) + buf := bytes.NewBufferString(`{"id":"invalid"}`) w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/stop", buf) h.ServeHTTP(w, r) @@ -83,4 +86,37 @@ func TestGnomockd(t *testing.T) { require.Equal(t, http.StatusInternalServerError, res.StatusCode) }) + + t.Run("fixed host port using custom named ports", func(t *testing.T) { + t.Parallel() + + port := gnomock.TCP(27017) + port.HostPort = 43210 + + body, err := json.Marshal(struct { + Options gnomock.Options `json:"options"` + }{ + gnomock.Options{ + CustomNamedPorts: gnomock.NamedPorts{ + gnomock.DefaultPort: port, + }, + }, + }) + require.NoError(t, err) + + h := gnomockd.Handler() + w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/start/mongo", bytes.NewBuffer(body)) + h.ServeHTTP(w, r) + + res := w.Result() + t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) + require.Equal(t, http.StatusOK, res.StatusCode) + + body, err = io.ReadAll(res.Body) + require.NoError(t, err) + + c := gnomock.Container{} + require.NoError(t, json.Unmarshal(body, &c)) + require.Equal(t, 43210, c.DefaultPort()) + }) } diff --git a/options.go b/options.go index 3ec04395..a8c13a79 100644 --- a/options.go +++ b/options.go @@ -117,6 +117,10 @@ func WithOptions(options *Options) Option { o.Timeout = options.Timeout } + if options.CustomNamedPorts != nil { + o.CustomNamedPorts = options.CustomNamedPorts + } + o.Env = append(o.Env, options.Env...) o.Debug = options.Debug o.ContainerName = options.ContainerName @@ -160,6 +164,14 @@ func WithUseLocalImagesFirst() Option { } } +// WithCustomNamedPorts allows to define custom ports for a container. This +// option should be used to override the ports defined by presets. +func WithCustomNamedPorts(namedPorts NamedPorts) Option { + return func(o *Options) { + o.CustomNamedPorts = namedPorts + } +} + // WithRegistryAuth allows to access private docker images. The credentials // should be passes as a Base64 encoded string, where the content is a JSON // string with two fields: username and password. @@ -234,6 +246,18 @@ type Options struct { // instead of always pulling the images. UseLocalImagesFirst bool `json:"use_local_images_first"` + // CustomNamedPorts allows to override the ports set by the presets. This + // option is useful for cases when the presets need to be created with + // custom port definitions. This is an advanced feature and should be used + // with care. + // + // Note that when using this option, you should provide custom named ports + // with names matching the original ports returned by the used preset. + // + // When calling StartCustom directly from Go, it is possible to provide the + // ports directly to the function. + CustomNamedPorts NamedPorts `json:"custom_named_ports"` + // Base64 encoded JSON string with docker access credentials. JSON string // should include two fields: username and password. For Docker Hub, if 2FA // authentication is enabled, an access token should be used instead of a diff --git a/preset_test.go b/preset_test.go index 250967a1..5a5bdc6f 100644 --- a/preset_test.go +++ b/preset_test.go @@ -97,3 +97,23 @@ func TestPreset_duplicateContainerName(t *testing.T) { require.Error(t, gnomock.Stop(originalContainer)) require.NoError(t, gnomock.Stop(newContainer)) } + +func TestPreset_customNamedPorts(t *testing.T) { + t.Parallel() + + p := &testutil.TestPreset{Img: testutil.TestImage} + presetPorts := p.Ports() + pr := presetPorts["web80"] + pr.HostPort = 23080 + presetPorts["web80"] = pr + + container, err := gnomock.Start( + p, + gnomock.WithCustomNamedPorts(presetPorts), + gnomock.WithDebugMode(), + ) + + t.Cleanup(func() { require.NoError(t, gnomock.Stop(container)) }) + require.NoError(t, err) + require.Equal(t, 23080, container.Ports.Get("web80").Port) +} diff --git a/swagger/swagger.yaml b/swagger/swagger.yaml index f367a2ad..bc3aabd6 100644 --- a/swagger/swagger.yaml +++ b/swagger/swagger.yaml @@ -478,6 +478,8 @@ components: use_local_images_first: type: boolean description: If possible to avoid hitting the Docker Hub pull rate limit. + custom_named_ports: + $ref: '#/components/schemas/named-ports' auth: type: string description: >