├── .gitignore ├── Dockerfile ├── Gopkg.lock ├── Gopkg.toml ├── README.md ├── build-release.sh ├── circle.yml ├── commands.go ├── completions └── _ironcli ├── flags.go ├── install.sh ├── lambda.go ├── main.go ├── mq.go ├── mq_util.go ├── run.go └── worker.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 4 | *.o 5 | *.a 6 | *.so 7 | 8 | # Folders 9 | _obj 10 | _test 11 | 12 | # Architecture specific extensions/prefixes 13 | *.[568vq] 14 | [568vq].out 15 | 16 | *.cgo1.go 17 | *.cgo2.c 18 | _cgo_defun.c 19 | _cgo_gotypes.go 20 | _cgo_export.* 21 | 22 | _testmain.go 23 | 24 | *.exe 25 | *.test 26 | *.prof 27 | 28 | 29 | .idea/ 30 | ironcli_* 31 | ironcli 32 | iron.json 33 | ironcli-* 34 | mytoken.txt 35 | /iron 36 | install 37 | 38 | vendor 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM iron/busybox 2 | 3 | ADD ironcli /usr/local/bin/iron 4 | 5 | ENTRYPOINT ["/usr/local/bin/iron"] 6 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "master" 6 | name = "github.com/Azure/go-ansiterm" 7 | packages = [".","winterm"] 8 | revision = "d6e3b3328b783f23731bc4d058875b0371ff8109" 9 | 10 | [[projects]] 11 | name = "github.com/Sirupsen/logrus" 12 | packages = ["."] 13 | revision = "f006c2ac4710855cf0f916dd6b77acf6b048dc6e" 14 | version = "v1.0.3" 15 | 16 | [[projects]] 17 | name = "github.com/aws/aws-sdk-go" 18 | packages = ["aws","aws/awserr","aws/awsutil","aws/client","aws/client/metadata","aws/corehandlers","aws/credentials","aws/credentials/ec2rolecreds","aws/defaults","aws/ec2metadata","aws/request","aws/session","private/endpoints","private/protocol","private/protocol/json/jsonutil","private/protocol/jsonrpc","private/protocol/rest","private/protocol/restjson","private/signer/v4","service/lambda"] 19 | revision = "8f2725740345a0561fa09669f5f75f905404ef8b" 20 | version = "v1.1.36" 21 | 22 | [[projects]] 23 | name = "github.com/docker/docker" 24 | packages = ["pkg/jsonlog","pkg/jsonmessage","pkg/system","pkg/term","pkg/term/windows"] 25 | revision = "65370be888d940899593a001024f53d6b83b4bb0" 26 | 27 | [[projects]] 28 | name = "github.com/docker/go-units" 29 | packages = ["."] 30 | revision = "0dadbb0345b35ec7ef35e228dabb8de89a65bf52" 31 | version = "v0.3.2" 32 | 33 | [[projects]] 34 | name = "github.com/fatih/color" 35 | packages = ["."] 36 | revision = "533cd7fd8a85905f67a1753afb4deddc85ea174f" 37 | 38 | [[projects]] 39 | name = "github.com/fsouza/go-dockerclient" 40 | packages = [".","external/github.com/Sirupsen/logrus","external/github.com/docker/docker/opts","external/github.com/docker/docker/pkg/archive","external/github.com/docker/docker/pkg/fileutils","external/github.com/docker/docker/pkg/homedir","external/github.com/docker/docker/pkg/idtools","external/github.com/docker/docker/pkg/ioutils","external/github.com/docker/docker/pkg/longpath","external/github.com/docker/docker/pkg/pools","external/github.com/docker/docker/pkg/promise","external/github.com/docker/docker/pkg/stdcopy","external/github.com/docker/docker/pkg/system","external/github.com/docker/go-units","external/github.com/hashicorp/go-cleanhttp","external/github.com/opencontainers/runc/libcontainer/user","external/golang.org/x/net/context","external/golang.org/x/sys/unix"] 41 | revision = "1d4f4ae73768d3ca16a6fb964694f58dc5eba601" 42 | version = "docker-1.9/go-1.4" 43 | 44 | [[projects]] 45 | name = "github.com/go-ini/ini" 46 | packages = ["."] 47 | revision = "f280b3ba517bf5fc98922624f21fb0e7a92adaec" 48 | version = "v1.30.3" 49 | 50 | [[projects]] 51 | name = "github.com/iron-io/iron_go3" 52 | packages = ["api","config","mq","worker"] 53 | revision = "a4a7f74b73ac79e9ae1e5db51f7ef18cadaf4a20" 54 | 55 | [[projects]] 56 | name = "github.com/iron-io/lambda" 57 | packages = ["lambda"] 58 | revision = "197598b21c6918d143244cc69d4d443f062d3c78" 59 | version = "v0.1.0" 60 | 61 | [[projects]] 62 | name = "github.com/jmespath/go-jmespath" 63 | packages = ["."] 64 | revision = "3433f3ea46d9f8019119e7dd41274e112a2359a9" 65 | version = "0.2.2" 66 | 67 | [[projects]] 68 | name = "github.com/mattn/go-colorable" 69 | packages = ["."] 70 | revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" 71 | version = "v0.0.9" 72 | 73 | [[projects]] 74 | name = "github.com/mattn/go-isatty" 75 | packages = ["."] 76 | revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" 77 | version = "v0.0.3" 78 | 79 | [[projects]] 80 | name = "github.com/satori/go.uuid" 81 | packages = ["."] 82 | revision = "879c5887cd475cd7864858769793b2ceb0d44feb" 83 | version = "v1.1.0" 84 | 85 | [[projects]] 86 | branch = "master" 87 | name = "golang.org/x/crypto" 88 | packages = ["ssh/terminal"] 89 | revision = "bd6f299fb381e4c3393d1c4b1f0b94f5e77650c8" 90 | 91 | [[projects]] 92 | branch = "master" 93 | name = "golang.org/x/sys" 94 | packages = ["unix","windows"] 95 | revision = "95c6576299259db960f6c5b9b69ea52422860fce" 96 | 97 | [solve-meta] 98 | analyzer-name = "dep" 99 | analyzer-version = 1 100 | inputs-digest = "ce3ece95eb2a089e91f55e017b668f03b8bf261712140c5e9a6e63947f5db192" 101 | solver-name = "gps-cdcl" 102 | solver-version = 1 103 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | name = "github.com/aws/aws-sdk-go" 26 | version = "~1.1.9" 27 | 28 | [[constraint]] 29 | name = "github.com/docker/docker" 30 | revision = "65370be888d940899593a001024f53d6b83b4bb0" 31 | 32 | [[constraint]] 33 | name = "github.com/fatih/color" 34 | revision = "533cd7fd8a85905f67a1753afb4deddc85ea174f" 35 | 36 | [[constraint]] 37 | name = "github.com/iron-io/iron_go3" 38 | revision = "a4a7f74b73ac79e9ae1e5db51f7ef18cadaf4a20" 39 | 40 | [[constraint]] 41 | name = "github.com/iron-io/lambda" 42 | version = "0.1.0" 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IronCLI 2 | 3 | Go version of the Iron.io command line tools. 4 | 5 | ## Install 6 | 7 | ### Quick and Easy (Recommended) 8 | 9 | `curl -sSL https://cli.iron.io/install | sh` 10 | 11 | If you're concerned about the [potential insecurity](http://curlpipesh.tumblr.com/) 12 | of using `curl | sh`, feel free to use a two-step version of our installation and examine our 13 | installation script: 14 | 15 | ```bash 16 | curl -f -sSL https://cli.iron.io/install -O 17 | sh install 18 | ``` 19 | 20 | #### `curl | sh` as an Installation Method? 21 | 22 | We'd like to explain why we're telling you to `curl | sh` to install this software. 23 | The script at https://cli.iron.io/install has some relatively simple logic to download the 24 | right `ironcli` binary for your platform. When you run that script by piping the `curl` output 25 | into `sh`, you're trusting us that it's safe and won't harm your computer. We hope that you do! 26 | But if you don't please see the section just below this one on how to download and run the binary 27 | yourself without an install script. 28 | 29 | ### See [other installation methods](#other-installation-methods) for more options. 30 | 31 | ## Getting Started 32 | 33 | #### Before Getting Started 34 | 35 | Before you can use IronWorker, be sure you've [created a free account with 36 | Iron.io](http://www.iron.io) and [setup your Iron.io credentials on your 37 | system](http://dev.iron.io/worker/reference/configuration/) (either in a json 38 | file or using ENV variables). You only need to do that once for your machine. If 39 | you've done that, then you can continue. 40 | 41 | [See the official docs](http://dev.iron.io/worker/cli/) for more detailed info on using Docker for IronWorker. 42 | 43 | #### Actually Getting Started 44 | 45 | The easiest way to get started is by digging around. 46 | 47 | `$ iron --help` for example usage and a list of commands 48 | 49 | ## Contributing 50 | 51 | Give us a pull request! File a bug! 52 | 53 | Since go1.5, we are lab rats in the go1.5 vendoring experiment. This eliminates 54 | the need to modify import paths and depend on package maintainers not to break things. 55 | For more info, see: . 56 | 57 | We use [dep](https://github.com/golang/dep) to manage the vendoring. To build ironcli: 58 | 59 | ```sh 60 | dep ensure 61 | go build 62 | ``` 63 | 64 | ## Other Installation Methods 65 | 66 | ### Download Yourself 67 | 68 | Grab the latest version for your system on the [Releases](https://github.com/iron-io/ironcli/releases) page. 69 | 70 | You can either run the binary directly or add somewhere in your $PATH. 71 | 72 | ### Use the iron/cli Docker image 73 | 74 | If you have Docker installed, then you don't need to install anything else to use this. 75 | All the commands are the same, but instead of starting the command with `iron`, change it to: 76 | 77 | ```sh 78 | docker run --rm -it -v "$PWD":/app -w /app iron/cli ... 79 | ``` 80 | 81 | If you're using the Docker image, you either need to have your `iron.json` file in the local directory (it won't pick it up from $HOME), 82 | or set your Iron credentials in environment variables: 83 | 84 | ```sh 85 | export IRON_TOKEN=YOURTOKEN 86 | export IRON_PROJECT_ID=YOURPROJECT_ID 87 | ``` 88 | 89 | And then use `-e` flags with the docker run command: 90 | 91 | ```sh 92 | docker run --rm -it -e IRON_TOKEN -e IRON_PROJECT_ID -v "$PWD":/app -w /app iron/cli ... 93 | ``` 94 | 95 | on OSX with [HomeBrew](https://brew.sh): 96 | 97 | ```sh 98 | brew install ironcli 99 | ``` 100 | 101 | -------------------------------------------------------------------------------- /build-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ex 3 | 4 | # builds for each OS and then uploads to a fresh github release. 5 | # make an access token first here: https://github.com/settings/tokens 6 | # and save it somewhere. 7 | # 8 | # must have go compiler boot strapped for all OS for go <= 1.4 -- try this: 9 | # % git clone git://github.com/davecheney/golang-crosscompile.git 10 | # % source golang-crosscompile/crosscompile.bash 11 | # % go-crosscompile-build-all 12 | # 13 | # this is not the world's greatest script but it gets the job done, you 14 | # must have installed: {git, curl, python} 15 | 16 | if [ -z "${GH_DEPLOY_KEY}" ]; then 17 | echo "GH_DEPLOY_KEY must be set" 18 | exit 1 19 | fi 20 | 21 | if [ -z "${GH_DEPLOY_USER}" ]; then 22 | echo "GH_DEPLOY_USER must be set" 23 | exit 1 24 | fi 25 | 26 | git fetch --all 27 | git checkout master 28 | git reset --hard origin/master 29 | 30 | # CircleCI has these set in the project 31 | name=${GH_DEPLOY_USER} 32 | tok=${GH_DEPLOY_KEY} 33 | 34 | # bump version 35 | perl -i -pe 's/\d+\.\d+\.\K(\d+)/$1+1/e' main.go 36 | perl -i -pe 's/\d+\.\d+\.\K(\d+)/$1+1/e' install.sh 37 | version=$(grep -Eo "[0-9]+\.[0-9]+\.[0-9]+" install.sh) 38 | 39 | # add to git 40 | git add -u 41 | git commit -m "$version release" 42 | git push origin master 43 | 44 | url='https://api.github.com/repos/iron-io/ironcli/releases' 45 | 46 | # create release 47 | output=$(curl -s -u $name:$tok -d "{\"tag_name\": \"$version\", \"name\": \"$version\"}" $url) 48 | upload_url=$(echo "$output" | python -c 'import json,sys;obj=json.load(sys.stdin);print obj["upload_url"]' | sed -E "s/\{.*//") 49 | html_url=$(echo "$output" | python -c 'import json,sys;obj=json.load(sys.stdin);print obj["html_url"]') 50 | 51 | # NOTE: do the builds after the version has been bumped in main.go 52 | echo "uploading exe..." 53 | GOOS=windows GOARCH=amd64 go build -o bin/ironcli.exe 54 | curl --progress-bar --data-binary "@bin/ironcli.exe" -H "Content-Type: application/octet-stream" -u $name:$tok $upload_url\?name\=ironcli.exe >/dev/null 55 | echo "uploading elf..." 56 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/ironcli_linux 57 | curl --progress-bar --data-binary "@bin/ironcli_linux" -H "Content-Type: application/octet-stream" -u $name:$tok $upload_url\?name\=ironcli_linux >/dev/null 58 | echo "uploading mach-o..." 59 | GOOS=darwin GOARCH=amd64 go build -o bin/ironcli_mac 60 | curl --progress-bar --data-binary "@bin/ironcli_mac" -H "Content-Type: application/octet-stream" -u $name:$tok $upload_url\?name\=ironcli_mac >/dev/null 61 | 62 | echo "Done! Go edit the description: $html_url" 63 | exit 0 64 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | environment: 3 | CHECKOUT_DIR: $HOME/$CIRCLE_PROJECT_REPONAME 4 | GOPATH: $HOME/go 5 | GOROOT: $HOME/golang/go 6 | PATH: $GOROOT/bin:$GOPATH/bin:/$PATH 7 | GH_IRON: $GOPATH/src/github.com/iron-io 8 | GO_PROJECT: ../go/src/github.com/iron-io/$CIRCLE_PROJECT_REPONAME 9 | services: 10 | - docker 11 | 12 | checkout: 13 | post: 14 | - mkdir -p "$GH_IRON" 15 | - cp -R "$CHECKOUT_DIR" "$GH_IRON/$CIRCLE_PROJECT_REPONAME" 16 | 17 | dependencies: 18 | pre: 19 | - wget https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz 20 | - mkdir -p $HOME/golang 21 | - tar -C $HOME/golang -xvzf go1.8.3.linux-amd64.tar.gz 22 | - go get -u github.com/golang/dep/... 23 | override: 24 | # this was being dumb, don't want it to auto detect we are a go repo b/c vendoring 25 | - which go && go version 26 | - cd $GO_PROJECT && dep ensure 27 | 28 | test: 29 | pre: 30 | - dep ensure: 31 | pwd: $GO_PROJECT 32 | override: 33 | - go build .: 34 | pwd: $GO_PROJECT 35 | 36 | deployment: 37 | this: 38 | branch: release 39 | commands: 40 | - echo $DOCKER_HUB_CREDS > $HOME/.dockercfg 41 | - git config --global user.name "Tony Stark" 42 | - git config --global user.email "deploys@iron.io" 43 | - cd $GO_PROJECT && dep ensure 44 | - cd $GO_PROJECT && ./build-release.sh 45 | - cd $GO_PROJECT && go build 46 | - docker build -t iron/cli:latest $GO_PROJECT 47 | - docker tag iron/cli:latest iron/cli:$(grep -Eo "[0-9]+\.[0-9]+\.[0-9]+" $GO_PROJECT/install.sh) 48 | - docker push iron/cli 49 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Contains each command and its configuration 4 | 5 | // TODO(reed): fix: empty schedule payload not working ? 6 | 7 | import ( 8 | "encoding/base64" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "io/ioutil" 13 | "math/big" 14 | "net/http" 15 | "os" 16 | "strings" 17 | "time" 18 | 19 | "github.com/iron-io/iron_go3/config" 20 | "github.com/iron-io/iron_go3/worker" 21 | ) 22 | 23 | // TODO(reed): default flags for everybody 24 | 25 | // The idea is: 26 | // parse flags -- if help, Usage() && quit 27 | // -> validate arguments, configure command 28 | // -> configure client 29 | // -> run command 30 | // 31 | // if anything goes wrong, peace 32 | type Command interface { 33 | Flags(...string) error // parse subcommand specific flags 34 | Args() error // validate arguments 35 | Config() error // configure env variables 36 | Usage() // custom command help TODO(reed): all local now? 37 | Run() // cmd specific 38 | } 39 | 40 | // A command is the base for all commands implementing the Command interface. 41 | type command struct { 42 | wrkr worker.Worker 43 | flags *WorkerFlags 44 | hud_URL_str string 45 | token *string 46 | projectID *string 47 | } 48 | 49 | // Deal with panics from iron_go3. 50 | func loadConfig(product, env string) (settings config.Settings, err error) { 51 | defer func() { 52 | if r := recover(); r != nil { 53 | settings = config.Settings{} 54 | err = errors.New(r.(string)) 55 | } 56 | }() 57 | 58 | return config.ConfigWithEnv(product, env), err 59 | } 60 | 61 | // All Commands will do similar configuration 62 | func (bc *command) Config() error { 63 | var err error 64 | bc.wrkr.Settings, err = loadConfig("iron_worker", *envFlag) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | if *projectIDFlag != "" { 70 | bc.wrkr.Settings.ProjectId = *projectIDFlag 71 | } 72 | if *tokenFlag != "" { 73 | bc.wrkr.Settings.Token = *tokenFlag 74 | } 75 | 76 | if bc.wrkr.Settings.ProjectId == "" { 77 | return errors.New("did not find project id in any config files or env variables") 78 | } 79 | if bc.wrkr.Settings.Token == "" { 80 | return errors.New("did not find token in any config files or env variables") 81 | } 82 | 83 | bc.hud_URL_str = `Check https://hud-e.iron.io/worker/projects/` + bc.wrkr.Settings.ProjectId + "/" 84 | 85 | fmt.Println(LINES, `Configuring client`) 86 | 87 | pName, err := projectName(bc.wrkr.Settings) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | fmt.Printf(`%s Project '%s' with id='%s'`, BLANKS, pName, bc.wrkr.Settings.ProjectId) 93 | fmt.Println() 94 | return nil 95 | } 96 | 97 | func projectName(config config.Settings) (string, error) { 98 | // get project name -- go api won't play ball 99 | resp, err := http.Get(fmt.Sprintf("%s://%s:%d/%s/projects/%s?oauth=%s", 100 | config.Scheme, config.Host, config.Port, 101 | config.ApiVersion, config.ProjectId, config.Token)) 102 | 103 | if err != nil { 104 | return "", err 105 | } 106 | 107 | var reply struct { 108 | Name string `json:"name"` 109 | } 110 | err = json.NewDecoder(resp.Body).Decode(&reply) 111 | return reply.Name, err 112 | } 113 | 114 | type DockerLoginCmd struct { 115 | command 116 | Email *string `json:"email"` 117 | Username *string `json:"username"` 118 | Password *string `json:"password"` 119 | Serveraddress *string `json:"serveraddress"` 120 | } 121 | 122 | type UploadCmd struct { 123 | command 124 | 125 | name *string 126 | config *string 127 | configFile *string 128 | maxConc *int 129 | retries *int 130 | retriesDelay *int 131 | defaultPriority *int 132 | host *string 133 | zip *string 134 | codes worker.Code // for fields, not code 135 | cmd string 136 | envVars *envSlice 137 | } 138 | 139 | type RegisterCmd struct { 140 | command 141 | 142 | name *string 143 | config *string 144 | configFile *string 145 | maxConc *int 146 | retries *int 147 | retriesDelay *int 148 | defaultPriority *int 149 | host *string 150 | codes worker.Code // for fields, not code 151 | cmd string 152 | envVars *envSlice 153 | } 154 | 155 | type QueueCmd struct { 156 | command 157 | 158 | // flags 159 | payload *string 160 | payloadFile *string 161 | priority *int 162 | timeout *int 163 | delay *int 164 | wait *bool 165 | cluster *string 166 | label *string 167 | encryptionKey *string 168 | encryptionKeyFile *string 169 | n *int 170 | 171 | // payload 172 | task worker.Task 173 | } 174 | 175 | type SchedCmd struct { 176 | command 177 | payload *string 178 | payloadFile *string 179 | priority *int 180 | timeout *int 181 | delay *int 182 | maxConc *int 183 | runEvery *int 184 | runTimes *int 185 | cluster *string 186 | endAt *string // time.RubyTime 187 | startAt *string // time.RubyTime 188 | label *string 189 | 190 | sched worker.Schedule 191 | } 192 | 193 | type StatusCmd struct { 194 | command 195 | taskID string 196 | } 197 | 198 | type LogCmd struct { 199 | command 200 | taskID string 201 | } 202 | 203 | func (s *SchedCmd) Flags(args ...string) error { 204 | s.flags = NewWorkerFlagSet() 205 | 206 | s.payload = s.flags.payload() 207 | s.payloadFile = s.flags.payloadFile() 208 | s.priority = s.flags.priority() 209 | s.timeout = s.flags.timeout() 210 | s.delay = s.flags.delay() 211 | s.maxConc = s.flags.maxConc() 212 | s.runEvery = s.flags.runEvery() 213 | s.runTimes = s.flags.runTimes() 214 | s.endAt = s.flags.endAt() 215 | s.startAt = s.flags.startAt() 216 | s.cluster = s.flags.cluster() 217 | s.label = s.flags.label() 218 | 219 | err := s.flags.Parse(args) 220 | if err != nil { 221 | return err 222 | } 223 | 224 | return s.flags.validateAllFlags() 225 | } 226 | 227 | func (s *SchedCmd) Args() error { 228 | if s.flags.NArg() != 1 { 229 | return errors.New("error: schedule takes one argument, a code name") 230 | } 231 | 232 | delay := time.Duration(*s.delay) * time.Second 233 | timeout := time.Duration(*s.timeout) * time.Second 234 | 235 | var priority *int 236 | if *s.priority > -3 && *s.priority < 3 { 237 | priority = s.priority 238 | } 239 | 240 | s.sched = worker.Schedule{ 241 | CodeName: s.flags.Arg(0), 242 | Delay: &delay, 243 | Timeout: &timeout, 244 | Priority: priority, 245 | RunTimes: s.runTimes, 246 | Cluster: *s.cluster, 247 | Label: *s.label, 248 | } 249 | 250 | payload := *s.payload 251 | if *s.payloadFile != "" { 252 | pload, err := ioutil.ReadFile(*s.payloadFile) 253 | if err != nil { 254 | return err 255 | } 256 | payload = string(pload) 257 | } 258 | 259 | if payload != "" { 260 | s.sched.Payload = payload 261 | } else { 262 | s.sched.Payload = "{}" // if we don't set this, it gets a 400 from API. 263 | } 264 | 265 | if *s.endAt != "" { 266 | t, _ := time.Parse(time.RFC3339, *s.endAt) // checked in validateFlags() 267 | s.sched.EndAt = &t 268 | } 269 | if *s.startAt != "" { 270 | t, _ := time.Parse(time.RFC3339, *s.startAt) 271 | s.sched.StartAt = &t 272 | } 273 | if *s.maxConc != unset { 274 | s.sched.MaxConcurrency = s.maxConc 275 | } 276 | if *s.runEvery != unset { 277 | s.sched.RunEvery = s.runEvery 278 | } 279 | 280 | return nil 281 | } 282 | 283 | func (s *SchedCmd) Usage() { 284 | fmt.Fprintln(os.Stderr, `usage: iron worker schedule [OPTIONS] CODE_PACKAGE_NAME`) 285 | s.flags.PrintDefaults() 286 | } 287 | 288 | func (s *SchedCmd) Run() { 289 | fmt.Println(LINES, "Scheduling task '"+s.sched.CodeName+"'") 290 | 291 | ids, err := s.wrkr.Schedule(s.sched) 292 | if err != nil { 293 | fmt.Println(BLANKS, err) 294 | return 295 | } 296 | id := ids[0] 297 | 298 | fmt.Printf("%s Scheduled task with id='%s'\n", BLANKS, id) 299 | fmt.Println(BLANKS, s.hud_URL_str+"scheduled_jobs/"+id+INFO) 300 | } 301 | 302 | func (q *QueueCmd) Flags(args ...string) error { 303 | q.flags = NewWorkerFlagSet() 304 | 305 | q.payload = q.flags.payload() 306 | q.payloadFile = q.flags.payloadFile() 307 | q.priority = q.flags.priority() 308 | q.timeout = q.flags.timeout() 309 | q.delay = q.flags.delay() 310 | q.wait = q.flags.wait() 311 | q.cluster = q.flags.cluster() 312 | q.label = q.flags.label() 313 | q.encryptionKey = q.flags.encryptionKey() 314 | q.encryptionKeyFile = q.flags.encryptionKeyFile() 315 | q.n = q.flags.n() 316 | 317 | err := q.flags.Parse(args) 318 | if err != nil { 319 | return err 320 | } 321 | 322 | return q.flags.validateAllFlags() 323 | } 324 | 325 | // Takes 1 arg for worker name 326 | func (q *QueueCmd) Args() error { 327 | if q.flags.NArg() != 1 { 328 | return errors.New("error: queue takes one argument, a code name") 329 | } 330 | 331 | payload := *q.payload 332 | if *q.payloadFile != "" { 333 | pload, err := ioutil.ReadFile(*q.payloadFile) 334 | if err != nil { 335 | return err 336 | } 337 | payload = string(pload) 338 | } 339 | 340 | delay := time.Duration(*q.delay) * time.Second 341 | timeout := time.Duration(*q.timeout) * time.Second 342 | 343 | var priority int = -3 344 | if *q.priority > -3 && *q.priority < 3 { 345 | priority = *q.priority 346 | } 347 | 348 | encryptionKey := []byte(*q.encryptionKey) 349 | if *q.encryptionKeyFile != "" { 350 | var err error 351 | encryptionKey, err = ioutil.ReadFile(*q.encryptionKeyFile) 352 | if err != nil { 353 | return err 354 | } 355 | } 356 | 357 | if *q.n < 1 { 358 | *q.n = 1 359 | } 360 | 361 | q.task = worker.Task{ 362 | CodeName: q.flags.Arg(0), 363 | Payload: payload, 364 | Priority: priority, 365 | Timeout: &timeout, 366 | Delay: &delay, 367 | Cluster: *q.cluster, 368 | Label: *q.label, 369 | } 370 | 371 | if len(encryptionKey) > 0 { 372 | tasks, err := worker.EncryptPayloads(encryptionKey, q.task) 373 | if err != nil { 374 | return err 375 | } 376 | q.task = tasks[0] 377 | } 378 | 379 | return nil 380 | } 381 | 382 | func (q *QueueCmd) Usage() { 383 | fmt.Fprintln(os.Stderr, `usage: iron worker queue [OPTIONS] CODE_PACKAGE_NAME`) 384 | q.flags.PrintDefaults() 385 | } 386 | 387 | func (q *QueueCmd) Run() { 388 | fmt.Println(LINES, "Queueing task '"+q.task.CodeName+"'") 389 | 390 | tasks := make([]worker.Task, *q.n) 391 | for i := 0; i < *q.n; i++ { 392 | tasks[i] = q.task 393 | } 394 | 395 | ids, err := q.wrkr.TaskQueue(tasks...) 396 | if err != nil { 397 | fmt.Println(BLANKS, err) 398 | return 399 | } 400 | id := ids[0] 401 | 402 | fmt.Printf("%s Queued task with id='%s'\n", BLANKS, id) 403 | fmt.Println(BLANKS, q.hud_URL_str+"tasks/"+id+INFO) 404 | 405 | if *q.wait { 406 | fmt.Println(LINES, yellow("Waiting for task to start running")) 407 | 408 | done := make(chan struct{}) 409 | go runWatch(done, "queued") 410 | q.waitForStatusChange(id, "queued") 411 | done <- struct{}{} 412 | <-done // await pong (to print things well) 413 | 414 | // TODO print actual queued time? 415 | fmt.Println(LINES, yellow("Task running, waiting for completion")) 416 | 417 | done = make(chan struct{}) 418 | go runWatch(done, "running") 419 | ti := q.waitForStatusChange(id, "running", "preparing") 420 | done <- struct{}{} 421 | <-done // wait for pong 422 | if ti.Msg != "" { 423 | fmt.Fprintln(os.Stderr, "error running task:", ti.Msg) 424 | return 425 | } 426 | 427 | log, err := q.wrkr.TaskLog(id) 428 | if err != nil { 429 | fmt.Fprintln(os.Stderr, "error getting log:", err) 430 | return 431 | } 432 | 433 | // TODO print actual run time? 434 | fmt.Println(LINES, green("Done")) 435 | fmt.Println(LINES, "Printing Log:") 436 | fmt.Printf("%s", string(log)) 437 | } 438 | } 439 | 440 | func (q *QueueCmd) waitForStatusChange(taskId string, status ...string) worker.TaskInfo { 441 | outer: 442 | for { 443 | info, err := q.wrkr.TaskInfo(taskId) 444 | if err != nil { 445 | fmt.Fprintln(os.Stderr, "error getting task info:", err) 446 | return info 447 | } 448 | 449 | for _, s := range status { 450 | if info.Status == s { 451 | time.Sleep(100 * time.Millisecond) 452 | continue outer 453 | } 454 | } 455 | 456 | return info 457 | } 458 | } 459 | 460 | func runWatch(done chan struct{}, state string) { 461 | start := time.Now() 462 | var elapsed time.Duration 463 | var h, m, s, ms int64 464 | for { 465 | select { 466 | case <-time.After(time.Millisecond): 467 | case <-done: 468 | fmt.Fprintln(os.Stdout, LINES, state+":", fmt.Sprintf("%v:%v:%v:%v\r", h, m, s, ms)) 469 | done <- struct{}{} // pong 470 | return 471 | } 472 | elapsed = time.Since(start) 473 | 474 | h = mod(elapsed.Hours(), 24) 475 | m = mod(elapsed.Minutes(), 60) 476 | s = mod(elapsed.Seconds(), 60) 477 | ms = mod(float64(elapsed.Nanoseconds())/1000, 100) 478 | 479 | fmt.Fprint(os.Stdout, LINES, " "+state+":", fmt.Sprintf(" %v:%v:%v:%v\r", h, m, s, ms)) 480 | } 481 | } 482 | 483 | // mod calculates the modulos of a float64 against and int64. 484 | func mod(val float64, mod int64) int64 { 485 | raw := big.NewInt(int64(val)) 486 | return raw.Mod(raw, big.NewInt(mod)).Int64() 487 | } 488 | 489 | func (s *StatusCmd) Flags(args ...string) error { 490 | s.flags = NewWorkerFlagSet() 491 | err := s.flags.Parse(args) 492 | if err != nil { 493 | return err 494 | } 495 | 496 | return s.flags.validateAllFlags() 497 | } 498 | 499 | // Takes one parameter, the task_id to acquire status of 500 | func (s *StatusCmd) Args() error { 501 | if s.flags.NArg() != 1 { 502 | return errors.New("error: status takes one argument, a task_id") 503 | } 504 | s.taskID = s.flags.Arg(0) 505 | return nil 506 | } 507 | 508 | func (s *StatusCmd) Usage() { 509 | fmt.Fprintln(os.Stderr, `usage: iron worker status [OPTIONS] task_id`) 510 | s.flags.PrintDefaults() 511 | } 512 | 513 | func (s *StatusCmd) Run() { 514 | fmt.Println(LINES, `Getting status of task with id='`+s.taskID+`'`) 515 | taskInfo, err := s.wrkr.TaskInfo(s.taskID) 516 | if err != nil { 517 | fmt.Println(err) 518 | } 519 | fmt.Println(BLANKS, taskInfo.Status) 520 | } 521 | 522 | func (l *LogCmd) Flags(args ...string) error { 523 | l.flags = NewWorkerFlagSet() 524 | err := l.flags.Parse(args) 525 | if err != nil { 526 | return err 527 | } 528 | return l.flags.validateAllFlags() 529 | } 530 | 531 | // Takes one parameter, the task_id to log 532 | func (l *LogCmd) Args() error { 533 | if l.flags.NArg() < 1 { 534 | return errors.New("error: log takes one argument, a task_id") 535 | } 536 | l.taskID = l.flags.Arg(0) 537 | return nil 538 | } 539 | 540 | func (l *LogCmd) Usage() { 541 | fmt.Fprintln(os.Stderr, `usage: iron worker log [OPTIONS] task_id`) 542 | l.flags.PrintDefaults() 543 | } 544 | 545 | func (l *LogCmd) Run() { 546 | fmt.Println(LINES, "Getting log for task with id='"+l.taskID+"'") 547 | out, err := l.wrkr.TaskLog(l.taskID) 548 | if err != nil { 549 | fmt.Println(err) 550 | return 551 | } 552 | fmt.Println(string(out)) 553 | } 554 | func (l *DockerLoginCmd) Flags(args ...string) error { 555 | l.flags = NewWorkerFlagSet() 556 | 557 | l.Email = l.flags.dockerRepoEmail() 558 | l.Password = l.flags.dockerRepoPass() 559 | l.Serveraddress = l.flags.dockerRepoUrl() 560 | l.Username = l.flags.dockerRepoUserName() 561 | 562 | err := l.flags.Parse(args) 563 | if err != nil { 564 | return err 565 | } 566 | return l.flags.validateAllFlags() 567 | } 568 | 569 | // Takes one parameter, the task_id to log 570 | func (l *DockerLoginCmd) Args() error { 571 | if *l.Email == "" || *l.Username == "" || *l.Password == "" || l.Email == nil || l.Username == nil || l.Password == nil { 572 | return errors.New("you should set email(-e), password(-p), username(-u) parameters") 573 | } 574 | 575 | return nil 576 | } 577 | 578 | func (l *DockerLoginCmd) Usage() { 579 | fmt.Fprintln(os.Stderr, `usage: iron docker login -u -p -e -url`) 580 | l.flags.PrintDefaults() 581 | } 582 | 583 | func (l *DockerLoginCmd) Run() { 584 | fmt.Println(LINES, "Storing docker repo credentials") 585 | 586 | //{"username": "string", "password": "string", "email": "string", "serveraddress" : "string", "auth": ""} 587 | bytes, err := json.Marshal(*l) 588 | if err != nil { 589 | fmt.Fprintf(os.Stderr, "error marshaling credentials to json: %v", err) 590 | return 591 | } 592 | authString := base64.StdEncoding.EncodeToString(bytes) 593 | 594 | auth := map[string]string{ 595 | "auth": authString, 596 | } 597 | msg, err := dockerLogin(&l.wrkr, &auth) 598 | if err != nil { 599 | fmt.Fprintln(os.Stderr, err) 600 | return 601 | } 602 | fmt.Println(BLANKS, green(`Added docker repo credentials: `+msg)) 603 | } 604 | 605 | func (u *UploadCmd) Flags(args ...string) error { 606 | u.flags = NewWorkerFlagSet() 607 | u.name = u.flags.name() 608 | u.maxConc = u.flags.maxConc() 609 | u.retries = u.flags.retries() 610 | u.retriesDelay = u.flags.retriesDelay() 611 | u.defaultPriority = u.flags.defaultPriority() 612 | u.config = u.flags.config() 613 | u.configFile = u.flags.configFile() 614 | u.zip = u.flags.zip() 615 | u.envVars = u.flags.envVars() 616 | 617 | err := u.flags.Parse(args) 618 | if err != nil { 619 | return err 620 | } 621 | return u.flags.validateAllFlags() 622 | } 623 | 624 | // `iron worker upload [--zip ZIPFILE] --name NAME IMAGE [COMMAND]` 625 | func (u *UploadCmd) Args() error { 626 | if u.flags.NArg() < 1 { 627 | return errors.New("command takes at least one argument. see -help") 628 | } 629 | 630 | u.codes.Command = strings.TrimSpace(strings.Join(u.flags.Args()[1:], " ")) 631 | u.codes.Image = u.flags.Arg(0) 632 | 633 | if *u.name == "" { 634 | return errors.New("must specify -name for your worker") 635 | } else { 636 | u.codes.Name = *u.name 637 | } 638 | 639 | if *u.zip != "" { 640 | // make sure it exists and it's a zip 641 | if !strings.HasSuffix(*u.zip, ".zip") { 642 | return errors.New("file extension must be .zip, got: " + *u.zip) 643 | } 644 | if _, err := os.Stat(*u.zip); err != nil { 645 | return err 646 | } 647 | } 648 | 649 | if *u.retries != unset { 650 | u.codes.Retries = u.retries 651 | } 652 | if *u.retriesDelay != unset { 653 | u.codes.RetriesDelay = u.retriesDelay 654 | } 655 | 656 | if *u.maxConc != unset { 657 | u.codes.MaxConcurrency = *u.maxConc 658 | } 659 | u.codes.Config = *u.config 660 | if *u.defaultPriority != unset { 661 | u.codes.DefaultPriority = *u.defaultPriority 662 | } 663 | 664 | if u.host != nil && *u.host != "" { 665 | u.codes.Host = *u.host 666 | } 667 | 668 | if *u.configFile != "" { 669 | pload, err := ioutil.ReadFile(*u.configFile) 670 | if err != nil { 671 | return err 672 | } 673 | u.codes.Config = string(pload) 674 | } 675 | 676 | if *u.envVars != nil { 677 | if envSlice, ok := u.envVars.Get().(envSlice); ok { 678 | envVarsMap := make(map[string]string, len(envSlice)) 679 | for _, envItem := range envSlice { 680 | envVarsMap[envItem.Name] = envItem.Value 681 | } 682 | u.codes.EnvVars = envVarsMap 683 | } 684 | } 685 | 686 | return nil 687 | } 688 | 689 | func (u *UploadCmd) Usage() { 690 | fmt.Fprintln(os.Stderr, `usage: iron worker upload [-zip my.zip] -name NAME [OPTIONS] some/image[:tag] [command...]`) 691 | u.flags.PrintDefaults() 692 | } 693 | 694 | func (u *UploadCmd) Run() { 695 | if u.codes.Host != "" { 696 | fmt.Println(LINES, `Spinning up '`+u.codes.Name+`'`) 697 | } else { 698 | fmt.Println(LINES, `Uploading worker '`+u.codes.Name+`'`) 699 | } 700 | code, err := u.wrkr.CodePackageZipUpload(*u.zip, u.codes) 701 | if err != nil { 702 | fmt.Println(err) 703 | return 704 | } 705 | if code.Host != "" { 706 | fmt.Println(BLANKS, green(`Hosted at: '`+code.Host+`'`)) 707 | } else { 708 | fmt.Println(BLANKS, green(`Uploaded code package with id='`+code.Id+`'`)) 709 | } 710 | fmt.Println(BLANKS, green(u.hud_URL_str+"codes/"+code.Id+INFO)) 711 | } 712 | 713 | func (u *RegisterCmd) Flags(args ...string) error { 714 | u.flags = NewWorkerFlagSet() 715 | u.name = u.flags.name() 716 | u.maxConc = u.flags.maxConc() 717 | u.retries = u.flags.retries() 718 | u.retriesDelay = u.flags.retriesDelay() 719 | u.defaultPriority = u.flags.defaultPriority() 720 | u.config = u.flags.config() 721 | u.configFile = u.flags.configFile() 722 | u.envVars = u.flags.envVars() 723 | 724 | err := u.flags.Parse(args) 725 | if err != nil { 726 | return err 727 | } 728 | return u.flags.validateAllFlags() 729 | } 730 | 731 | // `iron worker register IMAGE` 732 | func (u *RegisterCmd) Args() error { 733 | if u.flags.NArg() < 1 { 734 | return errors.New("command takes at least one argument. see -help") 735 | } 736 | 737 | u.codes.Command = strings.TrimSpace(strings.Join(u.flags.Args()[1:], " ")) 738 | u.codes.Image = u.flags.Arg(0) 739 | 740 | if u.name != nil && *u.name != "" { 741 | u.codes.Name = *u.name 742 | } else { 743 | u.codes.Name = u.codes.Image 744 | if strings.ContainsRune(u.codes.Name, ':') { 745 | arr := strings.SplitN(u.codes.Name, ":", 2) 746 | u.codes.Name = arr[0] 747 | } 748 | } 749 | 750 | if *u.retries != unset { 751 | u.codes.Retries = u.retries 752 | } 753 | if *u.retriesDelay != unset { 754 | u.codes.RetriesDelay = u.retriesDelay 755 | } 756 | if *u.maxConc != unset { 757 | u.codes.MaxConcurrency = *u.maxConc 758 | } 759 | u.codes.Config = *u.config 760 | u.codes.DefaultPriority = *u.defaultPriority 761 | 762 | if u.host != nil && *u.host != "" { 763 | u.codes.Host = *u.host 764 | } 765 | 766 | if *u.configFile != "" { 767 | pload, err := ioutil.ReadFile(*u.configFile) 768 | if err != nil { 769 | return err 770 | } 771 | u.codes.Config = string(pload) 772 | } 773 | 774 | if *u.envVars != nil { 775 | if envSlice, ok := u.envVars.Get().(envSlice); ok { 776 | envVarsMap := make(map[string]string, len(envSlice)) 777 | for _, envItem := range envSlice { 778 | envVarsMap[envItem.Name] = envItem.Value 779 | } 780 | u.codes.EnvVars = envVarsMap 781 | } 782 | } 783 | 784 | return nil 785 | } 786 | 787 | func (u *RegisterCmd) Usage() { 788 | fmt.Fprintln(os.Stderr, `usage: iron worker register some/image[:tag]`) 789 | u.flags.PrintDefaults() 790 | } 791 | 792 | func (u *RegisterCmd) Run() { 793 | if u.codes.Host != "" { 794 | fmt.Println(LINES, `Spinning up '`+u.codes.Name+`'`) 795 | } else { 796 | fmt.Println(LINES, `Registering worker '`+u.codes.Name+`'`) 797 | } 798 | code, err := u.wrkr.CodePackageUpload(u.codes) 799 | if err != nil { 800 | fmt.Println(err) 801 | return 802 | } 803 | if code.Host != "" { 804 | fmt.Println(BLANKS, green(`Hosted at: '`+code.Host+`'`)) 805 | } else { 806 | fmt.Println(BLANKS, green(`Registered code package with id='`+code.Id+`'`)) 807 | } 808 | fmt.Println(BLANKS, green(u.hud_URL_str+"codes/"+code.Id+INFO)) 809 | } 810 | -------------------------------------------------------------------------------- /completions/_ironcli: -------------------------------------------------------------------------------- 1 | #compdef ironcli 2 | 3 | # oh-my-zsh completion plugin 4 | # put me in ~/.oh-my-zsh/custom/plugins/ironcli 5 | 6 | local -a _1st_arguments 7 | 8 | _1st_arguments=( 9 | "upload":"upload a code package with a .worker" 10 | "queue":"queue some codes" 11 | "schedule":"schedule a task" 12 | "log":"print task's log" 13 | "status":"get status of a task" 14 | ) 15 | 16 | _arguments '*:: :->command' 17 | 18 | if (( CURRENT == 1 )); then 19 | _describe -t commands "ironcli command" _1st_arguments 20 | return 21 | fi 22 | 23 | local -a _command_args 24 | case "$words[1]" in 25 | queue) 26 | _command_args=( 27 | '-payload''[payload to pass to task]' \ 28 | '-payload-file''[payload from file to pass to task]' \ 29 | '-priority''[0(default), 1 or 2 priority queue to use]' \ 30 | '-timeout''[0(default) up to user allowed max runtime for task in seconds; 0 = max allowed timeout]' \ 31 | '-delay''[seconds to delay before queueing task]' \ 32 | '-wait''[wait for task to complete and print log]' \ 33 | ) 34 | ;; 35 | schedule) 36 | _command_args=( 37 | '-payload''[payload to pass to task]' \ 38 | '-payload-file''[payload from file to pass to task]' \ 39 | '-priority''[0(default), 1 or 2 priority queue to use]' \ 40 | '-timeout''[0(default) up to user allowed max runtime for task in seconds; 0 = max allowed timeout]' \ 41 | '-delay''[seconds to delay before queueing task]' \ 42 | '-max-concurrency''[maximum allowed concurrency]' \ 43 | '-run-every''[time between runs in sec (>=60)]' \ 44 | '-run-times''[number of times to run, default 1]' \ 45 | '-start-at''[time to start task, form: "Mon Jan 2 15:04:05 -0700 2006"]' \ 46 | '-end-at''[time to end task, form: "Mon Jan 2 15:04:05 -0700 2006"]' \ 47 | ) 48 | ;; 49 | esac 50 | 51 | _arguments \ 52 | $_command_args \ 53 | && return 0 54 | -------------------------------------------------------------------------------- /flags.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // the flag package makes it impossible to distinguish from flags that existed/didn't? so use this 14 | const unset = -1000 15 | 16 | type WorkerFlags struct { 17 | *flag.FlagSet 18 | } 19 | 20 | func NewWorkerFlagSet() *WorkerFlags { 21 | flags := flag.NewFlagSet("command", flag.ContinueOnError) 22 | flags.Usage = func() {} 23 | return &WorkerFlags{flags} 24 | } 25 | 26 | func (wf *WorkerFlags) name() *string { 27 | return wf.String("name", "", "override code package name") 28 | } 29 | func (wf *WorkerFlags) dockerRepoPass() *string { 30 | return wf.String("p", "", "docker repo password") 31 | } 32 | func (wf *WorkerFlags) dockerRepoUserName() *string { 33 | return wf.String("u", "", "docker repo user name") 34 | } 35 | func (wf *WorkerFlags) dockerRepoUrl() *string { 36 | return wf.String("url", "", "docker repo url, if you're using custom repo") 37 | } 38 | func (wf *WorkerFlags) dockerRepoEmail() *string { 39 | return wf.String("e", "", "docker repo user email") 40 | } 41 | 42 | func (wf *WorkerFlags) host() *string { 43 | return wf.String("host", "", "paas host") 44 | } 45 | 46 | func (wf *WorkerFlags) payload() *string { 47 | return wf.String("payload", "", "give worker payload") 48 | } 49 | 50 | func (wf *WorkerFlags) configFile() *string { 51 | return wf.String("config-file", "", "upload file for worker config") 52 | } 53 | 54 | func (wf *WorkerFlags) payloadFile() *string { 55 | return wf.String("payload-file", "", "give worker payload of file contents") 56 | } 57 | 58 | func (wf *WorkerFlags) priority() *int { 59 | return wf.Int("priority", unset, "0(default), 1 or 2; uses worker's default priority if unset") 60 | } 61 | 62 | func (wf *WorkerFlags) defaultPriority() *int { 63 | return wf.Int("default-priority", unset, "0(default), 1 or 2") 64 | } 65 | 66 | func (wf *WorkerFlags) timeout() *int { 67 | return wf.Int("timeout", 0, "0(default) up to user allowed max runtime for task in seconds; 0 = max allowed timeout") 68 | } 69 | 70 | func (wf *WorkerFlags) delay() *int { 71 | return wf.Int("delay", 0, "seconds to delay before queueing task") 72 | } 73 | 74 | func (wf *WorkerFlags) wait() *bool { 75 | return wf.Bool("wait", false, "wait for task to complete and print log") 76 | } 77 | 78 | func (wf *WorkerFlags) maxConc() *int { 79 | return wf.Int("max-concurrency", unset, "max workers to run in parallel. default is no limit") 80 | } 81 | 82 | func (wf *WorkerFlags) runEvery() *int { 83 | return wf.Int("run-every", unset, "time between runs in seconds (>= 60), default is run once") 84 | } 85 | 86 | func (wf *WorkerFlags) runTimes() *int { 87 | return wf.Int("run-times", 0, "number of times a task will run") 88 | } 89 | 90 | func (wf *WorkerFlags) endAt() *string { 91 | return wf.String("end-at", "", "time or datetime in RFC3339 format: '2006-01-02T15:04:05Z07:00'") 92 | } 93 | 94 | func (wf *WorkerFlags) startAt() *string { 95 | return wf.String("start-at", "", "time or datetime in RFC3339 format: '2006-01-02T15:04:05Z07:00'") 96 | } 97 | 98 | func (wf *WorkerFlags) retries() *int { 99 | return wf.Int("retries", unset, "max times to retry failed task, max 10, default 0") 100 | } 101 | 102 | func (wf *WorkerFlags) retriesDelay() *int { 103 | return wf.Int("retries-delay", unset, "time between retries, in seconds. default 0") 104 | } 105 | 106 | func (wf *WorkerFlags) config() *string { 107 | return wf.String("config", "", "provide config string (re: JSON/YAML) that will be available in file on upload") 108 | } 109 | 110 | func (wf *WorkerFlags) zip() *string { 111 | return wf.String("zip", "", "optional: name of zip file where code resides") 112 | } 113 | 114 | func (wf *WorkerFlags) cluster() *string { 115 | return wf.String("cluster", "", "optional: specify cluster to queue task on") 116 | } 117 | 118 | func (wf *WorkerFlags) label() *string { 119 | return wf.String("label", "", "optional: specify label for a task") 120 | } 121 | 122 | func (wf *WorkerFlags) encryptionKey() *string { 123 | return wf.String("encryption-key", "", "optional: specify an rsa public encryption key") 124 | } 125 | 126 | func (wf *WorkerFlags) encryptionKeyFile() *string { 127 | return wf.String("encryption-key-file", "", "optional: specify the location of a file containing an rsa public encryption key") 128 | } 129 | 130 | func (wf *WorkerFlags) n() *int { 131 | return wf.Int("n", 1, "optional: how many of this task to queue. default: 1") 132 | } 133 | 134 | // -- envSlice Value 135 | type envVariable struct { 136 | Name string 137 | Value string 138 | } 139 | 140 | type envSlice []envVariable 141 | 142 | func (s *envSlice) Set(val string) error { 143 | if !strings.Contains(val, "=") { 144 | return errors.New("Environment variable format is 'ENVNAME=value'") 145 | } 146 | pair := strings.SplitN(val, "=", 2) 147 | envVar := envVariable{Name: pair[0], Value: pair[1]} 148 | *s = append(*s, envVar) 149 | return nil 150 | } 151 | 152 | func (s *envSlice) Get() interface{} { 153 | return *s 154 | } 155 | 156 | func (s *envSlice) String() string { return fmt.Sprintf("%v", *s) } 157 | 158 | func (wf *WorkerFlags) envVars() *envSlice { 159 | var sameNamedFlags envSlice 160 | wf.Var(&sameNamedFlags, "e", "optional: specify environment variable for your code in format 'ENVNAME=value'") 161 | return &sameNamedFlags 162 | } 163 | 164 | // TODO(reed): pretty sure there's a better way to get types from flags... 165 | func (wf *WorkerFlags) validateAllFlags() error { 166 | if timeout := wf.Lookup("timeout"); timeout != nil { 167 | _, err := strconv.Atoi(timeout.Value.String()) 168 | if err != nil { 169 | return err 170 | } 171 | } 172 | 173 | if configFile := wf.Lookup("config-file"); configFile != nil { 174 | configFile := configFile.Value.String() 175 | if configFile != "" { 176 | if _, err := os.Stat(configFile); os.IsNotExist(err) { 177 | return err 178 | } 179 | } 180 | } 181 | 182 | if payloadFile := wf.Lookup("payload-file"); payloadFile != nil { 183 | payloadFile := payloadFile.Value.String() 184 | if payloadFile != "" { 185 | if _, err := os.Stat(payloadFile); os.IsNotExist(err) { 186 | return err 187 | } 188 | } 189 | } 190 | 191 | if priority := wf.Lookup("priority"); priority != nil { 192 | _, err := strconv.Atoi(priority.Value.String()) 193 | if err != nil { 194 | return err 195 | } 196 | } 197 | 198 | if endat := wf.Lookup("end-at"); endat != nil { 199 | endat := endat.Value.String() 200 | if endat != "" { 201 | _, err := time.Parse(time.RFC3339, endat) 202 | if err != nil { 203 | return err 204 | } 205 | } 206 | } 207 | 208 | if startat := wf.Lookup("start-at"); startat != nil { 209 | startat := startat.Value.String() 210 | if startat != "" { 211 | _, err := time.Parse(time.RFC3339, startat) 212 | if err != nil { 213 | return err 214 | } 215 | } 216 | } 217 | 218 | return nil 219 | } 220 | 221 | type MqFlags struct { 222 | *flag.FlagSet 223 | } 224 | 225 | func NewMqFlagSet() *MqFlags { 226 | flags := flag.NewFlagSet("commands", flag.ContinueOnError) 227 | flags.Usage = func() {} 228 | return &MqFlags{flags} 229 | } 230 | 231 | func (mf *MqFlags) validateAllFlags() error { 232 | if payloadFile := mf.Lookup("f"); payloadFile != nil { 233 | payloadFile := payloadFile.Value.String() 234 | if payloadFile != "" { 235 | if _, err := os.Stat(payloadFile); os.IsNotExist(err) { 236 | return err 237 | } 238 | } 239 | } 240 | return nil 241 | } 242 | 243 | func (mf *MqFlags) filename() *string { 244 | return mf.String("f", "", "optional: provide a json file of messages to be posted") 245 | } 246 | 247 | func (mf *MqFlags) outputfile() *string { 248 | return mf.String("o", "", "optional: write json output to a file") 249 | } 250 | func (mf *MqFlags) perPage() *int { 251 | return mf.Int("perPage", 30, "optional: amount of queues shown per page (default: 30)") 252 | } 253 | func (mf *MqFlags) page() *string { 254 | return mf.String("page", "0", "optional: starting page (default: 0)") 255 | } 256 | 257 | func (mf *MqFlags) filter() *string { 258 | return mf.String("filter", "", "optional: prefix filter (default: \"\")") 259 | } 260 | 261 | func (mf *MqFlags) n() *int { 262 | return mf.Int("n", 1, "optional: number of messages to get") 263 | } 264 | 265 | func (mf *MqFlags) timeout() *int { 266 | return mf.Int("t", 60, "optional: timeout until message is put back on queue") 267 | } 268 | func (mf *MqFlags) subscriberList() *bool { 269 | return mf.Bool("subscriber-list", false, "optional: printout all subscriber names and URLs") 270 | } 271 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | # 4 | # This script is meant for quick & easy install via: 5 | # 'curl -sSL https://cli.iron.io/install | sh' 6 | # or: 7 | # 'wget -qO- https://cli.iron.io/install | sh' 8 | # 9 | 10 | # UPDATE RELEASE HERE AFTER A NEW VERSION IS RELEASED 11 | # TODO latest ? 12 | release='0.1.6' 13 | 14 | command_exists() { 15 | command -v "$@" > /dev/null 2>&1 16 | } 17 | 18 | case "$(uname -m)" in 19 | *64) 20 | ;; 21 | *) 22 | echo >&2 'Error: you are not using a 64bit platform.' 23 | echo >&2 'Iron CLI currently only supports 64bit platforms.' 24 | exit 1 25 | ;; 26 | esac 27 | 28 | if command_exists iron ; then 29 | echo >&2 'Warning: "iron" command appears to already exist.' 30 | echo >&2 'If you are just upgrading your iron cli client, ignore this and wait a few seconds.' 31 | echo >&2 'You may press Ctrl+C now to abort this process.' 32 | ( set -x; sleep 5 ) 33 | fi 34 | 35 | user="$(id -un 2>/dev/null || true)" 36 | 37 | sh_c='sh -c' 38 | if [ "$user" != 'root' ]; then 39 | if command_exists sudo; then 40 | sh_c='sudo -E sh -c' 41 | elif command_exists su; then 42 | sh_c='su -c' 43 | else 44 | echo >&2 'Error: this installer needs the ability to run commands as root.' 45 | echo >&2 'We are unable to find either "sudo" or "su" available to make this happen.' 46 | exit 1 47 | fi 48 | fi 49 | 50 | curl='' 51 | if command_exists curl; then 52 | curl='curl -sSL -o' 53 | elif command_exists wget; then 54 | curl='wget -qO' 55 | elif command_exists busybox && busybox --list-modules | grep -q wget; then 56 | curl='busybox wget -qO' 57 | else 58 | echo >&2 'Error: this installer needs the ability to run wget or curl.' 59 | echo >&2 'We are unable to find either "wget" or "curl" available to make this happen.' 60 | exit 1 61 | fi 62 | 63 | url='https://github.com/iron-io/ironcli/releases/download' 64 | 65 | # perform some very rudimentary platform detection 66 | case "$(uname)" in 67 | Linux) 68 | $sh_c "$curl /usr/local/bin/iron $url/$release/ironcli_linux" 69 | $sh_c "chmod +x /usr/local/bin/iron" 70 | /usr/local/bin/iron --version 71 | exit 0 72 | ;; 73 | Darwin) 74 | $sh_c "$curl /usr/local/bin/iron $url/$release/ironcli_mac" 75 | $sh_c "chmod +x /usr/local/bin/iron" 76 | /usr/local/bin/iron --version 77 | exit 0 78 | ;; 79 | WindowsNT) 80 | $sh_c "$curl $url/$release/ironcli.exe" 81 | # TODO how to make executable? chmod? 82 | ironcli.exe --version 83 | exit 0 84 | ;; 85 | esac 86 | 87 | cat >&2 <<'EOF' 88 | 89 | Either your platform is not easily detectable or is not supported by this 90 | installer script (yet - PRs welcome! [install.sh]). 91 | Please visit the following URL for more detailed installation instructions: 92 | 93 | https://github.com/iron-io/ironcli 94 | 95 | EOF 96 | exit 1 97 | -------------------------------------------------------------------------------- /lambda.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/zip" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "net/http" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | 15 | "github.com/aws/aws-sdk-go/aws" 16 | aws_credentials "github.com/aws/aws-sdk-go/aws/credentials" 17 | aws_session "github.com/aws/aws-sdk-go/aws/session" 18 | aws_lambda "github.com/aws/aws-sdk-go/service/lambda" 19 | "github.com/docker/docker/pkg/jsonmessage" 20 | "github.com/iron-io/iron_go3/config" 21 | "github.com/iron-io/lambda/lambda" 22 | ) 23 | 24 | var availableRuntimes = []string{"nodejs", "python2.7", "java8"} 25 | 26 | const ( 27 | skipFunctionName = iota 28 | requireFunctionName 29 | ) 30 | 31 | type LambdaFlags struct { 32 | *flag.FlagSet 33 | } 34 | 35 | func (lf *LambdaFlags) validateAllFlags(fnRequired int) error { 36 | fn := lf.Lookup("function-name") 37 | // Everything except import needs a function 38 | if fnRequired == requireFunctionName && (fn == nil || fn.Value.String() == "") { 39 | return errors.New(fmt.Sprintf("Please specify function-name.")) 40 | } 41 | 42 | selectedRuntime := lf.Lookup("runtime") 43 | if selectedRuntime != nil { 44 | validRuntime := false 45 | for _, r := range availableRuntimes { 46 | if selectedRuntime.Value.String() == r { 47 | validRuntime = true 48 | } 49 | } 50 | 51 | if !validRuntime { 52 | return fmt.Errorf("Invalid runtime. Supported runtimes %s", availableRuntimes) 53 | } 54 | } 55 | 56 | return nil 57 | } 58 | 59 | func (lf *LambdaFlags) functionName() *string { 60 | return lf.String("function-name", "", "Name of function. This is usually follows Docker image naming conventions.") 61 | } 62 | 63 | func (lf *LambdaFlags) handler() *string { 64 | return lf.String("handler", "", "function/class that is the entrypoint for this function. Of the form . for nodejs/Python, :: for Java.") 65 | } 66 | 67 | func (lf *LambdaFlags) runtime() *string { 68 | return lf.String("runtime", "", fmt.Sprintf("Runtime that your Lambda function depends on. Valid values are %s.", strings.Join(availableRuntimes, ", "))) 69 | } 70 | 71 | func (lf *LambdaFlags) clientContext() *string { 72 | return lf.String("client-context", "", "") 73 | } 74 | 75 | func (lf *LambdaFlags) payload() *string { 76 | return lf.String("payload", "", "Payload to pass to the Lambda function. This is usually a JSON object.") 77 | } 78 | 79 | func (lf *LambdaFlags) image() *string { 80 | return lf.String("image", "", "By default the name of the Docker image is the name of the Lambda function. Use this to set a custom name.") 81 | } 82 | 83 | func (lf *LambdaFlags) version() *string { 84 | return lf.String("version", "$LATEST", "Version of the function to import.") 85 | } 86 | 87 | func (lf *LambdaFlags) downloadOnly() *bool { 88 | return lf.Bool("download-only", false, "Only download the function into a directory. Will not create a Docker image.") 89 | } 90 | 91 | func (lf *LambdaFlags) awsProfile() *string { 92 | return lf.String("profile", "", "AWS Profile to load from credentials file.") 93 | } 94 | 95 | func (lf *LambdaFlags) awsRegion() *string { 96 | return lf.String("region", "us-east-1", "AWS region to use.") 97 | } 98 | 99 | type lambdaCmd struct { 100 | settings config.Settings 101 | flags *LambdaFlags 102 | token *string 103 | projectID *string 104 | } 105 | 106 | type LambdaCreateCmd struct { 107 | lambdaCmd 108 | 109 | functionName *string 110 | runtime *string 111 | handler *string 112 | fileNames []string 113 | } 114 | 115 | func (lcc *LambdaCreateCmd) Args() error { 116 | if lcc.flags.NArg() < 1 { 117 | return errors.New(`lambda create requires at least one file`) 118 | } 119 | 120 | for _, arg := range lcc.flags.Args() { 121 | lcc.fileNames = append(lcc.fileNames, arg) 122 | } 123 | 124 | return nil 125 | } 126 | 127 | func (lcc *LambdaCreateCmd) Usage() { 128 | fmt.Fprintln(os.Stderr, `usage: iron lambda create-function --function-name NAME --runtime RUNTIME --handler HANDLER file [files...] 129 | 130 | Create Docker image that can run your Lambda function. The files are the contents of the zip file to be uploaded to AWS Lambda. 131 | `) 132 | lcc.flags.PrintDefaults() 133 | } 134 | 135 | func (lcc *LambdaCreateCmd) Config() error { 136 | return nil 137 | } 138 | 139 | func (lcc *LambdaCreateCmd) Flags(args ...string) error { 140 | flags := flag.NewFlagSet("commands", flag.ContinueOnError) 141 | flags.Usage = func() {} 142 | lcc.flags = &LambdaFlags{flags} 143 | 144 | lcc.functionName = lcc.flags.functionName() 145 | lcc.handler = lcc.flags.handler() 146 | lcc.runtime = lcc.flags.runtime() 147 | 148 | if err := lcc.flags.Parse(args); err != nil { 149 | return err 150 | } 151 | 152 | return lcc.flags.validateAllFlags(requireFunctionName) 153 | } 154 | 155 | type DockerJsonWriter struct { 156 | under io.Writer 157 | w io.Writer 158 | } 159 | 160 | func NewDockerJsonWriter(under io.Writer) *DockerJsonWriter { 161 | r, w := io.Pipe() 162 | go func() { 163 | err := jsonmessage.DisplayJSONMessagesStream(r, under, 1, true, nil) 164 | if err != nil { 165 | fmt.Fprintln(os.Stderr, red(err)) 166 | os.Exit(1) 167 | } 168 | }() 169 | return &DockerJsonWriter{under, w} 170 | } 171 | 172 | func (djw *DockerJsonWriter) Write(p []byte) (int, error) { 173 | return djw.w.Write(p) 174 | } 175 | 176 | func (lcc *LambdaCreateCmd) Run() { 177 | files := make([]lambda.FileLike, 0, len(lcc.fileNames)) 178 | opts := lambda.CreateImageOptions{ 179 | Name: *lcc.functionName, 180 | Base: fmt.Sprintf("iron/lambda-%s", *lcc.runtime), 181 | Package: "", 182 | Handler: *lcc.handler, 183 | OutputStream: NewDockerJsonWriter(os.Stdout), 184 | RawJSONStream: true, 185 | } 186 | 187 | if *lcc.handler == "" { 188 | fmt.Fprintln(os.Stderr, red("No handler specified.")) 189 | os.Exit(1) 190 | } 191 | 192 | // For Java we allow only 1 file and it MUST be a JAR. 193 | if *lcc.runtime == "java8" { 194 | if len(lcc.fileNames) != 1 { 195 | fmt.Fprintln(os.Stderr, red("Java Lambda functions can only include 1 file and it must be a JAR file.")) 196 | os.Exit(1) 197 | } 198 | 199 | if filepath.Ext(lcc.fileNames[0]) != ".jar" { 200 | fmt.Fprintln(os.Stderr, red("Java Lambda function package must be a JAR file.")) 201 | os.Exit(1) 202 | } 203 | 204 | opts.Package = filepath.Base(lcc.fileNames[0]) 205 | } 206 | 207 | for _, fileName := range lcc.fileNames { 208 | file, err := os.Open(fileName) 209 | defer file.Close() 210 | if err != nil { 211 | fmt.Fprintln(os.Stderr, red(err)) 212 | os.Exit(1) 213 | } 214 | files = append(files, file) 215 | } 216 | 217 | err := lambda.CreateImage(opts, files...) 218 | if err != nil { 219 | fmt.Fprintln(os.Stderr, red(err)) 220 | os.Exit(1) 221 | } 222 | } 223 | 224 | type LambdaTestFunctionCmd struct { 225 | lambdaCmd 226 | 227 | functionName *string 228 | clientContext *string 229 | payload *string 230 | } 231 | 232 | func (lcc *LambdaTestFunctionCmd) Args() error { 233 | return nil 234 | } 235 | 236 | func (lcc *LambdaTestFunctionCmd) Usage() { 237 | fmt.Fprintln(os.Stderr, `usage: iron lambda test-function --function-name NAME [--client-context ] [--payload ] 238 | 239 | Runs local Dockerized Lambda function and writes output to stdout. 240 | `) 241 | lcc.flags.PrintDefaults() 242 | } 243 | 244 | func (lcc *LambdaTestFunctionCmd) Config() error { 245 | return nil 246 | } 247 | 248 | func (lcc *LambdaTestFunctionCmd) Flags(args ...string) error { 249 | flags := flag.NewFlagSet("commands", flag.ContinueOnError) 250 | flags.Usage = func() {} 251 | lcc.flags = &LambdaFlags{flags} 252 | 253 | lcc.functionName = lcc.flags.functionName() 254 | lcc.clientContext = lcc.flags.clientContext() 255 | lcc.payload = lcc.flags.payload() 256 | 257 | if err := lcc.flags.Parse(args); err != nil { 258 | return err 259 | } 260 | 261 | return lcc.flags.validateAllFlags(requireFunctionName) 262 | } 263 | 264 | func (lcc *LambdaTestFunctionCmd) Run() { 265 | exists, err := lambda.ImageExists(*lcc.functionName) 266 | if err != nil { 267 | fmt.Fprintln(os.Stderr, red("Error communicating with Docker daemon", err)) 268 | os.Exit(1) 269 | } 270 | 271 | if !exists { 272 | fmt.Fprintln(os.Stderr, red(fmt.Sprintf("Function %s does not exist.", *lcc.functionName))) 273 | os.Exit(1) 274 | } 275 | 276 | payload := "" 277 | if lcc.payload != nil { 278 | payload = *lcc.payload 279 | } 280 | // Redirect output to stdout. 281 | err = lambda.RunImageWithPayload(*lcc.functionName, payload) 282 | if err != nil { 283 | fmt.Fprintln(os.Stderr, red(err)) 284 | os.Exit(1) 285 | } 286 | } 287 | 288 | type LambdaPublishCmd struct { 289 | lambdaCmd 290 | 291 | functionName *string 292 | } 293 | 294 | func (lcc *LambdaPublishCmd) Args() error { 295 | return nil 296 | } 297 | 298 | func (lcc *LambdaPublishCmd) Usage() { 299 | fmt.Fprintln(os.Stderr, `usage: iron lambda publish-function --function-name NAME 300 | 301 | Pushes Lambda function to Docker Hub and registers with IronWorker. 302 | If you do not want to use IronWorker, simply run 'docker push NAME' instead. 303 | `) 304 | lcc.flags.PrintDefaults() 305 | } 306 | 307 | func (lcc *LambdaPublishCmd) Config() error { 308 | return nil 309 | } 310 | 311 | func (lcc *LambdaPublishCmd) Flags(args ...string) error { 312 | flags := flag.NewFlagSet("commands", flag.ContinueOnError) 313 | flags.Usage = func() {} 314 | lcc.flags = &LambdaFlags{flags} 315 | 316 | lcc.functionName = lcc.flags.functionName() 317 | 318 | if err := lcc.flags.Parse(args); err != nil { 319 | return err 320 | } 321 | 322 | return lcc.flags.validateAllFlags(requireFunctionName) 323 | } 324 | 325 | func (lcc *LambdaPublishCmd) Run() { 326 | exists, err := lambda.ImageExists(*lcc.functionName) 327 | if err != nil { 328 | fmt.Fprintln(os.Stderr, red("Error communicating with Docker daemon:", err)) 329 | os.Exit(1) 330 | } 331 | 332 | if !exists { 333 | fmt.Fprintln(os.Stderr, red(fmt.Sprintf("Function %s does not exist:", *lcc.functionName))) 334 | os.Exit(1) 335 | } 336 | 337 | err = lambda.PushImage(lambda.PushImageOptions{ 338 | NameVersion: *lcc.functionName, 339 | OutputStream: NewDockerJsonWriter(os.Stdout), 340 | RawJSONStream: true, 341 | }) 342 | if err != nil { 343 | fmt.Fprintln(os.Stderr, red("Error pushing image:", err)) 344 | os.Exit(1) 345 | } 346 | 347 | err = lambda.RegisterWithIron(*lcc.functionName) 348 | if err != nil { 349 | fmt.Fprintln(os.Stderr, red("Error registering with IronWorker:", err)) 350 | os.Exit(1) 351 | } 352 | } 353 | 354 | type LambdaImportCmd struct { 355 | lambdaCmd 356 | 357 | arn string 358 | version *string 359 | downloadOnly *bool 360 | awsProfile *string 361 | image *string 362 | awsRegion *string 363 | } 364 | 365 | func (lcc *LambdaImportCmd) Args() error { 366 | if lcc.flags.NArg() < 1 { 367 | return errors.New(`import requires an AWS function ARN to import.`) 368 | } 369 | 370 | lcc.arn = lcc.flags.Arg(0) 371 | return nil 372 | } 373 | 374 | func (lcc *LambdaImportCmd) Usage() { 375 | fmt.Fprintln(os.Stderr, `usage: iron lambda aws-import [--region ] [--profile ] [--version ] [--download-only] [--image ] ARN 376 | 377 | Converts an existing Lambda function to an image. 378 | 379 | The function code is downloaded to a directory in the current working directory 380 | that has the same name as the Lambda function. 381 | `) 382 | lcc.flags.PrintDefaults() 383 | } 384 | 385 | func (lcc *LambdaImportCmd) Config() error { 386 | return nil 387 | } 388 | 389 | func (lcc *LambdaImportCmd) Flags(args ...string) error { 390 | flags := flag.NewFlagSet("commands", flag.ContinueOnError) 391 | flags.Usage = func() {} 392 | lcc.flags = &LambdaFlags{flags} 393 | 394 | lcc.version = lcc.flags.version() 395 | lcc.downloadOnly = lcc.flags.downloadOnly() 396 | lcc.awsProfile = lcc.flags.awsProfile() 397 | lcc.image = lcc.flags.image() 398 | lcc.awsRegion = lcc.flags.awsRegion() 399 | 400 | if err := lcc.flags.Parse(args); err != nil { 401 | return err 402 | } 403 | 404 | return lcc.flags.validateAllFlags(skipFunctionName) 405 | } 406 | 407 | func (lcc *LambdaImportCmd) downloadToFile(url string) (string, error) { 408 | downloadResp, err := http.Get(url) 409 | if err != nil { 410 | return "", err 411 | } 412 | defer downloadResp.Body.Close() 413 | 414 | // zip reader needs ReaderAt, hence the indirection. 415 | tmpFile, err := ioutil.TempFile("", "lambda-function-") 416 | if err != nil { 417 | return "", err 418 | } 419 | 420 | io.Copy(tmpFile, downloadResp.Body) 421 | tmpFile.Close() 422 | return tmpFile.Name(), nil 423 | } 424 | 425 | func (lcc *LambdaImportCmd) unzipAndGetTopLevelFiles(dst, src string) (files []lambda.FileLike, topErr error) { 426 | files = make([]lambda.FileLike, 0) 427 | 428 | zipReader, err := zip.OpenReader(src) 429 | if err != nil { 430 | return files, err 431 | } 432 | defer zipReader.Close() 433 | 434 | var fd *os.File 435 | for _, f := range zipReader.File { 436 | path := filepath.Join(dst, f.Name) 437 | fmt.Printf("Extracting '%s' to '%s'\n", f.Name, path) 438 | if f.FileInfo().IsDir() { 439 | os.Mkdir(path, 0644) 440 | // Only top-level dirs go into the list since that is what CreateImage expects. 441 | if filepath.Dir(f.Name) == filepath.Base(f.Name) { 442 | fd, topErr = os.Open(path) 443 | if topErr != nil { 444 | break 445 | } 446 | files = append(files, fd) 447 | } 448 | } else { 449 | // We do not close fd here since we may want to use it to dockerize. 450 | fd, topErr = os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644) 451 | if topErr != nil { 452 | break 453 | } 454 | 455 | var zipFd io.ReadCloser 456 | zipFd, topErr = f.Open() 457 | if topErr != nil { 458 | break 459 | } 460 | 461 | _, topErr = io.Copy(fd, zipFd) 462 | if topErr != nil { 463 | // OK to skip closing fd here. 464 | break 465 | } 466 | 467 | zipFd.Close() 468 | 469 | // Only top-level files go into the list since that is what CreateImage expects. 470 | if filepath.Dir(f.Name) == "." { 471 | _, topErr = fd.Seek(0, 0) 472 | if topErr != nil { 473 | break 474 | } 475 | 476 | files = append(files, fd) 477 | } else { 478 | fd.Close() 479 | } 480 | } 481 | } 482 | return 483 | } 484 | 485 | func (lcc *LambdaImportCmd) getFunction() (*aws_lambda.GetFunctionOutput, error) { 486 | creds := aws_credentials.NewChainCredentials([]aws_credentials.Provider{ 487 | &aws_credentials.EnvProvider{}, 488 | &aws_credentials.SharedCredentialsProvider{ 489 | Filename: "", // Look in default location. 490 | Profile: *lcc.awsProfile, 491 | }, 492 | }) 493 | 494 | conf := aws.NewConfig().WithCredentials(creds).WithCredentialsChainVerboseErrors(true).WithRegion(*lcc.awsRegion) 495 | sess := aws_session.New(conf) 496 | conn := aws_lambda.New(sess) 497 | resp, err := conn.GetFunction(&aws_lambda.GetFunctionInput{ 498 | FunctionName: aws.String(lcc.arn), 499 | Qualifier: aws.String(*lcc.version), 500 | }) 501 | 502 | return resp, err 503 | } 504 | 505 | func (lcc *LambdaImportCmd) Run() { 506 | function, err := lcc.getFunction() 507 | if err != nil { 508 | fmt.Fprintln(os.Stderr, red("Error getting function information", err)) 509 | os.Exit(1) 510 | } 511 | functionName := *function.Configuration.FunctionName 512 | 513 | err = os.Mkdir(fmt.Sprintf("./%s", functionName), os.ModePerm) 514 | if err != nil { 515 | fmt.Fprintln(os.Stderr, red("Error creating directory: '"+functionName+"':", err)) 516 | os.Exit(1) 517 | } 518 | 519 | tmpFileName, err := lcc.downloadToFile(*function.Code.Location) 520 | if err != nil { 521 | fmt.Fprintln(os.Stderr, red("Error downloading code", err)) 522 | os.Exit(1) 523 | } 524 | defer os.Remove(tmpFileName) 525 | 526 | files := make([]lambda.FileLike, 0) 527 | 528 | if *function.Configuration.Runtime == "java8" { 529 | fmt.Println("Found Java Lambda function. Going to assume code is a single JAR file.") 530 | path := filepath.Join(functionName, "function.jar") 531 | os.Rename(tmpFileName, path) 532 | fd, err := os.Open(path) 533 | if err != nil { 534 | fmt.Fprintln(os.Stderr, red(err)) 535 | os.Exit(1) 536 | } 537 | 538 | files = append(files, fd) 539 | } else { 540 | files, err = lcc.unzipAndGetTopLevelFiles(functionName, tmpFileName) 541 | if err != nil { 542 | fmt.Fprintln(os.Stderr, red(err)) 543 | os.Exit(1) 544 | } 545 | } 546 | 547 | if *lcc.downloadOnly { 548 | // Since we are a command line program that will quit soon, it is OK to 549 | // let the OS clean `files` up. 550 | return 551 | } 552 | 553 | opts := lambda.CreateImageOptions{ 554 | Name: functionName, 555 | Base: fmt.Sprintf("iron/lambda-%s", *function.Configuration.Runtime), 556 | Package: "", 557 | Handler: *function.Configuration.Handler, 558 | OutputStream: NewDockerJsonWriter(os.Stdout), 559 | RawJSONStream: true, 560 | } 561 | 562 | if *lcc.image != "" { 563 | opts.Name = *lcc.image 564 | } 565 | 566 | if *function.Configuration.Runtime == "java8" { 567 | opts.Package = filepath.Base(files[0].(*os.File).Name()) 568 | } 569 | 570 | err = lambda.CreateImage(opts, files...) 571 | if err != nil { 572 | fmt.Fprintln(os.Stderr, red("Error creating image", err)) 573 | os.Exit(1) 574 | } 575 | } 576 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Package contains the command line interface for iron-worker. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "os" 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/fatih/color" 12 | ) 13 | 14 | var ( 15 | // These are located after binary on command line 16 | // TODO(reed): kind of awkward, since there are 2 different flag sets now: 17 | // e.g. 18 | // ironcli -token=123456789 upload -max-concurrency=10 my_worker 19 | versionFlag = flag.Bool("version", false, "print the version number") 20 | helpFlag = flag.Bool("help", false, "show this") 21 | hFlag = flag.Bool("h", false, "show this") 22 | tokenFlag = flag.String("token", "", "provide OAuth token") 23 | projectIDFlag = flag.String("project-id", "", "provide project ID") 24 | envFlag = flag.String("env", "", "provide specific dev environment") 25 | 26 | red, yellow, green func(a ...interface{}) string 27 | 28 | // i.e. worker: { commands... } 29 | // mq: { commands... } 30 | commands = map[string]commander{ 31 | "run": single{ 32 | new(RunCmd), 33 | }, 34 | 35 | "docker": mapper{ 36 | "login": new(DockerLoginCmd), 37 | }, 38 | "register": single{ 39 | new(RegisterCmd), 40 | }, 41 | "worker": mapper{ 42 | "upload": new(UploadCmd), 43 | "queue": new(QueueCmd), 44 | "schedule": new(SchedCmd), 45 | "status": new(StatusCmd), 46 | "log": new(LogCmd), 47 | }, 48 | "mq": mapper{ 49 | "push": new(PushCmd), 50 | "pop": new(PopCmd), 51 | "reserve": new(ReserveCmd), 52 | "delete": new(DeleteCmd), 53 | "peek": new(PeekCmd), 54 | "clear": new(ClearCmd), 55 | "list": new(ListCmd), 56 | "create": new(CreateCmd), 57 | "rm": new(RmCmd), 58 | "info": new(InfoCmd), 59 | }, 60 | "lambda": mapper{ 61 | "create-function": new(LambdaCreateCmd), 62 | "test-function": new(LambdaTestFunctionCmd), 63 | "publish-function": new(LambdaPublishCmd), 64 | "aws-import": new(LambdaImportCmd), 65 | }, 66 | } 67 | ) 68 | 69 | const ( 70 | LINES = "-----> " 71 | BLANKS = " " 72 | INFO = " for more info" 73 | 74 | Version = "0.1.6" 75 | ) 76 | 77 | func usage() { 78 | fmt.Fprintln(os.Stderr, "usage: ", os.Args[0], `[product] [command] [flags] [args] 79 | 80 | where [product] is one of: 81 | 82 | mq Commands to manage messages and queues on IronMQ. 83 | worker Commands to queue and view IronWorker tasks. 84 | docker Login to Docker Registry. 85 | register Register an image or code package with IronWorker. 86 | lambda Commands to convert AWS Lambda functions to Docker containers. 87 | 88 | run '`+os.Args[0], `[product] -help for a list of commands. 89 | run '`+os.Args[0], `[product] [command] -help' for [command]'s flags/args. 90 | `) 91 | fmt.Fprintln(os.Stderr, `[flags]:`) 92 | flag.PrintDefaults() 93 | os.Exit(0) 94 | } 95 | 96 | func pusage(p string) { 97 | prod, ok := commands[p] 98 | if !ok { 99 | fmt.Fprintln(os.Stderr, red("invalid product ", `"`+p+`", `, "see -help")) 100 | os.Exit(1) 101 | } 102 | subs := prod.Commands() 103 | if len(subs) > 0 { 104 | fmt.Fprintln(os.Stderr, p, "commands:") 105 | for _, cmd := range subs { 106 | fmt.Fprintln(os.Stderr, "\t", cmd) 107 | } 108 | } 109 | } 110 | 111 | type commander interface { 112 | // Given a full set of command line args, call Args and Flags with 113 | // whatever position needed to be sufficiently rad. 114 | Command(args ...string) (Command, error) 115 | Commands() []string 116 | } 117 | 118 | type ( 119 | // mapper expects > 0 args, calls flags after first arg 120 | mapper map[string]Command 121 | // runner calls flags on first (zeroeth) arg 122 | single struct{ cmd Command } 123 | ) 124 | 125 | func (s single) Commands() []string { return []string{} } // --help handled in Flags() 126 | func (s single) Command(args ...string) (Command, error) { 127 | err := s.cmd.Flags(args[0:]...) 128 | if err == nil { 129 | err = s.cmd.Args() 130 | } 131 | return s.cmd, err 132 | } 133 | 134 | func (m mapper) Commands() []string { 135 | var c []string 136 | for cmd := range m { 137 | c = append(c, cmd) 138 | } 139 | return c 140 | } 141 | 142 | func (m mapper) Command(args ...string) (Command, error) { 143 | c, ok := m[args[0]] 144 | if !ok { 145 | return nil, fmt.Errorf("command not found: %s", args[0]) 146 | } 147 | err := c.Flags(args[1:]...) 148 | if err == nil { 149 | err = c.Args() 150 | } 151 | return c, err 152 | } 153 | 154 | func main() { 155 | if runtime.GOOS == "windows" { 156 | red = fmt.Sprint 157 | yellow = fmt.Sprint 158 | green = fmt.Sprint 159 | } else { 160 | red = color.New(color.FgRed).SprintFunc() 161 | yellow = color.New(color.FgYellow).SprintFunc() 162 | green = color.New(color.FgGreen).SprintFunc() 163 | } 164 | 165 | flag.Parse() 166 | 167 | if *helpFlag || *hFlag { 168 | usage() 169 | } else if *versionFlag { 170 | fmt.Println(Version) 171 | os.Exit(0) 172 | } 173 | 174 | if flag.NArg() < 1 { 175 | usage() 176 | } 177 | 178 | product := flag.Arg(0) 179 | cmds, ok := commands[product] 180 | if !ok || flag.NArg() < 2 { 181 | pusage(product) 182 | os.Exit(0) 183 | } 184 | 185 | cmdName := flag.Arg(1) 186 | cmd, err := cmds.Command(flag.Args()[1:]...) 187 | 188 | if err != nil { 189 | // A single command or mapper subcommand will fail with ErrHelp. 190 | if err == flag.ErrHelp { 191 | if cmd != nil { 192 | cmd.Usage() 193 | } 194 | os.Exit(0) 195 | } else { 196 | // A mapper top level command with -h as the 'subcommand' will not fail 197 | // with ErrHelp, but complain about invalid flags, so we need to handle 198 | // it separately. 199 | switch strings.TrimSpace(cmdName) { 200 | case "-h", "help", "--help", "-help": 201 | pusage(product) 202 | os.Exit(0) 203 | } 204 | 205 | fmt.Fprintln(os.Stderr, red(err)) 206 | os.Exit(1) 207 | } 208 | } 209 | 210 | err = cmd.Config() 211 | if err != nil { 212 | fmt.Fprintln(os.Stderr, red(err)) 213 | os.Exit(2) 214 | } 215 | 216 | cmd.Run() 217 | } 218 | -------------------------------------------------------------------------------- /mq.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | 11 | "github.com/iron-io/iron_go3/config" 12 | "github.com/iron-io/iron_go3/mq" 13 | ) 14 | 15 | // type Cmd interface { 16 | // Flags(...string) error // parse subcommand specific flags 17 | // Args() error // validate arguments 18 | // Config() error // configure env variables 19 | // Usage() func() // custom command help TODO(reed): all local now? 20 | // Run() // cmd specific 21 | // } 22 | 23 | type mqCmd struct { 24 | settings config.Settings 25 | flags *MqFlags 26 | token *string 27 | projectID *string 28 | } 29 | 30 | func (mc *mqCmd) Config() error { 31 | var err error 32 | mc.settings, err = loadConfig("iron_mq", *envFlag) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | if *projectIDFlag != "" { 38 | mc.settings.ProjectId = *projectIDFlag 39 | } 40 | 41 | if *tokenFlag != "" { 42 | mc.settings.Token = *tokenFlag 43 | } 44 | 45 | if mc.settings.ProjectId == "" { 46 | return errors.New("did not find project id in any config files or env variables") 47 | } 48 | if mc.settings.Token == "" { 49 | return errors.New("did not find token in any config files or env variables") 50 | } 51 | 52 | if !isPipedOut() { 53 | fmt.Printf("%sConfiguring client\n", LINES) 54 | pName, err := mqProjectName(mc.settings) 55 | if err != nil { 56 | return err 57 | } 58 | if pName == "" { 59 | fmt.Printf("%sCould not find project name.", BLANKS) 60 | } else { 61 | fmt.Printf(`%sProject '%s' with id='%s'`, BLANKS, pName, mc.settings.ProjectId) 62 | } 63 | fmt.Println() 64 | } 65 | return nil 66 | } 67 | 68 | type ClearCmd struct { 69 | mqCmd 70 | 71 | queue_name string 72 | } 73 | 74 | func (c *ClearCmd) Usage() { 75 | fmt.Fprintln(os.Stderr, "usage: iron mq clear QUEUE_NAME") 76 | c.flags.PrintDefaults() 77 | } 78 | 79 | func (c *ClearCmd) Flags(args ...string) error { 80 | c.flags = NewMqFlagSet() 81 | 82 | if err := c.flags.Parse(args); err != nil { 83 | return err 84 | } 85 | return nil 86 | } 87 | 88 | func (c *ClearCmd) Args() error { 89 | if c.flags.NArg() < 1 { 90 | return errors.New(`clear requires one arg 91 | 92 | usage: iron mq clear QUEUE_NAME`) 93 | } 94 | c.queue_name = c.flags.Arg(0) 95 | return nil 96 | } 97 | 98 | func (c *ClearCmd) Run() { 99 | q := mq.ConfigNew(c.queue_name, &c.settings) 100 | if err := q.Clear(); err != nil { 101 | fmt.Println(red("Error clearing queue:", err)) 102 | return 103 | } 104 | fmt.Fprintln(os.Stderr, green(LINES, "Queue ", q.Name, " has been successfully cleared")) 105 | } 106 | 107 | func (p *PeekCmd) Usage() { 108 | fmt.Fprintln(os.Stderr, `usage: iron mq peek [--n number] QUEUE_NAME 109 | 110 | n: peek n numbers of messages(default: 1, max: 100)`) 111 | p.flags.PrintDefaults() 112 | } 113 | 114 | type CreateCmd struct { 115 | mqCmd 116 | 117 | queue_name string 118 | } 119 | 120 | func (c *CreateCmd) Flags(args ...string) error { 121 | c.flags = NewMqFlagSet() 122 | err := c.flags.Parse(args) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | return c.flags.validateAllFlags() 128 | } 129 | 130 | func (c *CreateCmd) Args() error { 131 | if c.flags.NArg() < 1 { 132 | return errors.New("create requires at least one argument\nusage: iron mq create QUEUE_NAME") 133 | } 134 | c.queue_name = c.flags.Arg(0) 135 | return nil 136 | } 137 | 138 | func (c *CreateCmd) Usage() { 139 | fmt.Println(`usage: iron mq create QUEUE_NAME`) 140 | c.flags.PrintDefaults() 141 | } 142 | 143 | func (c *CreateCmd) Run() { 144 | fmt.Printf("%sCreating queue \"%s\"\n", BLANKS, c.queue_name) 145 | q := mq.ConfigNew(c.queue_name, &c.settings) 146 | _, err := q.PushStrings("") 147 | if err != nil { 148 | fmt.Fprintln(os.Stderr, red(BLANKS, "create error: ", err)) 149 | return 150 | } 151 | err = q.Clear() 152 | if err != nil { 153 | fmt.Fprintln(os.Stderr, red(BLANKS, "create error: ", err)) 154 | } 155 | 156 | fmt.Println(green(LINES, "Queue ", q.Name, " has been successfully created.")) 157 | printQueueHudURL(BLANKS, q) 158 | } 159 | 160 | type DeleteCmd struct { 161 | mqCmd 162 | 163 | filequeue_name *string 164 | queue_name string 165 | ids []string 166 | } 167 | 168 | func (d *DeleteCmd) Usage() { 169 | fmt.Fprintln(os.Stderr, `usage: iron mq delete [-i file] QUEUE_NAME "MSG_ID" "MSG_ID"... 170 | 171 | Delete a message of a queue 172 | -i: json file with a set of ids to be deleted. Format should be {"ids": ["123", "456", ...]}`) 173 | d.flags.PrintDefaults() 174 | } 175 | 176 | func (d *DeleteCmd) Flags(args ...string) error { 177 | d.flags = NewMqFlagSet() 178 | 179 | d.filequeue_name = d.flags.filename() 180 | 181 | if err := d.flags.Parse(args); err != nil { 182 | return err 183 | } 184 | return d.flags.validateAllFlags() 185 | } 186 | 187 | func (d *DeleteCmd) Args() error { 188 | if d.flags.NArg() < 1 { 189 | return errors.New(`delete requires a queue name`) 190 | } 191 | d.queue_name = d.flags.Arg(0) 192 | 193 | // Read and parse piped info 194 | if isPipedIn() { 195 | ids, err := readIds() 196 | if err != nil { 197 | return err 198 | } 199 | d.ids = append(d.ids, ids...) 200 | } 201 | 202 | if *d.filequeue_name != "" { 203 | b, err := ioutil.ReadFile(*d.filequeue_name) 204 | if err != nil { 205 | return err 206 | } 207 | 208 | // Use the message struct so its compatible with output files from reserve 209 | var msgs []mq.Message 210 | err = json.Unmarshal(b, &msgs) 211 | if err != nil { 212 | return err 213 | } 214 | for _, msg := range msgs { 215 | d.ids = append(d.ids, msg.Id) 216 | } 217 | } 218 | 219 | if d.flags.NArg() > 1 { 220 | d.ids = append(d.ids, d.flags.Args()[1:]...) 221 | } 222 | 223 | if len(d.ids) < 1 { 224 | return errors.New("delete requires at least one message id") 225 | } 226 | return nil 227 | } 228 | 229 | // This doesn't work with reserved messages 230 | // TODO: add --reserved flag to work with reserved messages 231 | // TODO: Make the message not found error more descriptive. 232 | func (d *DeleteCmd) Run() { 233 | q := mq.ConfigNew(d.queue_name, &d.settings) 234 | 235 | err := q.DeleteMessages(d.ids) 236 | if err != nil { 237 | fmt.Println(red(BLANKS, err)) 238 | return 239 | } 240 | 241 | plural := "" 242 | if len(d.ids) > 1 { 243 | plural = "s" 244 | } 245 | fmt.Println(green(BLANKS, "Done deleting message", plural)) 246 | 247 | } 248 | 249 | type InfoCmd struct { 250 | mqCmd 251 | 252 | queue_name string 253 | subscriberList *bool 254 | } 255 | 256 | func (i *InfoCmd) Usage() { 257 | fmt.Fprintln(os.Stderr, `usage: iron mq info [--subscriber-list] QUEUE_NAME 258 | 259 | --subscriber-list: Prints out the list of current subscribers. This is only available on push queues.`) 260 | i.flags.PrintDefaults() 261 | } 262 | 263 | func (i *InfoCmd) Flags(args ...string) error { 264 | i.flags = NewMqFlagSet() 265 | i.subscriberList = i.flags.subscriberList() 266 | if err := i.flags.Parse(args); err != nil { 267 | return err 268 | } 269 | 270 | return i.flags.validateAllFlags() 271 | } 272 | 273 | func (i *InfoCmd) Args() error { 274 | if i.flags.NArg() < 1 { 275 | return errors.New(`info requires a queue name`) 276 | } 277 | 278 | i.queue_name = i.flags.Arg(0) 279 | return nil 280 | } 281 | 282 | func (i *InfoCmd) Run() { 283 | q := mq.ConfigNew(i.queue_name, &i.settings) 284 | info, err := q.Info() 285 | if err != nil { 286 | fmt.Fprintln(os.Stderr, red(err)) 287 | return 288 | } 289 | fmt.Printf("%sName: %s\n", BLANKS, info.Name) 290 | fmt.Printf("%sCurrent Size: %d\n", BLANKS, info.Size) 291 | fmt.Printf("%sTotal messages: %d\n", BLANKS, info.TotalMessages) 292 | fmt.Printf("%sMessage expiration: %d\n", BLANKS, info.MessageExpiration) 293 | fmt.Printf("%sMessage timeout: %d\n", BLANKS, info.MessageTimeout) 294 | if info.Push != nil { 295 | fmt.Printf("%sType: %s\n", BLANKS, info.Type) 296 | fmt.Printf("%sSubscribers: %d\n", BLANKS, len(info.Push.Subscribers)) 297 | fmt.Printf("%sRetries: %d\n", BLANKS, info.Push.Retries) 298 | fmt.Printf("%sRetries delay: %d\n", BLANKS, info.Push.RetriesDelay) 299 | if *i.subscriberList { 300 | fmt.Printf("%sSubscriber list\n", LINES) 301 | printSubscribers(info) 302 | fmt.Println() 303 | } 304 | } 305 | printQueueHudURL(BLANKS, q) 306 | } 307 | 308 | type ListCmd struct { 309 | mqCmd 310 | 311 | //flags 312 | page *string 313 | perPage *int 314 | filter *string 315 | } 316 | 317 | func (l *ListCmd) Flags(args ...string) error { 318 | l.flags = NewMqFlagSet() 319 | 320 | l.page = l.flags.page() 321 | l.perPage = l.flags.perPage() 322 | l.filter = l.flags.filter() 323 | 324 | err := l.flags.Parse(args) 325 | if err != nil { 326 | return err 327 | } 328 | return l.flags.validateAllFlags() 329 | } 330 | 331 | func (l *ListCmd) Args() error { 332 | return nil 333 | } 334 | 335 | func (l *ListCmd) Usage() { 336 | fmt.Fprintln(os.Stderr, `usage: iron mq list [--perPage perPpage] [--page page] 337 | --perPage perPage: Amount of queues showed per page 338 | --page page: starting page number 339 | --filter filter: filter using a specified prefix`) 340 | l.flags.PrintDefaults() 341 | } 342 | 343 | func (l *ListCmd) Run() { 344 | queues, err := mq.FilterPage(*l.filter, *l.page, *l.perPage) 345 | if err != nil { 346 | fmt.Println(BLANKS, err) 347 | return 348 | } 349 | if isPipedOut() { 350 | for _, q := range queues { 351 | fmt.Println(q.Name) 352 | } 353 | } else { 354 | fmt.Println(LINES, "Listing queues") 355 | for _, q := range queues { 356 | fmt.Println(BLANKS, "*", q.Name) 357 | } 358 | if tag, err := getHudTag(l.settings); err == nil { 359 | fmt.Printf("%s Go to hud-e.iron.io/mq/%s/projects/%s/queues for more info", 360 | BLANKS, 361 | tag, 362 | l.settings.ProjectId) 363 | } 364 | fmt.Println() 365 | } 366 | } 367 | 368 | type PeekCmd struct { 369 | mqCmd 370 | 371 | n *int 372 | queue_name string 373 | } 374 | 375 | func (p *PeekCmd) Flags(args ...string) error { 376 | p.flags = NewMqFlagSet() 377 | p.n = p.flags.n() 378 | 379 | if err := p.flags.Parse(args); err != nil { 380 | return err 381 | } 382 | 383 | return p.flags.validateAllFlags() 384 | } 385 | 386 | func (p *PeekCmd) Args() error { 387 | if p.flags.NArg() < 1 { 388 | return errors.New(`peek requires one arg 389 | 390 | usage: iron mq peek [--n numer] QUEUE_NAME`) 391 | } 392 | p.queue_name = p.flags.Arg(0) 393 | return nil 394 | } 395 | 396 | func (p *PeekCmd) Run() { 397 | q := mq.ConfigNew(p.queue_name, &p.settings) 398 | 399 | msgs, err := q.PeekN(*p.n) 400 | if err != nil { 401 | fmt.Fprintln(os.Stderr, red(err)) 402 | return 403 | } 404 | 405 | if len(msgs) < 1 { 406 | fmt.Fprintln(os.Stderr, red("Queue is empty.")) 407 | return 408 | } 409 | 410 | if !isPipedOut() { 411 | plural := "" 412 | if *p.n > 1 { 413 | plural = "s" 414 | } 415 | fmt.Println(green(LINES, "Message", plural, " successfully peeked")) 416 | fmt.Println() 417 | fmt.Println("-------- ID ------ | Body") 418 | } 419 | printMessages(msgs) 420 | } 421 | 422 | type PopCmd struct { 423 | mqCmd 424 | 425 | queue_name string 426 | n *int 427 | outputfile *string 428 | file *os.File 429 | } 430 | 431 | func (p *PopCmd) Usage() { 432 | fmt.Fprintln(os.Stderr, `usage: iron mq pop [-n int] [-o file] QUEUE_NAME 433 | 434 | pop reserves then deletes a message from the queue 435 | n: number of messages to pop off the queue, default: 1 436 | o: write results in json to a file`) 437 | p.flags.PrintDefaults() 438 | } 439 | 440 | func (p *PopCmd) Flags(args ...string) error { 441 | p.flags = NewMqFlagSet() 442 | 443 | p.n = p.flags.n() 444 | p.outputfile = p.flags.outputfile() 445 | 446 | if err := p.flags.Parse(args); err != nil { 447 | return err 448 | } 449 | return p.flags.validateAllFlags() 450 | } 451 | 452 | func (p *PopCmd) Args() error { 453 | if p.flags.NArg() < 1 { 454 | return errors.New(`pop requires a queue name 455 | 456 | usage: iron mq pop [-n n] [-o file] QUEUE_NAME`) 457 | } 458 | if *p.outputfile != "" { 459 | f, err := os.Create(*p.outputfile) 460 | if err != nil { 461 | return err 462 | } 463 | p.file = f 464 | } 465 | 466 | p.queue_name = p.flags.Arg(0) 467 | return nil 468 | } 469 | 470 | func (p *PopCmd) Run() { 471 | q := mq.ConfigNew(p.queue_name, &p.settings) 472 | 473 | messages, err := q.PopN(*p.n) 474 | if err != nil { 475 | fmt.Fprintln(os.Stderr, red(err)) 476 | } 477 | 478 | // If anything here fails, we still want to print out what was deleted before exiting 479 | if p.file != nil { 480 | b, err := json.Marshal(messages) 481 | if err != nil { 482 | fmt.Fprintln(os.Stderr, red(err)) 483 | printMessages(messages) 484 | } 485 | _, err = p.file.Write(b) 486 | if err != nil { 487 | fmt.Fprintln(os.Stderr, red(err)) 488 | printMessages(messages) 489 | } 490 | } 491 | 492 | if isPipedOut() { 493 | printMessages(messages) 494 | } else { 495 | plural := "" 496 | if *p.n > 1 { 497 | plural = "s" 498 | } 499 | fmt.Println(green(LINES, "Message", plural, " successfully popped off ", q.Name)) 500 | fmt.Println() 501 | fmt.Println("-------- ID ------ | Body") 502 | printMessages(messages) 503 | } 504 | } 505 | 506 | type PushCmd struct { 507 | mqCmd 508 | filename *string 509 | messages []string 510 | queue_name string 511 | } 512 | 513 | func (p *PushCmd) Usage() { 514 | fmt.Fprintln(os.Stderr, `usage: iron mq push [-f file] QUEUE_NAME "MESSAGE" "MESSAGE"... 515 | 516 | f: json file with message bodies to be used. Format should be '{"messages": ["1", "2", "3"...]}'`) 517 | p.flags.PrintDefaults() 518 | } 519 | 520 | func (p *PushCmd) Flags(args ...string) error { 521 | p.flags = NewMqFlagSet() 522 | 523 | p.filename = p.flags.filename() 524 | 525 | if err := p.flags.Parse(args); err != nil { 526 | return err 527 | } 528 | 529 | return p.flags.validateAllFlags() 530 | } 531 | 532 | func (p *PushCmd) Args() error { 533 | if p.flags.NArg() < 1 && !isPipedIn() { 534 | return errors.New(`push requires the queue name 535 | 536 | usage: iron mq push [-f file] QUEUE_NAME "MESSAGE"...`) 537 | } 538 | 539 | p.queue_name = p.flags.Arg(0) 540 | 541 | if *p.filename != "" { 542 | b, err := ioutil.ReadFile(*p.filename) 543 | if err != nil { 544 | return err 545 | } 546 | 547 | messageStruct := struct { 548 | Messages []string `json:"messages"` 549 | }{} 550 | err = json.Unmarshal(b, &messageStruct) 551 | if err != nil { 552 | return err 553 | } 554 | 555 | p.messages = append(p.messages, messageStruct.Messages...) 556 | } 557 | 558 | if p.flags.NArg() > 1 { 559 | p.messages = append(p.messages, p.flags.Args()[1:]...) 560 | } 561 | 562 | if len(p.messages) < 1 { 563 | return errors.New(`push requires at least one message 564 | 565 | usage: iron mq push [-f file] QUEUE_NAME "MESSAGE" "MESSAGE 2"...`) 566 | } 567 | return nil 568 | } 569 | 570 | func (p *PushCmd) Run() { 571 | q := mq.ConfigNew(p.queue_name, &p.settings) 572 | 573 | ids, err := q.PushStrings(p.messages...) 574 | if err != nil { 575 | fmt.Fprintln(os.Stderr, red(err)) 576 | } 577 | 578 | if isPipedOut() { 579 | for _, id := range ids { 580 | fmt.Println(id) 581 | } 582 | } else { 583 | fmt.Println(green(LINES, "Message succesfully pushed!")) 584 | fmt.Printf("%sMessage IDs:\n", BLANKS) 585 | fmt.Printf("%s", BLANKS) 586 | for _, id := range ids { 587 | fmt.Printf("%s ", id) 588 | } 589 | fmt.Println() 590 | } 591 | } 592 | 593 | type ReserveCmd struct { 594 | mqCmd 595 | queue_name string 596 | n *int 597 | timeout *int 598 | outputfile *string 599 | file *os.File 600 | } 601 | 602 | func (r *ReserveCmd) Usage() { 603 | fmt.Fprintln(os.Stderr, `usage: iron mq reserve [-t timeout] [-n n] [-o file] QUEUE_NAME 604 | 605 | t: timeout until message is put back on the queue, default: 60 606 | n: number of messages to reserve 607 | o: write results in json to a file`) 608 | r.flags.PrintDefaults() 609 | } 610 | 611 | func (r *ReserveCmd) Flags(args ...string) error { 612 | r.flags = NewMqFlagSet() 613 | 614 | r.n = r.flags.n() 615 | r.timeout = r.flags.timeout() 616 | r.outputfile = r.flags.outputfile() 617 | 618 | if err := r.flags.Parse(args); err != nil { 619 | return err 620 | } 621 | return r.flags.validateAllFlags() 622 | 623 | } 624 | func (r *ReserveCmd) Args() error { 625 | if r.flags.NArg() < 1 { 626 | return errors.New(`reserve requires a queue name 627 | 628 | usage: iron mq reserve [-t timeout] [-n n] [-o file] QUEUE_NAME`) 629 | } 630 | if *r.outputfile != "" { 631 | f, err := os.Create(*r.outputfile) 632 | if err != nil { 633 | return err 634 | } 635 | r.file = f 636 | } 637 | 638 | r.queue_name = r.flags.Arg(0) 639 | return nil 640 | } 641 | 642 | func (r *ReserveCmd) Run() { 643 | q := mq.ConfigNew(r.queue_name, &r.settings) 644 | messages, err := q.GetNWithTimeout(*r.n, *r.timeout) 645 | if err != nil { 646 | fmt.Fprintln(os.Stderr, red(err)) 647 | } 648 | 649 | // If anything here fails, we still want to print out what was reserved before exiting 650 | if r.file != nil { 651 | b, err := json.Marshal(messages) 652 | if err != nil { 653 | fmt.Fprintln(os.Stderr, red(err)) 654 | printReservedMessages(messages) 655 | return 656 | } 657 | _, err = r.file.Write(b) 658 | if err != nil { 659 | fmt.Fprintln(os.Stderr, red(err)) 660 | printReservedMessages(messages) 661 | return 662 | } 663 | } 664 | 665 | if len(messages) < 1 { 666 | fmt.Fprintln(os.Stderr, red("Queue is empty")) 667 | return 668 | } 669 | 670 | if isPipedOut() { 671 | printReservedMessages(messages) 672 | } else { 673 | fmt.Println(green(LINES, "Messages successfully reserved")) 674 | fmt.Println("--------- ID ------|------- Reservation ID -------- | Body") 675 | printReservedMessages(messages) 676 | } 677 | } 678 | 679 | type RmCmd struct { 680 | mqCmd 681 | 682 | queue_name string 683 | } 684 | 685 | func (r *RmCmd) Usage() { 686 | fmt.Fprintln(os.Stderr, `usage: iron mq remove QUEUE_NAME 687 | 688 | Delete a queue from a project`) 689 | r.flags.PrintDefaults() 690 | } 691 | 692 | func (r *RmCmd) Flags(args ...string) error { 693 | r.flags = NewMqFlagSet() 694 | if err := r.flags.Parse(args); err != nil { 695 | return err 696 | } 697 | return nil 698 | } 699 | 700 | func (r *RmCmd) Args() error { 701 | if r.flags.NArg() < 1 && !isPipedIn() { 702 | return errors.New("rm requires a queue name.") 703 | } 704 | 705 | r.queue_name = r.flags.Arg(0) 706 | return nil 707 | } 708 | func (r *RmCmd) Run() { 709 | var queues []mq.Queue 710 | 711 | if isPipedIn() { 712 | scanner := bufio.NewScanner(os.Stdin) 713 | for scanner.Scan() { 714 | name := scanner.Text() 715 | queues = append(queues, mq.ConfigNew(name, &r.settings)) 716 | } 717 | if err := scanner.Err(); err != nil { 718 | fmt.Fprintln(os.Stderr, err) 719 | } 720 | } else { 721 | queues = append(queues, mq.ConfigNew(r.queue_name, &r.settings)) 722 | } 723 | 724 | for _, q := range queues { 725 | err := q.Delete() 726 | if err != nil { 727 | fmt.Println(red("Error deleting queue ", q.Name, ": ", err)) 728 | } else { 729 | fmt.Println(green(LINES, q.Name, " has been sucessfully deleted.")) 730 | } 731 | } 732 | } 733 | -------------------------------------------------------------------------------- /mq_util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | 11 | "github.com/iron-io/iron_go3/config" 12 | "github.com/iron-io/iron_go3/mq" 13 | ) 14 | 15 | func printMessages(msgs []mq.Message) { 16 | for _, msg := range msgs { 17 | fmt.Printf("%s %q\n", msg.Id, msg.Body) 18 | } 19 | } 20 | 21 | func printReservedMessages(msgs []mq.Message) { 22 | for _, msg := range msgs { 23 | fmt.Printf("%s %s %q\n", msg.Id, msg.ReservationId, msg.Body) 24 | } 25 | } 26 | 27 | // BLANKS name: url.com/endpoint 28 | func printSubscribers(info mq.QueueInfo) { 29 | for _, subscriber := range info.Push.Subscribers { 30 | fmt.Printf("%s%s\n", BLANKS, subscriber.URL) 31 | } 32 | } 33 | 34 | // This is based on the format of func printMessages([]*mq.Message) 35 | func readIds() ([]string, error) { 36 | var ids []string 37 | scanner := bufio.NewScanner(os.Stdin) 38 | for scanner.Scan() { 39 | message := scanner.Text() 40 | if len(message) > 19 { 41 | id := message[:19] // We want the first 19 characters of the line, since an id is 19 characters long 42 | ids = append(ids, id) 43 | } 44 | } 45 | return ids, scanner.Err() 46 | } 47 | 48 | // Check if stdout is being piped 49 | func isPipedOut() bool { 50 | fi, _ := os.Stdout.Stat() 51 | return (fi.Mode() & os.ModeNamedPipe) == os.ModeNamedPipe 52 | } 53 | 54 | func isPipedIn() bool { 55 | fi, _ := os.Stdin.Stat() 56 | return (fi.Mode() & os.ModeNamedPipe) == os.ModeNamedPipe 57 | } 58 | 59 | // TODO: Figure out the region for the hud url 60 | // seriously though 61 | // this is super duper hacky 62 | // it only works with the public cluster mq-aws-us-east-1-1 63 | func getHudTag(settings config.Settings) (string, error) { 64 | res, err := http.Get("https://auth.iron.io/1/clusters?oauth=" + settings.Token) 65 | if err != nil { 66 | return "", err 67 | } 68 | defer res.Body.Close() 69 | 70 | clusters := struct { 71 | Clusters []struct { 72 | Tag string `json:"tag"` 73 | URL string `json:"url"` 74 | } `json:"clusters"` 75 | }{} 76 | b, err := ioutil.ReadAll(res.Body) 77 | if err != nil { 78 | fmt.Println(err) 79 | return "", err 80 | } 81 | err = json.Unmarshal(b, &clusters) 82 | if err != nil { 83 | return "", err 84 | } 85 | queueHost := settings.Host 86 | for _, cluster := range clusters.Clusters { 87 | if cluster.URL == queueHost { 88 | return cluster.Tag, err 89 | } 90 | } 91 | return "", fmt.Errorf("no hud tags found") 92 | } 93 | 94 | func printQueueHudURL(prefix string, q mq.Queue) { 95 | if tag, err := getHudTag(q.Settings); err == nil { 96 | fmt.Printf("%sVisit hud-e.iron.io/mq/%s/projects/%s/queues/%s for more info.\n", prefix, 97 | tag, 98 | q.Settings.ProjectId, 99 | q.Name) 100 | } 101 | } 102 | 103 | func mqProjectName(settings config.Settings) (string, error) { 104 | res, err := http.Get("https://auth.iron.io/1/projects/" + settings.ProjectId + "?oauth=" + settings.Token) 105 | if err != nil { 106 | return "", err 107 | } 108 | defer res.Body.Close() 109 | projects := struct { 110 | Project struct { 111 | Name string `json:"name"` 112 | } `json:"project"` 113 | }{} 114 | err = json.NewDecoder(res.Body).Decode(&projects) 115 | if err != nil { 116 | return "", err 117 | } 118 | return projects.Project.Name, nil 119 | } 120 | -------------------------------------------------------------------------------- /run.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // TODO extract into package or such 9 | // 10 | // because this is "frikkin rad" it's basically upload 11 | // with the args moved up one. 12 | 13 | type RunCmd struct { 14 | UploadCmd 15 | } 16 | 17 | func (r *RunCmd) Usage() { 18 | fmt.Fprintln(os.Stderr, `usage: iron run [-zip my.zip] -name NAME [OPTIONS] some/image[:tag] [command...]`) 19 | r.flags.PrintDefaults() 20 | } 21 | 22 | func (r *RunCmd) Args() error { 23 | r.UploadCmd.codes.Host = "true" 24 | return r.UploadCmd.Args() 25 | } 26 | -------------------------------------------------------------------------------- /worker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | 8 | "github.com/iron-io/iron_go3/api" 9 | "github.com/iron-io/iron_go3/worker" 10 | ) 11 | 12 | // TODO move this into iron_go3? 13 | func dockerLogin(w *worker.Worker, args *map[string]string) (msg string, err error) { 14 | data, err := json.Marshal(args) 15 | reader := bytes.NewReader(data) 16 | 17 | req, err := http.NewRequest("POST", api.Action(w.Settings, "credentials").URL.String(), reader) 18 | if err != nil { 19 | return "", err 20 | } 21 | 22 | req.Header.Set("Accept", "application/json") 23 | req.Header.Set("Accept-Encoding", "gzip/deflate") 24 | req.Header.Set("Authorization", "OAuth "+w.Settings.Token) 25 | req.Header.Set("Content-Type", "application/json") 26 | req.Header.Set("User-Agent", w.Settings.UserAgent) 27 | 28 | response, err := http.DefaultClient.Do(req) 29 | if err != nil { 30 | return "", err 31 | } 32 | 33 | if err = api.ResponseAsError(response); err != nil { 34 | return "", err 35 | } 36 | 37 | var res struct { 38 | Msg string `json:"msg"` 39 | } 40 | 41 | err = json.NewDecoder(response.Body).Decode(&res) 42 | return res.Msg, err 43 | } 44 | --------------------------------------------------------------------------------