github.com/thediveo/morbyd@v0.11.1/run/options_test.go (about)

     1  // Copyright 2024 Harald Albrecht.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package run
    16  
    17  import (
    18  	"bytes"
    19  	"net"
    20  	"os"
    21  	"strings"
    22  
    23  	"github.com/docker/docker/api/types/container"
    24  	"github.com/docker/docker/api/types/mount"
    25  	"github.com/docker/docker/api/types/network"
    26  	"github.com/docker/go-connections/nat"
    27  
    28  	. "github.com/onsi/ginkgo/v2"
    29  	. "github.com/onsi/gomega"
    30  	"github.com/onsi/gomega/gstruct"
    31  	. "github.com/thediveo/success"
    32  )
    33  
    34  func opts(opts ...Opt) Options {
    35  	GinkgoHelper()
    36  	o := Options{}
    37  	for _, opt := range opts {
    38  		Expect(opt(&o)).To(Succeed())
    39  	}
    40  	return o
    41  }
    42  
    43  var _ = Describe("run (container) options", func() {
    44  
    45  	It("processes input/output options", func() {
    46  		var (
    47  			stdin  strings.Reader
    48  			stdout bytes.Buffer
    49  			stderr bytes.Buffer
    50  		)
    51  
    52  		Expect(opts()).To(And(
    53  			HaveField("In", BeNil()),
    54  			HaveField("Out", BeNil()),
    55  			HaveField("Err", BeNil()),
    56  		))
    57  
    58  		Expect(opts(WithCombinedOutput(&stdout))).To(And(
    59  			HaveField("Conf.Tty", BeFalse()),
    60  			HaveField("In", BeNil()),
    61  			HaveField("Out", BeIdenticalTo(&stdout)),
    62  			HaveField("Err", BeIdenticalTo(&stdout)),
    63  		))
    64  
    65  		Expect(opts(WithDemuxedOutput(&stdout, &stderr))).To(And(
    66  			HaveField("Conf.Tty", BeFalse()),
    67  			HaveField("In", BeNil()),
    68  			HaveField("Out", BeIdenticalTo(&stdout)),
    69  			HaveField("Err", BeIdenticalTo(&stderr)),
    70  		))
    71  
    72  		Expect(opts(WithInput(&stdin))).To(And(
    73  			HaveField("Conf.Tty", BeFalse()),
    74  			HaveField("In", BeIdenticalTo(&stdin)),
    75  			HaveField("Out", BeNil()),
    76  			HaveField("Err", BeNil()),
    77  		))
    78  	})
    79  
    80  	It("processes run options", func() {
    81  		o := opts(
    82  			WithName("loosing_lattice"),
    83  			WithCommand("/bin/bash", "-c", "false"),
    84  			WithEnvVars("foo=bar", "baz="),
    85  			WithLabels("hellorld="),
    86  			ClearLabels(),
    87  			WithLabels("foo=bar", "baz="),
    88  			WithStopSignal("SIGDOOZE"),
    89  			WithStopTimeout(42),
    90  			WithTTY(),
    91  			WithAutoRemove(),
    92  			WithPrivileged(),
    93  			WithCapAdd("CAP_SUCCESS"),
    94  			WithCapDropAll(),
    95  			WithCgroupnsMode("c-host"),
    96  			WithIPCMode("i-host"),
    97  			WithNetworkMode("n-host"),
    98  			WithPIDMode("p-host"),
    99  			WithTmpfs("/tmp"),
   100  			WithTmpfsOpts("/temp", "tmpfs-size=42"),
   101  			WithDevice("/dev/foo"),
   102  			WithDevice("/dev/foo:/dev/fool"),
   103  			WithDevice("/dev/foo:/dev/fool:r"),
   104  			WithReadOnlyRootfs(),
   105  			WithSecurityOpt("all=unconfined"),
   106  			WithNetwork("one"),
   107  			WithNetwork("two"),
   108  			WithConsoleSize(666, 42),
   109  			WithHostname("foohost"),
   110  			WithRestartPolicy("always", 666),
   111  			WithAllPortsPublished(),
   112  			WithPublishedPort("[fe80::dead:beef]:2345:1234"),
   113  			WithPublishedPort("127.0.0.1:1234"),
   114  			WithPublishedPort("127.0.0.2:12345/udp"),
   115  			WithCustomInit(),
   116  		)
   117  
   118  		Expect(o.Name).To(Equal("loosing_lattice"))
   119  		Expect(o.Conf.Cmd).To(ConsistOf("/bin/bash", "-c", "false"))
   120  		Expect(o.Conf.Env).To(ConsistOf("foo=bar", "baz="))
   121  		Expect(o.Conf.Labels).NotTo(HaveKey("hellorld"))
   122  		Expect(o.Conf.Labels).To(And(
   123  			HaveKeyWithValue("foo", "bar"),
   124  			HaveKeyWithValue("baz", ""),
   125  		))
   126  		Expect(o.Conf.StopSignal).To(Equal("SIGDOOZE"))
   127  		Expect(*o.Conf.StopTimeout).To(Equal(42))
   128  		Expect(o.Conf.Tty).To(BeTrue())
   129  		Expect(o.Host.Privileged).To(BeTrue())
   130  		Expect(o.Host.CapAdd).To(ConsistOf("CAP_SUCCESS"))
   131  		Expect(o.Host.CapDrop).To(ConsistOf("ALL"))
   132  		Expect(o.Host.CgroupnsMode).To(Equal(container.CgroupnsMode("c-host")))
   133  		Expect(o.Host.IpcMode).To(Equal(container.IpcMode("i-host")))
   134  		Expect(o.Host.NetworkMode).To(Equal(container.NetworkMode("n-host")))
   135  		Expect(o.Host.PidMode).To(Equal(container.PidMode("p-host")))
   136  		Expect(o.Host.Tmpfs).To(HaveKeyWithValue("/tmp", ""))
   137  		Expect(o.Host.Tmpfs).To(HaveKeyWithValue("/temp", "tmpfs-size=42"))
   138  		Expect(o.Host.Devices).To(ConsistOf(
   139  			container.DeviceMapping{PathOnHost: "/dev/foo", PathInContainer: "/dev/foo", CgroupPermissions: "rwm"},
   140  			container.DeviceMapping{PathOnHost: "/dev/foo", PathInContainer: "/dev/fool", CgroupPermissions: "rwm"},
   141  			container.DeviceMapping{PathOnHost: "/dev/foo", PathInContainer: "/dev/fool", CgroupPermissions: "r"},
   142  		))
   143  		Expect(o.Host.ReadonlyRootfs).To(BeTrue())
   144  		Expect(o.Host.SecurityOpt).To(ConsistOf("all=unconfined"))
   145  		Expect(o.Net.EndpointsConfig).To(And(
   146  			HaveKeyWithValue("one", &network.EndpointSettings{NetworkID: "one"}),
   147  			HaveKeyWithValue("two", &network.EndpointSettings{NetworkID: "two"}),
   148  		))
   149  		Expect(o.Host.ConsoleSize).To(Equal([2]uint{42, 666}))
   150  		Expect(o.Conf.Hostname).To(Equal("foohost"))
   151  		Expect(o.Host.RestartPolicy).To(Equal(container.RestartPolicy{
   152  			Name:              "always",
   153  			MaximumRetryCount: 666,
   154  		}))
   155  
   156  		Expect(o.Host.PublishAllPorts).To(BeTrue())
   157  		Expect(o.Conf.ExposedPorts).To(HaveLen(2))
   158  		Expect(o.Conf.ExposedPorts).To(HaveKey(nat.Port("1234/tcp")))
   159  		Expect(o.Conf.ExposedPorts).To(HaveKey(nat.Port("12345/udp")))
   160  		Expect(o.Host.PortBindings).To(HaveLen(2))
   161  		Expect(o.Host.PortBindings).To(HaveKeyWithValue(
   162  			nat.Port("1234/tcp"),
   163  			ConsistOf(
   164  				nat.PortBinding{HostIP: "127.0.0.1", HostPort: "0"},
   165  				nat.PortBinding{HostIP: "fe80::dead:beef", HostPort: "2345"})))
   166  		Expect(o.Host.PortBindings).To(HaveKeyWithValue(
   167  			nat.Port("12345/udp"),
   168  			ConsistOf(nat.PortBinding{HostIP: "127.0.0.2", HostPort: "0"})))
   169  
   170  		Expect(o.Host.Init).To(gstruct.PointTo(BeTrue()))
   171  
   172  		o = opts(WithLabel("foo=bar"))
   173  		Expect(o.Conf.Labels).To(HaveKeyWithValue("foo", "bar"))
   174  
   175  		o = Options{}
   176  		Expect(WithLabels("=")(&o)).NotTo(Succeed())
   177  
   178  		o = opts(WithTmpfsOpts("/temp", "tmpfs-size=42"))
   179  		Expect(o.Host.Tmpfs).To(HaveKeyWithValue("/temp", "tmpfs-size=42"))
   180  	})
   181  
   182  	It("rejects invalid published port mappings", func() {
   183  		var o Options
   184  		Expect(WithPublishedPort("abcd")(&o)).To(HaveOccurred())
   185  	})
   186  
   187  	It("rejects invalid volume specs", func() {
   188  		var o Options
   189  		Expect(WithVolume("rappel:zappel:humba:tätärä")(&o)).To(
   190  			MatchError("malformed WithVolume parameter \"rappel:zappel:humba:tätärä\", reason: invalid spec: rappel:zappel:humba:tätärä: too many colons"))
   191  	})
   192  
   193  	It("rejects invalid mount specs", func() {
   194  		var o Options
   195  		Expect(WithMount("type=bind,source=/foo,target=/bar,private")(&o)).To(
   196  			MatchError("invalid WithMount parameter, reason: invalid field 'private' must be a key=value pair"))
   197  	})
   198  
   199  	It("rejects invalid devices", func() {
   200  		var o Options
   201  		Expect(WithDevice(":::::")(&o)).Error().To(
   202  			MatchError(ContainSubstring("malformed WithDevice parameter")))
   203  		Expect(WithDevice("::")(&o)).Error().To(
   204  			MatchError("WithDevice host path parameter must not be empty"))
   205  	})
   206  
   207  	It("splits into volumes, binds, and mounts", func() {
   208  		o := opts(
   209  			WithVolume("/foo"),
   210  			WithVolume("/run:/run2:ro"),
   211  			WithVolume("/fool:/bar:z"),
   212  			WithVolume(".:/run"),
   213  			WithMount("type=volume,source=/foo,target=/bar,readonly"),
   214  		)
   215  
   216  		Expect(o.Conf.Volumes).To(HaveLen(1))
   217  		Expect(o.Conf.Volumes).To(HaveKey("/foo"))
   218  
   219  		Expect(o.Host.Binds).To(ConsistOf(
   220  			"/run:/run2:ro",
   221  			"/fool:/bar:z",
   222  			Successful(os.Getwd())+":/run",
   223  		))
   224  
   225  		Expect(o.Host.Mounts).To(ConsistOf(
   226  			mount.Mount{
   227  				Type:     "volume",
   228  				Source:   "/foo",
   229  				Target:   "/bar",
   230  				ReadOnly: true,
   231  			},
   232  		))
   233  	})
   234  
   235  	It("returns invalid volume string when converting to binds unmodified", Serial, func() {
   236  		Expect(bindVolumeToBind("")).To(BeEmpty())
   237  
   238  		cwd := Successful(os.Getwd())
   239  		defer os.Chdir(cwd) //nolint:golint,errcheck
   240  		tmpdir := Successful(os.MkdirTemp("", "on-my-way-out-*"))
   241  		defer os.RemoveAll(tmpdir) //nolint:golint,errcheck
   242  		Expect(os.Chdir(tmpdir)).To(Succeed())
   243  		Expect(os.RemoveAll(tmpdir)).To(Succeed())
   244  		Expect(bindVolumeToBind("./relative:/absolute")).To(Equal("./relative:/absolute"))
   245  	})
   246  
   247  	It("rejects when given a network in invalid long form", func() {
   248  		var o Options
   249  		Expect(WithNetwork("foo=bar")(&o)).NotTo(Succeed())
   250  	})
   251  
   252  	DescribeTable("published port mapping syntax",
   253  		func(mapping string, expectedIP net.IP, expectedHostPort int, expectedCntrPort int, expectedL4Proto string, ok bool) {
   254  			ip, hp, cp, l4p, err := parsePortMapping(mapping)
   255  			if !ok {
   256  				Expect(err).To(HaveOccurred())
   257  				Expect(ip).To(BeNil())
   258  				Expect(hp).To(BeZero())
   259  				Expect(cp).To(BeZero())
   260  				Expect(l4p).To(BeEmpty())
   261  				return
   262  			}
   263  			Expect(err).NotTo(HaveOccurred())
   264  			Expect(ip).To(Equal(expectedIP))
   265  			Expect(hp).To(Equal(uint16(expectedHostPort)))
   266  			Expect(cp).To(Equal(uint16(expectedCntrPort)))
   267  			Expect(l4p).To(Equal(expectedL4Proto))
   268  		},
   269  
   270  		Entry(nil, "", nil, 0, 0, "", false),
   271  
   272  		// Nope
   273  		Entry(nil, "0", nil, 0, 0, "", false),
   274  		Entry(nil, "123abc", nil, 0, 0, "", false),
   275  		Entry(nil, "2345:123abc", nil, 0, 0, "", false),
   276  		Entry(nil, "2345xyz:123abc", nil, 0, 0, "", false),
   277  
   278  		// Everything wrong with a potential IPv6 host address...
   279  		Entry(nil, "[1234:", nil, 0, 0, "", false),
   280  		Entry(nil, "[1234]", nil, 0, 0, "", false),
   281  		Entry(nil, "[1234]:", nil, 0, 0, "", false),
   282  
   283  		// Aisle of Plenty :D
   284  		Entry(nil, "[::1]:2345:1234:7890", nil, 0, 0, "", false),
   285  		Entry(nil, "127.0.0.1:2345:1234:7890", nil, 0, 0, "", false),
   286  
   287  		// Oddballs, odd, yet fine.
   288  		Entry(nil, "1234/tcp/udp", nil, 0, 1234, "tcp/udp", true),
   289  
   290  		// Good cases.
   291  		Entry(nil, "1234", nil, 0, 1234, "tcp", true),
   292  		Entry(nil, "1234/udp", nil, 0, 1234, "udp", true),
   293  
   294  		Entry(nil, "127.0.0.1:1234", net.ParseIP("127.0.0.1").To4(), 0, 1234, "tcp", true),
   295  		Entry(nil, "[::1]:1234", net.ParseIP("::1"), 0, 1234, "tcp", true),
   296  
   297  		Entry(nil, "2345:1234", nil, 2345, 1234, "tcp", true),
   298  		Entry(nil, "127.0.0.1:2345:1234", net.ParseIP("127.0.0.1").To4(), 2345, 1234, "tcp", true),
   299  		Entry(nil, "[::1]:2345:1234", net.ParseIP("::1"), 2345, 1234, "tcp", true),
   300  	)
   301  })