diff --git a/cmd/kpod/common.go b/cmd/kpod/common.go index a5d7a002..91997e1f 100644 --- a/cmd/kpod/common.go +++ b/cmd/kpod/common.go @@ -136,15 +136,15 @@ func validateFlags(c *cli.Context, flags []cli.Flag) error { // Common flags shared between commands var createFlags = []cli.Flag{ - cli.StringSliceFlag{ + cli.StringSliceFlag{ // Name: "add-host", Usage: "Add a custom host-to-IP mapping (host:ip) (default [])", }, - cli.StringSliceFlag{ + cli.StringSliceFlag{ // Name: "attach, a", Usage: "Attach to STDIN, STDOUT or STDERR (default [])", }, - cli.Int64Flag{ + cli.StringFlag{ Name: "blkio-weight", Usage: "Block IO weight (relative weight) accepts a weight value between 10 and 1000.", }, @@ -168,11 +168,11 @@ var createFlags = []cli.Flag{ Name: "cpu-count", Usage: "Limit the number of CPUs available for execution by the container.", }, - cli.StringFlag{ + cli.StringFlag{ // Name: "cid-file", Usage: "Write the container ID to the file", }, - cli.Int64Flag{ + cli.Uint64Flag{ Name: "cpu-period", Usage: "Limit the CPU CFS (Completely Fair Scheduler) period", }, @@ -180,7 +180,7 @@ var createFlags = []cli.Flag{ Name: "cpu-quota", Usage: "Limit the CPU CFS (Completely Fair Scheduler) quota", }, - cli.Int64Flag{ + cli.Uint64Flag{ Name: "cpu-rt-period", Usage: "Limit the CPU real-time period in microseconds", }, @@ -188,7 +188,7 @@ var createFlags = []cli.Flag{ Name: "cpu-rt-runtime", Usage: "Limit the CPU real-time runtime in microseconds", }, - cli.Int64Flag{ + cli.Uint64Flag{ Name: "cpu-shares", Usage: "CPU shares (relative weight)", }, @@ -208,7 +208,7 @@ var createFlags = []cli.Flag{ Name: "detach, d", Usage: "Run container in background and print container ID", }, - cli.StringFlag{ + cli.StringFlag{ // Name: "detach-keys", Usage: "Override the key sequence for detaching a container. Format is a single character `[a-Z]` or `ctrl-` where `` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`", }, @@ -253,7 +253,7 @@ var createFlags = []cli.Flag{ Usage: "Set environment variables in container", }, cli.StringSliceFlag{ - Name: "env-file", + Name: "env-file", // Usage: "Read in a file of environment variables", }, cli.StringSliceFlag{ @@ -280,7 +280,7 @@ var createFlags = []cli.Flag{ Name: "ip6", Usage: "Container IPv6 address (e.g. 2001:db8::1b99)", }, - cli.StringFlag{ + cli.StringFlag{ // Name: "ipc", Usage: "IPC Namespace to use", }, @@ -292,7 +292,7 @@ var createFlags = []cli.Flag{ Name: "label", Usage: "Set metadata on container (default [])", }, - cli.StringSliceFlag{ + cli.StringSliceFlag{ // Name: "label-file", Usage: "Read in a line delimited file of labels (default [])", }, @@ -324,7 +324,7 @@ var createFlags = []cli.Flag{ Name: "memory-swap", Usage: "Swap limit equal to memory plus swap: '-1' to enable unlimited swap", }, - cli.StringFlag{ + cli.Int64Flag{ Name: "memory-swappiness", Usage: "Tune container memory swappiness (0 to 100) (default -1)", }, @@ -344,7 +344,7 @@ var createFlags = []cli.Flag{ Name: "network-alias", Usage: "Add network-scoped alias for the container (default [])", }, - cli.BoolFlag{ + cli.BoolFlag{ // Name: "oom-kill-disable", Usage: "Disable OOM Killer", }, diff --git a/cmd/kpod/create.go b/cmd/kpod/create.go index 54307f86..067a2f6b 100644 --- a/cmd/kpod/create.go +++ b/cmd/kpod/create.go @@ -7,94 +7,114 @@ import ( "github.com/pkg/errors" "github.com/urfave/cli" pb "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime" + "strings" + + "github.com/docker/go-units" + "github.com/kubernetes-incubator/cri-o/libpod" + ann "github.com/kubernetes-incubator/cri-o/pkg/annotations" + "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" + "strconv" +) + +type mountType string + +// Type constants +const ( + // TypeBind is the type for mounting host dir + TypeBind mountType = "bind" + // TypeVolume is the type for remote storage volumes + TypeVolume mountType = "volume" + // TypeTmpfs is the type for mounting tmpfs + TypeTmpfs mountType = "tmpfs" + // TypeNamedPipe is the type for mounting Windows named pipes + TypeNamedPipe mountType = "npipe" +) + +var ( + defaultEnvVariables = []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "TERM=xterm"} ) type createResourceConfig struct { - blkioWeight int64 - blkioDevice []string - cpuShares int64 - cpuCount int64 - cpuPeriod int64 + blkioDevice []string // blkio-weight-device + blkioWeight uint16 // blkio-weight + cpuPeriod uint64 // cpu-period + cpuQuota int64 // cpu-quota + cpuRtPeriod uint64 // cpu-rt-period + cpuRtRuntime int64 // cpu-rt-runtime + cpuShares uint64 // cpu-shares + cpus string // cpus cpusetCpus string - cpusetNames string - cpuFile string - cpuMems string - cpuQuota int64 - cpuRtPeriod int64 - cpuRtRuntime int64 - cpus int64 - deviceReadBps []string - deviceReadIops []string - deviceWriteBps []string - deviceWriteIops []string - memory string - memoryReservation string - memorySwap string - memorySwapiness string - kernelMemory string - oomScoreAdj string - pidsLimit string + cpusetMems string // cpuset-mems + deviceReadBps []string // device-read-bps + deviceReadIops []string // device-read-iops + deviceWriteBps []string // device-write-bps + deviceWriteIops []string // device-write-iops + disableOomKiller bool // oom-kill-disable + kernelMemory int64 // kernel-memory + memory int64 //memory + memoryReservation int64 // memory-reservation + memorySwap int64 //memory-swap + memorySwapiness uint64 // memory-swappiness + oomScoreAdj int //oom-score-adj + pidsLimit int64 // pids-limit shmSize string - ulimit []string + ulimit []string //ulimit } type createConfig struct { - additionalGroups []int64 - args []string - capAdd []string - capDrop []string - cgroupParent string - command string - detach bool - devices []*pb.Device - dnsOpt []string - dnsSearch []string - dnsServers []string - entrypoint string - env map[string]string - expose []string - groupAdd []string - hostname string - image string - interactive bool - ip6Address string - ipAddress string - labels map[string]string - linkLocalIP []string - logDriver string - logDriverOpt []string - macAddress string - mounts []*pb.Mount - name string - network string - networkAlias []string - nsIPC string - nsNet string - nsPID string - nsUser string - pod string - ports []*pb.PortMapping - privileged bool - publish []string - publishAll bool - readOnlyRootfs bool - resources createResourceConfig - rm bool - securityOpts []string - shmSize string - sigProxy bool - stdin bool - stopSignal string - stopTimeout int64 - storageOpts []string - sysctl string - tmpfs []string - tty bool - user int64 - userns string - volumes []string - volumesFrom []string - workDir string + args []string + capAdd []string // cap-add + capDrop []string // cap-drop + cidFile string + cgroupParent string // cgroup-parent + command []string + 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 []uint32 // group-add + hostname string //hostname + image string + 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 + name string //name + network string //network + networkAlias []string //network-alias + nsIPC string // ipc + nsNet string //net + nsPID string //pid + nsUser string + pod string //pod + privileged bool //privileged + publish []string //publish + publishAll bool //publish-all + readOnlyRootfs bool //read-only + resources createResourceConfig + rm bool //rm + securityOpts []string //security-opt + sigProxy bool //sig-proxy + stopSignal string // stop-signal + stopTimeout int64 // stop-timeout + storageOpts []string //storage-opt + sysctl map[string]string //sysctl + tmpfs []string // tmpfs + tty bool //tty + user uint32 //user + group uint32 // group + volumes []string //volume + volumesFrom []string //volumes-from + workDir string //workdir } var createDescription = "Creates a new container from the given image or" + @@ -115,46 +135,765 @@ var createCommand = cli.Command{ func createCmd(c *cli.Context) error { // TODO should allow user to create based off a directory on the host not just image // Need CLI support for this - if len(c.Args()) != 1 { - return errors.Errorf("must specify name of image to create from") - } if err := validateFlags(c, createFlags); err != nil { return err } - runtime, err := getRuntime(c) + //runtime, err := getRuntime(c) + runtime, err := libpod.NewRuntime() if err != nil { return errors.Wrapf(err, "error creating libpod runtime") } - createConfig, err := parseCreateOpts(c) + createConfig, err := parseCreateOpts(c, runtime) if err != nil { return err } + // Deal with the image after all the args have been checked + createImage := runtime.NewImage(createConfig.image) + if !createImage.HasImageLocal() { + // The image wasnt found by the user input'd name or its fqname + // Pull the image + fmt.Printf("Trying to pull %s...", createImage.PullName) + createImage.Pull() + } + runtimeSpec, err := createConfigToOCISpec(createConfig) if err != nil { return err } - ctr, err := runtime.NewContainer(runtimeSpec) + imageName, err := createImage.GetFQName() + if err != nil { + return err + } + fmt.Println(imageName) + imageID, err := createImage.GetImageID() + if err != nil { + return err + } + ctr, err := runtime.NewContainer(runtimeSpec, libpod.WithRootFSFromImage(imageID, imageName, false)) if err != nil { return err } - // Should we also call ctr.Create() to make the container in runc? + if err := ctr.Create(); err != nil { + return err + } + if c.String("cid-file") != "" { + libpod.WriteFile(ctr.ID(), c.String("cid-file")) + return nil + } fmt.Printf("%s\n", ctr.ID()) return nil } +/* The following funcs should land in parse.go */ +// +// +func stringSlicetoUint32Slice(inputSlice []string) ([]uint32, error) { + var outputSlice []uint32 + for _, v := range inputSlice { + u, err := strconv.ParseUint(v, 10, 32) + if err != nil { + return outputSlice, err + } + outputSlice = append(outputSlice, uint32(u)) + } + return outputSlice, nil +} + // Parses CLI options related to container creation into a config which can be // parsed into an OCI runtime spec -func parseCreateOpts(c *cli.Context) (*createConfig, error) { - return nil, errors.Errorf("NOT IMPLEMENTED") +func parseCreateOpts(c *cli.Context, runtime *libpod.Runtime) (*createConfig, error) { + var command []string + var memoryLimit, memoryReservation, memorySwap, memoryKernel int64 + var blkioWeight uint16 + var env []string + var labelValues []string + var uid, gid uint32 + sysctl := make(map[string]string) + labels := make(map[string]string) + + image := c.Args()[0] + + if len(c.Args()) < 1 { + return nil, errors.Errorf("you just provide an image name") + } + if len(c.Args()) > 1 { + command = c.Args()[1:] + } + + // LABEL VARIABLES + // TODO where should labels be verified to be x=y format + labelValues, labelErr := readKVStrings(c.StringSlice("label-file"), c.StringSlice("label")) + if labelErr != nil { + return &createConfig{}, errors.Wrapf(labelErr, "unable to process labels from --label and label-file") + } + // Process KEY=VALUE stringslice in string map for WithLabels func + if len(labelValues) > 0 { + for _, i := range labelValues { + spliti := strings.Split(i, "=") + labels[spliti[0]] = spliti[1] + } + } + + // ENVIRONMENT VARIABLES + // TODO where should env variables be verified to be x=y format + env, err := readKVStrings(c.StringSlice("env-file"), c.StringSlice("env")) + if err != nil { + return &createConfig{}, errors.Wrapf(err, "unable to process variables from --env and --env-file") + } + // Add default environment variables if nothing defined + if len(env) == 0 { + env = append(env, defaultEnvVariables...) + } + + if len(c.StringSlice("sysctl")) > 0 { + for _, inputSysctl := range c.StringSlice("sysctl") { + values := strings.Split(inputSysctl, "=") + sysctl[values[0]] = values[1] + } + } + + groupAdd, err := stringSlicetoUint32Slice(c.StringSlice("group-add")) + if err != nil { + return &createConfig{}, errors.Wrapf(err, "invalid value for groups provided") + } + + if c.String("user") != "" { + // TODO + // We need to mount the imagefs and get the uid/gid + // For now, user zeros + uid = 0 + gid = 0 + } + + if c.String("memory") != "" { + memoryLimit, err = units.RAMInBytes(c.String("memory")) + if err != nil { + return nil, errors.Wrapf(err, "invalid value for memory") + } + } + if c.String("memory-reservation") != "" { + memoryReservation, err = units.RAMInBytes(c.String("memory-reservation")) + if err != nil { + return nil, errors.Wrapf(err, "invalid value for memory-reservation") + } + } + if c.String("memory-swap") != "" { + memorySwap, err = units.RAMInBytes(c.String("memory-swap")) + if err != nil { + return nil, errors.Wrapf(err, "invalid value for memory-swap") + } + } + if c.String("kernel-memory") != "" { + memoryKernel, err = units.RAMInBytes(c.String("kernel-memory")) + if err != nil { + return nil, errors.Wrapf(err, "invalid value for kernel-memory") + } + } + if c.String("blkio-weight") != "" { + u, err := strconv.ParseUint(c.String("blkio-weight"), 10, 16) + if err != nil { + return nil, errors.Wrapf(err, "invalid value for blkio-weight") + } + blkioWeight = uint16(u) + } + + 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: groupAdd, + 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: blkioWeight, + blkioDevice: c.StringSlice("blkio-weight-device"), + cpuShares: c.Uint64("cpu-shares"), + //cpuCount: c.Int64("cpu-count"), + cpuPeriod: c.Uint64("cpu-period"), + cpusetCpus: c.String("cpu-period"), + cpusetMems: c.String("cpuset-mems"), + cpuQuota: c.Int64("cpu-quota"), + cpuRtPeriod: c.Uint64("cpu-rt-period"), + cpuRtRuntime: c.Int64("cpu-rt-runtime"), + cpus: c.String("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"), + disableOomKiller: c.Bool("oom-kill-disable"), + memory: memoryLimit, + memoryReservation: memoryReservation, + memorySwap: memorySwap, + memorySwapiness: c.Uint64("memory-swapiness"), + kernelMemory: memoryKernel, + oomScoreAdj: c.Int("oom-score-adj"), + + pidsLimit: c.Int64("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: uid, + group: gid, + //userns: c.String("userns"), + volumes: c.StringSlice("volume"), + volumesFrom: c.StringSlice("volumes-from"), + workDir: c.String("workdir"), + } + + return config, nil } // Parses information needed to create a container into an OCI runtime spec func createConfigToOCISpec(config *createConfig) (*spec.Spec, error) { - return nil, errors.Errorf("NOT IMPLEMENTED") + + //blkio, err := config.CreateBlockIO() + //if err != nil { + // return &spec.Spec{}, err + //} + + spec := config.GetDefaultLinuxSpec() + spec.Process.Cwd = config.workDir + spec.Process.Args = config.command + + if config.tty { + spec.Process.Terminal = config.tty + } + + if config.user != 0 { + // User and Group must go together + spec.Process.User.UID = config.user + spec.Process.User.GID = config.group + } + if len(config.groupAdd) > 0 { + spec.Process.User.AdditionalGids = config.groupAdd + } + if len(config.env) > 0 { + spec.Process.Env = config.env + } + //TODO + // Need examples of capacity additions so I can load that properly + + if config.readOnlyRootfs { + spec.Root.Readonly = config.readOnlyRootfs + } + + if config.hostname != "" { + spec.Hostname = config.hostname + } + + // BIND MOUNTS + if len(config.volumes) > 0 { + spec.Mounts = append(spec.Mounts, config.GetVolumeMounts()...) + } + // TMPFS MOUNTS + if len(config.tmpfs) > 0 { + spec.Mounts = append(spec.Mounts, config.GetTmpfsMounts()...) + } + + // RESOURCES - MEMORY + if len(config.sysctl) > 0 { + spec.Linux.Sysctl = config.sysctl + } + if config.resources.memory != 0 { + spec.Linux.Resources.Memory.Limit = &config.resources.memory + } + if config.resources.memoryReservation != 0 { + spec.Linux.Resources.Memory.Reservation = &config.resources.memoryReservation + } + if config.resources.memorySwap != 0 { + spec.Linux.Resources.Memory.Swap = &config.resources.memorySwap + } + if config.resources.kernelMemory != 0 { + spec.Linux.Resources.Memory.Kernel = &config.resources.kernelMemory + } + if config.resources.memorySwapiness != 0 { + spec.Linux.Resources.Memory.Swappiness = &config.resources.memorySwapiness + } + if config.resources.disableOomKiller { + spec.Linux.Resources.Memory.DisableOOMKiller = &config.resources.disableOomKiller + } + + // RESOURCES - CPU + + if config.resources.cpuShares != 0 { + spec.Linux.Resources.CPU.Shares = &config.resources.cpuShares + } + if config.resources.cpuQuota != 0 { + spec.Linux.Resources.CPU.Quota = &config.resources.cpuQuota + } + if config.resources.cpuPeriod != 0 { + spec.Linux.Resources.CPU.Period = &config.resources.cpuPeriod + } + if config.resources.cpuRtRuntime != 0 { + spec.Linux.Resources.CPU.RealtimeRuntime = &config.resources.cpuRtRuntime + } + if config.resources.cpuRtPeriod != 0 { + spec.Linux.Resources.CPU.RealtimePeriod = &config.resources.cpuRtPeriod + } + if config.resources.cpus != "" { + spec.Linux.Resources.CPU.Cpus = config.resources.cpus + } + if config.resources.cpusetMems != "" { + spec.Linux.Resources.CPU.Mems = config.resources.cpusetMems + } + + // RESOURCES - PIDS + if config.resources.pidsLimit != 0 { + spec.Linux.Resources.Pids.Limit = config.resources.pidsLimit + } + + /* + Capabilities: &spec.LinuxCapabilities{ + // Rlimits []PosixRlimit // Where does this come from + // Type string + // Hard uint64 + // Limit uint64 + // NoNewPrivileges bool // No user input for this + // ApparmorProfile string // No user input for this + OOMScoreAdj: &config.resources.oomScoreAdj, + // Selinuxlabel + }, + Hooks: &spec.Hooks{}, + //Annotations + Resources: &spec.LinuxResources{ + Devices: config.GetDefaultDevices(), + BlockIO: &blkio, + //HugepageLimits: + Network: &spec.LinuxNetwork{ + // ClassID *uint32 + // Priorites []LinuxInterfacePriority + }, + }, + //CgroupsPath: + //Namespaces: []LinuxNamespace + //Devices + Seccomp: &spec.LinuxSeccomp{ + // DefaultAction: + // Architectures + // Syscalls: + }, + // RootfsPropagation + // MaskedPaths + // ReadonlyPaths: + // MountLabel + // IntelRdt + }, + } + */ + return &spec, nil +} + +func getStatFromPath(path string) unix.Stat_t { + s := unix.Stat_t{} + _ = unix.Stat(path, &s) + return s +} + +func makeThrottleArray(throttleInput []string) ([]spec.LinuxThrottleDevice, error) { + var ltds []spec.LinuxThrottleDevice + for _, i := range throttleInput { + t, err := validateBpsDevice(i) + if err != nil { + return []spec.LinuxThrottleDevice{}, err + } + ltd := spec.LinuxThrottleDevice{} + ltd.Rate = t.rate + ltdStat := getStatFromPath(t.path) + ltd.Major = int64(unix.Major(ltdStat.Rdev)) + ltd.Minor = int64(unix.Major(ltdStat.Rdev)) + ltds = append(ltds, ltd) + } + return ltds, nil + +} + +func (c *createConfig) CreateBlockIO() (spec.LinuxBlockIO, error) { + bio := spec.LinuxBlockIO{} + bio.Weight = &c.resources.blkioWeight + if len(c.resources.blkioDevice) > 0 { + var lwds []spec.LinuxWeightDevice + for _, i := range c.resources.blkioDevice { + wd, err := validateweightDevice(i) + if err != nil { + return bio, errors.Wrapf(err, "invalid values for blkio-weight-device") + } + wdStat := getStatFromPath(wd.path) + lwd := spec.LinuxWeightDevice{ + Weight: &wd.weight, + } + lwd.Major = int64(unix.Major(wdStat.Rdev)) + lwd.Minor = int64(unix.Minor(wdStat.Rdev)) + lwds = append(lwds, lwd) + } + } + if len(c.resources.deviceReadBps) > 0 { + readBps, err := makeThrottleArray(c.resources.deviceReadBps) + if err != nil { + return bio, err + } + bio.ThrottleReadBpsDevice = readBps + } + if len(c.resources.deviceWriteBps) > 0 { + writeBpds, err := makeThrottleArray(c.resources.deviceWriteBps) + if err != nil { + return bio, err + } + bio.ThrottleWriteBpsDevice = writeBpds + } + if len(c.resources.deviceReadIops) > 0 { + readIops, err := makeThrottleArray(c.resources.deviceReadIops) + if err != nil { + return bio, err + } + bio.ThrottleReadIOPSDevice = readIops + } + if len(c.resources.deviceWriteIops) > 0 { + writeIops, err := makeThrottleArray(c.resources.deviceWriteIops) + if err != nil { + return bio, err + } + bio.ThrottleWriteIOPSDevice = writeIops + } + + return bio, nil +} + +func (c *createConfig) GetDefaultMounts() []spec.Mount { + return []spec.Mount{ + { + Destination: "/proc", + Type: "proc", + Source: "proc", + Options: []string{"nosuid", "noexec", "nodev"}, + }, + { + Destination: "/dev", + Type: "tmpfs", + Source: "tmpfs", + Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"}, + }, + { + Destination: "/dev/pts", + Type: "devpts", + Source: "devpts", + Options: []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620", "gid=5"}, + }, + { + Destination: "/sys", + Type: "sysfs", + Source: "sysfs", + Options: []string{"nosuid", "noexec", "nodev", "ro"}, + }, + { + Destination: "/sys/fs/cgroup", + Type: "cgroup", + Source: "cgroup", + Options: []string{"ro", "nosuid", "noexec", "nodev"}, + }, + { + Destination: "/dev/mqueue", + Type: "mqueue", + Source: "mqueue", + Options: []string{"nosuid", "noexec", "nodev"}, + }, + { + Destination: "/dev/shm", + Type: "tmpfs", + Source: "shm", + Options: []string{"nosuid", "noexec", "nodev", "mode=1777"}, + }, + } +} +func iPtr(i int64) *int64 { return &i } + +func (c *createConfig) GetDefaultDevices() []spec.LinuxDeviceCgroup { + return []spec.LinuxDeviceCgroup{ + { + Allow: false, + Access: "rwm", + }, + { + Allow: true, + Type: "c", + Major: iPtr(1), + Minor: iPtr(5), + Access: "rwm", + }, + { + Allow: true, + Type: "c", + Major: iPtr(1), + Minor: iPtr(3), + Access: "rwm", + }, + { + Allow: true, + Type: "c", + Major: iPtr(1), + Minor: iPtr(9), + Access: "rwm", + }, + { + Allow: true, + Type: "c", + Major: iPtr(1), + Minor: iPtr(8), + Access: "rwm", + }, + { + Allow: true, + Type: "c", + Major: iPtr(5), + Minor: iPtr(0), + Access: "rwm", + }, + { + Allow: true, + Type: "c", + Major: iPtr(5), + Minor: iPtr(1), + Access: "rwm", + }, + { + Allow: false, + Type: "c", + Major: iPtr(10), + Minor: iPtr(229), + Access: "rwm", + }, + } +} + +func defaultCapabilities() []string { + return []string{ + "CAP_CHOWN", + "CAP_DAC_OVERRIDE", + "CAP_FSETID", + "CAP_FOWNER", + "CAP_MKNOD", + "CAP_NET_RAW", + "CAP_SETGID", + "CAP_SETUID", + "CAP_SETFCAP", + "CAP_SETPCAP", + "CAP_NET_BIND_SERVICE", + "CAP_SYS_CHROOT", + "CAP_KILL", + "CAP_AUDIT_WRITE", + } +} + +func (c *createConfig) GetDefaultLinuxSpec() spec.Spec { + s := spec.Spec{ + Version: spec.Version, + Root: &spec.Root{}, + } + s.Annotations = c.GetAnnotations() + s.Mounts = c.GetDefaultMounts() + s.Process = &spec.Process{ + Capabilities: &spec.LinuxCapabilities{ + Bounding: defaultCapabilities(), + Permitted: defaultCapabilities(), + Inheritable: defaultCapabilities(), + Effective: defaultCapabilities(), + }, + } + s.Linux = &spec.Linux{ + MaskedPaths: []string{ + "/proc/kcore", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + }, + ReadonlyPaths: []string{ + "/proc/asound", + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger", + }, + Namespaces: []spec.LinuxNamespace{ + {Type: "mount"}, + {Type: "network"}, + {Type: "uts"}, + {Type: "pid"}, + {Type: "ipc"}, + }, + Devices: []spec.LinuxDevice{}, + Resources: &spec.LinuxResources{ + Devices: c.GetDefaultDevices(), + }, + } + + return s +} + +func (c *createConfig) GetAnnotations() map[string]string { + a := getDefaultAnnotations() + // TODO + // Which annotations do we want added by default + if c.tty { + a["io.kubernetes.cri-o.TTY"] = "true" + } + return a +} + +func getDefaultAnnotations() map[string]string { + var a map[string]string + a = make(map[string]string) + a[ann.Annotations] = "" + a[ann.ContainerID] = "" + a[ann.ContainerName] = "" + a[ann.ContainerType] = "" + a[ann.Created] = "" + a[ann.HostName] = "" + a[ann.IP] = "" + a[ann.Image] = "" + a[ann.ImageName] = "" + a[ann.ImageRef] = "" + a[ann.KubeName] = "" + a[ann.Labels] = "" + a[ann.LogPath] = "" + a[ann.Metadata] = "" + a[ann.Name] = "" + a[ann.PrivilegedRuntime] = "" + a[ann.ResolvPath] = "" + a[ann.HostnamePath] = "" + a[ann.SandboxID] = "" + a[ann.SandboxName] = "" + a[ann.ShmPath] = "" + a[ann.MountPoint] = "" + a[ann.TrustedSandbox] = "" + a[ann.TTY] = "false" + a[ann.Stdin] = "" + a[ann.StdinOnce] = "" + a[ann.Volumes] = "" + + return a +} + +//GetTmpfsMounts takes user provided input for bind mounts and creates Mount structs +func (c *createConfig) GetVolumeMounts() []spec.Mount { + var m []spec.Mount + var options []string + for _, i := range c.volumes { + spliti := strings.Split(i, ":") + if len(spliti) > 2 { + options = strings.Split(spliti[2], ",") + } + // always add rbind bc mount ignores the bind filesystem when mounting + options = append(options, "rbind") + m = append(m, spec.Mount{ + Destination: spliti[1], + Type: string(TypeBind), + Source: spliti[0], + Options: options, + }) + } + return m +} + +//GetTmpfsMounts takes user provided input for tmpfs mounts and creates Mount structs +func (c *createConfig) GetTmpfsMounts() []spec.Mount { + var m []spec.Mount + for _, i := range c.tmpfs { + // Default options if nothing passed + options := []string{"rw", "noexec", "nosuid", "nodev", "size=65536k"} + spliti := strings.Split(i, ":") + destPath := spliti[0] + if len(spliti) > 1 { + options = strings.Split(spliti[1], ",") + } + m = append(m, spec.Mount{ + Destination: destPath, + Type: string(TypeTmpfs), + Options: options, + }) + } + return m +} + +func (c *createConfig) GetContainerCreateOptions(cli *cli.Context) ([]libpod.CtrCreateOption, error) { + /* + WithStorageConfig + WithImageConfig + WithSignaturePolicy + WithOCIRuntime + WithConmonPath + WithConmonEnv + WithCgroupManager + WithStaticDir + WithTmpDir + WithSELinux + WithPidsLimit // dont need + WithMaxLogSize + WithNoPivotRoot + WithRootFSFromPath + WithRootFSFromImage + WithStdin // done + WithSharedNamespaces + WithLabels //done + WithAnnotations // dont need + WithName // done + WithStopSignal + WithPodName + */ + var options []libpod.CtrCreateOption + + // Uncomment after talking to mheon about unimplemented funcs + // options = append(options, libpod.WithLabels(c.labels)) + + if c.interactive { + options = append(options, libpod.WithStdin()) + } + if c.name != "" { + logrus.Info("appending name %s", c.name) + options = append(options, libpod.WithName(c.name)) + } + + return options, nil } diff --git a/cmd/kpod/run.go b/cmd/kpod/run.go index 562f1fa0..9c923e2b 100644 --- a/cmd/kpod/run.go +++ b/cmd/kpod/run.go @@ -3,7 +3,9 @@ package main import ( "fmt" + "github.com/kubernetes-incubator/cri-o/libpod" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -19,35 +21,86 @@ var runCommand = cli.Command{ } func runCmd(c *cli.Context) error { - if len(c.Args()) != 1 { - return errors.Errorf("must specify name of image to create from") - } if err := validateFlags(c, createFlags); err != nil { return err } - runtime, err := getRuntime(c) + runtime, err := libpod.NewRuntime() if err != nil { return errors.Wrapf(err, "error creating libpod runtime") } - createConfig, err := parseCreateOpts(c) + createConfig, err := parseCreateOpts(c, runtime) if err != nil { return err } + createImage := runtime.NewImage(createConfig.image) + + if !createImage.HasImageLocal() { + // The image wasnt found by the user input'd name or its fqname + // Pull the image + fmt.Printf("Trying to pull %s...", createImage.PullName) + createImage.Pull() + } + runtimeSpec, err := createConfigToOCISpec(createConfig) + logrus.Debug("spec is ", runtimeSpec) if err != nil { return err } - ctr, err := runtime.NewContainer(runtimeSpec) + imageName, err := createImage.GetFQName() + if err != nil { + return err + } + logrus.Debug("imageName is ", imageName) + + imageID, err := createImage.GetImageID() + if err != nil { + return err + } + logrus.Debug("imageID is ", imageID) + + options, err := createConfig.GetContainerCreateOptions(c) + if err != nil { + return errors.Wrapf(err, "unable to parse new container options") + } + + // Gather up the options for NewContainer which consist of With... funcs + options = append(options, libpod.WithRootFSFromImage(imageID, imageName, false)) + ctr, err := runtime.NewContainer(runtimeSpec, options...) if err != nil { return err } - // Should we also call ctr.Create() to make the container in runc? + logrus.Debug("new container created ", ctr.ID()) + if err := ctr.Create(); err != nil { + return err + } + logrus.Debug("container storage created for ", ctr.ID()) - fmt.Printf("%s\n", ctr.ID()) + if c.String("cid-file") != "" { + libpod.WriteFile(ctr.ID(), c.String("cid-file")) + return nil + } + // Start the container + if err := ctr.Start(); err != nil { + return errors.Wrapf(err, "unable to start container ", ctr.ID()) + } + logrus.Debug("started container ", ctr.ID()) + if createConfig.tty { + // Attach to the running container + keys := "" + if c.String("detach-keys") != "" { + keys = c.String("detach-keys") + } + logrus.Debug("trying to attach to the container %s", ctr.ID()) + if err := ctr.Attach(false, keys); err != nil { + return errors.Wrapf(err, "unable to attach to container %s", ctr.ID()) + } + } else { + fmt.Printf("%s\n", ctr.ID()) + } return nil } diff --git a/libpod/container.go b/libpod/container.go index 390b20b3..27ec24e9 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -10,11 +10,13 @@ import ( "github.com/containers/storage" "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/term" crioAnnotations "github.com/kubernetes-incubator/cri-o/pkg/annotations" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/ulule/deepcopier" + "k8s.io/client-go/tools/remotecommand" ) // ContainerState represents the current state of a container @@ -391,8 +393,32 @@ func (c *Container) Exec(cmd []string, tty bool, stdin bool) (string, error) { // Attach attaches to a container // Returns fully qualified URL of streaming server for the container -func (c *Container) Attach(stdin, tty bool) (string, error) { - return "", ErrNotImplemented +func (c *Container) Attach(noStdin bool, keys string) error { + // Check the validity of the provided keys first + var err error + detachKeys := []byte{} + if len(keys) > 0 { + detachKeys, err = term.ToBytes(keys) + if err != nil { + return errors.Wrapf(err, "invalid detach keys") + } + } + cStatus := c.state.State + + if !(cStatus == ContainerStateRunning || cStatus == ContainerStateCreated) { + return errors.Errorf("%s is not created or running", c.Name()) + } + resize := make(chan remotecommand.TerminalSize) + defer close(resize) + err = c.attachContainerSocket(resize, noStdin, detachKeys) + if err != nil { + return err + } + // TODO + // Re-enable this when mheon is done wth it + //c.ContainerStateToDisk(c) + + return nil } // Mount mounts a container's filesystem on the host diff --git a/libpod/container_attach.go b/libpod/container_attach.go new file mode 100644 index 00000000..6516a311 --- /dev/null +++ b/libpod/container_attach.go @@ -0,0 +1,144 @@ +package libpod + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strconv" + + "github.com/docker/docker/pkg/term" + "github.com/kubernetes-incubator/cri-o/utils" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" + "k8s.io/client-go/tools/remotecommand" + kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" + "net" +) + +/* Sync with stdpipe_t in conmon.c */ +const ( + AttachPipeStdin = 1 + AttachPipeStdout = 2 + AttachPipeStderr = 3 +) + +// attachContainerSocket connects to the container's attach socket and deals with the IO +func (c *Container) attachContainerSocket(resize <-chan remotecommand.TerminalSize, noStdIn bool, detachKeys []byte) error { + inputStream := os.Stdin + outputStream := os.Stdout + errorStream := os.Stderr + defer inputStream.Close() + tty, err := strconv.ParseBool(c.runningSpec.Annotations["io.kubernetes.cri-o.TTY"]) + if err != nil { + return errors.Wrapf(err, "unable to parse annotations in %s", c.ID) + } + if !tty { + return errors.Errorf("no tty available for %s", c.ID()) + } + + oldTermState, err := term.SaveState(inputStream.Fd()) + + if err != nil { + return errors.Wrapf(err, "unable to save terminal state") + } + + defer term.RestoreTerminal(inputStream.Fd(), oldTermState) + + // Put both input and output into raw + if !noStdIn { + term.SetRawTerminal(inputStream.Fd()) + } + + controlPath := filepath.Join(c.state.RunDir, "ctl") + controlFile, err := os.OpenFile(controlPath, unix.O_WRONLY, 0) + if err != nil { + return errors.Wrapf(err, "failed to open container ctl file: %v") + } + + kubecontainer.HandleResizing(resize, func(size remotecommand.TerminalSize) { + logrus.Debug("Got a resize event: %+v", size) + _, err := fmt.Fprintf(controlFile, "%d %d %d\n", 1, size.Height, size.Width) + if err != nil { + logrus.Warn("Failed to write to control file to resize terminal: %v", err) + } + }) + attachSocketPath := filepath.Join(c.runtime.ociRuntime.socketsDir, c.ID(), "attach") + logrus.Debug("connecting to socket ", attachSocketPath) + + conn, err := net.DialUnix("unixpacket", nil, &net.UnixAddr{Name: attachSocketPath, Net: "unixpacket"}) + if err != nil { + return errors.Wrapf(err, "failed to connect to container's attach socket: %v") + } + defer conn.Close() + + receiveStdoutError := make(chan error) + if outputStream != nil || errorStream != nil { + go func() { + receiveStdoutError <- redirectResponseToOutputStreams(outputStream, errorStream, conn) + }() + } + + stdinDone := make(chan error) + go func() { + var err error + if inputStream != nil && !noStdIn { + _, err = utils.CopyDetachable(conn, inputStream, detachKeys) + conn.CloseWrite() + } + stdinDone <- err + }() + + select { + case err := <-receiveStdoutError: + return err + case err := <-stdinDone: + if _, ok := err.(utils.DetachError); ok { + return nil + } + if outputStream != nil || errorStream != nil { + return <-receiveStdoutError + } + } + return nil +} + +func redirectResponseToOutputStreams(outputStream, errorStream io.Writer, conn io.Reader) error { + var err error + buf := make([]byte, 8192+1) /* Sync with conmon STDIO_BUF_SIZE */ + for { + nr, er := conn.Read(buf) + if nr > 0 { + var dst io.Writer + switch buf[0] { + case AttachPipeStdout: + dst = outputStream + case AttachPipeStderr: + dst = errorStream + default: + logrus.Infof("Got unexpected attach type %+d", buf[0]) + } + + if dst != nil { + nw, ew := dst.Write(buf[1:nr]) + if ew != nil { + err = ew + break + } + if nr != nw+1 { + err = io.ErrShortWrite + break + } + } + } + if er == io.EOF { + break + } + if er != nil { + err = er + break + } + } + return err +} diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index 45990d2d..a1351e1d 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -22,7 +22,6 @@ type ContainerFilter func(*Container) bool func (r *Runtime) NewContainer(spec *spec.Spec, options ...CtrCreateOption) (ctr *Container, err error) { r.lock.Lock() defer r.lock.Unlock() - if !r.valid { return nil, ErrRuntimeStopped } diff --git a/libpod/runtime_img.go b/libpod/runtime_img.go index feb0ef3f..6a4704ec 100644 --- a/libpod/runtime_img.go +++ b/libpod/runtime_img.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io" + "net" "os" "strings" "syscall" @@ -47,6 +48,8 @@ var ( // DirTransport is the transport for pushing and pulling // images to and from a directory DirTransport = "dir" + // TransportNames are the supported transports in string form + TransportNames = [...]string{DefaultRegistry, DockerArchive, OCIArchive, "ostree:", "dir:"} ) // CopyOptions contains the options given when pushing or pulling images @@ -95,6 +98,260 @@ type imageDecomposeStruct struct { transport string } +func (k *KpodImage) assembleFqName() string { + return fmt.Sprintf("%s/%s:%s", k.Registry, k.ImageName, k.Tag) +} + +func (k *KpodImage) assembleFqNameTransport() string { + return fmt.Sprintf("%s%s/%s:%s", k.Transport, k.Registry, k.ImageName, k.Tag) +} + +//KpodImage describes basic attributes of an image +type KpodImage struct { + Name string + ID string + fqname string + hasImageLocal bool + Runtime + Registry string + ImageName string + Tag string + HasRegistry bool + Transport string + beenDecomposed bool + PullName string +} + +// NewImage creates a new image object based on its name +func (r *Runtime) NewImage(name string) KpodImage { + return KpodImage{ + Name: name, + Runtime: *r, + } +} + +// GetImageID returns the image ID of the image +func (k *KpodImage) GetImageID() (string, error) { + if k.ID != "" { + return k.ID, nil + } + image, _ := k.GetFQName() + img, err := k.Runtime.GetImage(image) + if err != nil { + return "", err + } + return img.ID, nil +} + +// GetFQName returns the fully qualified image name if it can be determined +func (k *KpodImage) GetFQName() (string, error) { + // Check if the fqname has already been found + if k.fqname != "" { + return k.fqname, nil + } + err := k.Decompose() + if err != nil { + return "", err + } + k.fqname = k.assembleFqName() + return k.fqname, nil +} + +func (k *KpodImage) findImageOnRegistry() error { + searchRegistries, err := GetRegistries() + + if err != nil { + return errors.Wrapf(err, " the image name '%s' is incomplete.", k.Name) + } + + for _, searchRegistry := range searchRegistries { + k.Registry = searchRegistry + err = k.GetManifest() + if err == nil { + k.fqname = k.assembleFqName() + return nil + + } + } + return errors.Errorf("unable to find image on any configured registries") + +} + +// GetManifest tries to GET an images manifest, returns nil on success and err on failure +func (k *KpodImage) GetManifest() error { + pullRef, err := alltransports.ParseImageName(k.assembleFqNameTransport()) + if err != nil { + return errors.Errorf("unable to parse '%s'", k.assembleFqName()) + } + imageSource, err := pullRef.NewImageSource(nil) + if err != nil { + return errors.Errorf("unable to create new image source") + } + _, _, err = imageSource.GetManifest() + if err == nil { + return nil + } + return err +} + +//Decompose breaks up an image name into its parts +func (k *KpodImage) Decompose() error { + k.beenDecomposed = true + k.Transport = "docker://" + decomposeName := k.Name + for _, transport := range TransportNames { + if strings.HasPrefix(k.Name, transport) { + k.Transport = transport + decomposeName = strings.Replace(k.Name, transport, "", -1) + break + } + } + if k.Transport == "dir:" { + return nil + } + var imageError = fmt.Sprintf("unable to parse '%s'\n", k.Name) + imgRef, err := reference.Parse(decomposeName) + if err != nil { + return errors.Wrapf(err, imageError) + } + tagged, isTagged := imgRef.(reference.NamedTagged) + k.Tag = "latest" + if isTagged { + k.Tag = tagged.Tag() + } + k.HasRegistry = true + registry := reference.Domain(imgRef.(reference.Named)) + if registry == "" { + k.HasRegistry = false + } + k.ImageName = reference.Path(imgRef.(reference.Named)) + + // account for image names with directories in them like + // umohnani/get-started:part1 + if k.HasRegistry { + k.Registry = registry + k.fqname = k.assembleFqName() + k.PullName = k.assembleFqName() + + registries, err := getRegistries() + if err != nil { + return nil + } + if StringInSlice(k.Registry, registries) { + return nil + } + // We need to check if the registry name is legit + _, err = net.LookupAddr(k.Registry) + if err == nil { + return nil + } + // Combine the Registry and Image Name together and blank out the Registry Name + k.ImageName = fmt.Sprintf("%s/%s", k.Registry, k.ImageName) + k.Registry = "" + + } + // No Registry means we check the globals registries configuration file + // and assemble a list of candidate sources to try + //searchRegistries, err := GetRegistries() + err = k.findImageOnRegistry() + k.PullName = k.assembleFqName() + if err != nil { + return errors.Wrapf(err, " the image name '%s' is incomplete.", k.Name) + } + return nil +} + +// HasImageLocal returns a bool true if the image is already pulled +func (k *KpodImage) HasImageLocal() bool { + _, err := k.Runtime.GetImage(k.Name) + if err == nil { + return true + } + fqname, _ := k.GetFQName() + + _, err = k.Runtime.GetImage(fqname) + if err == nil { + return true + } + return false +} + +// HasLatest determines if we have the latest image local +func (k *KpodImage) HasLatest() (bool, error) { + if !k.HasImageLocal() { + return false, nil + } + fqname, err := k.GetFQName() + if err != nil { + return false, err + } + pullRef, err := alltransports.ParseImageName(fqname) + if err != nil { + return false, err + } + _, _, err = pullRef.(types.ImageSource).GetManifest() + if err != nil { + return false, err + } + return false, nil +} + +// Pull is a wrapper function to pull and image +func (k *KpodImage) Pull() error { + // If the image hasn't been decomposed yet + if !k.beenDecomposed { + err := k.Decompose() + if err != nil { + return err + } + } + k.Runtime.PullImage(k.PullName, CopyOptions{Writer: os.Stdout, SignaturePolicyPath: k.Runtime.config.SignaturePolicyPath}) + return nil +} + +// GetRegistries gets the searchable registries from the global registration file. +func GetRegistries() ([]string, error) { + registryConfigPath := "" + envOverride := os.Getenv("REGISTRIES_CONFIG_PATH") + if len(envOverride) > 0 { + registryConfigPath = envOverride + } + searchRegistries, err := sysregistries.GetRegistries(&types.SystemContext{SystemRegistriesConfPath: registryConfigPath}) + if err != nil { + return nil, errors.Errorf("unable to parse the registries.conf file") + } + return searchRegistries, nil +} + +// GetInsecureRegistries obtains the list of inseure registries from the global registration file. +func GetInsecureRegistries() ([]string, error) { + registryConfigPath := "" + envOverride := os.Getenv("REGISTRIES_CONFIG_PATH") + if len(envOverride) > 0 { + registryConfigPath = envOverride + } + registries, err := sysregistries.GetInsecureRegistries(&types.SystemContext{SystemRegistriesConfPath: registryConfigPath}) + if err != nil { + return nil, errors.Errorf("unable to parse the registries.conf file") + } + return registries, nil +} + +// getRegistries returns both searchable and insecure registries from the global conf file. +func getRegistries() ([]string, error) { + var r []string + registries, err := GetRegistries() + if err != nil { + return r, err + } + insecureRegistries, err := GetInsecureRegistries() + if err != nil { + return r, err + } + r = append(registries, insecureRegistries...) + return r, nil +} + // ImageFilter is a function to determine whether an image is included in // command output. Images to be outputted are tested using the function. A true // return will include the image, a false return will exclude it. diff --git a/libpod/util.go b/libpod/util.go new file mode 100644 index 00000000..77f73468 --- /dev/null +++ b/libpod/util.go @@ -0,0 +1,34 @@ +package libpod + +import ( + "os" + "path/filepath" +) + +// WriteFile writes a provided string to a provided path +func WriteFile(content string, path string) error { + baseDir := filepath.Dir(path) + if baseDir != "" { + if _, err := os.Stat(path); err != nil { + return err + } + } + f, err := os.Create(path) + defer f.Close() + if err != nil { + return err + } + f.WriteString(content) + f.Sync() + return nil +} + +// StringInSlice determines if a string is in a string slice, returns bool +func StringInSlice(s string, sl []string) bool { + for _, i := range sl { + if i == s { + return true + } + } + return false +}