399 lines
13 KiB
Go
399 lines
13 KiB
Go
package client
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"text/tabwriter"
|
|
"time"
|
|
|
|
"github.com/docker/go-connections/nat"
|
|
"github.com/hyperhq/hyper-api/types"
|
|
"github.com/hyperhq/hyper-api/types/container"
|
|
"github.com/hyperhq/hyper-api/types/filters"
|
|
"github.com/hyperhq/hyper-api/types/network"
|
|
"github.com/hyperhq/hyper-api/types/strslice"
|
|
Cli "github.com/hyperhq/hypercli/cli"
|
|
ropts "github.com/hyperhq/hypercli/opts"
|
|
flag "github.com/hyperhq/hypercli/pkg/mflag"
|
|
"github.com/hyperhq/hypercli/pkg/signal"
|
|
"github.com/hyperhq/hypercli/runconfig/opts"
|
|
"golang.org/x/net/context"
|
|
)
|
|
|
|
// CmdCron is the parent subcommand for all cron commands
|
|
//
|
|
// Usage: hyper cron <COMMAND> [OPTIONS]
|
|
func (cli *DockerCli) CmdCron(args ...string) error {
|
|
cmd := Cli.Subcmd("cron", []string{"COMMAND [OPTIONS]"}, cronUsage(), false)
|
|
cmd.Require(flag.Min, 1)
|
|
err := cmd.ParseFlags(args, true)
|
|
cmd.Usage()
|
|
return err
|
|
}
|
|
|
|
// CmdCronCreate creates a new cron with a given name
|
|
//
|
|
// Usage: hyper cron create [OPTIONS]
|
|
func (cli *DockerCli) CmdCronCreate(args ...string) error {
|
|
cmd := Cli.Subcmd("cron create", []string{"IMAGE"}, "Create a cron job", false)
|
|
var (
|
|
flSecurityGroups = ropts.NewListOpts(nil)
|
|
flEnv = ropts.NewListOpts(opts.ValidateEnv)
|
|
flLabels = ropts.NewListOpts(opts.ValidateEnv)
|
|
flEnvFile = ropts.NewListOpts(nil)
|
|
flVolumes = ropts.NewListOpts(nil)
|
|
flLinks = ropts.NewListOpts(opts.ValidateLink)
|
|
flLabelsFile = ropts.NewListOpts(nil)
|
|
flPublish = ropts.NewListOpts(nil)
|
|
flExpose = ropts.NewListOpts(nil)
|
|
|
|
flName = cmd.String([]string{"-name"}, "", "Cron name")
|
|
flContainerName = cmd.String([]string{"-container-name"}, "", "Cron container name")
|
|
flEntrypoint = cmd.String([]string{"-entrypoint"}, "", "Overwrite the default ENTRYPOINT of the image")
|
|
flNetMode = cmd.String([]string{}, "bridge", "Connect containers to a network, only bridge is supported now")
|
|
flStopSignal = cmd.String([]string{"-stop-signal"}, signal.DefaultStopSignal, fmt.Sprintf("Signal to stop a container, %v by default", signal.DefaultStopSignal))
|
|
flContainerSize = cmd.String([]string{"-size"}, "s4", "The size of cron containers (e.g. s1, s2, s3, s4, m1, m2, m3, l1, l2, l3)")
|
|
flWorkingDir = cmd.String([]string{"w", "-workdir"}, "", "Working directory inside the container")
|
|
flHostname = cmd.String([]string{"h", "-hostname"}, "", "Container host name")
|
|
flNoAutoVolume = cmd.Bool([]string{"-noauto-volume"}, false, "Do not create volumes specified in image")
|
|
flPublishAll = cmd.Bool([]string{"P", "-publish-all"}, false, "Publish all exposed ports to random ports")
|
|
flRestartPolicy = cmd.String([]string{"-restart"}, "no", "Restart policy to apply when a container exits")
|
|
flMailTo = cmd.String([]string{"-mailto"}, "", "Mail to while the cron has something")
|
|
flMailPolicy = cmd.String([]string{"-mail"}, "on-failure", "Mail policy to apply when to send email")
|
|
flAccessKey = cmd.String([]string{"-access-key"}, "", "Access key to run the cron job")
|
|
flSecretKey = cmd.String([]string{"-secret-key"}, "", "Secret key to run the cron job")
|
|
|
|
flMinute = cmd.String([]string{"-minute"}, "0", "The minutes of cron expression")
|
|
flHour = cmd.String([]string{"-hour"}, "0", "The hour of cron expression")
|
|
flDom = cmd.String([]string{"-dom"}, "*", "The day of month of cron expression")
|
|
flDow = cmd.String([]string{"-week"}, "*", "The day of week of cron expression")
|
|
flMonth = cmd.String([]string{"-month"}, "*", "The month of cron expression")
|
|
)
|
|
cmd.Var(&flLabels, []string{"l", "-label"}, "Set meta data on a container")
|
|
cmd.Var(&flLabelsFile, []string{"-label-file"}, "Read in a line delimited file of labels")
|
|
cmd.Var(&flEnv, []string{"e", "-env"}, "Set environment variables")
|
|
cmd.Var(&flEnvFile, []string{"-env-file"}, "Read in a file of environment variables")
|
|
cmd.Var(&flSecurityGroups, []string{"-sg"}, "Security group for each container")
|
|
cmd.Var(&flVolumes, []string{"v", "--volume"}, "Volume for each container")
|
|
cmd.Var(&flLinks, []string{"-link"}, "Add link to another container")
|
|
cmd.Var(&flPublish, []string{"p", "-publish"}, "Publish a container's port(s) to the host")
|
|
cmd.Var(&flExpose, []string{"-expose"}, "Expose a port or a range of ports")
|
|
|
|
cmd.Require(flag.Min, 1)
|
|
err := cmd.ParseFlags(args, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if (*flAccessKey == "" && *flSecretKey != "") || (*flAccessKey != "" && *flSecretKey == "") {
|
|
return fmt.Errorf("You must specify access key and secret key at the same time")
|
|
}
|
|
|
|
var (
|
|
parsedArgs = cmd.Args()
|
|
runCmd strslice.StrSlice
|
|
entrypoint strslice.StrSlice
|
|
image = cmd.Arg(0)
|
|
)
|
|
if len(parsedArgs) > 1 {
|
|
runCmd = strslice.StrSlice(parsedArgs[1:])
|
|
}
|
|
if *flEntrypoint != "" {
|
|
entrypoint = strslice.StrSlice{*flEntrypoint}
|
|
}
|
|
|
|
if _, _, err = cli.client.ImageInspectWithRaw(context.Background(), image, false); err != nil && strings.Contains(err.Error(), "No such image") {
|
|
if err := cli.pullImage(context.Background(), image); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// collect all the environment variables for the container
|
|
envVariables, err := opts.ReadKVStrings(flEnvFile.GetAll(), flEnv.GetAll())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// collect all the labels for the container
|
|
labels, err := opts.ReadKVStrings(flLabelsFile.GetAll(), flLabels.GetAll())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
labels = append(labels, fmt.Sprintf("sh_hyper_instancetype=%s", *flContainerSize))
|
|
for _, sg := range flSecurityGroups.GetAll() {
|
|
if sg == "" {
|
|
continue
|
|
}
|
|
labels = append(labels, fmt.Sprintf("sh_hyper_sg_%s=yes", sg))
|
|
}
|
|
if *flNoAutoVolume {
|
|
labels = append(labels, "sh_hyper_noauto_volume=true")
|
|
}
|
|
|
|
var (
|
|
domainname string
|
|
hostname = *flHostname
|
|
parts = strings.SplitN(hostname, ".", 2)
|
|
)
|
|
if len(parts) > 1 {
|
|
hostname = parts[0]
|
|
domainname = parts[1]
|
|
}
|
|
ports, portBindings, err := nat.ParsePortSpecs(flPublish.GetAll())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Merge in exposed ports to the map of published ports
|
|
for _, e := range flExpose.GetAll() {
|
|
if strings.Contains(e, ":") {
|
|
return fmt.Errorf("Invalid port format for --expose: %s", e)
|
|
}
|
|
//support two formats for expose, original format <portnum>/[<proto>] or <startport-endport>/[<proto>]
|
|
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 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 err
|
|
}
|
|
if _, exists := ports[p]; !exists {
|
|
ports[p] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
var binds []string
|
|
// add any bind targets to the list of container volumes
|
|
for bind := range flVolumes.GetMap() {
|
|
if arr := opts.VolumeSplitN(bind, 2); len(arr) > 1 {
|
|
// after creating the bind mount we want to delete it from the flVolumes values because
|
|
// we do not want bind mounts being committed to image configs
|
|
binds = append(binds, bind)
|
|
flVolumes.Delete(bind)
|
|
}
|
|
}
|
|
restartPolicy, err := opts.ParseRestartPolicy(*flRestartPolicy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
config := &container.Config{
|
|
Hostname: hostname,
|
|
Domainname: domainname,
|
|
Tty: true,
|
|
ExposedPorts: ports,
|
|
Env: envVariables,
|
|
Cmd: runCmd,
|
|
Image: image,
|
|
Volumes: flVolumes.GetMap(),
|
|
Entrypoint: entrypoint,
|
|
WorkingDir: *flWorkingDir,
|
|
Labels: opts.ConvertKVStringsToMap(labels),
|
|
StopSignal: *flStopSignal,
|
|
}
|
|
|
|
hostConfig := &container.HostConfig{
|
|
Binds: binds,
|
|
PortBindings: portBindings,
|
|
Links: flLinks.GetAll(),
|
|
PublishAllPorts: *flPublishAll,
|
|
NetworkMode: container.NetworkMode(*flNetMode),
|
|
RestartPolicy: restartPolicy,
|
|
}
|
|
networkingConfig := &network.NetworkingConfig{
|
|
EndpointsConfig: make(map[string]*network.EndpointSettings),
|
|
}
|
|
|
|
if hostConfig.NetworkMode.IsUserDefined() && len(hostConfig.Links) > 0 {
|
|
epConfig := networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)]
|
|
if epConfig == nil {
|
|
epConfig = &network.EndpointSettings{}
|
|
}
|
|
epConfig.Links = make([]string, len(hostConfig.Links))
|
|
copy(epConfig.Links, hostConfig.Links)
|
|
networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = epConfig
|
|
}
|
|
|
|
if *flMinute == "0" && *flHour == "0" && *flDom == "*" && *flDow == "*" && *flMonth == "*" {
|
|
return fmt.Errorf("must specify at least one schedule")
|
|
}
|
|
|
|
sv := types.Cron{
|
|
ContainerName: *flContainerName,
|
|
Schedule: *flMinute + " " + *flHour + " " + *flDom + " " + *flMonth + " " + *flDow,
|
|
OwnerEmail: *flMailTo,
|
|
Config: config,
|
|
HostConfig: hostConfig,
|
|
NetConfig: networkingConfig,
|
|
AccessKey: *flAccessKey,
|
|
SecretKey: *flSecretKey,
|
|
MailPolicy: *flMailPolicy,
|
|
}
|
|
|
|
_, err = cli.client.CronCreate(context.Background(), *flName, sv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(cli.out, "Cron %s is created.\n", *flName)
|
|
return nil
|
|
}
|
|
|
|
// CmdCronDelete deletes one or more crons
|
|
//
|
|
// Usage: hyper cron rm cron [cron...]
|
|
func (cli *DockerCli) CmdCronRm(args ...string) error {
|
|
cmd := Cli.Subcmd("cron rm", []string{"cron [cron...]"}, "Remove one or more cron job", false)
|
|
cmd.Require(flag.Min, 1)
|
|
if err := cmd.ParseFlags(args, true); err != nil {
|
|
return err
|
|
}
|
|
|
|
status := 0
|
|
for _, sn := range cmd.Args() {
|
|
if err := cli.client.CronDelete(context.Background(), sn); err != nil {
|
|
fmt.Fprintf(cli.err, "%s\n", err)
|
|
status = 1
|
|
continue
|
|
}
|
|
fmt.Fprintf(cli.out, "%s\n", sn)
|
|
}
|
|
if status != 0 {
|
|
return Cli.StatusError{StatusCode: status}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CmdCronLs lists all the crons
|
|
//
|
|
// Usage: hyper cron ls [OPTIONS]
|
|
func (cli *DockerCli) CmdCronLs(args ...string) error {
|
|
cmd := Cli.Subcmd("cron ls", nil, "Lists all crons", true)
|
|
|
|
flFilter := ropts.NewListOpts(nil)
|
|
cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided")
|
|
|
|
cmd.Require(flag.Exact, 0)
|
|
err := cmd.ParseFlags(args, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Consolidate all filter flags, and sanity check them early.
|
|
// They'll get process after get response from server.
|
|
cronFilterArgs := filters.NewArgs()
|
|
for _, f := range flFilter.GetAll() {
|
|
if cronFilterArgs, err = filters.ParseFlag(f, cronFilterArgs); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
options := types.CronListOptions{
|
|
Filters: cronFilterArgs,
|
|
}
|
|
|
|
crons, err := cli.client.CronList(context.Background(), options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
|
|
fmt.Fprintf(w, "Name\tSchedule\tImage\tCommand\n")
|
|
for _, cron := range crons {
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", cron.Name, cron.Schedule, cron.Config.Image, strings.Join([]string(cron.Config.Cmd), " "))
|
|
}
|
|
|
|
w.Flush()
|
|
return nil
|
|
}
|
|
|
|
// CmdCronInspect
|
|
//
|
|
// Usage: hyper cron inspect [OPTIONS] CRON [CRON...]
|
|
func (cli *DockerCli) CmdCronInspect(args ...string) error {
|
|
cmd := Cli.Subcmd("cron inspect", []string{"cron [cron...]"}, "Display detailed information on the given cron", true)
|
|
tmplStr := cmd.String([]string{"f", "-format"}, "", "Format the output using the given go template")
|
|
|
|
cmd.Require(flag.Min, 1)
|
|
cmd.ParseFlags(args, true)
|
|
|
|
if err := cmd.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
inspectSearcher := func(name string) (interface{}, []byte, error) {
|
|
i, err := cli.client.CronInspect(ctx, name)
|
|
return i, nil, err
|
|
}
|
|
|
|
return cli.inspectElements(*tmplStr, cmd.Args(), inspectSearcher)
|
|
}
|
|
|
|
// CmdCronHistory
|
|
//
|
|
// Usage: hyper cron history [OPTIONS] CRON
|
|
func (cli *DockerCli) CmdCronHistory(args ...string) error {
|
|
cmd := Cli.Subcmd("cron history", []string{"cron"}, "Show the execution history (last 100) of a cron job", true)
|
|
flSince := cmd.String([]string{"-since"}, "", "Show history since timestamp")
|
|
flTail := cmd.String([]string{"-tail"}, "all", "Number of lines to show from the end of the history")
|
|
|
|
cmd.Require(flag.Min, 1)
|
|
cmd.ParseFlags(args, true)
|
|
|
|
if err := cmd.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := context.Background()
|
|
name := cmd.Args()[0]
|
|
cronHistory, err := cli.client.CronHistory(ctx, name, *flSince, *flTail)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
|
|
fmt.Fprintf(w, "Container\tStart\tEnd\tStatus\tMessage\n")
|
|
for _, h := range cronHistory {
|
|
status := h.Status
|
|
if status == "success" {
|
|
status = "done"
|
|
} else if status == "error" {
|
|
status = "failed"
|
|
} else {
|
|
status = "-"
|
|
}
|
|
if h.FinishedAt == 0 {
|
|
fmt.Fprintf(w, "%s\t%v\t-\t%s\t%s\n", h.Container, time.Unix(h.StartedAt, 0).UTC(), status, h.Message)
|
|
} else {
|
|
fmt.Fprintf(w, "%s\t%v\t%v\t%s\t%s\n", h.Container, time.Unix(h.StartedAt, 0).UTC(), time.Unix(h.FinishedAt, 0).UTC(), status, h.Message)
|
|
}
|
|
}
|
|
|
|
w.Flush()
|
|
return nil
|
|
}
|
|
|
|
func cronUsage() string {
|
|
cronCommands := [][]string{
|
|
{"create", "Create a cron job"},
|
|
{"inspect", "Display detailed information on the given cron"},
|
|
{"ls", "List all crons"},
|
|
{"history", "Show execution history of a cron job"},
|
|
{"rm", "Remove one or more cron job"},
|
|
}
|
|
|
|
help := "Commands:\n"
|
|
|
|
for _, cmd := range cronCommands {
|
|
help += fmt.Sprintf(" %-25.25s%s\n", cmd[0], cmd[1])
|
|
}
|
|
|
|
help += fmt.Sprintf("\nRun 'hyper cron COMMAND --help' for more information on a command.")
|
|
return help
|
|
}
|