From 9e15503cd0264e18cdcff42441d18404b1f2fa65 Mon Sep 17 00:00:00 2001 From: Michael Wan Date: Mon, 2 Apr 2018 22:30:53 -0400 Subject: [PATCH] refactor: mv params parse and valid part to pkg package that client and daemon can both use Signed-off-by: Michael Wan --- cli/container.go | 331 ++----------- cli/container_test.go | 464 ------------------ cli/update.go | 11 +- daemon/mgr/container_validation.go | 13 + daemon/mgr/spec_devices.go | 4 +- .../hostconfig.go => opts/devicemappings.go} | 29 +- pkg/opts/devicemappings_test.go | 163 ++++++ pkg/opts/diskquota.go | 35 ++ pkg/opts/diskquota_test.go | 36 ++ pkg/opts/intel_rdt.go | 9 + pkg/opts/intel_rdt_test.go | 29 ++ pkg/opts/labels.go | 34 ++ pkg/opts/labels_test.go | 63 +++ pkg/opts/memory.go | 15 + pkg/opts/memory_swap.go | 24 + pkg/opts/memory_swap_test.go | 56 +++ pkg/opts/memory_swappiness.go | 13 + pkg/opts/memory_swappiness_test.go | 47 ++ pkg/opts/memory_test.go | 56 +++ pkg/opts/networks.go | 115 +++++ pkg/opts/networks_test.go | 136 +++++ pkg/opts/oom_score.go | 12 + pkg/opts/portbindings.go | 49 ++ pkg/opts/portbindings_test.go | 54 ++ pkg/opts/ports.go | 55 +++ pkg/opts/ports_test.go | 33 ++ pkg/opts/restart_policy.go | 61 +++ pkg/opts/restart_policy_test.go | 92 ++++ pkg/opts/spec_annotation.go | 29 ++ pkg/opts/spec_annotation_test.go | 36 ++ pkg/opts/sysctl.go | 28 ++ pkg/opts/sysctl_test.go | 42 ++ pkg/runconfig/hostconfig_test.go | 91 ---- 33 files changed, 1401 insertions(+), 864 deletions(-) delete mode 100644 cli/container_test.go create mode 100644 daemon/mgr/container_validation.go rename pkg/{runconfig/hostconfig.go => opts/devicemappings.go} (51%) create mode 100644 pkg/opts/devicemappings_test.go create mode 100644 pkg/opts/diskquota.go create mode 100644 pkg/opts/diskquota_test.go create mode 100644 pkg/opts/intel_rdt.go create mode 100644 pkg/opts/intel_rdt_test.go create mode 100644 pkg/opts/labels.go create mode 100644 pkg/opts/labels_test.go create mode 100644 pkg/opts/memory.go create mode 100644 pkg/opts/memory_swap.go create mode 100644 pkg/opts/memory_swap_test.go create mode 100644 pkg/opts/memory_swappiness.go create mode 100644 pkg/opts/memory_swappiness_test.go create mode 100644 pkg/opts/memory_test.go create mode 100644 pkg/opts/networks.go create mode 100644 pkg/opts/networks_test.go create mode 100644 pkg/opts/oom_score.go create mode 100644 pkg/opts/portbindings.go create mode 100644 pkg/opts/portbindings_test.go create mode 100644 pkg/opts/ports.go create mode 100644 pkg/opts/ports_test.go create mode 100644 pkg/opts/restart_policy.go create mode 100644 pkg/opts/restart_policy_test.go create mode 100644 pkg/opts/spec_annotation.go create mode 100644 pkg/opts/spec_annotation_test.go create mode 100644 pkg/opts/sysctl.go create mode 100644 pkg/opts/sysctl_test.go delete mode 100644 pkg/runconfig/hostconfig_test.go diff --git a/cli/container.go b/cli/container.go index 90c5e7d174..1b9c85f191 100644 --- a/cli/container.go +++ b/cli/container.go @@ -1,16 +1,11 @@ package main import ( - "fmt" - "net" - "strconv" "strings" "github.com/alibaba/pouch/apis/types" - "github.com/alibaba/pouch/pkg/runconfig" - "github.com/docker/go-connections/nat" + "github.com/alibaba/pouch/pkg/opts" - units "github.com/docker/go-units" strfmt "github.com/go-openapi/strfmt" ) @@ -74,130 +69,91 @@ type container struct { } func (c *container) config() (*types.ContainerCreateConfig, error) { - labels, err := parseLabels(c.labels) + labels, err := opts.ParseLabels(c.labels) if err != nil { return nil, err } - if err := validateMemorySwappiness(c.memorySwappiness); err != nil { + if err := opts.ValidateMemorySwappiness(c.memorySwappiness); err != nil { return nil, err } - memory, err := parseMemory(c.memory) + memory, err := opts.ParseMemory(c.memory) if err != nil { return nil, err } - memorySwap, err := parseMemorySwap(c.memorySwap) + memorySwap, err := opts.ParseMemorySwap(c.memorySwap) if err != nil { return nil, err } - intelRdtL3Cbm, err := parseIntelRdt(c.IntelRdtL3Cbm) + intelRdtL3Cbm, err := opts.ParseIntelRdt(c.IntelRdtL3Cbm) if err != nil { return nil, err } - deviceMappings, err := parseDeviceMappings(c.devices) + deviceMappings, err := opts.ParseDeviceMappings(c.devices) if err != nil { return nil, err } - restartPolicy, err := parseRestartPolicy(c.restartPolicy) + restartPolicy, err := opts.ParseRestartPolicy(c.restartPolicy) if err != nil { return nil, err } - sysctls, err := parseSysctls(c.sysctls) + if err := opts.ValidateRestartPolicy(restartPolicy); err != nil { + return nil, err + } + + sysctls, err := opts.ParseSysctls(c.sysctls) if err != nil { return nil, err } - diskQuota, err := parseDiskQuota(c.diskQuota) + diskQuota, err := opts.ParseDiskQuota(c.diskQuota) if err != nil { return nil, err } - if err := validateOOMScore(c.oomScoreAdj); err != nil { + if err := opts.ValidateDiskQuota(diskQuota); err != nil { return nil, err } - specAnnotation, err := validateAnnotation(c.specAnnotation) + specAnnotation, err := opts.ParseAnnotation(c.specAnnotation) if err != nil { return nil, err } - var networkMode string - if len(c.networks) == 0 { - networkMode = "bridge" + if err := opts.ValidateOOMScore(c.oomScoreAdj); err != nil { + return nil, err } - networkingConfig := &types.NetworkingConfig{ - EndpointsConfig: map[string]*types.EndpointSettings{}, + + networkingConfig, networkMode, err := opts.ParseNetworks(c.networks) + if err != nil { + return nil, err } - for _, network := range c.networks { - name, parameter, mode, err := parseNetwork(network) - if err != nil { - return nil, err - } - - if networkMode == "" || mode == "mode" { - networkMode = name - } - - if name == "container" { - networkMode = fmt.Sprintf("%s:%s", name, parameter) - } else if ipaddr := net.ParseIP(parameter); ipaddr != nil { - networkingConfig.EndpointsConfig[name] = &types.EndpointSettings{ - IPAddress: parameter, - IPAMConfig: &types.EndpointIPAMConfig{ - IPV4Address: parameter, - }, - } - } + + if err := opts.ValidateNetworks(networkingConfig); err != nil { + return nil, err } - // parse port binding - tmpPorts, tmpPortBindings, err := nat.ParsePortSpecs(c.ports) + portBindings, err := opts.ParsePortBinding(c.ports) if err != nil { return nil, err } - // translate ports and portbingings - ports := map[string]interface{}{} - for n, p := range tmpPorts { - ports[string(n)] = p - } - portBindings := make(types.PortMap) - for n, pbs := range tmpPortBindings { - portBindings[string(n)] = []types.PortBinding{} - for _, tmpPb := range pbs { - pb := types.PortBinding{HostIP: tmpPb.HostIP, HostPort: tmpPb.HostPort} - portBindings[string(n)] = append(portBindings[string(n)], pb) - } + + // FIXME(ziren): do we need verify portBinding ??? + if err := opts.ValidatePortBinding(portBindings); err != nil { + return nil, err } - for _, e := range c.expose { - if strings.Contains(e, ":") { - return nil, fmt.Errorf("invalid port format for --expose: %s", e) - } - - //support two formats for expose, original format /[] or /[] - proto, port := nat.SplitProtoPort(e) - //parse the start and end port and create a sequence of ports to expose - //if expose a port, the start and end port are the same - start, end, err := nat.ParsePortRange(port) - if err != nil { - return nil, fmt.Errorf("invalid range format for --expose: %s, error: %s", e, err) - } - for i := start; i <= end; i++ { - p, err := nat.NewPort(proto, strconv.FormatUint(i, 10)) - if err != nil { - return nil, err - } - if _, exists := ports[string(p)]; !exists { - ports[string(p)] = struct{}{} - } - } + ports, err := opts.ParseExposedPorts(c.ports, c.expose) + if err != nil { + return nil, err } + config := &types.ContainerCreateConfig{ ContainerConfig: types.ContainerConfig{ Tty: c.tty, @@ -265,222 +221,3 @@ func (c *container) config() (*types.ContainerCreateConfig, error) { return config, nil } - -func parseSysctls(sysctls []string) (map[string]string, error) { - results := make(map[string]string) - for _, sysctl := range sysctls { - fields, err := parseSysctl(sysctl) - if err != nil { - return nil, err - } - k, v := fields[0], fields[1] - results[k] = v - } - return results, nil -} - -func parseSysctl(sysctl string) ([]string, error) { - fields := strings.SplitN(sysctl, "=", 2) - if len(fields) != 2 { - return nil, fmt.Errorf("invalid sysctl %s: sysctl must be in format of key=value", sysctl) - } - return fields, nil -} - -func parseLabels(labels []string) (map[string]string, error) { - results := make(map[string]string) - for _, label := range labels { - fields, err := parseLabel(label) - if err != nil { - return nil, err - } - k, v := fields[0], fields[1] - results[k] = v - } - return results, nil -} - -func parseLabel(label string) ([]string, error) { - fields := strings.SplitN(label, "=", 2) - if len(fields) != 2 { - return nil, fmt.Errorf("invalid label %s: label must be in format of key=value", label) - } - return fields, nil -} - -func parseDeviceMappings(devices []string) ([]*types.DeviceMapping, error) { - results := []*types.DeviceMapping{} - for _, device := range devices { - deviceMapping, err := parseDevice(device) - if err != nil { - return nil, err - } - results = append(results, deviceMapping) - } - return results, nil -} - -func parseDevice(device string) (*types.DeviceMapping, error) { - deviceMapping, err := runconfig.ParseDevice(device) - if err != nil { - return nil, fmt.Errorf("parse devices error: %s", err) - } - if !runconfig.ValidDeviceMode(deviceMapping.CgroupPermissions) { - return nil, fmt.Errorf("%s invalid device mode: %s", device, deviceMapping.CgroupPermissions) - } - return deviceMapping, nil -} - -func parseMemory(memory string) (int64, error) { - if memory == "" { - return 0, nil - } - result, err := units.RAMInBytes(memory) - if err != nil { - return 0, err - } - return result, nil -} - -func parseMemorySwap(memorySwap string) (int64, error) { - if memorySwap == "" { - return 0, nil - } - if memorySwap == "-1" { - return -1, nil - } - result, err := units.RAMInBytes(memorySwap) - if err != nil { - return 0, err - } - return result, nil -} - -func validateMemorySwappiness(memorySwappiness int64) error { - if memorySwappiness != -1 && (memorySwappiness < 0 || memorySwappiness > 100) { - return fmt.Errorf("invalid memory swappiness: %d (its range is -1 or 0-100)", memorySwappiness) - } - return nil -} - -func parseIntelRdt(intelRdtL3Cbm string) (string, error) { - // FIXME: add Intel RDT L3 Cbm validation - return intelRdtL3Cbm, nil -} - -func parseRestartPolicy(restartPolicy string) (*types.RestartPolicy, error) { - policy := &types.RestartPolicy{} - - if restartPolicy == "" { - policy.Name = "no" - return policy, nil - } - - fields := strings.Split(restartPolicy, ":") - policy.Name = fields[0] - - switch policy.Name { - case "always", "unless-stopped", "no": - case "on-failure": - if len(fields) > 2 { - return nil, fmt.Errorf("invalid restart policy: %s", restartPolicy) - } - if len(fields) == 2 { - n, err := strconv.Atoi(fields[1]) - if err != nil { - return nil, fmt.Errorf("invalid restart policy: %v", err) - } - policy.MaximumRetryCount = int64(n) - } - default: - return nil, fmt.Errorf("invalid restart policy: %s", restartPolicy) - } - - return policy, nil -} - -// network format as below: -// [network]:[ip_address], such as: mynetwork:172.17.0.2 or mynetwork(ip alloc by ipam) or 172.17.0.2(default network is bridge) -// [network_mode]:[parameter], such as: host(use host network) or container:containerID(use exist container network) -// [network_mode]:[parameter]:mode, such as: mynetwork:172.17.0.2:mode(if the container has multi-networks, the network is the default network mode) -func parseNetwork(network string) (string, string, string, error) { - var ( - name string - parameter string - mode string - ) - if network == "" { - return "", "", "", fmt.Errorf("invalid network: cannot be empty") - } - arr := strings.Split(network, ":") - switch len(arr) { - case 1: - if ipaddr := net.ParseIP(arr[0]); ipaddr != nil { - parameter = arr[0] - } else { - name = arr[0] - } - case 2: - name = arr[0] - if name == "container" { - parameter = arr[1] - } else if ipaddr := net.ParseIP(arr[1]); ipaddr != nil { - parameter = arr[1] - } else { - mode = arr[1] - } - default: - name = arr[0] - parameter = arr[1] - mode = arr[2] - } - - return name, parameter, mode, nil -} - -func parseDiskQuota(quotas []string) (map[string]string, error) { - var quotaMaps = make(map[string]string) - - for _, quota := range quotas { - if quota == "" { - return nil, fmt.Errorf("invalid format for disk quota: %s", quota) - } - - parts := strings.Split(quota, "=") - switch len(parts) { - case 1: - quotaMaps["/"] = parts[0] - case 2: - quotaMaps[parts[0]] = parts[1] - default: - return nil, fmt.Errorf("invalid format for disk quota: %s", quota) - } - } - - return quotaMaps, nil -} - -// validateOOMScore validates oom score -func validateOOMScore(score int64) error { - if score < -1000 || score > 1000 { - return fmt.Errorf("oom-score-adj should be in range [-1000, 1000]") - } - - return nil -} - -// validateAnnotation validates runtime annotations format. -func validateAnnotation(annotations []string) (map[string]string, error) { - specAnnotation := make(map[string]string) - - for _, annotation := range annotations { - splits := strings.Split(annotation, "=") - if len(splits) != 2 || splits[0] == "" || splits[1] == "" { - return nil, fmt.Errorf("invalid format for spec annotation: %s, correct format should be key=value, neither should be nil", annotation) - } - - specAnnotation[splits[0]] = splits[1] - } - - return specAnnotation, nil -} diff --git a/cli/container_test.go b/cli/container_test.go deleted file mode 100644 index 99486b42f5..0000000000 --- a/cli/container_test.go +++ /dev/null @@ -1,464 +0,0 @@ -package main - -import ( - "fmt" - "reflect" - "testing" - - "github.com/alibaba/pouch/apis/types" - "github.com/stretchr/testify/assert" -) - -func TestParseRestartPolicy(t *testing.T) { - type TestCase struct { - input string - expectedName string - expectedCount int64 - err error - } - - cases := []TestCase{ - { - input: "always", - expectedName: "always", - expectedCount: 0, - err: nil, - }, - { - input: "no", - expectedName: "no", - expectedCount: 0, - err: nil, - }, - { - input: "unless-stopped", - expectedName: "unless-stopped", - expectedCount: 0, - err: nil, - }, - { - input: "on-failure:1", - expectedName: "on-failure", - expectedCount: 1, - err: nil, - }, - { - input: "on-failure", - expectedName: "on-failure", - expectedCount: 0, - err: nil, - }, - { - input: "on-failure:1:2", - expectedName: "on-failure", - expectedCount: 0, - err: fmt.Errorf("invalid restart policy: %s", "on-failure:1:2"), - }, - } - - for _, cs := range cases { - policy, err := parseRestartPolicy(cs.input) - assert.Equal(t, cs.err, err) - if err == nil { - assert.Equal(t, cs.expectedName, policy.Name) - assert.Equal(t, cs.expectedCount, policy.MaximumRetryCount) - } - } -} - -func TestParseLabels(t *testing.T) { - type result struct { - labels map[string]string - err error - } - type TestCase struct { - input []string - expected result - } - - testCases := []TestCase{ - { - input: []string{"a=b"}, - expected: result{ - labels: map[string]string{ - "a": "b", - }, - err: nil, - }, - }, - { - input: []string{"a=b", "a=b"}, - expected: result{ - labels: map[string]string{ - "a": "b", - }, - err: nil, - }, - }, - { - // FIXME: this case should throw error - input: []string{"a=b", "a=bb"}, - expected: result{ - labels: map[string]string{ - "a": "bb", - }, - err: nil, - }, - }, - { - input: []string{"ThisIsALableWithoutEqualMark"}, - expected: result{ - labels: nil, - err: fmt.Errorf("invalid label ThisIsALableWithoutEqualMark: label must be in format of key=value"), - }, - }, - } - - for _, testCase := range testCases { - labels, err := parseLabels(testCase.input) - assert.Equal(t, testCase.expected.err, err) - assert.Equal(t, testCase.expected.labels, labels) - } -} - -func TestParseMemory(t *testing.T) { - type result struct { - memory int64 - err error - } - type TestCase struct { - input string - expected result - } - - testCases := []TestCase{ - { - input: "", - expected: result{ - memory: 0, - err: nil, - }, - }, - { - input: "0", - expected: result{ - memory: 0, - err: nil, - }, - }, - { - input: "100m", - expected: result{ - memory: 104857600, - err: nil, - }, - }, - { - input: "10asdfg", - expected: result{ - memory: 0, - err: fmt.Errorf("invalid size: '%s'", "10asdfg"), - }, - }, - } - - for _, testCase := range testCases { - memory, err := parseMemory(testCase.input) - assert.Equal(t, testCase.expected.err, err) - assert.Equal(t, testCase.expected.memory, memory) - } -} - -func TestParseMemorySwap(t *testing.T) { - type result struct { - memorySwap int64 - err error - } - type TestCase struct { - input string - expected result - } - - testCases := []TestCase{ - { - input: "", - expected: result{ - memorySwap: 0, - err: nil, - }, - }, - { - input: "-1", - expected: result{ - memorySwap: -1, - err: nil, - }, - }, - { - input: "100m", - expected: result{ - memorySwap: 104857600, - err: nil, - }, - }, - { - input: "10asdfg", - expected: result{ - memorySwap: 0, - err: fmt.Errorf("invalid size: '%s'", "10asdfg"), - }, - }, - } - - for _, testCase := range testCases { - memorySwap, err := parseMemorySwap(testCase.input) - assert.Equal(t, testCase.expected.err, err) - assert.Equal(t, testCase.expected.memorySwap, memorySwap) - } -} - -func TestValidateMemorySwappiness(t *testing.T) { - type TestCase struct { - input int64 - expected error - } - - testCases := []TestCase{ - { - input: -1, - expected: nil, - }, - { - input: 0, - expected: nil, - }, - { - input: 100, - expected: nil, - }, - { - input: 38, - expected: nil, - }, - { - input: -5, - expected: fmt.Errorf("invalid memory swappiness: %d (its range is -1 or 0-100)", -5), - }, - { - input: 200, - expected: fmt.Errorf("invalid memory swappiness: %d (its range is -1 or 0-100)", 200), - }, - } - - for _, testCase := range testCases { - err := validateMemorySwappiness(testCase.input) - assert.Equal(t, testCase.expected, err) - } -} - -func Test_parseDeviceMapping(t *testing.T) { - type args struct { - device string - } - tests := []struct { - name string - args args - want *types.DeviceMapping - wantErr bool - }{ - { - name: "deviceMapping1", - args: args{ - device: "/dev/deviceName:/dev/deviceName:mrw", - }, - want: &types.DeviceMapping{ - PathOnHost: "/dev/deviceName", - PathInContainer: "/dev/deviceName", - CgroupPermissions: "mrw", - }, - - wantErr: false, - }, - { - name: "deviceMapping2", - args: args{ - device: "/dev/deviceName:", - }, - want: &types.DeviceMapping{ - PathOnHost: "/dev/deviceName", - PathInContainer: "/dev/deviceName", - CgroupPermissions: "rwm", - }, - wantErr: false, - }, - { - name: "deviceMappingWrong1", - args: args{ - device: "/dev/deviceName:/dev/deviceName:rrw", - }, - wantErr: true, - }, - { - name: "deviceMappingWrong2", - args: args{ - device: "/dev/deviceName:/dev/deviceName:arw", - }, - wantErr: true, - }, - { - name: "deviceMappingWrong3", - args: args{ - device: "/dev/deviceName:/dev/deviceName:mrw:mrw", - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := parseDevice(tt.args.device) - if (err != nil) != tt.wantErr { - t.Errorf("parseDeviceMappings() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("parseDeviceMappings() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_parseSysctls(t *testing.T) { - type result struct { - sysctls map[string]string - err error - } - type TestCases struct { - input []string - expect result - } - - testCases := []TestCases{ - { - input: []string{"a=b"}, - expect: result{ - sysctls: map[string]string{"a": "b"}, - err: nil, - }, - }, - { - input: []string{"ab"}, - expect: result{ - sysctls: nil, - err: fmt.Errorf("invalid sysctl %s: sysctl must be in format of key=value", "ab"), - }, - }, - } - - for _, testCase := range testCases { - sysctl, err := parseSysctls(testCase.input) - assert.Equal(t, testCase.expect.sysctls, sysctl) - assert.Equal(t, testCase.expect.err, err) - } -} - -func Test_parseNetwork(t *testing.T) { - type net struct { - name string - parameter string - mode string - } - type result struct { - network net - err error - } - type TestCases struct { - input string - expect result - } - - testCases := []TestCases{ - { - input: "", - expect: result{ - err: fmt.Errorf("invalid network: cannot be empty"), - network: net{name: "", parameter: "", mode: ""}, - }, - }, - { - input: "121.0.0.1", - expect: result{ - err: nil, - network: net{name: "", parameter: "121.0.0.1", mode: ""}, - }, - }, - { - input: "myHost", - expect: result{ - err: nil, - network: net{name: "myHost", parameter: "", mode: ""}, - }, - }, - { - input: "myHost:121.0.0.1", - expect: result{ - err: nil, - network: net{name: "myHost", parameter: "121.0.0.1", mode: ""}, - }, - }, - { - input: "container:9ca6ac", - expect: result{ - err: nil, - network: net{name: "container", parameter: "9ca6ac", mode: ""}, - }, - }, - { - input: "bridge:121.0.0.1:mode", - expect: result{ - err: nil, - network: net{name: "bridge", parameter: "121.0.0.1", mode: "mode"}, - }, - }, - { - input: "bridge:mode", - expect: result{ - err: nil, - network: net{name: "bridge", parameter: "", mode: "mode"}, - }, - }, - } - - for _, testCase := range testCases { - name, parameter, mode, error := parseNetwork(testCase.input) - assert.Equal(t, testCase.expect.err, error) - assert.Equal(t, testCase.expect.network.name, name) - assert.Equal(t, testCase.expect.network.parameter, parameter) - assert.Equal(t, testCase.expect.network.mode, mode) - } -} - -func Test_parseIntelRdt(t *testing.T) { - type args struct { - intelRdtL3Cbm string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := parseIntelRdt(tt.args.intelRdtL3Cbm) - if (err != nil) != tt.wantErr { - t.Errorf("parseIntelRdt() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("parseIntelRdt() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/cli/update.go b/cli/update.go index e3136926c2..65a02b2185 100644 --- a/cli/update.go +++ b/cli/update.go @@ -4,6 +4,7 @@ import ( "context" "github.com/alibaba/pouch/apis/types" + "github.com/alibaba/pouch/pkg/opts" "github.com/alibaba/pouch/pkg/reference" "github.com/spf13/cobra" @@ -58,21 +59,21 @@ func (uc *UpdateCommand) updateRun(args []string) error { container := args[0] ctx := context.Background() - labels, err := parseLabels(uc.labels) + labels, err := opts.ParseLabels(uc.labels) if err != nil { return err } - if err := validateMemorySwappiness(uc.memorySwappiness); err != nil { + if err := opts.ValidateMemorySwappiness(uc.memorySwappiness); err != nil { return err } - memory, err := parseMemory(uc.memory) + memory, err := opts.ParseMemory(uc.memory) if err != nil { return err } - memorySwap, err := parseMemorySwap(uc.memorySwap) + memorySwap, err := opts.ParseMemorySwap(uc.memorySwap) if err != nil { return err } @@ -87,7 +88,7 @@ func (uc *UpdateCommand) updateRun(args []string) error { BlkioWeight: uc.blkioWeight, } - restartPolicy, err := parseRestartPolicy(uc.restartPolicy) + restartPolicy, err := opts.ParseRestartPolicy(uc.restartPolicy) if err != nil { return err } diff --git a/daemon/mgr/container_validation.go b/daemon/mgr/container_validation.go new file mode 100644 index 0000000000..a0cb97fad0 --- /dev/null +++ b/daemon/mgr/container_validation.go @@ -0,0 +1,13 @@ +package mgr + +import ( + "github.com/alibaba/pouch/apis/types" +) + +// verifyContainerSetting is to verify the correctness of hostconfig and config. +func (mgr *ContainerManager) verifyContainerSetting(hostConfig *types.HostConfig, config *types.ContainerConfig) error { + if config != nil { + // TODO + } + return nil +} diff --git a/daemon/mgr/spec_devices.go b/daemon/mgr/spec_devices.go index 65205f63f7..e26c1fb437 100644 --- a/daemon/mgr/spec_devices.go +++ b/daemon/mgr/spec_devices.go @@ -7,7 +7,7 @@ import ( "path/filepath" "strings" - "github.com/alibaba/pouch/pkg/runconfig" + "github.com/alibaba/pouch/pkg/opts" "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/libcontainer/devices" @@ -109,7 +109,7 @@ func setupDevices(ctx context.Context, meta *ContainerMeta, spec *SpecWrapper) e } } else { for _, deviceMapping := range meta.HostConfig.Devices { - if !runconfig.ValidDeviceMode(deviceMapping.CgroupPermissions) { + if !opts.ValidateDeviceMode(deviceMapping.CgroupPermissions) { return fmt.Errorf("%s invalid device mode: %s", deviceMapping.PathOnHost, deviceMapping.CgroupPermissions) } d, dPermissions, err := devicesFromPath(deviceMapping.PathOnHost, deviceMapping.PathInContainer, deviceMapping.CgroupPermissions) diff --git a/pkg/runconfig/hostconfig.go b/pkg/opts/devicemappings.go similarity index 51% rename from pkg/runconfig/hostconfig.go rename to pkg/opts/devicemappings.go index 03e010f7d5..e703dca7d8 100644 --- a/pkg/runconfig/hostconfig.go +++ b/pkg/opts/devicemappings.go @@ -1,4 +1,4 @@ -package runconfig +package opts import ( "fmt" @@ -7,8 +7,27 @@ import ( "github.com/alibaba/pouch/apis/types" ) -// ParseDevice parses a device mapping string to a container.DeviceMapping struct -func ParseDevice(device string) (*types.DeviceMapping, error) { +// ParseDeviceMappings parse devicemappings +func ParseDeviceMappings(devices []string) ([]*types.DeviceMapping, error) { + results := []*types.DeviceMapping{} + for _, device := range devices { + deviceMapping, err := parseDevice(device) + if err != nil { + return nil, fmt.Errorf("failed to parse devices: %v", err) + } + + if !ValidateDeviceMode(deviceMapping.CgroupPermissions) { + return nil, fmt.Errorf("%s invalid device mode: %s", device, deviceMapping.CgroupPermissions) + } + + results = append(results, deviceMapping) + } + return results, nil + +} + +// parseDevice parses a device mapping string to a container.DeviceMapping struct +func parseDevice(device string) (*types.DeviceMapping, error) { src := "" dst := "" permissions := "rwm" @@ -38,9 +57,9 @@ func ParseDevice(device string) (*types.DeviceMapping, error) { return deviceMapping, nil } -// ValidDeviceMode checks if the mode for device is valid or not. +// ValidateDeviceMode checks if the mode for device is valid or not. // valid mode is a composition of r (read), w (write), and m (mknod). -func ValidDeviceMode(mode string) bool { +func ValidateDeviceMode(mode string) bool { var legalDeviceMode = map[rune]bool{ 'r': true, 'w': true, diff --git a/pkg/opts/devicemappings_test.go b/pkg/opts/devicemappings_test.go new file mode 100644 index 0000000000..ae60dcdad4 --- /dev/null +++ b/pkg/opts/devicemappings_test.go @@ -0,0 +1,163 @@ +package opts + +import ( + "fmt" + "reflect" + "testing" + + "github.com/alibaba/pouch/apis/types" + + "github.com/stretchr/testify/assert" +) + +func TestParseDeviceMapping(t *testing.T) { + type args struct { + device []string + } + tests := []struct { + name string + args args + want []*types.DeviceMapping + wantErr bool + }{ + { + name: "deviceMapping1", + args: args{ + device: []string{"/dev/deviceName:/dev/deviceName:mrw"}, + }, + want: []*types.DeviceMapping{{ + PathOnHost: "/dev/deviceName", + PathInContainer: "/dev/deviceName", + CgroupPermissions: "mrw", + }}, + + wantErr: false, + }, + { + name: "deviceMapping2", + args: args{ + device: []string{"/dev/deviceName:"}, + }, + want: []*types.DeviceMapping{{ + PathOnHost: "/dev/deviceName", + PathInContainer: "/dev/deviceName", + CgroupPermissions: "rwm", + }}, + wantErr: false, + }, + { + name: "deviceMappingWrong1", + args: args{ + device: []string{"/dev/deviceName:/dev/deviceName:rrw"}, + }, + wantErr: true, + }, + { + name: "deviceMappingWrong2", + args: args{ + device: []string{"/dev/deviceName:/dev/deviceName:arw"}, + }, + wantErr: true, + }, + { + name: "deviceMappingWrong3", + args: args{ + device: []string{"/dev/deviceName:/dev/deviceName:mrw:mrw"}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseDeviceMappings(tt.args.device) + if (err != nil) != tt.wantErr { + t.Errorf("parseDeviceMappings() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseDeviceMappings() = %v, want %v", got, tt.want) + } + }) + } +} + +type tDeviceModeCase struct { + input string + expected bool +} + +type tParseDeviceCase struct { + input string + expected *types.DeviceMapping + err error +} + +func Test_parseDevice(t *testing.T) { + for _, tc := range []tParseDeviceCase{ + { + input: "/dev/zero:/dev/zero:rwm", + expected: &types.DeviceMapping{ + PathOnHost: "/dev/zero", + PathInContainer: "/dev/zero", + CgroupPermissions: "rwm", + }, + err: nil, + }, { + input: "/dev/zero:rwm", + expected: &types.DeviceMapping{ + PathOnHost: "/dev/zero", + PathInContainer: "rwm", + CgroupPermissions: "rwm", + }, + err: nil, + }, { + input: "/dev/zero", + expected: &types.DeviceMapping{ + PathOnHost: "/dev/zero", + PathInContainer: "/dev/zero", + CgroupPermissions: "rwm", + }, + err: nil, + }, { + input: "/dev/zero:/dev/testzero:rwm", + expected: &types.DeviceMapping{ + PathOnHost: "/dev/zero", + PathInContainer: "/dev/testzero", + CgroupPermissions: "rwm", + }, + err: nil, + }, { + input: "/dev/zero:/dev/testzero:rwm:tooLong", + expected: nil, + err: fmt.Errorf("invalid device specification: /dev/zero:/dev/testzero:rwm:tooLong"), + }, + } { + output, err := parseDevice(tc.input) + assert.Equal(t, tc.err, err, tc.input) + assert.Equal(t, tc.expected, output, tc.input) + } +} + +func TestValidateDeviceMode(t *testing.T) { + for _, modeCase := range []tDeviceModeCase{ + { + input: "rwm", + expected: true, + }, { + input: "r", + expected: true, + }, { + input: "rw", + expected: true, + }, { + input: "rr", + expected: false, + }, { + input: "rxm", + expected: false, + }, + } { + isValid := ValidateDeviceMode(modeCase.input) + assert.Equal(t, modeCase.expected, isValid, modeCase.input) + } +} diff --git a/pkg/opts/diskquota.go b/pkg/opts/diskquota.go new file mode 100644 index 0000000000..99bd619875 --- /dev/null +++ b/pkg/opts/diskquota.go @@ -0,0 +1,35 @@ +package opts + +import ( + "fmt" + "strings" +) + +// ParseDiskQuota parses diskquota configurations of container. +func ParseDiskQuota(quotas []string) (map[string]string, error) { + var quotaMaps = make(map[string]string) + + for _, quota := range quotas { + if quota == "" { + return nil, fmt.Errorf("invalid format for disk quota: %s", quota) + } + + parts := strings.Split(quota, "=") + switch len(parts) { + case 1: + quotaMaps["/"] = parts[0] + case 2: + quotaMaps[parts[0]] = parts[1] + default: + return nil, fmt.Errorf("invalid format for disk quota: %s", quota) + } + } + + return quotaMaps, nil +} + +// ValidateDiskQuota verifies diskquota configurations of container. +func ValidateDiskQuota(quotaMaps map[string]string) error { + // TODO + return nil +} diff --git a/pkg/opts/diskquota_test.go b/pkg/opts/diskquota_test.go new file mode 100644 index 0000000000..500fa87538 --- /dev/null +++ b/pkg/opts/diskquota_test.go @@ -0,0 +1,36 @@ +package opts + +import ( + "reflect" + "testing" +) + +func TestParseDiskQuota(t *testing.T) { + type args struct { + diskquota []string + } + tests := []struct { + name string + args args + want map[string]string + wantErr bool + }{ + // TODO: Add test cases. + {name: "test1", args: args{diskquota: []string{""}}, want: nil, wantErr: true}, + {name: "test2", args: args{diskquota: []string{"foo=foo=foo"}}, want: nil, wantErr: true}, + {name: "test3", args: args{diskquota: []string{"foo"}}, want: map[string]string{"/": "foo"}, wantErr: false}, + {name: "test3", args: args{diskquota: []string{"foo=foo"}}, want: map[string]string{"foo": "foo"}, wantErr: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseDiskQuota(tt.args.diskquota) + if (err != nil) != tt.wantErr { + t.Errorf("ParseDiskQuota() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseDiskQuota() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/opts/intel_rdt.go b/pkg/opts/intel_rdt.go new file mode 100644 index 0000000000..5a5b336170 --- /dev/null +++ b/pkg/opts/intel_rdt.go @@ -0,0 +1,9 @@ +package opts + +// ParseIntelRdt parses inter-rdt params of container +func ParseIntelRdt(intelRdtL3Cbm string) (string, error) { + // FIXME: add Intel RDT L3 Cbm validation + return intelRdtL3Cbm, nil +} + +// TODO: ValidateInterRdt diff --git a/pkg/opts/intel_rdt_test.go b/pkg/opts/intel_rdt_test.go new file mode 100644 index 0000000000..448bd6f745 --- /dev/null +++ b/pkg/opts/intel_rdt_test.go @@ -0,0 +1,29 @@ +package opts + +import "testing" + +func TestParseIntelRdt(t *testing.T) { + type args struct { + intelRdtL3Cbm string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseIntelRdt(tt.args.intelRdtL3Cbm) + if (err != nil) != tt.wantErr { + t.Errorf("parseIntelRdt() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("parseIntelRdt() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/opts/labels.go b/pkg/opts/labels.go new file mode 100644 index 0000000000..79353f4fd2 --- /dev/null +++ b/pkg/opts/labels.go @@ -0,0 +1,34 @@ +package opts + +import ( + "fmt" + "strings" +) + +// ParseLabels parses the labels params of container. +func ParseLabels(labels []string) (map[string]string, error) { + results := make(map[string]string) + for _, label := range labels { + fields, err := parseLabel(label) + if err != nil { + return nil, err + } + k, v := fields[0], fields[1] + results[k] = v + } + return results, nil +} + +func parseLabel(label string) ([]string, error) { + fields := strings.SplitN(label, "=", 2) + if len(fields) != 2 { + return nil, fmt.Errorf("invalid label %s: label must be in format of key=value", label) + } + return fields, nil +} + +// ValidateLabels verifies the correct of labels +func ValidateLabels(map[string]string) error { + // TODO + return nil +} diff --git a/pkg/opts/labels_test.go b/pkg/opts/labels_test.go new file mode 100644 index 0000000000..8d8a668dc7 --- /dev/null +++ b/pkg/opts/labels_test.go @@ -0,0 +1,63 @@ +package opts + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseLabels(t *testing.T) { + type result struct { + labels map[string]string + err error + } + type TestCase struct { + input []string + expected result + } + + testCases := []TestCase{ + { + input: []string{"a=b"}, + expected: result{ + labels: map[string]string{ + "a": "b", + }, + err: nil, + }, + }, + { + input: []string{"a=b", "a=b"}, + expected: result{ + labels: map[string]string{ + "a": "b", + }, + err: nil, + }, + }, + { + // FIXME: this case should throw error + input: []string{"a=b", "a=bb"}, + expected: result{ + labels: map[string]string{ + "a": "bb", + }, + err: nil, + }, + }, + { + input: []string{"ThisIsALableWithoutEqualMark"}, + expected: result{ + labels: nil, + err: fmt.Errorf("invalid label ThisIsALableWithoutEqualMark: label must be in format of key=value"), + }, + }, + } + + for _, testCase := range testCases { + labels, err := ParseLabels(testCase.input) + assert.Equal(t, testCase.expected.err, err) + assert.Equal(t, testCase.expected.labels, labels) + } +} diff --git a/pkg/opts/memory.go b/pkg/opts/memory.go new file mode 100644 index 0000000000..40c6d46e6a --- /dev/null +++ b/pkg/opts/memory.go @@ -0,0 +1,15 @@ +package opts + +import units "github.com/docker/go-units" + +// ParseMemory parses the memory param of container. +func ParseMemory(memory string) (int64, error) { + if memory == "" { + return 0, nil + } + result, err := units.RAMInBytes(memory) + if err != nil { + return 0, err + } + return result, nil +} diff --git a/pkg/opts/memory_swap.go b/pkg/opts/memory_swap.go new file mode 100644 index 0000000000..6ed75fd38c --- /dev/null +++ b/pkg/opts/memory_swap.go @@ -0,0 +1,24 @@ +package opts + +import units "github.com/docker/go-units" + +// ParseMemorySwap parses the memory-swap param of container. +func ParseMemorySwap(memorySwap string) (int64, error) { + if memorySwap == "" { + return 0, nil + } + if memorySwap == "-1" { + return -1, nil + } + result, err := units.RAMInBytes(memorySwap) + if err != nil { + return 0, err + } + return result, nil +} + +// ValidateMemorySwap verifies the correctness of memory-swap. +func ValidateMemorySwap(memorySwap int64) error { + // TODO + return nil +} diff --git a/pkg/opts/memory_swap_test.go b/pkg/opts/memory_swap_test.go new file mode 100644 index 0000000000..02b6c24d91 --- /dev/null +++ b/pkg/opts/memory_swap_test.go @@ -0,0 +1,56 @@ +package opts + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseMemorySwap(t *testing.T) { + type result struct { + memorySwap int64 + err error + } + type TestCase struct { + input string + expected result + } + + testCases := []TestCase{ + { + input: "", + expected: result{ + memorySwap: 0, + err: nil, + }, + }, + { + input: "-1", + expected: result{ + memorySwap: -1, + err: nil, + }, + }, + { + input: "100m", + expected: result{ + memorySwap: 104857600, + err: nil, + }, + }, + { + input: "10asdfg", + expected: result{ + memorySwap: 0, + err: fmt.Errorf("invalid size: '%s'", "10asdfg"), + }, + }, + } + + for _, testCase := range testCases { + memorySwap, err := ParseMemorySwap(testCase.input) + assert.Equal(t, testCase.expected.err, err) + assert.Equal(t, testCase.expected.memorySwap, memorySwap) + } +} diff --git a/pkg/opts/memory_swappiness.go b/pkg/opts/memory_swappiness.go new file mode 100644 index 0000000000..719aea3c17 --- /dev/null +++ b/pkg/opts/memory_swappiness.go @@ -0,0 +1,13 @@ +package opts + +import "fmt" + +// TODO: ParseMemorySwappiness + +// ValidateMemorySwappiness verifies the correctness of memory-swappiness. +func ValidateMemorySwappiness(memorySwappiness int64) error { + if memorySwappiness != -1 && (memorySwappiness < 0 || memorySwappiness > 100) { + return fmt.Errorf("invalid memory swappiness: %d (its range is -1 or 0-100)", memorySwappiness) + } + return nil +} diff --git a/pkg/opts/memory_swappiness_test.go b/pkg/opts/memory_swappiness_test.go new file mode 100644 index 0000000000..2bf5e50243 --- /dev/null +++ b/pkg/opts/memory_swappiness_test.go @@ -0,0 +1,47 @@ +package opts + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidateMemorySwappiness(t *testing.T) { + type TestCase struct { + input int64 + expected error + } + + testCases := []TestCase{ + { + input: -1, + expected: nil, + }, + { + input: 0, + expected: nil, + }, + { + input: 100, + expected: nil, + }, + { + input: 38, + expected: nil, + }, + { + input: -5, + expected: fmt.Errorf("invalid memory swappiness: %d (its range is -1 or 0-100)", -5), + }, + { + input: 200, + expected: fmt.Errorf("invalid memory swappiness: %d (its range is -1 or 0-100)", 200), + }, + } + + for _, testCase := range testCases { + err := ValidateMemorySwappiness(testCase.input) + assert.Equal(t, testCase.expected, err) + } +} diff --git a/pkg/opts/memory_test.go b/pkg/opts/memory_test.go new file mode 100644 index 0000000000..1a18f8480e --- /dev/null +++ b/pkg/opts/memory_test.go @@ -0,0 +1,56 @@ +package opts + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseMemory(t *testing.T) { + type result struct { + memory int64 + err error + } + type TestCase struct { + input string + expected result + } + + testCases := []TestCase{ + { + input: "", + expected: result{ + memory: 0, + err: nil, + }, + }, + { + input: "0", + expected: result{ + memory: 0, + err: nil, + }, + }, + { + input: "100m", + expected: result{ + memory: 104857600, + err: nil, + }, + }, + { + input: "10asdfg", + expected: result{ + memory: 0, + err: fmt.Errorf("invalid size: '%s'", "10asdfg"), + }, + }, + } + + for _, testCase := range testCases { + memory, err := ParseMemory(testCase.input) + assert.Equal(t, testCase.expected.err, err) + assert.Equal(t, testCase.expected.memory, memory) + } +} diff --git a/pkg/opts/networks.go b/pkg/opts/networks.go new file mode 100644 index 0000000000..16b9bee96f --- /dev/null +++ b/pkg/opts/networks.go @@ -0,0 +1,115 @@ +package opts + +import ( + "fmt" + "net" + "strings" + + "github.com/alibaba/pouch/apis/types" +) + +// ParseNetworks parses network configurations of container. +func ParseNetworks(networks []string) (*types.NetworkingConfig, string, error) { + var networkMode string + if len(networks) == 0 { + networkMode = "bridge" + } + networkingConfig := &types.NetworkingConfig{ + EndpointsConfig: map[string]*types.EndpointSettings{}, + } + for _, network := range networks { + name, parameter, mode, err := parseNetwork(network) + if err != nil { + return nil, "", err + } + + if networkMode == "" || mode == "mode" { + networkMode = name + } + + if name == "container" { + networkMode = fmt.Sprintf("%s:%s", name, parameter) + } else if ipaddr := net.ParseIP(parameter); ipaddr != nil { + networkingConfig.EndpointsConfig[name] = &types.EndpointSettings{ + IPAddress: parameter, + IPAMConfig: &types.EndpointIPAMConfig{ + IPV4Address: parameter, + }, + } + } + } + + return networkingConfig, networkMode, nil +} + +// network format as below: +// [network]:[ip_address], such as: mynetwork:172.17.0.2 or mynetwork(ip alloc by ipam) or 172.17.0.2(default network is bridge) +// [network_mode]:[parameter], such as: host(use host network) or container:containerID(use exist container network) +// [network_mode]:[parameter]:mode, such as: mynetwork:172.17.0.2:mode(if the container has multi-networks, the network is the default network mode) +func parseNetwork(network string) (string, string, string, error) { + var ( + name string + parameter string + mode string + ) + if network == "" { + return "", "", "", fmt.Errorf("invalid network: cannot be empty") + } + arr := strings.Split(network, ":") + switch len(arr) { + case 1: + if ipaddr := net.ParseIP(arr[0]); ipaddr != nil { + parameter = arr[0] + } else { + name = arr[0] + } + case 2: + name = arr[0] + if name == "container" { + parameter = arr[1] + } else if ipaddr := net.ParseIP(arr[1]); ipaddr != nil { + parameter = arr[1] + } else { + mode = arr[1] + } + default: + name = arr[0] + parameter = arr[1] + mode = arr[2] + } + + return name, parameter, mode, nil +} + +// ValidateNetworks verifies network configurations of container. +func ValidateNetworks(nwConfig *types.NetworkingConfig) error { + if nwConfig == nil || len(nwConfig.EndpointsConfig) == 0 { + return nil + } + + // FIXME(ziren): this limitation may be removed in future + // Now not create more then one interface for container is not allowed + if len(nwConfig.EndpointsConfig) > 1 { + l := make([]string, 0, len(nwConfig.EndpointsConfig)) + for k := range nwConfig.EndpointsConfig { + l = append(l, k) + } + + return fmt.Errorf("Container cannot be connected to network endpoints: %s", strings.Join(l, ", ")) + } + + // len(nwConfig.EndpointsConfig) == 1 + for k, v := range nwConfig.EndpointsConfig { + if v == nil { + return fmt.Errorf("no EndpointSettings for %s", k) + } + if v.IPAMConfig != nil { + if v.IPAMConfig.IPV4Address != "" && net.ParseIP(v.IPAMConfig.IPV4Address).To4() == nil { + return fmt.Errorf("invalid IPv4 address: %s", v.IPAMConfig.IPV4Address) + } + // TODO: check IPv6Address + } + } + + return nil +} diff --git a/pkg/opts/networks_test.go b/pkg/opts/networks_test.go new file mode 100644 index 0000000000..dc0d6a9e30 --- /dev/null +++ b/pkg/opts/networks_test.go @@ -0,0 +1,136 @@ +package opts + +import ( + "fmt" + "reflect" + "testing" + + "github.com/alibaba/pouch/apis/types" + "github.com/stretchr/testify/assert" +) + +func TestParseNetworks(t *testing.T) { + type args struct { + networks []string + } + tests := []struct { + name string + args args + want *types.NetworkingConfig + want1 string + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1, err := ParseNetworks(tt.args.networks) + if (err != nil) != tt.wantErr { + t.Errorf("ParseNetworks() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseNetworks() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("ParseNetworks() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestVerifyNetworks(t *testing.T) { + type args struct { + nwConfig *types.NetworkingConfig + } + tests := []struct { + name string + args args + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ValidateNetworks(tt.args.nwConfig); (err != nil) != tt.wantErr { + t.Errorf("VerifyNetworks() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_parseNetwork(t *testing.T) { + type net struct { + name string + parameter string + mode string + } + type result struct { + network net + err error + } + type TestCases struct { + input string + expect result + } + + testCases := []TestCases{ + { + input: "", + expect: result{ + err: fmt.Errorf("invalid network: cannot be empty"), + network: net{name: "", parameter: "", mode: ""}, + }, + }, + { + input: "121.0.0.1", + expect: result{ + err: nil, + network: net{name: "", parameter: "121.0.0.1", mode: ""}, + }, + }, + { + input: "myHost", + expect: result{ + err: nil, + network: net{name: "myHost", parameter: "", mode: ""}, + }, + }, + { + input: "myHost:121.0.0.1", + expect: result{ + err: nil, + network: net{name: "myHost", parameter: "121.0.0.1", mode: ""}, + }, + }, + { + input: "container:9ca6ac", + expect: result{ + err: nil, + network: net{name: "container", parameter: "9ca6ac", mode: ""}, + }, + }, + { + input: "bridge:121.0.0.1:mode", + expect: result{ + err: nil, + network: net{name: "bridge", parameter: "121.0.0.1", mode: "mode"}, + }, + }, + { + input: "bridge:mode", + expect: result{ + err: nil, + network: net{name: "bridge", parameter: "", mode: "mode"}, + }, + }, + } + + for _, testCase := range testCases { + name, parameter, mode, error := parseNetwork(testCase.input) + assert.Equal(t, testCase.expect.err, error) + assert.Equal(t, testCase.expect.network.name, name) + assert.Equal(t, testCase.expect.network.parameter, parameter) + assert.Equal(t, testCase.expect.network.mode, mode) + } +} diff --git a/pkg/opts/oom_score.go b/pkg/opts/oom_score.go new file mode 100644 index 0000000000..bc60dc6910 --- /dev/null +++ b/pkg/opts/oom_score.go @@ -0,0 +1,12 @@ +package opts + +import "fmt" + +// ValidateOOMScore validates oom score +func ValidateOOMScore(score int64) error { + if score < -1000 || score > 1000 { + return fmt.Errorf("oom-score-adj should be in range [-1000, 1000]") + } + + return nil +} diff --git a/pkg/opts/portbindings.go b/pkg/opts/portbindings.go new file mode 100644 index 0000000000..44c083372b --- /dev/null +++ b/pkg/opts/portbindings.go @@ -0,0 +1,49 @@ +package opts + +import ( + "fmt" + + "github.com/alibaba/pouch/apis/types" + + "github.com/docker/go-connections/nat" +) + +// ParsePortBinding parse string list to PortMap +// FIXME(ziren): add examples +func ParsePortBinding(ports []string) (types.PortMap, error) { + // parse port binding + _, tmpPortBindings, err := nat.ParsePortSpecs(ports) + if err != nil { + return nil, err + } + + portBindings := make(types.PortMap) + for n, pbs := range tmpPortBindings { + portBindings[string(n)] = []types.PortBinding{} + for _, tmpPb := range pbs { + pb := types.PortBinding{HostIP: tmpPb.HostIP, HostPort: tmpPb.HostPort} + portBindings[string(n)] = append(portBindings[string(n)], pb) + } + } + + return portBindings, nil +} + +// ValidatePortBinding verify PortMap struct correctness. +func ValidatePortBinding(portBindings types.PortMap) error { + for port := range portBindings { + _, portStr := nat.SplitProtoPort(string(port)) + if _, err := nat.ParsePort(portStr); err != nil { + return fmt.Errorf("invalid port specification: %q, err: %v", portStr, err) + } + + for _, pb := range portBindings[port] { + _, err := nat.NewPort(nat.SplitProtoPort(pb.HostPort)) + if err != nil { + return fmt.Errorf("invalud port specification: %q, err: %v", pb.HostPort, err) + } + } + } + + return nil +} diff --git a/pkg/opts/portbindings_test.go b/pkg/opts/portbindings_test.go new file mode 100644 index 0000000000..2c0fa6f126 --- /dev/null +++ b/pkg/opts/portbindings_test.go @@ -0,0 +1,54 @@ +package opts + +import ( + "reflect" + "testing" + + "github.com/alibaba/pouch/apis/types" +) + +func TestParsePortBinding(t *testing.T) { + type args struct { + ports []string + } + tests := []struct { + name string + args args + want types.PortMap + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParsePortBinding(tt.args.ports) + if (err != nil) != tt.wantErr { + t.Errorf("ParsePortBinding() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParsePortBinding() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestVerifyPortBinding(t *testing.T) { + type args struct { + portBindings types.PortMap + } + tests := []struct { + name string + args args + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ValidatePortBinding(tt.args.portBindings); (err != nil) != tt.wantErr { + t.Errorf("VerifyPortBinding() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/opts/ports.go b/pkg/opts/ports.go new file mode 100644 index 0000000000..63639cf9af --- /dev/null +++ b/pkg/opts/ports.go @@ -0,0 +1,55 @@ +package opts + +import ( + "fmt" + "strconv" + "strings" + + "github.com/docker/go-connections/nat" +) + +// ParseExposedPorts parse ports. +// FIXME(ziren): add more explanation +func ParseExposedPorts(portList, expose []string) (map[string]interface{}, error) { + // translate ports + tmpPorts, _, err := nat.ParsePortSpecs(portList) + if err != nil { + return nil, err + } + + ports := map[string]interface{}{} + for n, p := range tmpPorts { + ports[string(n)] = p + } + for _, e := range expose { + if strings.Contains(e, ":") { + return nil, fmt.Errorf("invalid port format for --expose: %s", e) + } + + //support two formats for expose, original format /[] or /[] + proto, port := nat.SplitProtoPort(e) + //parse the start and end port and create a sequence of ports to expose + //if expose a port, the start and end port are the same + start, end, err := nat.ParsePortRange(port) + if err != nil { + return nil, fmt.Errorf("invalid range format for --expose: %s, error: %s", e, err) + } + for i := start; i <= end; i++ { + p, err := nat.NewPort(proto, strconv.FormatUint(i, 10)) + if err != nil { + return nil, err + } + if _, exists := ports[string(p)]; !exists { + ports[string(p)] = struct{}{} + } + } + } + + return ports, nil +} + +// ValidateExposedPorts verify the correction of exposed ports. +func ValidateExposedPorts(ports map[string]interface{}) error { + // TODO + return nil +} diff --git a/pkg/opts/ports_test.go b/pkg/opts/ports_test.go new file mode 100644 index 0000000000..ac55e27d28 --- /dev/null +++ b/pkg/opts/ports_test.go @@ -0,0 +1,33 @@ +package opts + +import ( + "reflect" + "testing" +) + +func TestParseExposedPorts(t *testing.T) { + type args struct { + portList []string + expose []string + } + tests := []struct { + name string + args args + want map[string]interface{} + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseExposedPorts(tt.args.portList, tt.args.expose) + if (err != nil) != tt.wantErr { + t.Errorf("ParseExposedPorts() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseExposedPorts() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/opts/restart_policy.go b/pkg/opts/restart_policy.go new file mode 100644 index 0000000000..5bce281e29 --- /dev/null +++ b/pkg/opts/restart_policy.go @@ -0,0 +1,61 @@ +package opts + +import ( + "fmt" + "strconv" + "strings" + + "github.com/alibaba/pouch/apis/types" +) + +// ParseRestartPolicy parses restart policy param of container. +func ParseRestartPolicy(restartPolicy string) (*types.RestartPolicy, error) { + policy := &types.RestartPolicy{} + + if restartPolicy == "" { + policy.Name = "no" + return policy, nil + } + + fields := strings.Split(restartPolicy, ":") + policy.Name = fields[0] + + switch policy.Name { + case "always", "unless-stopped", "no": + case "on-failure": + if len(fields) > 2 { + return nil, fmt.Errorf("invalid restart policy: %s", restartPolicy) + } + if len(fields) == 2 { + n, err := strconv.Atoi(fields[1]) + if err != nil { + return nil, fmt.Errorf("invalid restart policy: %v", err) + } + policy.MaximumRetryCount = int64(n) + } + default: + return nil, fmt.Errorf("invalid restart policy: %s", restartPolicy) + } + + return policy, nil +} + +// ValidateRestartPolicy verifies the correctness of restart policy of container. +func ValidateRestartPolicy(policy *types.RestartPolicy) error { + switch policy.Name { + case "always", "unless-stopped", "no": + if policy.MaximumRetryCount != 0 { + return fmt.Errorf("maximum retry count can not be used with restart policy: %s", policy.Name) + } + case "on-failure": + if policy.MaximumRetryCount < 0 { + return fmt.Errorf("maximum retry count can not be negative") + } + case "": + // ignore + default: + return fmt.Errorf("invalid restart policy '%s'", policy.Name) + } + + return nil +} diff --git a/pkg/opts/restart_policy_test.go b/pkg/opts/restart_policy_test.go new file mode 100644 index 0000000000..4644421181 --- /dev/null +++ b/pkg/opts/restart_policy_test.go @@ -0,0 +1,92 @@ +package opts + +import ( + "fmt" + "testing" + + "github.com/alibaba/pouch/apis/types" + "github.com/stretchr/testify/assert" +) + +func TestParseRestartPolicy(t *testing.T) { + type TestCase struct { + input string + expectedName string + expectedCount int64 + err error + } + + cases := []TestCase{ + { + input: "always", + expectedName: "always", + expectedCount: 0, + err: nil, + }, + { + input: "no", + expectedName: "no", + expectedCount: 0, + err: nil, + }, + { + input: "unless-stopped", + expectedName: "unless-stopped", + expectedCount: 0, + err: nil, + }, + { + input: "on-failure:1", + expectedName: "on-failure", + expectedCount: 1, + err: nil, + }, + { + input: "on-failure", + expectedName: "on-failure", + expectedCount: 0, + err: nil, + }, + { + input: "on-failure:1:2", + expectedName: "on-failure", + expectedCount: 0, + err: fmt.Errorf("invalid restart policy: %s", "on-failure:1:2"), + }, + } + + for _, cs := range cases { + policy, err := ParseRestartPolicy(cs.input) + assert.Equal(t, cs.err, err) + if err == nil { + assert.Equal(t, cs.expectedName, policy.Name) + assert.Equal(t, cs.expectedCount, policy.MaximumRetryCount) + } + } +} + +func TestValidateRestartPolicy(t *testing.T) { + type args struct { + policy *types.RestartPolicy + } + tests := []struct { + name string + args args + wantErr bool + }{ + // TODO: Add test cases. + {name: "test1", args: args{policy: &types.RestartPolicy{Name: "always", MaximumRetryCount: 2}}, wantErr: true}, + {name: "test2", args: args{policy: &types.RestartPolicy{Name: "unless-stopped", MaximumRetryCount: 2}}, wantErr: true}, + {name: "test3", args: args{policy: &types.RestartPolicy{Name: "no", MaximumRetryCount: 2}}, wantErr: true}, + {name: "test4", args: args{policy: &types.RestartPolicy{Name: "on-failure", MaximumRetryCount: -1}}, wantErr: true}, + {name: "test5", args: args{policy: &types.RestartPolicy{Name: "", MaximumRetryCount: 2}}, wantErr: false}, + {name: "test6", args: args{policy: &types.RestartPolicy{Name: "foo", MaximumRetryCount: 2}}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ValidateRestartPolicy(tt.args.policy); (err != nil) != tt.wantErr { + t.Errorf("VerifyRestartPolicy() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/opts/spec_annotation.go b/pkg/opts/spec_annotation.go new file mode 100644 index 0000000000..d856e8f1d8 --- /dev/null +++ b/pkg/opts/spec_annotation.go @@ -0,0 +1,29 @@ +package opts + +import ( + "fmt" + "strings" +) + +// ParseAnnotation parses runtime annotations format. +func ParseAnnotation(annotations []string) (map[string]string, error) { + specAnnotation := make(map[string]string) + + for _, annotation := range annotations { + splits := strings.Split(annotation, "=") + if len(splits) != 2 || splits[0] == "" || splits[1] == "" { + return nil, fmt.Errorf("invalid format for spec annotation: %s, correct format should be key=value, neither should be nil", annotation) + } + + specAnnotation[splits[0]] = splits[1] + } + + return specAnnotation, nil +} + +// ValidateAnnotation validate the correctness of spec annotation param of a container. +func ValidateAnnotation(annotations map[string]string) error { + // TODO + + return nil +} diff --git a/pkg/opts/spec_annotation_test.go b/pkg/opts/spec_annotation_test.go new file mode 100644 index 0000000000..e650145322 --- /dev/null +++ b/pkg/opts/spec_annotation_test.go @@ -0,0 +1,36 @@ +package opts + +import ( + "reflect" + "testing" +) + +func TestParseAnnotation(t *testing.T) { + type args struct { + annotations []string + } + tests := []struct { + name string + args args + want map[string]string + wantErr bool + }{ + // TODO: Add test cases. + {name: "test1", args: args{annotations: []string{""}}, want: nil, wantErr: true}, + {name: "test2", args: args{annotations: []string{"=foo"}}, want: nil, wantErr: true}, + {name: "test3", args: args{annotations: []string{"key="}}, want: nil, wantErr: true}, + {name: "test4", args: args{annotations: []string{"key=foo=bar"}}, want: nil, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseAnnotation(tt.args.annotations) + if (err != nil) != tt.wantErr { + t.Errorf("ParseAnnotation() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseAnnotation() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/opts/sysctl.go b/pkg/opts/sysctl.go new file mode 100644 index 0000000000..8fc0dfdb85 --- /dev/null +++ b/pkg/opts/sysctl.go @@ -0,0 +1,28 @@ +package opts + +import ( + "fmt" + "strings" +) + +// ParseSysctls parses the sysctl params of container +func ParseSysctls(sysctls []string) (map[string]string, error) { + results := make(map[string]string) + for _, sysctl := range sysctls { + fields, err := parseSysctl(sysctl) + if err != nil { + return nil, err + } + k, v := fields[0], fields[1] + results[k] = v + } + return results, nil +} + +func parseSysctl(sysctl string) ([]string, error) { + fields := strings.SplitN(sysctl, "=", 2) + if len(fields) != 2 { + return nil, fmt.Errorf("invalid sysctl %s: sysctl must be in format of key=value", sysctl) + } + return fields, nil +} diff --git a/pkg/opts/sysctl_test.go b/pkg/opts/sysctl_test.go new file mode 100644 index 0000000000..64b5bf654f --- /dev/null +++ b/pkg/opts/sysctl_test.go @@ -0,0 +1,42 @@ +package opts + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseSysctls(t *testing.T) { + type result struct { + sysctls map[string]string + err error + } + type TestCases struct { + input []string + expect result + } + + testCases := []TestCases{ + { + input: []string{"a=b"}, + expect: result{ + sysctls: map[string]string{"a": "b"}, + err: nil, + }, + }, + { + input: []string{"ab"}, + expect: result{ + sysctls: nil, + err: fmt.Errorf("invalid sysctl %s: sysctl must be in format of key=value", "ab"), + }, + }, + } + + for _, testCase := range testCases { + sysctl, err := ParseSysctls(testCase.input) + assert.Equal(t, testCase.expect.sysctls, sysctl) + assert.Equal(t, testCase.expect.err, err) + } +} diff --git a/pkg/runconfig/hostconfig_test.go b/pkg/runconfig/hostconfig_test.go deleted file mode 100644 index 8e56854176..0000000000 --- a/pkg/runconfig/hostconfig_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package runconfig - -import ( - "fmt" - "testing" - - "github.com/alibaba/pouch/apis/types" - - "github.com/stretchr/testify/assert" -) - -type tDeviceModeCase struct { - input string - expected bool -} - -type tParseDeviceCase struct { - input string - expected *types.DeviceMapping - err error -} - -func TestParseDevice(t *testing.T) { - for _, tc := range []tParseDeviceCase{ - { - input: "/dev/zero:/dev/zero:rwm", - expected: &types.DeviceMapping{ - PathOnHost: "/dev/zero", - PathInContainer: "/dev/zero", - CgroupPermissions: "rwm", - }, - err: nil, - }, { - input: "/dev/zero:rwm", - expected: &types.DeviceMapping{ - PathOnHost: "/dev/zero", - PathInContainer: "rwm", - CgroupPermissions: "rwm", - }, - err: nil, - }, { - input: "/dev/zero", - expected: &types.DeviceMapping{ - PathOnHost: "/dev/zero", - PathInContainer: "/dev/zero", - CgroupPermissions: "rwm", - }, - err: nil, - }, { - input: "/dev/zero:/dev/testzero:rwm", - expected: &types.DeviceMapping{ - PathOnHost: "/dev/zero", - PathInContainer: "/dev/testzero", - CgroupPermissions: "rwm", - }, - err: nil, - }, { - input: "/dev/zero:/dev/testzero:rwm:tooLong", - expected: nil, - err: fmt.Errorf("invalid device specification: /dev/zero:/dev/testzero:rwm:tooLong"), - }, - } { - output, err := ParseDevice(tc.input) - assert.Equal(t, tc.err, err, tc.input) - assert.Equal(t, tc.expected, output, tc.input) - } -} - -func TestValidDeviceMode(t *testing.T) { - for _, modeCase := range []tDeviceModeCase{ - { - input: "rwm", - expected: true, - }, { - input: "r", - expected: true, - }, { - input: "rw", - expected: true, - }, { - input: "rr", - expected: false, - }, { - input: "rxm", - expected: false, - }, - } { - isValid := ValidDeviceMode(modeCase.input) - assert.Equal(t, modeCase.expected, isValid, modeCase.input) - } -}