diff --git a/cmd/kpod/create.go b/cmd/kpod/create.go index f3c3a855..7db5fdbd 100644 --- a/cmd/kpod/create.go +++ b/cmd/kpod/create.go @@ -3,39 +3,39 @@ package main import ( "fmt" + "strings" + spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" "github.com/urfave/cli" pb "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime" - "strings" - ) type createResourceConfig struct { - blkioWeight int64 // blkio-weight + blkioWeight int64 // blkio-weight blkioDevice []string // blkio-weight - cpuShares int64 // cpu-shares - cpuCount int64 // cpu-count - cpuPeriod int64 // cpu-period + cpuShares int64 // cpu-shares + cpuCount int64 // cpu-count + cpuPeriod int64 // cpu-period cpusetCpus string cpusetNames string cpuFile string - cpuMems string // cpuset-mems - cpuQuota int64 // cpu-quota - cpuRtPeriod int64 // cpu-rt-period - cpuRtRuntime int64 // cpu-rt-runtime - cpus int64 // cpus + cpuMems string // cpuset-mems + cpuQuota int64 // cpu-quota + cpuRtPeriod int64 // cpu-rt-period + cpuRtRuntime int64 // cpu-rt-runtime + cpus int64 // cpus deviceReadBps []string // device-read-bps deviceReadIops []string // device-read-iops deviceWriteBps []string // device-write-bps deviceWriteIops []string // device-write-iops - memory string //memory - memoryReservation string // memory-reservation - memorySwap string //memory-swap - memorySwapiness string // memory-swappiness - kernelMemory string // kernel-memory - oomScoreAdj string //oom-score-adj - pidsLimit string // pids-limit + memory string //memory + memoryReservation string // memory-reservation + memorySwap string //memory-swap + memorySwapiness string // memory-swappiness + kernelMemory string // kernel-memory + oomScoreAdj string //oom-score-adj + pidsLimit string // pids-limit shmSize string ulimit []string //ulimit } @@ -45,58 +45,58 @@ type createConfig struct { args []string capAdd []string // cap-add capDrop []string // cap-drop - cgroupParent string // cgroup-parent + cgroupParent string // cgroup-parent command []string - detach bool // detach + detach bool // detach devices []*pb.Device // device - dnsOpt []string //dns-opt - dnsSearch []string //dns-search - dnsServers []string //dns - entrypoint string //entrypoint - env []string //env - expose []string //expose - groupAdd []string // group-add - hostname string //hostname + dnsOpt []string //dns-opt + dnsSearch []string //dns-search + dnsServers []string //dns + entrypoint string //entrypoint + env []string //env + expose []string //expose + groupAdd []string // group-add + hostname string //hostname image string - interactive bool //interactive - ip6Address string //ipv6 - ipAddress string //ip + interactive bool //interactive + ip6Address string //ipv6 + ipAddress string //ip labels map[string]string //label - linkLocalIP []string // link-local-ip - logDriver string // log-driver - logDriverOpt []string // log-opt - macAddress string //mac-address + linkLocalIP []string // link-local-ip + logDriver string // log-driver + logDriverOpt []string // log-opt + macAddress string //mac-address mounts []*pb.Mount - name string //name - network string //network + name string //name + network string //network networkAlias []string //network-alias - nsIPC string // ipc - nsNet string //net - nsPID string //pid + nsIPC string // ipc + nsNet string //net + nsPID string //pid nsUser string pod string //pod ports []*pb.PortMapping - privileged bool //privileged + privileged bool //privileged publish []string //publish - publishAll bool //publish-all - readOnlyRootfs bool //read-only + publishAll bool //publish-all + readOnlyRootfs bool //read-only resources createResourceConfig - rm bool //rm + rm bool //rm securityOpts []string //security-opt - shmSize string //shm-size - sigProxy bool //sig-proxy + shmSize string //shm-size + sigProxy bool //sig-proxy stdin bool - stopSignal string // stop-signal - stopTimeout int64 // stop-timeout - storageOpts []string //storage-opt + stopSignal string // stop-signal + stopTimeout int64 // stop-timeout + storageOpts []string //storage-opt sysctl map[string]string //sysctl - tmpfs []string // tmpfs - tty bool //tty - user int64 //user - userns string //userns - volumes []string //volume - volumesFrom []string //volumes-from - workDir string //workdir + tmpfs []string // tmpfs + tty bool //tty + user int64 //user + userns string //userns + volumes []string //volume + volumesFrom []string //volumes-from + workDir string //workdir } var createDescription = "Creates a new container from the given image or" + @@ -114,7 +114,7 @@ var createCommand = cli.Command{ ArgsUsage: "IMAGE [COMMAND [ARG...]]", } -func verifyImage(image string) bool{ +func verifyImage(image string) bool { return false } @@ -174,15 +174,15 @@ func parseCreateOpts(c *cli.Context) (*createConfig, error) { } if len(c.StringSlice("env")) > 0 { - for _, inputEnv := range(c.StringSlice("env")) { + for _, inputEnv := range c.StringSlice("env") { env = append(env, inputEnv) } - } else{ + } else { env = append(env, "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "TERM=xterm") } if len(c.StringSlice("sysctl")) > 0 { - for _, inputSysctl := range(c.StringSlice("sysctl")) { + for _, inputSysctl := range c.StringSlice("sysctl") { values := strings.Split(inputSysctl, "=") sysctl[values[0]] = values[1] } @@ -194,79 +194,79 @@ func parseCreateOpts(c *cli.Context) (*createConfig, error) { image := c.Args()[0] config := &createConfig{ - capAdd: c.StringSlice("cap-add"), - capDrop: c.StringSlice("cap-drop"), - cgroupParent: c.String("cgroup-parent"), - command: command, - detach: c.Bool("detach"), - dnsOpt: c.StringSlice("dns-opt"), - dnsSearch: c.StringSlice("dns-search"), - dnsServers: c.StringSlice("dns"), - entrypoint: c.String("entrypoint"), - env: env, - expose: c.StringSlice("env"), - groupAdd:c.StringSlice("group-add"), - hostname: c.String("hostname"), - image: image, - interactive: c.Bool("interactive"), - ip6Address: c.String("ipv6"), - ipAddress: c.String("ip"), - labels: labels, - linkLocalIP: c.StringSlice("link-local-ip"), - logDriver: c.String("log-driver"), - logDriverOpt: c.StringSlice("log-opt"), - macAddress:c.String("mac-address"), - name: c.String("name"), - network: c.String("network"), - networkAlias: c.StringSlice("network-alias"), - nsIPC: c.String("ipc"), - nsNet: c.String("net"), - nsPID: c.String("pid"), - pod: c.String("pod"), - privileged: c.Bool("privileged"), - publish: c.StringSlice("publish"), - publishAll: c.Bool("publish-all"), + capAdd: c.StringSlice("cap-add"), + capDrop: c.StringSlice("cap-drop"), + cgroupParent: c.String("cgroup-parent"), + command: command, + detach: c.Bool("detach"), + dnsOpt: c.StringSlice("dns-opt"), + dnsSearch: c.StringSlice("dns-search"), + dnsServers: c.StringSlice("dns"), + entrypoint: c.String("entrypoint"), + env: env, + expose: c.StringSlice("env"), + groupAdd: c.StringSlice("group-add"), + hostname: c.String("hostname"), + image: image, + interactive: c.Bool("interactive"), + ip6Address: c.String("ipv6"), + ipAddress: c.String("ip"), + labels: labels, + linkLocalIP: c.StringSlice("link-local-ip"), + logDriver: c.String("log-driver"), + logDriverOpt: c.StringSlice("log-opt"), + macAddress: c.String("mac-address"), + name: c.String("name"), + network: c.String("network"), + networkAlias: c.StringSlice("network-alias"), + nsIPC: c.String("ipc"), + nsNet: c.String("net"), + nsPID: c.String("pid"), + pod: c.String("pod"), + privileged: c.Bool("privileged"), + publish: c.StringSlice("publish"), + publishAll: c.Bool("publish-all"), readOnlyRootfs: c.Bool("read-only"), - resources:createResourceConfig{ - blkioWeight:c.Int64("blkio-weight"), - blkioDevice: c.StringSlice("blkio-device"), - cpuShares:c.Int64("cpu-shares"), - cpuCount: c.Int64("cpu-count"), - cpuPeriod:c.Int64("cpu-period"), - cpusetCpus:c.String("cpu-period"), - cpuMems:c.String("cpuset-mems"), - cpuQuota: c.Int64("cpu-quota"), - cpuRtPeriod: c.Int64("cpu-rt-period"), - cpuRtRuntime: c.Int64("cpu-rt-runtime"), - cpus: c.Int64("cpus"), - deviceReadBps: c.StringSlice("device-read-bps"), - deviceReadIops:c.StringSlice("device-read-iops"), - deviceWriteBps:c.StringSlice("device-write-bps"), - deviceWriteIops:c.StringSlice("device-write-iops"), - memory: c.String("memory"), + resources: createResourceConfig{ + blkioWeight: c.Int64("blkio-weight"), + blkioDevice: c.StringSlice("blkio-device"), + cpuShares: c.Int64("cpu-shares"), + cpuCount: c.Int64("cpu-count"), + cpuPeriod: c.Int64("cpu-period"), + cpusetCpus: c.String("cpu-period"), + cpuMems: c.String("cpuset-mems"), + cpuQuota: c.Int64("cpu-quota"), + cpuRtPeriod: c.Int64("cpu-rt-period"), + cpuRtRuntime: c.Int64("cpu-rt-runtime"), + cpus: c.Int64("cpus"), + deviceReadBps: c.StringSlice("device-read-bps"), + deviceReadIops: c.StringSlice("device-read-iops"), + deviceWriteBps: c.StringSlice("device-write-bps"), + deviceWriteIops: c.StringSlice("device-write-iops"), + memory: c.String("memory"), memoryReservation: c.String("memory-reservation"), - memorySwap: c.String("memory-swap"), - memorySwapiness:c.String("memory-swapiness"), - kernelMemory: c.String("kernel-memory"), - oomScoreAdj:c.String("oom-score-adj"), - pidsLimit: c.String("pids-limit"), - ulimit:c.StringSlice("ulimit"), + memorySwap: c.String("memory-swap"), + memorySwapiness: c.String("memory-swapiness"), + kernelMemory: c.String("kernel-memory"), + oomScoreAdj: c.String("oom-score-adj"), + pidsLimit: c.String("pids-limit"), + ulimit: c.StringSlice("ulimit"), }, - rm: c.Bool("rm"), - securityOpts:c.StringSlice("security-opt"), - shmSize: c.String("shm-size"), - sigProxy:c.Bool("sig-proxy"), - stopSignal:c.String("stop-signal"), - stopTimeout:c.Int64("stop-timeout"), - storageOpts:c.StringSlice("storage-opt"), - sysctl: sysctl, - tmpfs: c.StringSlice("tmpfs"), - tty: c.Bool("tty"), // - user: c.Int64("user"), - userns: c.String("userns"), - volumes:c.StringSlice("volume"), - volumesFrom:c.StringSlice("volumes-from"), - workDir:c.String("workdir"), + rm: c.Bool("rm"), + securityOpts: c.StringSlice("security-opt"), + shmSize: c.String("shm-size"), + sigProxy: c.Bool("sig-proxy"), + stopSignal: c.String("stop-signal"), + stopTimeout: c.Int64("stop-timeout"), + storageOpts: c.StringSlice("storage-opt"), + sysctl: sysctl, + tmpfs: c.StringSlice("tmpfs"), + tty: c.Bool("tty"), // + user: c.Int64("user"), + userns: c.String("userns"), + volumes: c.StringSlice("volume"), + volumesFrom: c.StringSlice("volumes-from"), + workDir: c.String("workdir"), } return config, nil @@ -277,14 +277,11 @@ func createConfigToOCISpec(config *createConfig) (*spec.Spec, error) { spec := &spec.Spec{ Version: "1.0.0.0", // where do I get this? Process: &spec.Process{ - Terminal: config.tty, - User: spec.User{ - }, - Args: config.command, - Env: config.env, - Capabilities: &spec.LinuxCapabilities{ - - }, + Terminal: config.tty, + User: spec.User{}, + Args: config.command, + Env: config.env, + Capabilities: &spec.LinuxCapabilities{}, }, Root: &spec.Root{ Readonly: config.readOnlyRootfs, @@ -296,15 +293,13 @@ func createConfigToOCISpec(config *createConfig) (*spec.Spec, error) { Linux: &spec.Linux{ // UIDMappings // GIDMappings - Sysctl: config.sysctl, + Sysctl: config.sysctl, Resources: &spec.LinuxResources{ - Devices: spec.LinuxDeviceCgroup, + // Devices: spec.LinuxDeviceCgroup, }, }, - } - return spec, errors.Errorf("NOT IMPLEMENTED") } diff --git a/cmd/kpod/parse.go b/cmd/kpod/parse.go new file mode 100644 index 00000000..4815e1be --- /dev/null +++ b/cmd/kpod/parse.go @@ -0,0 +1,661 @@ +package main + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net" + "os" + "path" + "regexp" + "strconv" + "strings" + + "github.com/pkg/errors" + pb "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime" +) + +var ( + whiteSpaces = " \t" + alphaRegexp = regexp.MustCompile(`[a-zA-Z]`) + domainRegexp = regexp.MustCompile(`^(:?(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]))(:?\.(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])))*)\.?\s*$`) +) + +// validateMACAddress validates a MAC address. +func validateMACAddress(val string) (string, error) { + _, err := net.ParseMAC(strings.TrimSpace(val)) + if err != nil { + return "", err + } + return val, nil +} + +// validateLink validates that the specified string has a valid link format (containerName:alias). +func validateLink(val string) (string, error) { + if _, _, err := parseLink(val); err != nil { + return val, err + } + return val, nil +} + +// validDeviceMode 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 { + var legalDeviceMode = map[rune]bool{ + 'r': true, + 'w': true, + 'm': true, + } + if mode == "" { + return false + } + for _, c := range mode { + if !legalDeviceMode[c] { + return false + } + legalDeviceMode[c] = false + } + return true +} + +// validateDevice validates a path for devices +// It will make sure 'val' is in the form: +// [host-dir:]container-path[:mode] +// It also validates the device mode. +func validateDevice(val string) (string, error) { + return validatePath(val, validDeviceMode) +} + +func validatePath(val string, validator func(string) bool) (string, error) { + var containerPath string + var mode string + + if strings.Count(val, ":") > 2 { + return val, fmt.Errorf("bad format for path: %s", val) + } + + split := strings.SplitN(val, ":", 3) + if split[0] == "" { + return val, fmt.Errorf("bad format for path: %s", val) + } + switch len(split) { + case 1: + containerPath = split[0] + val = path.Clean(containerPath) + case 2: + if isValid := validator(split[1]); isValid { + containerPath = split[0] + mode = split[1] + val = fmt.Sprintf("%s:%s", path.Clean(containerPath), mode) + } else { + containerPath = split[1] + val = fmt.Sprintf("%s:%s", split[0], path.Clean(containerPath)) + } + case 3: + containerPath = split[1] + mode = split[2] + if isValid := validator(split[2]); !isValid { + return val, fmt.Errorf("bad mode specified: %s", mode) + } + val = fmt.Sprintf("%s:%s:%s", split[0], containerPath, mode) + } + + if !path.IsAbs(containerPath) { + return val, fmt.Errorf("%s is not an absolute path", containerPath) + } + return val, nil +} + +func validateProto(proto string) bool { + for _, availableProto := range []string{"tcp", "udp"} { + if availableProto == proto { + return true + } + } + return false +} + +// validateAttach validates that the specified string is a valid attach option. +func validateAttach(val string) (string, error) { + s := strings.ToLower(val) + for _, str := range []string{"stdin", "stdout", "stderr"} { + if s == str { + return s, nil + } + } + return val, fmt.Errorf("valid streams are STDIN, STDOUT and STDERR") +} + +// validateEnv validates an environment variable and returns it. +// If no value is specified, it returns the current value using os.Getenv. +// +// As on ParseEnvFile and related to #16585, environment variable names +// are not validate what so ever, it's up to application inside docker +// to validate them or not. +func validateEnv(val string) (string, error) { + arr := strings.Split(val, "=") + if len(arr) > 1 { + return val, nil + } + if !doesEnvExist(val) { + return val, nil + } + return fmt.Sprintf("%s=%s", val, os.Getenv(val)), nil +} + +func doesEnvExist(name string) bool { + for _, entry := range os.Environ() { + parts := strings.SplitN(entry, "=", 2) + if parts[0] == name { + return true + } + } + return false +} + +// validateExtraHost validates that the specified string is a valid extrahost and returns it. +// ExtraHost is in the form of name:ip where the ip has to be a valid ip (ipv4 or ipv6). +func validateExtraHost(val string) (string, error) { + // allow for IPv6 addresses in extra hosts by only splitting on first ":" + arr := strings.SplitN(val, ":", 2) + if len(arr) != 2 || len(arr[0]) == 0 { + return "", fmt.Errorf("bad format for add-host: %q", val) + } + if _, err := validateIPAddress(arr[1]); err != nil { + return "", fmt.Errorf("invalid IP address in add-host: %q", arr[1]) + } + return val, nil +} + +// validateIPAddress validates an Ip address. +func validateIPAddress(val string) (string, error) { + var ip = net.ParseIP(strings.TrimSpace(val)) + if ip != nil { + return ip.String(), nil + } + return "", fmt.Errorf("%s is not an ip address", val) +} + +// validateDNSSearch validates domain for resolvconf search configuration. +// A zero length domain is represented by a dot (.). +func validateDNSSearch(val string) (string, error) { + if val = strings.Trim(val, " "); val == "." { + return val, nil + } + return validateDomain(val) +} + +func validateDomain(val string) (string, error) { + if alphaRegexp.FindString(val) == "" { + return "", fmt.Errorf("%s is not a valid domain", val) + } + ns := domainRegexp.FindSubmatch([]byte(val)) + if len(ns) > 0 && len(ns[1]) < 255 { + return string(ns[1]), nil + } + return "", fmt.Errorf("%s is not a valid domain", val) +} + +// validateLabel validates that the specified string is a valid label, and returns it. +// Labels are in the form on key=value. +func validateLabel(val string) (string, error) { + if strings.Count(val, "=") < 1 { + return "", fmt.Errorf("bad attribute format: %s", val) + } + return val, nil +} + +// parseDevice parses a device mapping string to a container.DeviceMapping struct +func parseDevice(device string) (*pb.Device, error) { + src := "" + dst := "" + permissions := "rwm" + arr := strings.Split(device, ":") + switch len(arr) { + case 3: + permissions = arr[2] + fallthrough + case 2: + if validDeviceMode(arr[1]) { + permissions = arr[1] + } else { + dst = arr[1] + } + fallthrough + case 1: + src = arr[0] + default: + return nil, fmt.Errorf("invalid device specification: %s", device) + } + + if dst == "" { + dst = src + } + + deviceMapping := &pb.Device{ + ContainerPath: dst, + HostPath: src, + Permissions: permissions, + } + return deviceMapping, nil +} + +// parseEnvFile reads a file with environment variables enumerated by lines +// +// ``Environment variable names used by the utilities in the Shell and +// Utilities volume of IEEE Std 1003.1-2001 consist solely of uppercase +// letters, digits, and the '_' (underscore) from the characters defined in +// Portable Character Set and do not begin with a digit. *But*, other +// characters may be permitted by an implementation; applications shall +// tolerate the presence of such names.'' +// -- http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html +// +// As of #16585, it's up to application inside docker to validate or not +// environment variables, that's why we just strip leading whitespace and +// nothing more. +func parseEnvFile(filename string) ([]string, error) { + fh, err := os.Open(filename) + if err != nil { + return []string{}, err + } + defer fh.Close() + + lines := []string{} + scanner := bufio.NewScanner(fh) + for scanner.Scan() { + // trim the line from all leading whitespace first + line := strings.TrimLeft(scanner.Text(), whiteSpaces) + // line is not empty, and not starting with '#' + if len(line) > 0 && !strings.HasPrefix(line, "#") { + data := strings.SplitN(line, "=", 2) + + // trim the front of a variable, but nothing else + variable := strings.TrimLeft(data[0], whiteSpaces) + if strings.ContainsAny(variable, whiteSpaces) { + return []string{}, errors.Errorf("variable %q has white spaces, poorly formatted environment", variable) + } + + if len(data) > 1 { + + // pass the value through, no trimming + lines = append(lines, fmt.Sprintf("%s=%s", variable, data[1])) + } else { + // if only a pass-through variable is given, clean it up. + lines = append(lines, fmt.Sprintf("%s=%s", strings.TrimSpace(line), os.Getenv(line))) + } + } + } + return lines, scanner.Err() +} + +// parseLink parses and validates the specified string as a link format (name:alias) +func parseLink(val string) (string, string, error) { + if val == "" { + return "", "", fmt.Errorf("empty string specified for links") + } + arr := strings.Split(val, ":") + if len(arr) > 2 { + return "", "", fmt.Errorf("bad format for links: %s", val) + } + if len(arr) == 1 { + return val, val, nil + } + // This is kept because we can actually get a HostConfig with links + // from an already created container and the format is not `foo:bar` + // but `/foo:/c1/bar` + if strings.HasPrefix(arr[0], "/") { + _, alias := path.Split(arr[1]) + return arr[0][1:], alias, nil + } + return arr[0], arr[1], nil +} + +func parseLoggingOpts(logDriver string, logDriverOpt []string) (map[string]string, error) { + logOptsMap := convertKVStringsToMap(logDriverOpt) + if logDriver == "none" && len(logDriverOpt) > 0 { + return map[string]string{}, errors.Errorf("invalid logging opts for driver %s", logDriver) + } + return logOptsMap, nil +} + +// takes a local seccomp daemon, reads the file contents for sending to the daemon +func parseSecurityOpts(securityOpts []string) ([]string, error) { + for key, opt := range securityOpts { + con := strings.SplitN(opt, "=", 2) + if len(con) == 1 && con[0] != "no-new-privileges" { + if strings.Index(opt, ":") != -1 { + con = strings.SplitN(opt, ":", 2) + } else { + return securityOpts, fmt.Errorf("Invalid --security-opt: %q", opt) + } + } + if con[0] == "seccomp" && con[1] != "unconfined" { + f, err := ioutil.ReadFile(con[1]) + if err != nil { + return securityOpts, fmt.Errorf("opening seccomp profile (%s) failed: %v", con[1], err) + } + b := bytes.NewBuffer(nil) + if err := json.Compact(b, f); err != nil { + return securityOpts, fmt.Errorf("compacting json for seccomp profile (%s) failed: %v", con[1], err) + } + securityOpts[key] = fmt.Sprintf("seccomp=%s", b.Bytes()) + } + } + + return securityOpts, nil +} + +// parses storage options per container into a map +func parseStorageOpts(storageOpts []string) (map[string]string, error) { + m := make(map[string]string) + for _, option := range storageOpts { + if strings.Contains(option, "=") { + opt := strings.SplitN(option, "=", 2) + m[opt[0]] = opt[1] + } else { + return nil, errors.Errorf("invalid storage option %q", option) + } + } + return m, nil +} + +// parsePortSpecs receives port specs in the format of ip:public:private/proto and parses +// these in to the internal types +func parsePortSpecs(ports []string) ([]*pb.PortMapping, error) { + var portMappings []*pb.PortMapping + for _, rawPort := range ports { + portMapping, err := parsePortSpec(rawPort) + if err != nil { + return nil, err + } + + portMappings = append(portMappings, portMapping...) + } + return portMappings, nil +} + +// parsePortSpec parses a port specification string into a slice of PortMappings +func parsePortSpec(rawPort string) ([]*pb.PortMapping, error) { + var proto string + rawIP, hostPort, containerPort := splitParts(rawPort) + proto, containerPort = splitProtoPort(containerPort) + + // Strip [] from IPV6 addresses + ip, _, err := net.SplitHostPort(rawIP + ":") + if err != nil { + return nil, fmt.Errorf("Invalid ip address %v: %s", rawIP, err) + } + if ip != "" && net.ParseIP(ip) == nil { + return nil, fmt.Errorf("Invalid ip address: %s", ip) + } + if containerPort == "" { + return nil, fmt.Errorf("No port specified: %s", rawPort) + } + + startPort, endPort, err := parsePortRange(containerPort) + if err != nil { + return nil, fmt.Errorf("Invalid containerPort: %s", containerPort) + } + + var startHostPort, endHostPort uint64 = 0, 0 + if len(hostPort) > 0 { + startHostPort, endHostPort, err = parsePortRange(hostPort) + if err != nil { + return nil, fmt.Errorf("Invalid hostPort: %s", hostPort) + } + } + + if hostPort != "" && (endPort-startPort) != (endHostPort-startHostPort) { + // Allow host port range iff containerPort is not a range. + // In this case, use the host port range as the dynamic + // host port range to allocate into. + if endPort != startPort { + return nil, fmt.Errorf("Invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort) + } + } + + if !validateProto(strings.ToLower(proto)) { + return nil, fmt.Errorf("invalid proto: %s", proto) + } + + protocol := pb.Protocol_TCP + if strings.ToLower(proto) == "udp" { + protocol = pb.Protocol_UDP + } + + var ports []*pb.PortMapping + for i := uint64(0); i <= (endPort - startPort); i++ { + containerPort = strconv.FormatUint(startPort+i, 10) + if len(hostPort) > 0 { + hostPort = strconv.FormatUint(startHostPort+i, 10) + } + // Set hostPort to a range only if there is a single container port + // and a dynamic host port. + if startPort == endPort && startHostPort != endHostPort { + hostPort = fmt.Sprintf("%s-%s", hostPort, strconv.FormatUint(endHostPort, 10)) + } + + ctrPort, err := strconv.ParseInt(containerPort, 10, 32) + if err != nil { + return nil, err + } + hPort, err := strconv.ParseInt(hostPort, 10, 32) + if err != nil { + return nil, err + } + + port := &pb.PortMapping{ + Protocol: protocol, + ContainerPort: int32(ctrPort), + HostPort: int32(hPort), + HostIp: ip, + } + + ports = append(ports, port) + } + return ports, nil +} + +// parsePortRange parses and validates the specified string as a port-range (8000-9000) +func parsePortRange(ports string) (uint64, uint64, error) { + if ports == "" { + return 0, 0, fmt.Errorf("empty string specified for ports") + } + if !strings.Contains(ports, "-") { + start, err := strconv.ParseUint(ports, 10, 16) + end := start + return start, end, err + } + + parts := strings.Split(ports, "-") + start, err := strconv.ParseUint(parts[0], 10, 16) + if err != nil { + return 0, 0, err + } + end, err := strconv.ParseUint(parts[1], 10, 16) + if err != nil { + return 0, 0, err + } + if end < start { + return 0, 0, fmt.Errorf("Invalid range specified for the Port: %s", ports) + } + return start, end, nil +} + +// splitParts separates the different parts of rawPort +func splitParts(rawport string) (string, string, string) { + parts := strings.Split(rawport, ":") + n := len(parts) + containerport := parts[n-1] + + switch n { + case 1: + return "", "", containerport + case 2: + return "", parts[0], containerport + case 3: + return parts[0], parts[1], containerport + default: + return strings.Join(parts[:n-2], ":"), parts[n-2], containerport + } +} + +// splitProtoPort splits a port in the format of port/proto +func splitProtoPort(rawPort string) (string, string) { + parts := strings.Split(rawPort, "/") + l := len(parts) + if len(rawPort) == 0 || l == 0 || len(parts[0]) == 0 { + return "", "" + } + if l == 1 { + return "tcp", rawPort + } + if len(parts[1]) == 0 { + return "tcp", parts[0] + } + return parts[1], parts[0] +} + +// reads a file of line terminated key=value pairs, and overrides any keys +// present in the file with additional pairs specified in the override parameter +func readKVStrings(files []string, override []string) ([]string, error) { + envVariables := []string{} + for _, ef := range files { + parsedVars, err := parseEnvFile(ef) + if err != nil { + return nil, err + } + envVariables = append(envVariables, parsedVars...) + } + // parse the '-e' and '--env' after, to allow override + envVariables = append(envVariables, override...) + + return envVariables, nil +} + +// convertKVStringsToMap converts ["key=value"] to {"key":"value"} +func convertKVStringsToMap(values []string) map[string]string { + result := make(map[string]string, len(values)) + for _, value := range values { + kv := strings.SplitN(value, "=", 2) + if len(kv) == 1 { + result[kv[0]] = "" + } else { + result[kv[0]] = kv[1] + } + } + + return result +} + +// NsIpc represents the container ipc stack. +type NsIpc string + +// IsPrivate indicates whether the container uses its private ipc stack. +func (n NsIpc) IsPrivate() bool { + return !(n.IsHost() || n.IsContainer()) +} + +// IsHost indicates whether the container uses the host's ipc stack. +func (n NsIpc) IsHost() bool { + return n == "host" +} + +// IsContainer indicates whether the container uses a container's ipc stack. +func (n NsIpc) IsContainer() bool { + parts := strings.SplitN(string(n), ":", 2) + return len(parts) > 1 && parts[0] == "container" +} + +// Valid indicates whether the ipc stack is valid. +func (n NsIpc) Valid() bool { + parts := strings.Split(string(n), ":") + switch mode := parts[0]; mode { + case "", "host": + case "container": + if len(parts) != 2 || parts[1] == "" { + return false + } + default: + return false + } + return true +} + +// Container returns the name of the container ipc stack is going to be used. +func (n NsIpc) Container() string { + parts := strings.SplitN(string(n), ":", 2) + if len(parts) > 1 { + return parts[1] + } + return "" +} + +// NsUser represents userns mode in the container. +type NsUser string + +// IsHost indicates whether the container uses the host's userns. +func (n NsUser) IsHost() bool { + return n == "host" +} + +// IsPrivate indicates whether the container uses the a private userns. +func (n NsUser) IsPrivate() bool { + return !(n.IsHost()) +} + +// Valid indicates whether the userns is valid. +func (n NsUser) Valid() bool { + parts := strings.Split(string(n), ":") + switch mode := parts[0]; mode { + case "", "host": + default: + return false + } + return true +} + +// NsPid represents the pid namespace of the container. +type NsPid string + +// IsPrivate indicates whether the container uses its own new pid namespace. +func (n NsPid) IsPrivate() bool { + return !(n.IsHost() || n.IsContainer()) +} + +// IsHost indicates whether the container uses the host's pid namespace. +func (n NsPid) IsHost() bool { + return n == "host" +} + +// IsContainer indicates whether the container uses a container's pid namespace. +func (n NsPid) IsContainer() bool { + parts := strings.SplitN(string(n), ":", 2) + return len(parts) > 1 && parts[0] == "container" +} + +// Valid indicates whether the pid namespace is valid. +func (n NsPid) Valid() bool { + parts := strings.Split(string(n), ":") + switch mode := parts[0]; mode { + case "", "host": + case "container": + if len(parts) != 2 || parts[1] == "" { + return false + } + default: + return false + } + return true +} + +// Container returns the name of the container whose pid namespace is going to be used. +func (n NsPid) Container() string { + parts := strings.SplitN(string(n), ":", 2) + if len(parts) > 1 { + return parts[1] + } + return "" +} diff --git a/cmd/kpod/parse_test.go b/cmd/kpod/parse_test.go new file mode 100644 index 00000000..c68b2f86 --- /dev/null +++ b/cmd/kpod/parse_test.go @@ -0,0 +1,168 @@ +package main + +import ( + "fmt" + "testing" +) + +func TestValidateMACAddress(t *testing.T) { + addresses := []string{"01:23:45:67:89:ab", + "01:23:45:67:89:ab:cd:ef", + "01:23:45:67:89:ab:cd:ef:00:00:01:23:45:67:89:ab:cd:ef:00:00", + "01-23-45-67-89-ab", + "01-23-45-67-89-ab-cd-ef", + "01-23-45-67-89-ab-cd-ef-00-00-01-23-45-67-89-ab-cd-ef-00-00", + "0123.4567.89ab", + "0123.4567.89ab.cdef", + "0123.4567.89ab.cdef.0000.0123.4567.89ab.cdef.0000"} + + for _, addr := range addresses { + _, err := validateMACAddress(addr) + if err != nil { + t.Fatalf("invalid mac address %q", addr) + } + } + + // test invalid mac address + invalidAddr := "567:78hy:uthg" + _, err := validateMACAddress(invalidAddr) + if err == nil { + t.Fatalf("should have returned an error. %q is not a valid mac address", invalidAddr) + } +} + +func TestValidateLink(t *testing.T) { + validLink := "containerName:alias" + _, err := validateLink(validLink) + if err != nil { + t.Fatalf("%q is a valid link, but error returned", validLink) + } + + invalidLinks := []string{"container:alias1:alias2", ""} + for _, link := range invalidLinks { + _, err := validateLink(link) + if err == nil { + t.Fatalf("should be invalid link %q, but err=nil", link) + } + } +} + +func TestValidDeviceMode(t *testing.T) { + validModes := []string{"r", "w", "m"} + for _, mode := range validModes { + valid := validDeviceMode(mode) + if !valid { + t.Fatalf("should be a valid mode %q", mode) + } + } + + invalidModes := []string{"", "a", "b", "blah"} + for _, mode := range invalidModes { + valid := validDeviceMode(mode) + if valid { + t.Fatalf("should be an invalid mode %q", mode) + } + } +} + +func TestValidateDevice(t *testing.T) { + validDevices := []string{"host-dir:/containerPath:w", + "host:/containerPath:r", + "host:/containerPath:m", + "/containerPath", + "/containerPath:r"} + for _, dev := range validDevices { + _, err := validateDevice(dev) + if err != nil { + fmt.Println(err) + t.Fatalf("%q should be a valid device, got invalid", dev) + } + } + + invalidDevices := []string{"host:/containerPath:h", + "containerPath:r", + "/containerPath:b", + "containerPath", + ""} + for _, dev := range invalidDevices { + _, err := validateDevice(dev) + if err == nil { + fmt.Println(err) + t.Fatalf("%q should be invalid device, got valid", dev) + } + } + +} + +func TestParseLoggingOpts(t *testing.T) { + for _, validOpts := range []struct { + logDriver string + logDriverOpts []string + }{ + { + logDriver: "testDriver", + logDriverOpts: []string{"key1=value1, key2=value2"}, + }, + { + logDriver: "testDriver", + }, + { + logDriver: "", + }, + } { + _, err := parseLoggingOpts(validOpts.logDriver, validOpts.logDriverOpts) + if err != nil { + t.Fatalf("expected valid logging options, got invalid %q", validOpts) + } + } + + for _, invalidOpts := range []struct { + logDriver string + logDriverOpts []string + }{ + { + logDriver: "none", + logDriverOpts: []string{"key1=value1, key2=value2"}, + }, + } { + _, err := parseLoggingOpts(invalidOpts.logDriver, invalidOpts.logDriverOpts) + if err == nil { + t.Fatalf("expected valid logging options, got invalid %q", invalidOpts) + } + } +} + +func TestParseStorageOpts(t *testing.T) { + validStorageOpts := []string{"key1=value1", "key2=value2"} + _, err := parseStorageOpts(validStorageOpts) + if err != nil { + t.Fatalf("expected valid storage opts, got invalid instead %q", validStorageOpts) + } + + invalidStorageOpts := []string{"", "onlyKey", "onlyValue"} + _, err = parseStorageOpts(invalidStorageOpts) + if err == nil { + t.Fatalf("expected invalid storage opts, got valid instead %q", invalidStorageOpts) + } +} + +func TestParsePortSpecs(t *testing.T) { + validPorts := []string{"123.125.234.123:8888:8888", + "123.125.234.123:8888:8888/udp", + "123.125.234.123:8888:8888/tcp", + "123.125.234.123:8888-8900:8888-8900"} + _, err := parsePortSpecs(validPorts) + if err != nil { + t.Fatalf("expected valid port, got invalid instead %v", err) + } + + invalidPorts := []string{"12.123.124.123.125:8000:8000", + "276.567.897.653:8000:8000", + "567:546:3455:8000:7896", + "123.124.125.134:8000:9000/blah", + ""} + _, err = parsePortSpecs(invalidPorts) + if err == nil { + t.Fatalf("expected invalid port, got valid instead %v", err) + } +}