Skip to content

Commit

Permalink
Support explicit port mappings in remote mode
Browse files Browse the repository at this point in the history
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 #441.
  • Loading branch information
orlangure committed Apr 8, 2022
1 parent 7b9fb06 commit 2a8a217
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 4 deletions.
19 changes: 16 additions & 3 deletions gnomock.go
Expand Up @@ -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)
Expand All @@ -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()

Expand Down Expand Up @@ -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
}

Expand Down
38 changes: 37 additions & 1 deletion internal/gnomockd/gnomockd_test.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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)

Expand All @@ -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())
})
}
24 changes: 24 additions & 0 deletions options.go
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions preset_test.go
Expand Up @@ -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)
}
2 changes: 2 additions & 0 deletions swagger/swagger.yaml
Expand Up @@ -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: >
Expand Down

0 comments on commit 2a8a217

Please sign in to comment.