This makes a quick pass through our tests;
Discard output/err
----------------------------------------------
Many tests were testing for error-conditions, but didn't discard output.
This produced a lot of noise when running the tests, and made it hard
to discover if there were actual failures, or if the output was expected.
For example:
=== RUN TestConfigCreateErrors
Error: "create" requires exactly 2 arguments.
See 'create --help'.
Usage: create [OPTIONS] CONFIG file|- [flags]
Create a config from a file or STDIN
Error: "create" requires exactly 2 arguments.
See 'create --help'.
Usage: create [OPTIONS] CONFIG file|- [flags]
Create a config from a file or STDIN
Error: error creating config
--- PASS: TestConfigCreateErrors (0.00s)
And after discarding output:
=== RUN TestConfigCreateErrors
--- PASS: TestConfigCreateErrors (0.00s)
Use sub-tests where possible
----------------------------------------------
Some tests were already set-up to use test-tables, and even had a usable
name (or in some cases "error" to check for). Change them to actual sub-
tests. Same test as above, but now with sub-tests and output discarded:
=== RUN TestConfigCreateErrors
=== RUN TestConfigCreateErrors/requires_exactly_2_arguments
=== RUN TestConfigCreateErrors/requires_exactly_2_arguments#01
=== RUN TestConfigCreateErrors/error_creating_config
--- PASS: TestConfigCreateErrors (0.00s)
--- PASS: TestConfigCreateErrors/requires_exactly_2_arguments (0.00s)
--- PASS: TestConfigCreateErrors/requires_exactly_2_arguments#01 (0.00s)
--- PASS: TestConfigCreateErrors/error_creating_config (0.00s)
PASS
It's not perfect in all cases (in the above, there's duplicate "expected"
errors, but Go conveniently adds "#01" for the duplicate). There's probably
also various tests I missed that could still use the same changes applied;
we can improve these in follow-ups.
Set cmd.Args to prevent test-failures
----------------------------------------------
When running tests from my IDE, it compiles the tests before running,
then executes the compiled binary to run the tests. Cobra doesn't like
that, because in that situation `os.Args` is taken as argument for the
command that's executed. The command that's tested now sees the test-
flags as arguments (`-test.v -test.run ..`), which causes various tests
to fail ("Command XYZ does not accept arguments").
# compile the tests:
go test -c -o foo.test
# execute the test:
./foo.test -test.v -test.run TestFoo
=== RUN TestFoo
Error: "foo" accepts no arguments.
The Cobra maintainers ran into the same situation, and for their own
use have added a special case to ignore `os.Args` in these cases;
https://github.com/spf13/cobra/blob/v1.8.1/command.go#L1078-L1083
args := c.args
// Workaround FAIL with "go test -v" or "cobra.test -test.v", see #155
if c.args == nil && filepath.Base(os.Args[0]) != "cobra.test" {
args = os.Args[1:]
}
Unfortunately, that exception is too specific (only checks for `cobra.test`),
so doesn't automatically fix the issue for other test-binaries. They did
provide a `cmd.SetArgs()` utility for this purpose
https://github.com/spf13/cobra/blob/v1.8.1/command.go#L276-L280
// SetArgs sets arguments for the command. It is set to os.Args[1:] by default, if desired, can be overridden
// particularly useful when testing.
func (c *Command) SetArgs(a []string) {
c.args = a
}
And the fix is to explicitly set the command's args to an empty slice to
prevent Cobra from falling back to using `os.Args[1:]` as arguments.
cmd := newSomeThingCommand()
cmd.SetArgs([]string{})
Some tests already take this issue into account, and I updated some tests
for this, but there's likely many other ones that can use the same treatment.
Perhaps the Cobra maintainers would accept a contribution to make their
condition less specific and to look for binaries ending with a `.test`
suffix (which is what compiled binaries usually are named as).
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
174 lines
4.5 KiB
Go
174 lines
4.5 KiB
Go
package opts
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/docker/docker/api/types/swarm"
|
|
"github.com/docker/go-connections/nat"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
portOptTargetPort = "target"
|
|
portOptPublishedPort = "published"
|
|
portOptProtocol = "protocol"
|
|
portOptMode = "mode"
|
|
)
|
|
|
|
// PortOpt represents a port config in swarm mode.
|
|
type PortOpt struct {
|
|
ports []swarm.PortConfig
|
|
}
|
|
|
|
// Set a new port value
|
|
//
|
|
//nolint:gocyclo
|
|
func (p *PortOpt) Set(value string) error {
|
|
longSyntax, err := regexp.MatchString(`\w+=\w+(,\w+=\w+)*`, value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if longSyntax {
|
|
csvReader := csv.NewReader(strings.NewReader(value))
|
|
fields, err := csvReader.Read()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pConfig := swarm.PortConfig{}
|
|
for _, field := range fields {
|
|
// TODO(thaJeztah): these options should not be case-insensitive.
|
|
key, val, ok := strings.Cut(strings.ToLower(field), "=")
|
|
if !ok || key == "" {
|
|
return fmt.Errorf("invalid field %s", field)
|
|
}
|
|
switch key {
|
|
case portOptProtocol:
|
|
if val != string(swarm.PortConfigProtocolTCP) && val != string(swarm.PortConfigProtocolUDP) && val != string(swarm.PortConfigProtocolSCTP) {
|
|
return fmt.Errorf("invalid protocol value %s", val)
|
|
}
|
|
|
|
pConfig.Protocol = swarm.PortConfigProtocol(val)
|
|
case portOptMode:
|
|
if val != string(swarm.PortConfigPublishModeIngress) && val != string(swarm.PortConfigPublishModeHost) {
|
|
return fmt.Errorf("invalid publish mode value %s", val)
|
|
}
|
|
|
|
pConfig.PublishMode = swarm.PortConfigPublishMode(val)
|
|
case portOptTargetPort:
|
|
tPort, err := strconv.ParseUint(val, 10, 16)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pConfig.TargetPort = uint32(tPort)
|
|
case portOptPublishedPort:
|
|
pPort, err := strconv.ParseUint(val, 10, 16)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pConfig.PublishedPort = uint32(pPort)
|
|
default:
|
|
return fmt.Errorf("invalid field key %s", key)
|
|
}
|
|
}
|
|
|
|
if pConfig.TargetPort == 0 {
|
|
return fmt.Errorf("missing mandatory field %q", portOptTargetPort)
|
|
}
|
|
|
|
if pConfig.PublishMode == "" {
|
|
pConfig.PublishMode = swarm.PortConfigPublishModeIngress
|
|
}
|
|
|
|
if pConfig.Protocol == "" {
|
|
pConfig.Protocol = swarm.PortConfigProtocolTCP
|
|
}
|
|
|
|
p.ports = append(p.ports, pConfig)
|
|
} else {
|
|
// short syntax
|
|
portConfigs := []swarm.PortConfig{}
|
|
ports, portBindingMap, err := nat.ParsePortSpecs([]string{value})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, portBindings := range portBindingMap {
|
|
for _, portBinding := range portBindings {
|
|
if portBinding.HostIP != "" {
|
|
return errors.New("hostip is not supported")
|
|
}
|
|
}
|
|
}
|
|
|
|
for port := range ports {
|
|
portConfig, err := ConvertPortToPortConfig(port, portBindingMap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
portConfigs = append(portConfigs, portConfig...)
|
|
}
|
|
p.ports = append(p.ports, portConfigs...)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Type returns the type of this option
|
|
func (p *PortOpt) Type() string {
|
|
return "port"
|
|
}
|
|
|
|
// String returns a string repr of this option
|
|
func (p *PortOpt) String() string {
|
|
ports := []string{}
|
|
for _, port := range p.ports {
|
|
repr := fmt.Sprintf("%v:%v/%s/%s", port.PublishedPort, port.TargetPort, port.Protocol, port.PublishMode)
|
|
ports = append(ports, repr)
|
|
}
|
|
return strings.Join(ports, ", ")
|
|
}
|
|
|
|
// Value returns the ports
|
|
func (p *PortOpt) Value() []swarm.PortConfig {
|
|
return p.ports
|
|
}
|
|
|
|
// ConvertPortToPortConfig converts ports to the swarm type
|
|
func ConvertPortToPortConfig(
|
|
port nat.Port,
|
|
portBindings map[nat.Port][]nat.PortBinding,
|
|
) ([]swarm.PortConfig, error) {
|
|
ports := []swarm.PortConfig{}
|
|
|
|
for _, binding := range portBindings[port] {
|
|
if p := net.ParseIP(binding.HostIP); p != nil && !p.IsUnspecified() {
|
|
// TODO(thaJeztah): use context-logger, so that this output can be suppressed (in tests).
|
|
logrus.Warnf("ignoring IP-address (%s:%s) service will listen on '0.0.0.0'", net.JoinHostPort(binding.HostIP, binding.HostPort), port)
|
|
}
|
|
|
|
startHostPort, endHostPort, err := nat.ParsePortRange(binding.HostPort)
|
|
|
|
if err != nil && binding.HostPort != "" {
|
|
return nil, fmt.Errorf("invalid hostport binding (%s) for port (%s)", binding.HostPort, port.Port())
|
|
}
|
|
|
|
for i := startHostPort; i <= endHostPort; i++ {
|
|
ports = append(ports, swarm.PortConfig{
|
|
// TODO Name: ?
|
|
Protocol: swarm.PortConfigProtocol(strings.ToLower(port.Proto())),
|
|
TargetPort: uint32(port.Int()),
|
|
PublishedPort: uint32(i),
|
|
PublishMode: swarm.PortConfigPublishModeIngress,
|
|
})
|
|
}
|
|
}
|
|
return ports, nil
|
|
}
|