mirror of
https://github.com/FlipsideCrypto/convox.git
synced 2026-02-06 10:56:56 +00:00
add resources console/export/import
This commit is contained in:
parent
53d3b03763
commit
dc93453d28
4
.github/workflows/push.yml
vendored
4
.github/workflows/push.yml
vendored
@ -1,7 +1,7 @@
|
||||
name: push
|
||||
on:
|
||||
push:
|
||||
branches: ["*"]
|
||||
branches:
|
||||
tags: ["*"]
|
||||
jobs:
|
||||
release:
|
||||
@ -20,4 +20,4 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: push
|
||||
run: docker push docker.pkg.github.com/convox/convox/convox:${VERSION}
|
||||
run: docker push docker.pkg.github.com/convox/convox/convox:${VERSION}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
## development #################################################################
|
||||
|
||||
FROM golang:1.12 AS development
|
||||
FROM golang:1.13 AS development
|
||||
|
||||
RUN apt-get update && apt-get -y install postgresql-client
|
||||
|
||||
RUN curl -s https://download.docker.com/linux/static/stable/x86_64/docker-18.03.1-ce.tgz | \
|
||||
tar -C /usr/bin --strip-components 1 -xz
|
||||
|
||||
19
Makefile
19
Makefile
@ -1,4 +1,4 @@
|
||||
.PHONY: all build clean clean-package compress generate mocks package release test
|
||||
.PHONY: all build clean clean-package compress generate generate-k8s generate-provider mocks package release test
|
||||
|
||||
commands = api atom build router
|
||||
|
||||
@ -18,16 +18,23 @@ clean-package:
|
||||
compress: $(binaries)
|
||||
upx-ucl -1 $^
|
||||
|
||||
generate:
|
||||
go run cmd/generate/main.go controllers > pkg/api/controllers.go
|
||||
go run cmd/generate/main.go routes > pkg/api/routes.go
|
||||
go run cmd/generate/main.go sdk > sdk/methods.go
|
||||
generate: generate-provider generate-k8s
|
||||
|
||||
generate-k8s:
|
||||
make -C pkg/atom generate
|
||||
make -C provider/k8s generate
|
||||
|
||||
mocks: generate
|
||||
generate-provider:
|
||||
go run cmd/generate/main.go controllers > pkg/api/controllers.go
|
||||
go run cmd/generate/main.go routes > pkg/api/routes.go
|
||||
go run cmd/generate/main.go sdk > sdk/methods.go
|
||||
|
||||
mocks: generate-provider
|
||||
make -C pkg/atom mocks
|
||||
make -C pkg/structs mocks
|
||||
mockery -case underscore -dir pkg/start -outpkg sdk -output pkg/mock/start -name Interface
|
||||
mockery -case underscore -dir sdk -outpkg sdk -output pkg/mock/sdk -name Interface
|
||||
mockery -case underscore -dir vendor/github.com/convox/stdcli -outpkg stdcli -output pkg/mock/stdcli -name Executor
|
||||
|
||||
package:
|
||||
$(GOPATH)/bin/packr
|
||||
|
||||
21
cmd/convox/Makefile
Normal file
21
cmd/convox/Makefile
Normal file
@ -0,0 +1,21 @@
|
||||
.PHONY: all build clean release release-gopath
|
||||
|
||||
all: build
|
||||
|
||||
build:
|
||||
go install ./...
|
||||
|
||||
clean:
|
||||
rm -f pkg/convox-*
|
||||
|
||||
release: release-gopath
|
||||
go get -u github.com/karalabe/xgo
|
||||
$(GOPATH)/bin/xgo -branch $(shell git rev-parse HEAD) -out pkg/convox -targets 'darwin/amd64,linux/amd64,windows/amd64' -ldflags "-X main.version=$(VERSION)" .
|
||||
mkdir -p pkg && docker run -v $(GOPATH):/gopath -i ubuntu tar czv /gopath/src/github.com/convox/convox/cmd/convox/pkg | tar xzv -C pkg --strip-components 8
|
||||
aws s3 cp pkg/convox-darwin-10.6-amd64 s3://convox/release/$(VERSION)/cli/darwin/convox --acl public-read
|
||||
aws s3 cp pkg/convox-linux-amd64 s3://convox/release/$(VERSION)/cli/linux/convox --acl public-read
|
||||
aws s3 cp pkg/convox-windows-4.0-amd64.exe s3://convox/release/$(VERSION)/cli/windows/convox.exe --acl public-read
|
||||
|
||||
# set up gopath in docker volume if running inside a container
|
||||
release-gopath:
|
||||
if [ -f /.dockerenv ]; then tar cz $(GOPATH) | docker run -v $(GOPATH):/gopath -i ubuntu tar xz -C /gopath --strip-components 2; fi
|
||||
17
cmd/convox/main.go
Normal file
17
cmd/convox/main.go
Normal file
@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "dev"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := cli.New("convox", version)
|
||||
|
||||
os.Exit(c.Execute(os.Args[1:]))
|
||||
}
|
||||
10
go.mod
10
go.mod
@ -7,10 +7,17 @@ require (
|
||||
github.com/Microsoft/hcsshim v0.8.7-0.20190801035247-8694eade7dd3 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.5.0
|
||||
github.com/aws/aws-sdk-go v1.21.10
|
||||
github.com/convox/changes v0.0.0-20191105034405-8c0df759a3b3
|
||||
github.com/convox/exec v0.0.0-20180905012044-cc13d277f897
|
||||
github.com/convox/hid v0.0.0-20180912192857-c67381b7ffff
|
||||
github.com/convox/logger v0.0.0-20180522214415-e39179955b52
|
||||
github.com/convox/rack v0.0.0-20191023140332-e19fce33f6c3 // indirect
|
||||
github.com/convox/stdapi v0.0.0-20190708203955-b81b71b6a680
|
||||
github.com/convox/stdcli v0.0.0-20190326115454-b78bee159e98
|
||||
github.com/convox/stdsdk v0.0.0-20190422120437-3e80a397e377
|
||||
github.com/convox/u2f v0.0.0-20180912192910-a73404142726
|
||||
github.com/convox/version v0.0.0-20160822184233-ffefa0d565d2
|
||||
github.com/creack/pty v1.1.9
|
||||
github.com/docker/docker v1.4.2-0.20190710153559-aa8249ae1b8b
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
@ -21,11 +28,13 @@ require (
|
||||
github.com/gobuffalo/packr v1.30.1
|
||||
github.com/gobwas/glob v0.2.3
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903 // indirect
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/websocket v1.4.0
|
||||
github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7 // indirect
|
||||
github.com/headzoo/surf v1.0.0
|
||||
github.com/headzoo/ut v0.0.0-20181013193318-a13b5a7a02ca // indirect
|
||||
github.com/imdario/mergo v0.3.7 // indirect
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/miekg/dns v1.1.15
|
||||
github.com/onsi/ginkgo v1.8.0 // indirect
|
||||
@ -35,6 +44,7 @@ require (
|
||||
github.com/stretchr/testify v1.3.0
|
||||
golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf
|
||||
golang.org/x/net v0.0.0-20191101175033-0deb6923b6d9 // indirect
|
||||
golang.org/x/sys v0.0.0-20191104094858-e8c54fb511f6 // indirect
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
|
||||
google.golang.org/api v0.9.0
|
||||
gopkg.in/inf.v0 v0.9.0 // indirect
|
||||
|
||||
22
go.sum
22
go.sum
@ -44,18 +44,34 @@ github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv
|
||||
github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
|
||||
github.com/containerd/ttrpc v0.0.0-20180920185216-2a805f718635/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
|
||||
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
|
||||
github.com/convox/changes v0.0.0-20191105034405-8c0df759a3b3 h1:S0AiQtEXMb5jBw4N6TaIPMcnCLV91Se+HEOpMmYyt+o=
|
||||
github.com/convox/changes v0.0.0-20191105034405-8c0df759a3b3/go.mod h1:RpXN2RCcOkPSccYY3xrmKPQzJNrGq+9+kbtFcAQpEv8=
|
||||
github.com/convox/exec v0.0.0-20180905012044-cc13d277f897 h1:8SnSpocZ8I5RlRo8NFKYrAa6pQYZp+4T/pWyD3GrwIk=
|
||||
github.com/convox/exec v0.0.0-20180905012044-cc13d277f897/go.mod h1:GAxysqvg1ZSl42aLnvLXmFkXkacF4AGql5ZKT7s1DGc=
|
||||
github.com/convox/hid v0.0.0-20180912192857-c67381b7ffff h1:B+2WWcb+QM8yJL6HyUsYhnazUOBbvKv8kKS7WTmhyRU=
|
||||
github.com/convox/hid v0.0.0-20180912192857-c67381b7ffff/go.mod h1:yBM17eJdl/WBU/dv5Ojnu63mNqOlv+tWazdcFIalph4=
|
||||
github.com/convox/inotify v0.0.0-20170313035821-b56f5149b5c6 h1:bigLg74wAp2d11vaK4/IkTbN95D5mlSqZ/+/O6YBgtQ=
|
||||
github.com/convox/inotify v0.0.0-20170313035821-b56f5149b5c6/go.mod h1:JOi7Az72uBLT9D2Qv/6FbUbC9W420st0MMIUgVYUyd0=
|
||||
github.com/convox/logger v0.0.0-20180522214415-e39179955b52 h1:HTaloo6by+4NE1A1mRYprWzsOy1PGH2tPGfgZ60dyHU=
|
||||
github.com/convox/logger v0.0.0-20180522214415-e39179955b52/go.mod h1:IcbSD+Pq+bQV2z/otiMCHLAYBrDR/jPFopFatrWjlMM=
|
||||
github.com/convox/rack v0.0.0-20191023140332-e19fce33f6c3 h1:ldQHJVSEAMKwYQ8GEFNJ/HJRDi+Z3ZF8CU8cD4fZCM0=
|
||||
github.com/convox/rack v0.0.0-20191023140332-e19fce33f6c3/go.mod h1:2dMGMyJnGzN0JlFFyaXzm+hK47fzNAhBvU9uqJy47jg=
|
||||
github.com/convox/stdapi v0.0.0-20190708203955-b81b71b6a680 h1:IRgaDO4+YanUYcvBlXkzJrgwFh/3BcQOYoI5gEUz2wQ=
|
||||
github.com/convox/stdapi v0.0.0-20190708203955-b81b71b6a680/go.mod h1:NkUGoKf5tAaHq7MW02AF9mSXSIHCZJ0ZG8+NqRI5t38=
|
||||
github.com/convox/stdcli v0.0.0-20190326115454-b78bee159e98 h1:jSUPA5xGpERhDUevVwj8YPXhIbKq+YJPXpF8U9M4p1o=
|
||||
github.com/convox/stdcli v0.0.0-20190326115454-b78bee159e98/go.mod h1:D+mhXWLSLHQ+I2zZzYfrSzONMlE6FnPFw9hM4oDcN8Y=
|
||||
github.com/convox/stdsdk v0.0.0-20190422120437-3e80a397e377 h1:PuSJ72MD0mYsMCTvTQ1YydIbQUWtEykNHyweI/vA0PY=
|
||||
github.com/convox/stdsdk v0.0.0-20190422120437-3e80a397e377/go.mod h1:y1vtmkDKBkWSQ6e2gPXAyz1NCuWZ2x3vrP/SFeDDNco=
|
||||
github.com/convox/u2f v0.0.0-20180912192910-a73404142726 h1:FY8dxqTRVj4lRmNoAkOeYxSnxZ40s64P/yxyVf8NBlo=
|
||||
github.com/convox/u2f v0.0.0-20180912192910-a73404142726/go.mod h1:Gw2uoNinY/uxw7+krj5aloiVD0asEaGSwfutlwip/sU=
|
||||
github.com/convox/version v0.0.0-20160822184233-ffefa0d565d2 h1:tdp/1KHBnbne0yT1yuKnAdOTBHRue9yQ4oON8rzGgZc=
|
||||
github.com/convox/version v0.0.0-20160822184233-ffefa0d565d2/go.mod h1:s8HHEf4LLsmPppeubX/A5bz1JpLYkDXbu+ciuYMTk8A=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@ -65,6 +81,8 @@ github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BU
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v1.4.2-0.20190710153559-aa8249ae1b8b h1:+Ga+YpCDpcY1fln6GI0fiiirpqHGcob5/Vk3oKNuGdU=
|
||||
github.com/docker/docker v1.4.2-0.20190710153559-aa8249ae1b8b/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/engine v1.4.2-0.20190717161051-705d9623b7c1 h1:HjO0YFIGk26fADKDJYuAoGneX9nrVVotZJ1Ctn15Vv4=
|
||||
github.com/docker/engine v1.4.2-0.20190717161051-705d9623b7c1/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
@ -349,6 +367,8 @@ github.com/ijc/Gotty v0.0.0-20170406111628-a8b993ba6abd h1:anPrsicrIi2ColgWTVPk+
|
||||
github.com/ijc/Gotty v0.0.0-20170406111628-a8b993ba6abd/go.mod h1:3LVOLeyx9XVvwPgrt2be44XgSqndprz1G18rSk8KD84=
|
||||
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
|
||||
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
|
||||
github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
||||
@ -623,6 +643,8 @@ golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190710143415-6ec70d6a5542 h1:6ZQFf1D2YYDDI7eSwW8adlkkavTB9sw5I24FVtEvNUQ=
|
||||
golang.org/x/sys v0.0.0-20190710143415-6ec70d6a5542/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191104094858-e8c54fb511f6 h1:ZJUmhYTp8GbGC0ViZRc2U+MIYQ8xx9MscsdXnclfIhw=
|
||||
golang.org/x/sys v0.0.0-20191104094858-e8c54fb511f6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@ -1035,6 +1035,56 @@ func (s *Server) ReleasePromote(c *stdapi.Context) error {
|
||||
return c.RenderOK()
|
||||
}
|
||||
|
||||
func (s *Server) ResourceConsole(c *stdapi.Context) error {
|
||||
if err := s.hook("ResourceConsoleValidate", c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
app := c.Var("app")
|
||||
name := c.Var("name")
|
||||
rw := c
|
||||
|
||||
var opts structs.ResourceConsoleOptions
|
||||
if err := stdapi.UnmarshalOptions(c.Request(), &opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := s.provider(c).WithContext(c.Context()).ResourceConsole(app, name, rw, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) ResourceExport(c *stdapi.Context) error {
|
||||
if err := s.hook("ResourceExportValidate", c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
app := c.Var("app")
|
||||
name := c.Var("name")
|
||||
|
||||
v, err := s.provider(c).WithContext(c.Context()).ResourceExport(app, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c, ok := interface{}(v).(io.Closer); ok {
|
||||
defer c.Close()
|
||||
}
|
||||
|
||||
if _, err := io.Copy(c, v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if vs, ok := interface{}(v).(Sortable); ok {
|
||||
sort.Slice(v, vs.Less)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) ResourceGet(c *stdapi.Context) error {
|
||||
if err := s.hook("ResourceGetValidate", c); err != nil {
|
||||
return err
|
||||
@ -1055,6 +1105,23 @@ func (s *Server) ResourceGet(c *stdapi.Context) error {
|
||||
return c.RenderJSON(v)
|
||||
}
|
||||
|
||||
func (s *Server) ResourceImport(c *stdapi.Context) error {
|
||||
if err := s.hook("ResourceImportValidate", c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
app := c.Var("app")
|
||||
name := c.Var("name")
|
||||
r := c
|
||||
|
||||
err := s.provider(c).WithContext(c.Context()).ResourceImport(app, name, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.RenderOK()
|
||||
}
|
||||
|
||||
func (s *Server) ResourceList(c *stdapi.Context) error {
|
||||
if err := s.hook("ResourceListValidate", c); err != nil {
|
||||
return err
|
||||
|
||||
@ -52,7 +52,10 @@ func (s *Server) setupRoutes(r stdapi.Router) {
|
||||
r.Route("GET", "/apps/{app}/releases/{id}", s.ReleaseGet)
|
||||
r.Route("GET", "/apps/{app}/releases", s.ReleaseList)
|
||||
r.Route("POST", "/apps/{app}/releases/{id}/promote", s.ReleasePromote)
|
||||
r.Route("SOCKET", "/apps/{app}/resources/{name}/console", s.ResourceConsole)
|
||||
r.Route("GET", "/apps/{app}/resources/{name}/data", s.ResourceExport)
|
||||
r.Route("GET", "/apps/{app}/resources/{name}", s.ResourceGet)
|
||||
r.Route("PUT", "/apps/{app}/resources/{name}/data", s.ResourceImport)
|
||||
r.Route("GET", "/apps/{app}/resources", s.ResourceList)
|
||||
r.Route("GET", "/apps/{app}/services", s.ServiceList)
|
||||
r.Route("POST", "/apps/{app}/services/{name}/restart", s.ServiceRestart)
|
||||
|
||||
32
pkg/cli/api.go
Normal file
32
pkg/cli/api.go
Normal file
@ -0,0 +1,32 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
"github.com/convox/stdsdk"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("api get", "query the rack api", Api, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Usage: "<path>",
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
}
|
||||
|
||||
func Api(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var v interface{}
|
||||
|
||||
if err := rack.Get(c.Arg(0), stdsdk.RequestOptions{}, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Writef("%s\n", string(data))
|
||||
}
|
||||
40
pkg/cli/api_test.go
Normal file
40
pkg/cli/api_test.go
Normal file
@ -0,0 +1,40 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/stdsdk"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestApi(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("Get", "/apps", stdsdk.RequestOptions{}, mock.Anything).Return(nil).Run(func(args mock.Arguments) {
|
||||
err := json.Unmarshal([]byte(`[{"name":"app1"}]`), args.Get(2))
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
res, err := testExecute(e, "api get /apps", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
require.Equal(t, "[\n {\n \"name\": \"app1\"\n }\n]\n", res.Stdout)
|
||||
require.Equal(t, "", res.Stderr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApiError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("Get", "/apps", stdsdk.RequestOptions{}, mock.Anything).Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "api get /apps", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
require.Equal(t, "", res.Stdout)
|
||||
require.Equal(t, "ERROR: err1\n", res.Stderr)
|
||||
})
|
||||
}
|
||||
581
pkg/cli/apps.go
Normal file
581
pkg/cli/apps.go
Normal file
@ -0,0 +1,581 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/convox/convox/pkg/common"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("apps", "list apps", Apps, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("apps cancel", "cancel an app update", AppsCancel, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack, flagApp},
|
||||
Usage: "[app]",
|
||||
Validate: stdcli.ArgsMax(1),
|
||||
})
|
||||
|
||||
register("apps create", "create an app", AppsCreate, stdcli.CommandOptions{
|
||||
Flags: append(stdcli.OptionFlags(structs.AppCreateOptions{}), flagRack, flagWait),
|
||||
Usage: "[name]",
|
||||
Validate: stdcli.ArgsMax(1),
|
||||
})
|
||||
|
||||
register("apps delete", "delete an app", AppsDelete, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack, flagWait},
|
||||
Usage: "<app>",
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("apps export", "export an app", AppsExport, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{
|
||||
flagApp,
|
||||
flagRack,
|
||||
stdcli.StringFlag("file", "f", "export to file"),
|
||||
},
|
||||
Usage: "[app]",
|
||||
Validate: stdcli.ArgsMax(1),
|
||||
})
|
||||
|
||||
register("apps import", "import an app", AppsImport, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{
|
||||
flagApp,
|
||||
flagRack,
|
||||
stdcli.StringFlag("file", "f", "import from file"),
|
||||
},
|
||||
Usage: "[app]",
|
||||
Validate: stdcli.ArgsMax(1),
|
||||
})
|
||||
|
||||
register("apps info", "get information about an app", AppsInfo, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagApp, flagRack},
|
||||
Usage: "[app]",
|
||||
Validate: stdcli.ArgsMax(1),
|
||||
})
|
||||
|
||||
register("apps lock", "enable termination protection", AppsLock, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagApp, flagRack},
|
||||
Usage: "[app]",
|
||||
Validate: stdcli.ArgsMax(1),
|
||||
})
|
||||
|
||||
register("apps params", "display app parameters", AppsParams, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagApp, flagRack},
|
||||
Usage: "[app]",
|
||||
Validate: stdcli.ArgsMax(1),
|
||||
})
|
||||
|
||||
register("apps params set", "set app parameters", AppsParamsSet, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagApp, flagRack, flagWait},
|
||||
Usage: "<Key=Value> [Key=Value]...",
|
||||
Validate: stdcli.ArgsMin(1),
|
||||
})
|
||||
|
||||
register("apps unlock", "disable termination protection", AppsUnlock, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagApp, flagRack},
|
||||
Usage: "[app]",
|
||||
Validate: stdcli.ArgsMax(1),
|
||||
})
|
||||
|
||||
register("apps wait", "wait for an app to finish updating", AppsWait, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagApp, flagRack},
|
||||
Usage: "[app]",
|
||||
Validate: stdcli.ArgsMax(1),
|
||||
})
|
||||
}
|
||||
|
||||
func Apps(rack sdk.Interface, c *stdcli.Context) error {
|
||||
as, err := rack.AppList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := c.Table("APP", "STATUS", "RELEASE")
|
||||
|
||||
for _, a := range as {
|
||||
t.AddRow(a.Name, a.Status, a.Release)
|
||||
}
|
||||
|
||||
return t.Print()
|
||||
}
|
||||
|
||||
func AppsCancel(rack sdk.Interface, c *stdcli.Context) error {
|
||||
app := coalesce(c.Arg(0), app(c))
|
||||
|
||||
c.Startf("Cancelling <app>%s</app>", app)
|
||||
|
||||
if err := rack.AppCancel(app); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
func AppsCreate(rack sdk.Interface, c *stdcli.Context) error {
|
||||
app := coalesce(c.Arg(0), app(c))
|
||||
|
||||
var opts structs.AppCreateOptions
|
||||
|
||||
if err := c.Options(&opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Startf("Creating <app>%s</app>", app)
|
||||
|
||||
if _, err := rack.AppCreate(app, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("wait") {
|
||||
if err := common.WaitForAppRunning(rack, app); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
func AppsDelete(rack sdk.Interface, c *stdcli.Context) error {
|
||||
app := c.Args[0]
|
||||
|
||||
c.Startf("Deleting <app>%s</app>", app)
|
||||
|
||||
if err := rack.AppDelete(app); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("wait") {
|
||||
if err := common.WaitForAppDeleted(rack, c, app); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
func AppsExport(rack sdk.Interface, c *stdcli.Context) error {
|
||||
app := coalesce(c.Arg(0), app(c))
|
||||
|
||||
var w io.Writer
|
||||
|
||||
if file := c.String("file"); file != "" {
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
w = f
|
||||
} else {
|
||||
if c.Writer().IsTerminal() {
|
||||
return fmt.Errorf("pipe this command into a file or specify --file")
|
||||
}
|
||||
w = c.Writer().Stdout
|
||||
c.Writer().Stdout = c.Writer().Stderr
|
||||
}
|
||||
|
||||
if err := appExport(rack, c, app, w); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func AppsImport(rack sdk.Interface, c *stdcli.Context) error {
|
||||
app := coalesce(c.Arg(0), app(c))
|
||||
|
||||
var r io.ReadCloser
|
||||
|
||||
if file := c.String("file"); file != "" {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r = f
|
||||
} else {
|
||||
if c.Reader().IsTerminal() {
|
||||
return fmt.Errorf("pipe a file into this command or specify --file")
|
||||
}
|
||||
r = ioutil.NopCloser(c.Reader())
|
||||
}
|
||||
|
||||
defer r.Close()
|
||||
|
||||
if err := appImport(rack, c, app, r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func AppsInfo(rack sdk.Interface, c *stdcli.Context) error {
|
||||
a, err := rack.AppGet(coalesce(c.Arg(0), app(c)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i := c.Info()
|
||||
|
||||
i.Add("Name", a.Name)
|
||||
i.Add("Status", a.Status)
|
||||
|
||||
i.Add("Generation", a.Generation)
|
||||
i.Add("Locked", fmt.Sprintf("%t", a.Locked))
|
||||
i.Add("Release", a.Release)
|
||||
|
||||
if a.Router != "" {
|
||||
i.Add("Router", a.Router)
|
||||
}
|
||||
|
||||
return i.Print()
|
||||
}
|
||||
|
||||
func AppsLock(rack sdk.Interface, c *stdcli.Context) error {
|
||||
app := coalesce(c.Arg(0), app(c))
|
||||
|
||||
c.Startf("Locking <app>%s</app>", app)
|
||||
|
||||
if err := rack.AppUpdate(app, structs.AppUpdateOptions{Lock: options.Bool(true)}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
func AppsParams(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var params map[string]string
|
||||
|
||||
app := coalesce(c.Arg(0), app(c))
|
||||
|
||||
if s.Version <= "20180708231844" {
|
||||
params, err = rack.AppParametersGet(app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
a, err := rack.AppGet(app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
params = a.Parameters
|
||||
}
|
||||
|
||||
keys := []string{}
|
||||
|
||||
for k := range params {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
i := c.Info()
|
||||
|
||||
for _, k := range keys {
|
||||
i.Add(k, params[k])
|
||||
}
|
||||
|
||||
return i.Print()
|
||||
}
|
||||
|
||||
func AppsParamsSet(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts := structs.AppUpdateOptions{
|
||||
Parameters: map[string]string{},
|
||||
}
|
||||
|
||||
for _, arg := range c.Args {
|
||||
parts := strings.SplitN(arg, "=", 2)
|
||||
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("Key=Value expected: %s", arg)
|
||||
}
|
||||
|
||||
opts.Parameters[parts[0]] = parts[1]
|
||||
}
|
||||
|
||||
c.Startf("Updating parameters")
|
||||
|
||||
if s.Version <= "20180708231844" {
|
||||
if err := rack.AppParametersSet(app(c), opts.Parameters); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := rack.AppUpdate(app(c), opts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.Bool("wait") {
|
||||
c.Writef("\n")
|
||||
|
||||
if err := common.WaitForAppWithLogs(rack, c, app(c)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a, err := rack.AppGet(app(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for k, v := range opts.Parameters {
|
||||
if a.Parameters[k] != v {
|
||||
return fmt.Errorf("rollback")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
func AppsUnlock(rack sdk.Interface, c *stdcli.Context) error {
|
||||
app := coalesce(c.Arg(0), app(c))
|
||||
|
||||
c.Startf("Unlocking <app>%s</app>", app)
|
||||
|
||||
if err := rack.AppUpdate(app, structs.AppUpdateOptions{Lock: options.Bool(false)}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
func AppsWait(rack sdk.Interface, c *stdcli.Context) error {
|
||||
app := coalesce(c.Arg(0), app(c))
|
||||
|
||||
c.Startf("Waiting for app")
|
||||
|
||||
c.Writef("\n")
|
||||
|
||||
if err := common.WaitForAppWithLogs(rack, c, app); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
func appExport(rack sdk.Interface, c *stdcli.Context, app string, w io.Writer) error {
|
||||
tmp, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
c.Startf("Exporting app <app>%s</app>", app)
|
||||
|
||||
a, err := rack.AppGet(app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for k, v := range a.Parameters {
|
||||
if v == "****" {
|
||||
delete(a.Parameters, k)
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(tmp, "app.json"), data, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.OK()
|
||||
|
||||
if a.Release != "" {
|
||||
c.Startf("Exporting env")
|
||||
|
||||
_, r, err := common.AppManifest(rack, app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(tmp, "env"), []byte(r.Env), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.OK()
|
||||
|
||||
if r.Build != "" {
|
||||
c.Startf("Exporting build <build>%s</build>", r.Build)
|
||||
|
||||
fd, err := os.OpenFile(filepath.Join(tmp, "build.tgz"), os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
if err := rack.BuildExport(app, r.Build, fd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.OK()
|
||||
}
|
||||
}
|
||||
|
||||
c.Startf("Packaging export")
|
||||
|
||||
tgz, err := common.Tarball(tmp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := w.Write(tgz); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.OK()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func appImport(rack sdk.Interface, c *stdcli.Context, app string, r io.Reader) error {
|
||||
tmp, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
gz, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := common.Unarchive(gz, tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var a structs.App
|
||||
|
||||
data, err := ioutil.ReadFile(filepath.Join(tmp, "app.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &a); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Startf("Creating app <app>%s</app>", app)
|
||||
|
||||
if _, err := rack.AppCreate(app, structs.AppCreateOptions{Generation: options.String(a.Generation)}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := common.WaitForAppRunning(rack, app); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.OK()
|
||||
|
||||
build := filepath.Join(tmp, "build.tgz")
|
||||
env := filepath.Join(tmp, "env")
|
||||
release := ""
|
||||
|
||||
if _, err := os.Stat(build); !os.IsNotExist(err) {
|
||||
fd, err := os.Open(build)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Startf("Importing build")
|
||||
|
||||
b, err := rack.BuildImport(app, fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.OK(b.Release)
|
||||
|
||||
release = b.Release
|
||||
}
|
||||
|
||||
if _, err := os.Stat(env); !os.IsNotExist(err) {
|
||||
data, err := ioutil.ReadFile(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Startf("Importing env")
|
||||
|
||||
r, err := rack.ReleaseCreate(app, structs.ReleaseCreateOptions{Env: options.String(string(data))})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.OK(r.Id)
|
||||
|
||||
release = r.Id
|
||||
}
|
||||
|
||||
if release != "" {
|
||||
c.Startf("Promoting <release>%s</release>", release)
|
||||
|
||||
if err := rack.ReleasePromote(app, release, structs.ReleasePromoteOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := common.WaitForAppRunning(rack, app); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.OK()
|
||||
}
|
||||
|
||||
if len(a.Parameters) > 0 {
|
||||
ae, err := rack.AppGet(app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
change := false
|
||||
|
||||
for k, v := range a.Parameters {
|
||||
if v != ae.Parameters[k] {
|
||||
change = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if change {
|
||||
c.Startf("Updating parameters")
|
||||
|
||||
if err := rack.AppUpdate(app, structs.AppUpdateOptions{Parameters: a.Parameters}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := common.WaitForAppRunning(rack, app); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.OK()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
634
pkg/cli/apps_test.go
Normal file
634
pkg/cli/apps_test.go
Normal file
@ -0,0 +1,634 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
"github.com/convox/convox/pkg/common"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestApps(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
a1 := structs.Apps{
|
||||
*fxApp(),
|
||||
*fxAppGeneration1(),
|
||||
structs.App{
|
||||
Name: "app2",
|
||||
Generation: "1",
|
||||
Status: "creating",
|
||||
},
|
||||
}
|
||||
i.On("AppList").Return(a1, nil)
|
||||
|
||||
res, err := testExecute(e, "apps", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"APP STATUS RELEASE ",
|
||||
"app1 running release1",
|
||||
"app1 running release1",
|
||||
"app2 creating ",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppList").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "apps", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsCancel(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppCancel", "app1").Return(nil)
|
||||
|
||||
res, err := testExecute(e, "apps cancel app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Cancelling app1... OK",
|
||||
})
|
||||
|
||||
res, err = testExecute(e, "apps cancel -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Cancelling app1... OK",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsCancelError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppCancel", "app1").Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "apps cancel app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{
|
||||
"Cancelling app1... ",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsCreate(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.AppCreateOptions{}
|
||||
i.On("AppCreate", "app1", opts).Return(fxApp(), nil)
|
||||
|
||||
res, err := testExecute(e, "apps create app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Creating app1... OK",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsCreateError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.AppCreateOptions{}
|
||||
i.On("AppCreate", "app1", opts).Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "apps create app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Creating app1... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsCreateGeneration1(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.AppCreateOptions{
|
||||
Generation: options.String("1"),
|
||||
}
|
||||
i.On("AppCreate", "app1", opts).Return(fxApp(), nil)
|
||||
|
||||
res, err := testExecute(e, "apps create app1 -g 1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Creating app1... OK",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsCreateWait(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.AppCreateOptions{}
|
||||
i.On("AppCreate", "app1", opts).Return(fxApp(), nil)
|
||||
i.On("AppGet", "app1").Return(&structs.App{Status: "creating"}, nil).Twice()
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil)
|
||||
|
||||
res, err := testExecute(e, "apps create app1 --wait", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Creating app1... OK",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsDelete(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppDelete", "app1").Return(nil)
|
||||
|
||||
res, err := testExecute(e, "apps delete app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Deleting app1... OK",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsDeleteError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppDelete", "app1").Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "apps delete app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Deleting app1... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsDeleteWait(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppDelete", "app1").Return(nil)
|
||||
i.On("AppGet", "app1").Return(&structs.App{Status: "deleting"}, nil).Twice()
|
||||
i.On("AppGet", "app1").Return(nil, fmt.Errorf("no such app: app1"))
|
||||
|
||||
res, err := testExecute(e, "apps delete app1 --wait", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Deleting app1... OK",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsExport(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil)
|
||||
i.On("ReleaseGet", "app1", "release1").Return(fxRelease(), nil)
|
||||
bdata, err := ioutil.ReadFile("testdata/build.tgz")
|
||||
require.NoError(t, err)
|
||||
i.On("BuildExport", "app1", "build1", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
|
||||
args.Get(2).(io.Writer).Write(bdata)
|
||||
})
|
||||
|
||||
tmp, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
res, err := testExecute(e, fmt.Sprintf("apps export -a app1 -f %s/app.tgz", tmp), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Exporting app app1... OK",
|
||||
"Exporting env... OK",
|
||||
"Exporting build build1... OK",
|
||||
"Packaging export... OK",
|
||||
})
|
||||
|
||||
fd, err := os.Open(filepath.Join(tmp, "app.tgz"))
|
||||
require.NoError(t, err)
|
||||
defer fd.Close()
|
||||
|
||||
gz, err := gzip.NewReader(fd)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = common.Unarchive(gz, tmp)
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := ioutil.ReadFile(filepath.Join(tmp, "app.json"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "{\"generation\":\"2\",\"locked\":false,\"name\":\"app1\",\"release\":\"release1\",\"router\":\"\",\"status\":\"running\",\"parameters\":{\"ParamFoo\":\"value1\",\"ParamOther\":\"value2\"}}", string(data))
|
||||
|
||||
data, err = ioutil.ReadFile(filepath.Join(tmp, "env"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "FOO=bar\nBAZ=quux", string(data))
|
||||
|
||||
data, err = ioutil.ReadFile(filepath.Join(tmp, "build.tgz"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, bdata, data)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsImport(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppCreate", "app1", structs.AppCreateOptions{Generation: options.String("2")}).Return(fxApp(), nil)
|
||||
i.On("AppGet", "app1").Return(&structs.App{Status: "creating"}, nil).Twice()
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil).Twice()
|
||||
bdata, err := ioutil.ReadFile("testdata/build.tgz")
|
||||
require.NoError(t, err)
|
||||
i.On("BuildImport", "app1", mock.Anything).Return(fxBuild(), nil).Run(func(args mock.Arguments) {
|
||||
rdata, err := ioutil.ReadAll(args.Get(1).(io.Reader))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, bdata, rdata)
|
||||
})
|
||||
i.On("ReleaseCreate", "app1", structs.ReleaseCreateOptions{Env: options.String("ALPHA=one\nBRAVO=two\n")}).Return(fxRelease(), nil)
|
||||
i.On("ReleasePromote", "app1", "release1", structs.ReleasePromoteOptions{}).Return(nil)
|
||||
i.On("AppGet", "app1").Return(&structs.App{Status: "creating"}, nil).Twice()
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil).Twice()
|
||||
i.On("AppUpdate", "app1", structs.AppUpdateOptions{Parameters: map[string]string{"Foo": "bar", "Baz": "qux"}}).Return(nil)
|
||||
i.On("AppGet", "app1").Return(&structs.App{Status: "creating"}, nil).Twice()
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil).Twice()
|
||||
|
||||
res, err := testExecute(e, "apps import -a app1 -f testdata/app.tgz", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Creating app app1... OK",
|
||||
"Importing build... OK, release1",
|
||||
"Importing env... OK, release1",
|
||||
"Promoting release1... OK",
|
||||
"Updating parameters... OK",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsImportNoBuild(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppCreate", "app1", structs.AppCreateOptions{Generation: options.String("2")}).Return(fxApp(), nil)
|
||||
i.On("AppGet", "app1").Return(&structs.App{Status: "creating"}, nil).Twice()
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil).Twice()
|
||||
i.On("AppUpdate", "app1", structs.AppUpdateOptions{Parameters: map[string]string{"Foo": "bar", "Baz": "qux"}}).Return(nil)
|
||||
i.On("AppGet", "app1").Return(&structs.App{Status: "creating"}, nil).Twice()
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil).Twice()
|
||||
|
||||
res, err := testExecute(e, "apps import -a app1 -f testdata/app.nobuild.tgz", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Creating app app1... OK",
|
||||
"Updating parameters... OK",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsImportNoParams(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppCreate", "app1", structs.AppCreateOptions{Generation: options.String("2")}).Return(fxApp(), nil)
|
||||
i.On("AppGet", "app1").Return(&structs.App{Status: "creating"}, nil).Twice()
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil).Twice()
|
||||
bdata, err := ioutil.ReadFile("testdata/build.tgz")
|
||||
require.NoError(t, err)
|
||||
i.On("BuildImport", "app1", mock.Anything).Return(fxBuild(), nil).Run(func(args mock.Arguments) {
|
||||
rdata, err := ioutil.ReadAll(args.Get(1).(io.Reader))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, bdata, rdata)
|
||||
})
|
||||
i.On("ReleaseCreate", "app1", structs.ReleaseCreateOptions{Env: options.String("ALPHA=one\nBRAVO=two\n")}).Return(fxRelease(), nil)
|
||||
i.On("ReleasePromote", "app1", "release1", structs.ReleasePromoteOptions{}).Return(nil)
|
||||
i.On("AppGet", "app1").Return(&structs.App{Status: "creating"}, nil).Twice()
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil).Twice()
|
||||
|
||||
res, err := testExecute(e, "apps import -a app1 -f testdata/app.noparams.tgz", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Creating app app1... OK",
|
||||
"Importing build... OK, release1",
|
||||
"Importing env... OK, release1",
|
||||
"Promoting release1... OK",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsImportSameParams(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppCreate", "app1", structs.AppCreateOptions{Generation: options.String("2")}).Return(fxApp(), nil)
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil).Twice()
|
||||
bdata, err := ioutil.ReadFile("testdata/build.tgz")
|
||||
require.NoError(t, err)
|
||||
i.On("BuildImport", "app1", mock.Anything).Return(fxBuild(), nil).Run(func(args mock.Arguments) {
|
||||
rdata, err := ioutil.ReadAll(args.Get(1).(io.Reader))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, bdata, rdata)
|
||||
})
|
||||
i.On("ReleaseCreate", "app1", structs.ReleaseCreateOptions{Env: options.String("ALPHA=one\nBRAVO=two\n")}).Return(fxRelease(), nil)
|
||||
i.On("ReleasePromote", "app1", "release1", structs.ReleasePromoteOptions{}).Return(nil)
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil).Twice()
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil).Once()
|
||||
|
||||
res, err := testExecute(e, "apps import -a app1 -f testdata/app.sameparams.tgz", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Creating app app1... OK",
|
||||
"Importing build... OK, release1",
|
||||
"Importing env... OK, release1",
|
||||
"Promoting release1... OK",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsInfo(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppGet", "app1").Return(fxAppRouter(), nil)
|
||||
|
||||
res, err := testExecute(e, "apps info app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Name app1",
|
||||
"Status running",
|
||||
"Generation 2",
|
||||
"Locked false",
|
||||
"Release release1",
|
||||
"Router router1",
|
||||
})
|
||||
|
||||
res, err = testExecute(e, "apps info -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Name app1",
|
||||
"Status running",
|
||||
"Generation 2",
|
||||
"Locked false",
|
||||
"Release release1",
|
||||
"Router router1",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsInfoRouter(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil)
|
||||
|
||||
res, err := testExecute(e, "apps info app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Name app1",
|
||||
"Status running",
|
||||
"Generation 2",
|
||||
"Locked false",
|
||||
"Release release1",
|
||||
})
|
||||
|
||||
res, err = testExecute(e, "apps info -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Name app1",
|
||||
"Status running",
|
||||
"Generation 2",
|
||||
"Locked false",
|
||||
"Release release1",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsInfoError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppGet", "app1").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "apps info app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestAppsParams(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil)
|
||||
|
||||
res, err := testExecute(e, "apps params app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"ParamFoo value1",
|
||||
"ParamOther value2",
|
||||
"ParamPassword ****",
|
||||
})
|
||||
|
||||
res, err = testExecute(e, "apps params -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"ParamFoo value1",
|
||||
"ParamOther value2",
|
||||
"ParamPassword ****",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsParamsError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("AppGet", "app1").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "apps params app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsParamsClassic(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
i.On("AppParametersGet", "app1").Return(fxParameters(), nil)
|
||||
|
||||
res, err := testExecute(e, "apps params app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"ParamFoo value1",
|
||||
"ParamOther value2",
|
||||
"ParamPassword ****",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsParamsSet(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
opts := structs.AppUpdateOptions{
|
||||
Parameters: map[string]string{
|
||||
"Foo": "bar",
|
||||
"Baz": "qux",
|
||||
},
|
||||
}
|
||||
i.On("AppUpdate", "app1", opts).Return(nil)
|
||||
|
||||
res, err := testExecute(e, "apps params set Foo=bar Baz=qux -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Updating parameters... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsParamsSetError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
opts := structs.AppUpdateOptions{
|
||||
Parameters: map[string]string{
|
||||
"Foo": "bar",
|
||||
"Baz": "qux",
|
||||
},
|
||||
}
|
||||
i.On("AppUpdate", "app1", opts).Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "apps params set Foo=bar Baz=qux -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Updating parameters... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsParamsSetClassic(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
i.On("AppParametersSet", "app1", map[string]string{"Foo": "bar", "Baz": "qux"}).Return(nil)
|
||||
|
||||
res, err := testExecute(e, "apps params set Foo=bar Baz=qux -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Updating parameters... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsWait(t *testing.T) {
|
||||
testClientWait(t, 100*time.Millisecond, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.LogsOptions{
|
||||
Prefix: options.Bool(true),
|
||||
Since: options.Duration(5 * time.Second),
|
||||
}
|
||||
i.On("AppGet", "app1").Return(&structs.App{Status: "creating"}, nil).Twice()
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil)
|
||||
i.On("AppLogs", "app1", opts).Return(testLogs(fxLogsSystem()), nil).Once()
|
||||
|
||||
res, err := testExecute(e, "apps wait app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Waiting for app... ",
|
||||
fxLogsSystem()[0],
|
||||
fxLogsSystem()[1],
|
||||
"OK",
|
||||
})
|
||||
|
||||
i.On("AppLogs", "app1", opts).Return(testLogs(fxLogsSystem()), nil).Once()
|
||||
|
||||
res, err = testExecute(e, "apps wait -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Waiting for app... ",
|
||||
fxLogsSystem()[0],
|
||||
fxLogsSystem()[1],
|
||||
"OK",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsWaitError(t *testing.T) {
|
||||
testClientWait(t, 100*time.Millisecond, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.LogsOptions{
|
||||
Prefix: options.Bool(true),
|
||||
Since: options.Duration(5 * time.Second),
|
||||
}
|
||||
i.On("AppGet", "app1").Return(nil, fmt.Errorf("err1"))
|
||||
i.On("AppLogs", "app1", opts).Return(nil, fmt.Errorf("err2"))
|
||||
|
||||
res, err := testExecute(e, "apps wait app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Waiting for app... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsRollback(t *testing.T) {
|
||||
testClientWait(t, 100*time.Millisecond, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.LogsOptions{
|
||||
Prefix: options.Bool(true),
|
||||
Since: options.Duration(5 * time.Second),
|
||||
}
|
||||
i.On("AppGet", "app1").Return(&structs.App{Status: "updating"}, nil).Once()
|
||||
i.On("AppGet", "app1").Return(&structs.App{Status: "rollback"}, nil).Once()
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil).Once()
|
||||
i.On("AppLogs", "app1", opts).Return(testLogs(fxLogsSystem()), nil).Once()
|
||||
|
||||
res, err := testExecute(e, "apps wait app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: rollback"})
|
||||
res.RequireStdout(t, []string{
|
||||
"Waiting for app... ",
|
||||
fxLogsSystem()[0],
|
||||
fxLogsSystem()[1],
|
||||
})
|
||||
|
||||
i.On("AppGet", "app1").Return(&structs.App{Status: "updating"}, nil).Once()
|
||||
i.On("AppGet", "app1").Return(&structs.App{Status: "rollback"}, nil).Once()
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil).Once()
|
||||
i.On("AppLogs", "app1", opts).Return(testLogs(fxLogsSystem()), nil).Once()
|
||||
|
||||
res, err = testExecute(e, "apps wait -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: rollback"})
|
||||
res.RequireStdout(t, []string{
|
||||
"Waiting for app... ",
|
||||
fxLogsSystem()[0],
|
||||
fxLogsSystem()[1],
|
||||
})
|
||||
})
|
||||
}
|
||||
97
pkg/cli/auth.go
Normal file
97
pkg/cli/auth.go
Normal file
@ -0,0 +1,97 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/convox/convox/pkg/token"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
"github.com/convox/stdsdk"
|
||||
)
|
||||
|
||||
var reSessionAuthentication = regexp.MustCompile(`^Session path="([^"]+)" token="([^"]+)"$`)
|
||||
|
||||
type AuthenticationError struct {
|
||||
error
|
||||
}
|
||||
|
||||
func (ae AuthenticationError) AuthenticationError() error {
|
||||
return ae.error
|
||||
}
|
||||
|
||||
type session struct {
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
func authenticator(c *stdcli.Context) stdsdk.Authenticator {
|
||||
return func(cl *stdsdk.Client, res *http.Response) (http.Header, error) {
|
||||
m := reSessionAuthentication.FindStringSubmatch(res.Header.Get("WWW-Authenticate"))
|
||||
if len(m) < 3 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
body := []byte{}
|
||||
headers := map[string]string{}
|
||||
|
||||
if m[2] == "true" {
|
||||
ares, err := cl.GetStream(m[1], stdsdk.RequestOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer ares.Body.Close()
|
||||
|
||||
dres, err := ioutil.ReadAll(ares.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.Writef("Waiting for security token... ")
|
||||
|
||||
data, err := token.Authenticate(dres)
|
||||
if err != nil {
|
||||
return nil, AuthenticationError{err}
|
||||
}
|
||||
|
||||
c.Writef("<ok>OK</ok>\n")
|
||||
|
||||
body = data
|
||||
headers["Challenge"] = ares.Header.Get("Challenge")
|
||||
}
|
||||
|
||||
var s session
|
||||
|
||||
ro := stdsdk.RequestOptions{
|
||||
Body: bytes.NewReader(body),
|
||||
Headers: stdsdk.Headers(headers),
|
||||
}
|
||||
|
||||
if err := cl.Post(m[1], ro, &s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.Id == "" {
|
||||
return nil, fmt.Errorf("invalid session")
|
||||
}
|
||||
|
||||
if err := c.SettingWriteKey("session", cl.Endpoint.Host, s.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h := http.Header{}
|
||||
|
||||
h.Set("Session", s.Id)
|
||||
|
||||
return h, nil
|
||||
}
|
||||
}
|
||||
|
||||
func currentSession(c *stdcli.Context) sdk.SessionFunc {
|
||||
return func(cl *sdk.Client) string {
|
||||
sid, _ := c.SettingReadKey("session", cl.Endpoint.Host)
|
||||
return sid
|
||||
}
|
||||
}
|
||||
342
pkg/cli/builds.go
Normal file
342
pkg/cli/builds.go
Normal file
@ -0,0 +1,342 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/convox/convox/pkg/common"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("build", "create a build", Build, stdcli.CommandOptions{
|
||||
Flags: append(stdcli.OptionFlags(structs.BuildCreateOptions{}), flagRack, flagApp, flagId),
|
||||
Usage: "[dir]",
|
||||
Validate: stdcli.ArgsMax(1),
|
||||
})
|
||||
|
||||
register("builds", "list builds", Builds, stdcli.CommandOptions{
|
||||
Flags: append(stdcli.OptionFlags(structs.BuildListOptions{}), flagRack, flagApp),
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("builds export", "export a build", BuildsExport, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{
|
||||
flagRack,
|
||||
flagApp,
|
||||
stdcli.StringFlag("file", "f", "import from file"),
|
||||
},
|
||||
Usage: "<build>",
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("builds import", "import a build", BuildsImport, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{
|
||||
flagRack,
|
||||
flagApp,
|
||||
flagId,
|
||||
stdcli.StringFlag("file", "f", "import from file"),
|
||||
},
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("builds info", "get information about a build", BuildsInfo, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack, flagApp},
|
||||
Usage: "<build>",
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("builds logs", "get logs for a build", BuildsLogs, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack, flagApp},
|
||||
Usage: "<build>",
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
}
|
||||
|
||||
func Build(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var stdout io.Writer
|
||||
|
||||
if c.Bool("id") {
|
||||
stdout = c.Writer().Stdout
|
||||
c.Writer().Stdout = c.Writer().Stderr
|
||||
}
|
||||
|
||||
b, err := build(rack, c, c.Bool("development"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Writef("Build: <build>%s</build>\n", b.Id)
|
||||
c.Writef("Release: <release>%s</release>\n", b.Release)
|
||||
|
||||
if c.Bool("id") {
|
||||
fmt.Fprintf(stdout, b.Release)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func build(rack sdk.Interface, c *stdcli.Context, development bool) (*structs.Build, error) {
|
||||
var opts structs.BuildCreateOptions
|
||||
|
||||
if development {
|
||||
opts.Development = options.Bool(true)
|
||||
}
|
||||
|
||||
if err := c.Options(&opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts.Description == nil {
|
||||
if err := exec.Command("git", "diff", "--quiet").Run(); err == nil {
|
||||
if data, err := exec.Command("git", "log", "-n", "1", "--pretty=%h %s", "--abbrev=10").CombinedOutput(); err == nil {
|
||||
opts.Description = options.String(fmt.Sprintf("build %s", strings.TrimSpace(string(data))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Startf("Packaging source")
|
||||
|
||||
data, err := common.Tarball(coalesce(c.Arg(0), "."))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.OK()
|
||||
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var b *structs.Build
|
||||
|
||||
if s.Version < "20180708231844" {
|
||||
c.Startf("Starting build")
|
||||
|
||||
b, err = rack.BuildCreateUpload(app(c), bytes.NewReader(data), opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
tmp, err := generateTempKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tmp += ".tgz"
|
||||
|
||||
c.Startf("Uploading source")
|
||||
|
||||
o, err := rack.ObjectStore(app(c), tmp, bytes.NewReader(data), structs.ObjectStoreOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.OK()
|
||||
|
||||
c.Startf("Starting build")
|
||||
|
||||
b, err = rack.BuildCreate(app(c), o.Url, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
c.OK()
|
||||
|
||||
r, err := rack.BuildLogs(app(c), b.Id, structs.LogsOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
count, _ := io.Copy(c, r)
|
||||
defer finalizeBuildLogs(rack, c, b, count)
|
||||
|
||||
for {
|
||||
b, err = rack.BuildGet(app(c), b.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if b.Status == "failed" {
|
||||
return nil, fmt.Errorf("build failed")
|
||||
}
|
||||
|
||||
if b.Status != "running" {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func finalizeBuildLogs(rack structs.Provider, c *stdcli.Context, b *structs.Build, count int64) error {
|
||||
r, err := rack.BuildLogs(b.App, b.Id, structs.LogsOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if int64(len(data)) > count {
|
||||
c.Write(data[count:])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Builds(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var opts structs.BuildListOptions
|
||||
|
||||
if err := c.Options(&opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bs, err := rack.BuildList(app(c), opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := c.Table("ID", "STATUS", "RELEASE", "STARTED", "ELAPSED", "DESCRIPTION")
|
||||
|
||||
for _, b := range bs {
|
||||
started := common.Ago(b.Started)
|
||||
elapsed := common.Duration(b.Started, b.Ended)
|
||||
|
||||
t.AddRow(b.Id, b.Status, b.Release, started, elapsed, b.Description)
|
||||
}
|
||||
|
||||
return t.Print()
|
||||
}
|
||||
|
||||
func BuildsExport(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var w io.Writer
|
||||
|
||||
if file := c.String("file"); file != "" {
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
w = f
|
||||
} else {
|
||||
if c.Writer().IsTerminal() {
|
||||
return fmt.Errorf("pipe this command into a file or specify --file")
|
||||
}
|
||||
w = c.Writer().Stdout
|
||||
c.Writer().Stdout = c.Writer().Stderr
|
||||
}
|
||||
|
||||
c.Startf("Exporting build")
|
||||
|
||||
if err := rack.BuildExport(app(c), c.Arg(0), w); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
func BuildsImport(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var stdout io.Writer
|
||||
|
||||
if c.Bool("id") {
|
||||
stdout = c.Writer().Stdout
|
||||
c.Writer().Stdout = c.Writer().Stderr
|
||||
}
|
||||
|
||||
var r io.ReadCloser
|
||||
|
||||
if file := c.String("file"); file != "" {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r = f
|
||||
} else {
|
||||
if c.Reader().IsTerminal() {
|
||||
return fmt.Errorf("pipe a file into this command or specify --file")
|
||||
}
|
||||
r = ioutil.NopCloser(c.Reader())
|
||||
}
|
||||
|
||||
defer r.Close()
|
||||
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Startf("Importing build")
|
||||
|
||||
var b *structs.Build
|
||||
|
||||
if s.Version <= "20180416200237" {
|
||||
b, err = rack.BuildImportMultipart(app(c), r)
|
||||
} else if s.Version <= "20180708231844" {
|
||||
b, err = rack.BuildImportUrl(app(c), r)
|
||||
} else {
|
||||
b, err = rack.BuildImport(app(c), r)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.OK(b.Release)
|
||||
|
||||
if c.Bool("id") {
|
||||
fmt.Fprintf(stdout, b.Release)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func BuildsInfo(rack sdk.Interface, c *stdcli.Context) error {
|
||||
b, err := rack.BuildGet(app(c), c.Arg(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i := c.Info()
|
||||
|
||||
i.Add("Id", b.Id)
|
||||
i.Add("Status", b.Status)
|
||||
i.Add("Release", b.Release)
|
||||
i.Add("Description", b.Description)
|
||||
i.Add("Started", common.Ago(b.Started))
|
||||
i.Add("Elapsed", common.Duration(b.Started, b.Ended))
|
||||
|
||||
return i.Print()
|
||||
}
|
||||
|
||||
func BuildsLogs(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var opts structs.LogsOptions
|
||||
|
||||
if err := c.Options(&opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r, err := rack.BuildLogs(app(c), c.Arg(0), opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
io.Copy(c, r)
|
||||
|
||||
return nil
|
||||
}
|
||||
311
pkg/cli/builds_test.go
Normal file
311
pkg/cli/builds_test.go
Normal file
@ -0,0 +1,311 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ObjectStore", "app1", mock.AnythingOfType("string"), mock.Anything, structs.ObjectStoreOptions{}).Return(&fxObject, nil).Run(func(args mock.Arguments) {
|
||||
require.Regexp(t, `tmp/[0-9a-f]{30}\.tgz`, args.Get(1).(string))
|
||||
})
|
||||
i.On("BuildCreate", "app1", "object://test", structs.BuildCreateOptions{Description: options.String("foo")}).Return(fxBuild(), nil)
|
||||
i.On("BuildLogs", "app1", "build1", structs.LogsOptions{}).Return(testLogs(fxLogs()), nil).Once()
|
||||
i.On("BuildGet", "app1", "build1").Return(fxBuildRunning(), nil).Once()
|
||||
i.On("BuildGet", "app1", "build4").Return(fxBuild(), nil)
|
||||
i.On("BuildLogs", "app1", "build1", structs.LogsOptions{}).Return(testLogs(fxLogs()), nil)
|
||||
|
||||
res, err := testExecute(e, "build ./testdata/httpd -a app1 -d foo", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Packaging source... OK",
|
||||
"Uploading source... OK",
|
||||
"Starting build... OK",
|
||||
"log1",
|
||||
"log2",
|
||||
"Build: build1",
|
||||
"Release: release1",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildFinalizeLogs(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ObjectStore", "app1", mock.AnythingOfType("string"), mock.Anything, structs.ObjectStoreOptions{}).Return(&fxObject, nil).Run(func(args mock.Arguments) {
|
||||
require.Regexp(t, `tmp/[0-9a-f]{30}\.tgz`, args.Get(1).(string))
|
||||
})
|
||||
i.On("BuildCreate", "app1", "object://test", structs.BuildCreateOptions{Description: options.String("foo")}).Return(fxBuild(), nil)
|
||||
i.On("BuildLogs", "app1", "build1", structs.LogsOptions{}).Return(testLogs(fxLogs()), nil).Once()
|
||||
i.On("BuildGet", "app1", "build1").Return(fxBuildRunning(), nil).Once()
|
||||
i.On("BuildGet", "app1", "build4").Return(fxBuild(), nil)
|
||||
i.On("BuildLogs", "app1", "build1", structs.LogsOptions{}).Return(testLogs(fxLogsLonger()), nil)
|
||||
|
||||
res, err := testExecute(e, "build ./testdata/httpd -a app1 -d foo", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Packaging source... OK",
|
||||
"Uploading source... OK",
|
||||
"Starting build... OK",
|
||||
"log1",
|
||||
"log2",
|
||||
"log3",
|
||||
"Build: build1",
|
||||
"Release: release1",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ObjectStore", "app1", mock.AnythingOfType("string"), mock.Anything, structs.ObjectStoreOptions{}).Return(&fxObject, nil).Run(func(args mock.Arguments) {
|
||||
require.Regexp(t, `tmp/[0-9a-f]{30}\.tgz`, args.Get(1).(string))
|
||||
})
|
||||
i.On("BuildCreate", "app1", "object://test", structs.BuildCreateOptions{Description: options.String("foo")}).Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "build ./testdata/httpd -a app1 -d foo", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{
|
||||
"Packaging source... OK",
|
||||
"Uploading source... OK",
|
||||
"Starting build... ",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildClassic(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
i.On("BuildCreateUpload", "app1", mock.Anything, structs.BuildCreateOptions{Description: options.String("foo")}).Return(fxBuild(), nil)
|
||||
i.On("BuildLogs", "app1", "build1", structs.LogsOptions{}).Return(testLogs(fxLogs()), nil)
|
||||
i.On("BuildGet", "app1", "build1").Return(fxBuildRunning(), nil).Once()
|
||||
i.On("BuildGet", "app1", "build4").Return(fxBuild(), nil)
|
||||
|
||||
res, err := testExecute(e, "build ./testdata/httpd -a app1 -d foo", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Packaging source... OK",
|
||||
"Starting build... OK",
|
||||
"log1",
|
||||
"log2",
|
||||
"Build: build1",
|
||||
"Release: release1",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuilds(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
b1 := structs.Builds{
|
||||
*fxBuild(),
|
||||
*fxBuildRunning(),
|
||||
*fxBuildFailed(),
|
||||
}
|
||||
i.On("BuildList", "app1", structs.BuildListOptions{}).Return(b1, nil)
|
||||
|
||||
res, err := testExecute(e, "builds -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"ID STATUS RELEASE STARTED ELAPSED DESCRIPTION",
|
||||
"build1 complete release1 2 days ago 2m0s desc ",
|
||||
"build4 running 2 days ago ",
|
||||
"build3 failed 2 days ago ",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildsError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("BuildList", "app1", structs.BuildListOptions{}).Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "builds -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildsExport(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
data, err := ioutil.ReadFile("testdata/build.tgz")
|
||||
require.NoError(t, err)
|
||||
i.On("BuildExport", "app1", "build1", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
|
||||
args.Get(2).(io.Writer).Write(data)
|
||||
})
|
||||
tmpd, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
tmpf := filepath.Join(tmpd, "export.tgz")
|
||||
|
||||
res, err := testExecute(e, fmt.Sprintf("builds export build1 -a app1 -f %s", tmpf), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Exporting build... OK"})
|
||||
tdata, err := ioutil.ReadFile(tmpf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, data, tdata)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildsExportStdout(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
data, err := ioutil.ReadFile("testdata/build.tgz")
|
||||
require.NoError(t, err)
|
||||
i.On("BuildExport", "app1", "build1", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
|
||||
args.Get(2).(io.Writer).Write(data)
|
||||
})
|
||||
|
||||
res, err := testExecute(e, "builds export build1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{"Exporting build... OK"})
|
||||
require.Equal(t, data, []byte(res.Stdout))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildsExportError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("BuildExport", "app1", "build1", mock.Anything).Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "builds export build1 -a app1 -f /dev/null", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Exporting build... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildsImport(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
data, err := ioutil.ReadFile("testdata/build.tgz")
|
||||
require.NoError(t, err)
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("BuildImport", "app1", mock.Anything).Return(fxBuild(), nil).Run(func(args mock.Arguments) {
|
||||
rdata, err := ioutil.ReadAll(args.Get(1).(io.Reader))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, data, rdata)
|
||||
})
|
||||
|
||||
res, err := testExecute(e, "builds import -a app1 -f testdata/build.tgz", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Importing build... OK, release1"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildsImportError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("BuildImport", "app1", mock.Anything).Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "builds import -a app1 -f testdata/build.tgz", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Importing build... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildsImportClassic(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
data, err := ioutil.ReadFile("testdata/build.tgz")
|
||||
require.NoError(t, err)
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
i.On("BuildImportMultipart", "app1", mock.Anything).Return(fxBuild(), nil).Run(func(args mock.Arguments) {
|
||||
rdata, err := ioutil.ReadAll(args.Get(1).(io.Reader))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, data, rdata)
|
||||
})
|
||||
|
||||
res, err := testExecute(e, "builds import -a app1 -f testdata/build.tgz", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Importing build... OK, release1"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildsInfo(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("BuildGet", "app1", "build1").Return(fxBuild(), nil)
|
||||
|
||||
res, err := testExecute(e, "builds info build1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Id build1",
|
||||
"Status complete",
|
||||
"Release release1",
|
||||
"Description desc",
|
||||
"Started 2 days ago",
|
||||
"Elapsed 2m0s",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildsInfoError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("BuildGet", "app1", "build1").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "builds info build1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildsLogs(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.LogsOptions{}
|
||||
i.On("BuildLogs", "app1", "build1", opts).Return(testLogs(fxLogs()), nil)
|
||||
|
||||
res, err := testExecute(e, "builds logs build1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
fxLogs()[0],
|
||||
fxLogs()[1],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildsLogsError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.LogsOptions{}
|
||||
i.On("BuildLogs", "app1", "build1", opts).Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "builds logs build1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
152
pkg/cli/certs.go
Normal file
152
pkg/cli/certs.go
Normal file
@ -0,0 +1,152 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/convox/convox/pkg/common"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("certs", "list certificates", Certs, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("certs delete", "delete a certificate", CertsDelete, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Usage: "<cert>",
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("certs generate", "generate a certificate", CertsGenerate, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagId, flagRack},
|
||||
Usage: "<domain> [domain...]",
|
||||
Validate: stdcli.ArgsMin(1),
|
||||
})
|
||||
|
||||
register("certs import", "import a certificate", CertsImport, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{
|
||||
flagId,
|
||||
flagRack,
|
||||
stdcli.StringFlag("chain", "", "intermediate certificate chain"),
|
||||
},
|
||||
Usage: "<pub> <key>",
|
||||
Validate: stdcli.Args(2),
|
||||
})
|
||||
}
|
||||
|
||||
func Certs(rack sdk.Interface, c *stdcli.Context) error {
|
||||
cs, err := rack.CertificateList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := c.Table("ID", "DOMAIN", "EXPIRES")
|
||||
|
||||
for _, c := range cs {
|
||||
t.AddRow(c.Id, c.Domain, common.Ago(c.Expiration))
|
||||
}
|
||||
|
||||
return t.Print()
|
||||
}
|
||||
|
||||
func CertsDelete(rack sdk.Interface, c *stdcli.Context) error {
|
||||
cert := c.Arg(0)
|
||||
|
||||
c.Startf("Deleting certificate <id>%s</id>", cert)
|
||||
|
||||
if err := rack.CertificateDelete(cert); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
func CertsGenerate(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var stdout io.Writer
|
||||
|
||||
if c.Bool("id") {
|
||||
stdout = c.Writer().Stdout
|
||||
c.Writer().Stdout = c.Writer().Stderr
|
||||
}
|
||||
|
||||
c.Startf("Generating certificate")
|
||||
|
||||
cr, err := rack.CertificateGenerate(c.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.OK(cr.Id)
|
||||
|
||||
if c.Bool("id") {
|
||||
fmt.Fprintf(stdout, cr.Id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CertsImport(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var stdout io.Writer
|
||||
|
||||
if c.Bool("id") {
|
||||
stdout = c.Writer().Stdout
|
||||
c.Writer().Stdout = c.Writer().Stderr
|
||||
}
|
||||
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pub, err := ioutil.ReadFile(c.Arg(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := ioutil.ReadFile(c.Arg(1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var opts structs.CertificateCreateOptions
|
||||
|
||||
if cf := c.String("chain"); cf != "" {
|
||||
chain, err := ioutil.ReadFile(cf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.Chain = options.String(string(chain))
|
||||
}
|
||||
|
||||
c.Startf("Importing certificate")
|
||||
|
||||
var cr *structs.Certificate
|
||||
|
||||
if s.Version <= "20180708231844" {
|
||||
cr, err = rack.CertificateCreateClassic(string(pub), string(key), opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
cr, err = rack.CertificateCreate(string(pub), string(key), opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.OK(cr.Id)
|
||||
|
||||
if c.Bool("id") {
|
||||
fmt.Fprintf(stdout, cr.Id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
130
pkg/cli/certs_test.go
Normal file
130
pkg/cli/certs_test.go
Normal file
@ -0,0 +1,130 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCerts(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("CertificateList").Return(structs.Certificates{*fxCertificate(), *fxCertificate()}, nil)
|
||||
|
||||
res, err := testExecute(e, "certs", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"ID DOMAIN EXPIRES ",
|
||||
"cert1 example.org 2 days from now",
|
||||
"cert1 example.org 2 days from now",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCertsError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("CertificateList").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "certs", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCertsDelete(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("CertificateDelete", "cert1").Return(nil)
|
||||
|
||||
res, err := testExecute(e, "certs delete cert1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Deleting certificate cert1... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCertsDeleteError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("CertificateDelete", "cert1").Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "certs delete cert1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Deleting certificate cert1... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCertsGenerate(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("CertificateGenerate", []string{"test.example.org", "other.example.org"}).Return(fxCertificate(), nil)
|
||||
|
||||
res, err := testExecute(e, "certs generate test.example.org other.example.org", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Generating certificate... OK, cert1"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCertsGenerateError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("CertificateGenerate", []string{"test.example.org", "other.example.org"}).Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "certs generate test.example.org other.example.org", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Generating certificate... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCertsImport(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
opts := structs.CertificateCreateOptions{Chain: options.String("chain\n")}
|
||||
i.On("CertificateCreate", "cert\n", "key\n", opts).Return(fxCertificate(), nil)
|
||||
|
||||
res, err := testExecute(e, "certs import testdata/cert.pem testdata/key.pem --chain testdata/chain.pem", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Importing certificate... OK, cert1"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCertsImportError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
opts := structs.CertificateCreateOptions{Chain: options.String("chain\n")}
|
||||
i.On("CertificateCreate", "cert\n", "key\n", opts).Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "certs import testdata/cert.pem testdata/key.pem --chain testdata/chain.pem", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Importing certificate... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCertsImportClassic(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
opts := structs.CertificateCreateOptions{Chain: options.String("chain\n")}
|
||||
i.On("CertificateCreateClassic", "cert\n", "key\n", opts).Return(fxCertificate(), nil)
|
||||
|
||||
res, err := testExecute(e, "certs import testdata/cert.pem testdata/key.pem --chain testdata/chain.pem", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Importing certificate... OK, cert1"})
|
||||
})
|
||||
}
|
||||
56
pkg/cli/cli.go
Normal file
56
pkg/cli/cli.go
Normal file
@ -0,0 +1,56 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/convox/convox/pkg/start"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
type HandlerFunc func(sdk.Interface, *stdcli.Context) error
|
||||
|
||||
var (
|
||||
Starter = start.New()
|
||||
WaitDuration = 5 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
flagApp = stdcli.StringFlag("app", "a", "app name")
|
||||
flagId = stdcli.BoolFlag("id", "", "put logs on stderr, release id on stdout")
|
||||
flagNoFollow = stdcli.BoolFlag("no-follow", "", "do not follow logs")
|
||||
flagRack = stdcli.StringFlag("rack", "r", "rack name")
|
||||
flagWait = stdcli.BoolFlag("wait", "w", "wait for completion")
|
||||
)
|
||||
|
||||
func New(name, version string) *Engine {
|
||||
e := &Engine{
|
||||
Engine: stdcli.New(name, version),
|
||||
}
|
||||
|
||||
e.Writer.Tags["app"] = stdcli.RenderColors(39)
|
||||
e.Writer.Tags["command"] = stdcli.RenderColors(244)
|
||||
e.Writer.Tags["dir"] = stdcli.RenderColors(246)
|
||||
e.Writer.Tags["build"] = stdcli.RenderColors(23)
|
||||
e.Writer.Tags["fail"] = stdcli.RenderColors(160)
|
||||
e.Writer.Tags["rack"] = stdcli.RenderColors(26)
|
||||
e.Writer.Tags["process"] = stdcli.RenderColors(27)
|
||||
e.Writer.Tags["release"] = stdcli.RenderColors(24)
|
||||
e.Writer.Tags["service"] = stdcli.RenderColors(33)
|
||||
e.Writer.Tags["setting"] = stdcli.RenderColors(246)
|
||||
e.Writer.Tags["system"] = stdcli.RenderColors(15)
|
||||
|
||||
for i := 0; i < 18; i++ {
|
||||
e.Writer.Tags[fmt.Sprintf("color%d", i)] = stdcli.RenderColors(237 + i)
|
||||
}
|
||||
|
||||
if dir := os.Getenv("CONVOX_CONFIG"); dir != "" {
|
||||
e.Settings = dir
|
||||
}
|
||||
|
||||
e.RegisterCommands()
|
||||
|
||||
return e
|
||||
}
|
||||
116
pkg/cli/cli_test.go
Normal file
116
pkg/cli/cli_test.go
Normal file
@ -0,0 +1,116 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
"github.com/convox/convox/pkg/common"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
shellquote "github.com/kballard/go-shellquote"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
fxObject = structs.Object{
|
||||
Url: "object://test",
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
fxStarted = time.Now().UTC().Add(-48 * time.Hour)
|
||||
)
|
||||
|
||||
func testClient(t *testing.T, fn func(*cli.Engine, *mocksdk.Interface)) {
|
||||
testClientWait(t, 1, fn)
|
||||
}
|
||||
|
||||
func testClientWait(t *testing.T, wait time.Duration, fn func(*cli.Engine, *mocksdk.Interface)) {
|
||||
os.Unsetenv("CONVOX_HOST")
|
||||
os.Unsetenv("CONVOX_PASSWORD")
|
||||
os.Unsetenv("CONVOX_RACK")
|
||||
os.Unsetenv("RACK_URL")
|
||||
|
||||
i := &mocksdk.Interface{}
|
||||
|
||||
cli.WaitDuration = wait
|
||||
common.ProviderWaitDuration = wait
|
||||
|
||||
e := cli.New("convox", "test")
|
||||
|
||||
e.Client = i
|
||||
|
||||
tmp, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
e.Settings = tmp
|
||||
// defer os.RemoveAll(tmp)
|
||||
|
||||
fn(e, i)
|
||||
|
||||
i.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func testExecute(e *cli.Engine, cmd string, stdin io.Reader) (*result, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
return testExecuteContext(ctx, e, cmd, stdin)
|
||||
}
|
||||
|
||||
func testExecuteContext(ctx context.Context, e *cli.Engine, cmd string, stdin io.Reader) (*result, error) {
|
||||
if stdin == nil {
|
||||
stdin = &bytes.Buffer{}
|
||||
}
|
||||
|
||||
stdout := bytes.Buffer{}
|
||||
stderr := bytes.Buffer{}
|
||||
|
||||
e.Reader.Reader = stdin
|
||||
|
||||
e.Writer.Color = false
|
||||
e.Writer.Stdout = &stdout
|
||||
e.Writer.Stderr = &stderr
|
||||
|
||||
cp, err := shellquote.Split(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
code := e.ExecuteContext(ctx, cp)
|
||||
|
||||
res := &result{
|
||||
Code: code,
|
||||
Stdout: stdout.String(),
|
||||
Stderr: stderr.String(),
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func testLogs(logs []string) io.ReadCloser {
|
||||
return ioutil.NopCloser(strings.NewReader(fmt.Sprintf("%s\n", strings.Join(logs, "\n"))))
|
||||
}
|
||||
|
||||
type result struct {
|
||||
Code int
|
||||
Stdout string
|
||||
Stderr string
|
||||
}
|
||||
|
||||
func (r *result) RequireStderr(t *testing.T, lines []string) {
|
||||
stderr := strings.Split(strings.TrimSuffix(r.Stderr, "\n"), "\n")
|
||||
require.Equal(t, lines, stderr)
|
||||
}
|
||||
|
||||
func (r *result) RequireStdout(t *testing.T, lines []string) {
|
||||
stdout := strings.Split(strings.TrimSuffix(r.Stdout, "\n"), "\n")
|
||||
require.Equal(t, lines, stdout)
|
||||
}
|
||||
100
pkg/cli/cp.go
Normal file
100
pkg/cli/cp.go
Normal file
@ -0,0 +1,100 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/convox/convox/pkg/common"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("cp", "copy files", Cp, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagApp, flagRack},
|
||||
Usage: "<[pid:]src> <[pid:]dst>",
|
||||
Validate: stdcli.Args(2),
|
||||
})
|
||||
}
|
||||
|
||||
func Cp(rack sdk.Interface, c *stdcli.Context) error {
|
||||
src := c.Arg(0)
|
||||
dst := c.Arg(1)
|
||||
|
||||
r, err := cpSource(rack, c, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cpDestination(rack, c, r, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cpDestination(rack sdk.Interface, c *stdcli.Context, r io.Reader, dst string) error {
|
||||
parts := strings.SplitN(dst, ":", 2)
|
||||
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
abs, err := filepath.Abs(parts[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rr, err := common.RebaseArchive(r, "/base", abs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return common.Unarchive(rr, "/")
|
||||
case 2:
|
||||
if !strings.HasPrefix(parts[1], "/") {
|
||||
return fmt.Errorf("must specify absolute paths for processes")
|
||||
}
|
||||
|
||||
rr, err := common.RebaseArchive(r, "/base", parts[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return rack.FilesUpload(app(c), parts[0], rr)
|
||||
default:
|
||||
return fmt.Errorf("unknown destination: %s", dst)
|
||||
}
|
||||
}
|
||||
|
||||
func cpSource(rack sdk.Interface, c *stdcli.Context, src string) (io.Reader, error) {
|
||||
parts := strings.SplitN(src, ":", 2)
|
||||
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
abs, err := filepath.Abs(parts[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r, err := common.Archive(abs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return common.RebaseArchive(r, abs, "/base")
|
||||
case 2:
|
||||
if !strings.HasPrefix(parts[1], "/") {
|
||||
return nil, fmt.Errorf("must specify absolute paths for processes")
|
||||
}
|
||||
|
||||
r, err := rack.FilesDownload(app(c), parts[0], parts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return common.RebaseArchive(r, parts[1], "/base")
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown source: %s", src)
|
||||
}
|
||||
}
|
||||
75
pkg/cli/cp_test.go
Normal file
75
pkg/cli/cp_test.go
Normal file
@ -0,0 +1,75 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCpUpload(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("FilesUpload", "app1", "0123456789", mock.Anything).Return(nil)
|
||||
|
||||
res, err := testExecute(e, "cp -a app1 testdata/file 0123456789:/tmp/", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCpUploadError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("FilesUpload", "app1", "0123456789", mock.Anything).Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "cp -a app1 testdata/file 0123456789:/tmp/", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCpDownload(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
tmpd, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
tmpf := filepath.Join(tmpd, "file")
|
||||
data, err := ioutil.ReadFile("testdata/file.tar")
|
||||
require.NoError(t, err)
|
||||
i.On("FilesDownload", "app1", "0123456789", "/tmp/file").Return(bytes.NewReader(data), nil)
|
||||
|
||||
res, err := testExecute(e, fmt.Sprintf("cp -a app1 0123456789:/tmp/file %s", tmpf), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{""})
|
||||
odata, err := ioutil.ReadFile("testdata/file")
|
||||
require.NoError(t, err)
|
||||
ddata, err := ioutil.ReadFile(tmpf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, odata, ddata)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCpDownloadError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
tmpd, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
tmpf := filepath.Join(tmpd, "file")
|
||||
i.On("FilesDownload", "app1", "0123456789", "/tmp/file").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, fmt.Sprintf("cp -a app1 0123456789:/tmp/file %s", tmpf), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
42
pkg/cli/deploy.go
Normal file
42
pkg/cli/deploy.go
Normal file
@ -0,0 +1,42 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("deploy", "create and promote a build", Deploy, stdcli.CommandOptions{
|
||||
Flags: append(stdcli.OptionFlags(structs.BuildCreateOptions{}), flagApp, flagId, flagRack, flagWait),
|
||||
Usage: "[dir]",
|
||||
Validate: stdcli.ArgsMax(1),
|
||||
})
|
||||
}
|
||||
|
||||
func Deploy(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var stdout io.Writer
|
||||
|
||||
if c.Bool("id") {
|
||||
stdout = c.Writer().Stdout
|
||||
c.Writer().Stdout = c.Writer().Stderr
|
||||
}
|
||||
|
||||
b, err := build(rack, c, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := releasePromote(rack, c, app(c), b.Release); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("id") {
|
||||
fmt.Fprintf(stdout, b.Release)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
105
pkg/cli/deploy_test.go
Normal file
105
pkg/cli/deploy_test.go
Normal file
@ -0,0 +1,105 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDeploy(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ObjectStore", "app1", mock.AnythingOfType("string"), mock.Anything, structs.ObjectStoreOptions{}).Return(&fxObject, nil).Run(func(args mock.Arguments) {
|
||||
require.Regexp(t, `tmp/[0-9a-f]{30}\.tgz`, args.Get(1).(string))
|
||||
})
|
||||
i.On("BuildCreate", "app1", "object://test", structs.BuildCreateOptions{Description: options.String("foo")}).Return(fxBuild(), nil)
|
||||
i.On("BuildLogs", "app1", "build1", structs.LogsOptions{}).Return(testLogs(fxLogs()), nil)
|
||||
i.On("BuildGet", "app1", "build1").Return(fxBuildRunning(), nil).Once()
|
||||
i.On("BuildGet", "app1", "build4").Return(fxBuild(), nil)
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil)
|
||||
i.On("ReleasePromote", "app1", "release1", structs.ReleasePromoteOptions{}).Return(nil)
|
||||
|
||||
res, err := testExecute(e, "deploy ./testdata/httpd -a app1 -d foo", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Packaging source... OK",
|
||||
"Uploading source... OK",
|
||||
"Starting build... OK",
|
||||
"log1",
|
||||
"log2",
|
||||
"Promoting release1... OK",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeployError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ObjectStore", "app1", mock.AnythingOfType("string"), mock.Anything, structs.ObjectStoreOptions{}).Return(&fxObject, nil).Run(func(args mock.Arguments) {
|
||||
require.Regexp(t, `tmp/[0-9a-f]{30}\.tgz`, args.Get(1).(string))
|
||||
})
|
||||
i.On("BuildCreate", "app1", "object://test", structs.BuildCreateOptions{Description: options.String("foo")}).Return(fxBuild(), nil)
|
||||
i.On("BuildLogs", "app1", "build1", structs.LogsOptions{}).Return(testLogs(fxLogs()), nil)
|
||||
i.On("BuildGet", "app1", "build1").Return(fxBuildRunning(), nil).Once()
|
||||
i.On("BuildGet", "app1", "build4").Return(fxBuild(), nil)
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil)
|
||||
i.On("ReleasePromote", "app1", "release1", structs.ReleasePromoteOptions{}).Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "deploy ./testdata/httpd -a app1 -d foo", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{
|
||||
"Packaging source... OK",
|
||||
"Uploading source... OK",
|
||||
"Starting build... OK",
|
||||
"log1",
|
||||
"log2",
|
||||
"Promoting release1... ",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeployWait(t *testing.T) {
|
||||
testClientWait(t, 100*time.Millisecond, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ObjectStore", "app1", mock.AnythingOfType("string"), mock.Anything, structs.ObjectStoreOptions{}).Return(&fxObject, nil).Run(func(args mock.Arguments) {
|
||||
require.Regexp(t, `tmp/[0-9a-f]{30}\.tgz`, args.Get(1).(string))
|
||||
})
|
||||
i.On("BuildCreate", "app1", "object://test", structs.BuildCreateOptions{Description: options.String("foo")}).Return(fxBuild(), nil)
|
||||
i.On("BuildLogs", "app1", "build1", structs.LogsOptions{}).Return(testLogs(fxLogs()), nil)
|
||||
i.On("BuildGet", "app1", "build1").Return(fxBuildRunning(), nil).Once()
|
||||
i.On("BuildGet", "app1", "build4").Return(fxBuild(), nil)
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil).Once()
|
||||
i.On("ReleasePromote", "app1", "release1", structs.ReleasePromoteOptions{}).Return(nil)
|
||||
i.On("AppGet", "app1").Return(fxAppUpdating(), nil).Twice()
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil)
|
||||
opts := structs.LogsOptions{Prefix: options.Bool(true), Since: options.Duration(5 * time.Second)}
|
||||
i.On("AppLogs", "app1", opts).Return(testLogs(fxLogsSystem()), nil)
|
||||
|
||||
res, err := testExecute(e, "deploy ./testdata/httpd -a app1 -d foo --wait", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Packaging source... OK",
|
||||
"Uploading source... OK",
|
||||
"Starting build... OK",
|
||||
"log1",
|
||||
"log2",
|
||||
"Promoting release1... ",
|
||||
fxLogsSystem()[0],
|
||||
fxLogsSystem()[1],
|
||||
"OK",
|
||||
})
|
||||
})
|
||||
}
|
||||
96
pkg/cli/engine.go
Normal file
96
pkg/cli/engine.go
Normal file
@ -0,0 +1,96 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
type Engine struct {
|
||||
*stdcli.Engine
|
||||
Client sdk.Interface
|
||||
}
|
||||
|
||||
func (e *Engine) Command(command, description string, fn HandlerFunc, opts stdcli.CommandOptions) {
|
||||
wfn := func(c *stdcli.Context) error {
|
||||
return fn(e.currentClient(c), c)
|
||||
}
|
||||
|
||||
e.Engine.Command(command, description, wfn, opts)
|
||||
}
|
||||
|
||||
func (e *Engine) CommandWithoutProvider(command, description string, fn HandlerFunc, opts stdcli.CommandOptions) {
|
||||
wfn := func(c *stdcli.Context) error {
|
||||
return fn(nil, c)
|
||||
}
|
||||
|
||||
e.Engine.Command(command, description, wfn, opts)
|
||||
}
|
||||
|
||||
func (e *Engine) RegisterCommands() {
|
||||
for _, c := range commands {
|
||||
if c.Rack {
|
||||
e.Command(c.Command, c.Description, c.Handler, c.Opts)
|
||||
} else {
|
||||
e.CommandWithoutProvider(c.Command, c.Description, c.Handler, c.Opts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) currentClient(c *stdcli.Context) sdk.Interface {
|
||||
if e.Client != nil {
|
||||
return e.Client
|
||||
}
|
||||
|
||||
host, err := currentHost(c)
|
||||
if err != nil {
|
||||
c.Fail(err)
|
||||
}
|
||||
|
||||
r := currentRack(c, host)
|
||||
|
||||
endpoint, err := currentEndpoint(c, r)
|
||||
if err != nil {
|
||||
c.Fail(err)
|
||||
}
|
||||
|
||||
sc, err := sdk.New(endpoint)
|
||||
if err != nil {
|
||||
c.Fail(err)
|
||||
}
|
||||
|
||||
sc.Authenticator = authenticator(c)
|
||||
sc.Rack = r
|
||||
sc.Session = currentSession(c)
|
||||
|
||||
return sc
|
||||
}
|
||||
|
||||
var commands = []command{}
|
||||
|
||||
type command struct {
|
||||
Command string
|
||||
Description string
|
||||
Handler HandlerFunc
|
||||
Opts stdcli.CommandOptions
|
||||
Rack bool
|
||||
}
|
||||
|
||||
func register(cmd, description string, fn HandlerFunc, opts stdcli.CommandOptions) {
|
||||
commands = append(commands, command{
|
||||
Command: cmd,
|
||||
Description: description,
|
||||
Handler: fn,
|
||||
Opts: opts,
|
||||
Rack: true,
|
||||
})
|
||||
}
|
||||
|
||||
func registerWithoutProvider(cmd, description string, fn HandlerFunc, opts stdcli.CommandOptions) {
|
||||
commands = append(commands, command{
|
||||
Command: cmd,
|
||||
Description: description,
|
||||
Handler: fn,
|
||||
Opts: opts,
|
||||
Rack: false,
|
||||
})
|
||||
}
|
||||
320
pkg/cli/env.go
Normal file
320
pkg/cli/env.go
Normal file
@ -0,0 +1,320 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/convox/convox/pkg/common"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("env", "list env vars", Env, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack, flagApp},
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("env edit", "edit env interactively", EnvEdit, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{
|
||||
flagApp,
|
||||
flagRack,
|
||||
flagWait,
|
||||
stdcli.BoolFlag("promote", "p", "promote the release"),
|
||||
},
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("env get", "get an env var", EnvGet, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack, flagApp},
|
||||
Usage: "<var>",
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("env set", "set env var(s)", EnvSet, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{
|
||||
flagApp,
|
||||
flagId,
|
||||
flagRack,
|
||||
flagWait,
|
||||
stdcli.BoolFlag("replace", "", "replace all environment variables with given ones"),
|
||||
stdcli.BoolFlag("promote", "p", "promote the release"),
|
||||
},
|
||||
Usage: "<key=value> [key=value]...",
|
||||
})
|
||||
|
||||
register("env unset", "unset env var(s)", EnvUnset, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{
|
||||
flagApp,
|
||||
flagId,
|
||||
flagRack,
|
||||
flagWait,
|
||||
stdcli.BoolFlag("promote", "p", "promote the release"),
|
||||
},
|
||||
Usage: "<key> [key]...",
|
||||
Validate: stdcli.ArgsMin(1),
|
||||
})
|
||||
}
|
||||
|
||||
func Env(rack sdk.Interface, c *stdcli.Context) error {
|
||||
env, err := common.AppEnvironment(rack, app(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Writef("%s\n", env.String())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnvEdit(rack sdk.Interface, c *stdcli.Context) error {
|
||||
env, err := common.AppEnvironment(rack, app(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmp, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file := filepath.Join(tmp, fmt.Sprintf("%s.env", app(c)))
|
||||
|
||||
fd, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := fd.Write([]byte(env.String())); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fd.Close()
|
||||
|
||||
editor := "vi"
|
||||
|
||||
if e := os.Getenv("EDITOR"); e != "" {
|
||||
editor = e
|
||||
}
|
||||
|
||||
if err := c.Terminal(editor, file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nenv := structs.Environment{}
|
||||
|
||||
if err := nenv.Load(bytes.TrimSpace(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nks := []string{}
|
||||
|
||||
for k := range nenv {
|
||||
nks = append(nks, fmt.Sprintf("<info>%s</info>", k))
|
||||
}
|
||||
|
||||
sort.Strings(nks)
|
||||
|
||||
c.Startf(fmt.Sprintf("Setting %s", strings.Join(nks, ", ")))
|
||||
|
||||
var r *structs.Release
|
||||
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Version <= "20180708231844" {
|
||||
r, err = rack.EnvironmentSet(app(c), []byte(nenv.String()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
r, err = rack.ReleaseCreate(app(c), structs.ReleaseCreateOptions{Env: options.String(nenv.String())})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.OK()
|
||||
|
||||
c.Writef("Release: <release>%s</release>\n", r.Id)
|
||||
|
||||
if c.Bool("promote") {
|
||||
if err := releasePromote(rack, c, app(c), r.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnvGet(rack sdk.Interface, c *stdcli.Context) error {
|
||||
env, err := common.AppEnvironment(rack, app(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k := c.Arg(0)
|
||||
|
||||
v, ok := env[k]
|
||||
if !ok {
|
||||
return fmt.Errorf("env not found: %s", k)
|
||||
}
|
||||
|
||||
c.Writef("%s\n", v)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnvSet(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var stdout io.Writer
|
||||
|
||||
if c.Bool("id") {
|
||||
stdout = c.Writer().Stdout
|
||||
c.Writer().Stdout = c.Writer().Stderr
|
||||
}
|
||||
|
||||
env := structs.Environment{}
|
||||
var err error
|
||||
|
||||
if !c.Bool("replace") {
|
||||
env, err = common.AppEnvironment(rack, app(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
args := []string(c.Args)
|
||||
keys := []string{}
|
||||
|
||||
if !c.Reader().IsTerminal() {
|
||||
s := bufio.NewScanner(c.Reader())
|
||||
for s.Scan() {
|
||||
args = append(args, s.Text())
|
||||
}
|
||||
}
|
||||
|
||||
for _, arg := range args {
|
||||
parts := strings.SplitN(arg, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
keys = append(keys, fmt.Sprintf("<info>%s</info>", parts[0]))
|
||||
env[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
c.Startf(fmt.Sprintf("Setting %s", strings.Join(keys, ", ")))
|
||||
|
||||
var r *structs.Release
|
||||
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Version <= "20180708231844" {
|
||||
r, err = rack.EnvironmentSet(app(c), []byte(env.String()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
r, err = rack.ReleaseCreate(app(c), structs.ReleaseCreateOptions{Env: options.String(env.String())})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.OK()
|
||||
|
||||
c.Writef("Release: <release>%s</release>\n", r.Id)
|
||||
|
||||
if c.Bool("promote") {
|
||||
if err := releasePromote(rack, c, app(c), r.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.Bool("id") {
|
||||
fmt.Fprintf(stdout, r.Id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnvUnset(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var stdout io.Writer
|
||||
|
||||
if c.Bool("id") {
|
||||
stdout = c.Writer().Stdout
|
||||
c.Writer().Stdout = c.Writer().Stderr
|
||||
}
|
||||
|
||||
env, err := common.AppEnvironment(rack, app(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keys := []string{}
|
||||
|
||||
for _, arg := range c.Args {
|
||||
keys = append(keys, fmt.Sprintf("<info>%s</info>", arg))
|
||||
delete(env, arg)
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
c.Startf(fmt.Sprintf("Unsetting %s", strings.Join(keys, ", ")))
|
||||
|
||||
var r *structs.Release
|
||||
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Version <= "20180708231844" {
|
||||
for _, e := range c.Args {
|
||||
r, err = rack.EnvironmentUnset(app(c), e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
r, err = rack.ReleaseCreate(app(c), structs.ReleaseCreateOptions{Env: options.String(env.String())})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.OK()
|
||||
|
||||
c.Writef("Release: <release>%s</release>\n", r.Id)
|
||||
|
||||
if c.Bool("promote") {
|
||||
if err := releasePromote(rack, c, app(c), r.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.Bool("id") {
|
||||
fmt.Fprintf(stdout, r.Id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
228
pkg/cli/env_test.go
Normal file
228
pkg/cli/env_test.go
Normal file
@ -0,0 +1,228 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEnv(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.ReleaseListOptions{Limit: options.Int(1)}
|
||||
i.On("ReleaseList", "app1", opts).Return(structs.Releases{*fxRelease()}, nil)
|
||||
i.On("ReleaseGet", "app1", "release1").Return(fxRelease(), nil)
|
||||
|
||||
res, err := testExecute(e, "env -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"BAZ=quux",
|
||||
"FOO=bar",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnvError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.ReleaseListOptions{Limit: options.Int(1)}
|
||||
i.On("ReleaseList", "app1", opts).Return(structs.Releases{*fxRelease()}, nil)
|
||||
i.On("ReleaseGet", "app1", "release1").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "env -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnvGet(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.ReleaseListOptions{Limit: options.Int(1)}
|
||||
i.On("ReleaseList", "app1", opts).Return(structs.Releases{*fxRelease()}, nil)
|
||||
i.On("ReleaseGet", "app1", "release1").Return(fxRelease(), nil)
|
||||
|
||||
res, err := testExecute(e, "env get FOO -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"bar"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnvGetError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.ReleaseListOptions{Limit: options.Int(1)}
|
||||
i.On("ReleaseList", "app1", opts).Return(structs.Releases{*fxRelease()}, nil)
|
||||
i.On("ReleaseGet", "app1", "release1").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "env get FOO -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnvGetMissing(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.ReleaseListOptions{Limit: options.Int(1)}
|
||||
i.On("ReleaseList", "app1", opts).Return(structs.Releases{*fxRelease()}, nil)
|
||||
i.On("ReleaseGet", "app1", "release1").Return(fxRelease(), nil)
|
||||
|
||||
res, err := testExecute(e, "env get FOOO -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: env not found: FOOO"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnvSet(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
opts := structs.ReleaseListOptions{Limit: options.Int(1)}
|
||||
i.On("ReleaseList", "app1", opts).Return(structs.Releases{*fxRelease()}, nil)
|
||||
i.On("ReleaseGet", "app1", "release1").Return(fxRelease(), nil)
|
||||
ropts := structs.ReleaseCreateOptions{Env: options.String("AAA=bbb\nBAZ=quux\nCCC=ddd\nFOO=bar")}
|
||||
i.On("ReleaseCreate", "app1", ropts).Return(fxRelease(), nil)
|
||||
|
||||
res, err := testExecute(e, "env set AAA=bbb CCC=ddd -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Setting AAA, CCC... OK",
|
||||
"Release: release1",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnvSetError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
opts := structs.ReleaseListOptions{Limit: options.Int(1)}
|
||||
i.On("ReleaseList", "app1", opts).Return(structs.Releases{*fxRelease()}, nil)
|
||||
i.On("ReleaseGet", "app1", "release1").Return(fxRelease(), nil)
|
||||
ropts := structs.ReleaseCreateOptions{Env: options.String("AAA=bbb\nBAZ=quux\nCCC=ddd\nFOO=bar")}
|
||||
i.On("ReleaseCreate", "app1", ropts).Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "env set AAA=bbb CCC=ddd -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Setting AAA, CCC... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnvSetClassic(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
opts := structs.ReleaseListOptions{Limit: options.Int(1)}
|
||||
i.On("ReleaseList", "app1", opts).Return(structs.Releases{*fxRelease()}, nil)
|
||||
i.On("ReleaseGet", "app1", "release1").Return(fxRelease(), nil)
|
||||
i.On("EnvironmentSet", "app1", []byte("AAA=bbb\nBAZ=quux\nCCC=ddd\nFOO=bar")).Return(fxRelease(), nil)
|
||||
|
||||
res, err := testExecute(e, "env set AAA=bbb CCC=ddd -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Setting AAA, CCC... OK",
|
||||
"Release: release1",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnvSetReplace(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
ropts := structs.ReleaseCreateOptions{Env: options.String("AAA=bbb\nCCC=ddd")}
|
||||
i.On("ReleaseCreate", "app1", ropts).Return(fxRelease(), nil)
|
||||
|
||||
res, err := testExecute(e, "env set AAA=bbb CCC=ddd -a app1 --replace", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Setting AAA, CCC... OK",
|
||||
"Release: release1",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnvSetReplaceError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
ropts := structs.ReleaseCreateOptions{Env: options.String("AAA=bbb\nCCC=ddd")}
|
||||
i.On("ReleaseCreate", "app1", ropts).Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "env set AAA=bbb CCC=ddd -a app1 --replace", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Setting AAA, CCC... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnvUnset(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
opts := structs.ReleaseListOptions{Limit: options.Int(1)}
|
||||
i.On("ReleaseList", "app1", opts).Return(structs.Releases{*fxRelease()}, nil)
|
||||
i.On("ReleaseGet", "app1", "release1").Return(fxRelease(), nil)
|
||||
ropts := structs.ReleaseCreateOptions{Env: options.String("BAZ=quux")}
|
||||
i.On("ReleaseCreate", "app1", ropts).Return(fxRelease(), nil)
|
||||
|
||||
res, err := testExecute(e, "env unset FOO -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Unsetting FOO... OK",
|
||||
"Release: release1",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnvUnsetError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
opts := structs.ReleaseListOptions{Limit: options.Int(1)}
|
||||
i.On("ReleaseList", "app1", opts).Return(structs.Releases{*fxRelease()}, nil)
|
||||
i.On("ReleaseGet", "app1", "release1").Return(fxRelease(), nil)
|
||||
ropts := structs.ReleaseCreateOptions{Env: options.String("BAZ=quux")}
|
||||
i.On("ReleaseCreate", "app1", ropts).Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "env unset FOO -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Unsetting FOO... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnvUnsetClassic(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
opts := structs.ReleaseListOptions{Limit: options.Int(1)}
|
||||
i.On("ReleaseList", "app1", opts).Return(structs.Releases{*fxRelease()}, nil)
|
||||
i.On("ReleaseGet", "app1", "release1").Return(fxRelease(), nil)
|
||||
i.On("EnvironmentUnset", "app1", "FOO").Return(fxRelease(), nil)
|
||||
|
||||
res, err := testExecute(e, "env unset FOO -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Unsetting FOO... OK",
|
||||
"Release: release1",
|
||||
})
|
||||
})
|
||||
}
|
||||
45
pkg/cli/exec.go
Normal file
45
pkg/cli/exec.go
Normal file
@ -0,0 +1,45 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("exec", "execute a command in a running process", Exec, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack, flagApp},
|
||||
Usage: "<pid> <command>",
|
||||
Validate: stdcli.ArgsMin(2),
|
||||
})
|
||||
}
|
||||
|
||||
func Exec(rack sdk.Interface, c *stdcli.Context) error {
|
||||
pid := c.Arg(0)
|
||||
command := strings.Join(c.Args[1:], " ")
|
||||
|
||||
opts := structs.ProcessExecOptions{}
|
||||
|
||||
if w, h, err := c.TerminalSize(); err == nil {
|
||||
opts.Height = options.Int(h)
|
||||
opts.Width = options.Int(w)
|
||||
}
|
||||
|
||||
if !stdcli.IsTerminal(os.Stdin) {
|
||||
opts.Tty = options.Bool(false)
|
||||
}
|
||||
|
||||
restore := c.TerminalRaw()
|
||||
defer restore()
|
||||
|
||||
code, err := rack.ProcessExec(app(c), pid, command, c, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return stdcli.Exit(code)
|
||||
}
|
||||
47
pkg/cli/exec_test.go
Normal file
47
pkg/cli/exec_test.go
Normal file
@ -0,0 +1,47 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExec(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.ProcessExecOptions{Tty: options.Bool(false)}
|
||||
i.On("ProcessExec", "app1", "0123456789", "bash", mock.Anything, opts).Return(4, nil).Run(func(args mock.Arguments) {
|
||||
data, err := ioutil.ReadAll(args.Get(3).(io.Reader))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "in", string(data))
|
||||
args.Get(3).(io.Writer).Write([]byte("out"))
|
||||
})
|
||||
|
||||
res, err := testExecute(e, "exec 0123456789 bash -a app1", strings.NewReader("in"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 4, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
require.Equal(t, "out", res.Stdout)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExecError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.ProcessExecOptions{Tty: options.Bool(false)}
|
||||
i.On("ProcessExec", "app1", "0123456789", "bash", mock.Anything, opts).Return(0, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "exec 0123456789 bash -a app1", strings.NewReader("in"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
304
pkg/cli/fixtures_test.go
Normal file
304
pkg/cli/fixtures_test.go
Normal file
@ -0,0 +1,304 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
)
|
||||
|
||||
func fxApp() *structs.App {
|
||||
return &structs.App{
|
||||
Name: "app1",
|
||||
Generation: "2",
|
||||
Parameters: fxParameters(),
|
||||
Release: "release1",
|
||||
Status: "running",
|
||||
}
|
||||
}
|
||||
|
||||
func fxAppGeneration1() *structs.App {
|
||||
return &structs.App{
|
||||
Name: "app1",
|
||||
Generation: "1",
|
||||
Parameters: fxParameters(),
|
||||
Release: "release1",
|
||||
Status: "running",
|
||||
}
|
||||
}
|
||||
|
||||
func fxAppRouter() *structs.App {
|
||||
return &structs.App{
|
||||
Name: "app1",
|
||||
Generation: "2",
|
||||
Parameters: fxParameters(),
|
||||
Release: "release1",
|
||||
Router: "router1",
|
||||
Status: "running",
|
||||
}
|
||||
}
|
||||
|
||||
func fxAppUpdating() *structs.App {
|
||||
return &structs.App{
|
||||
Name: "app1",
|
||||
Generation: "2",
|
||||
Parameters: fxParameters(),
|
||||
Release: "release1",
|
||||
Status: "updating",
|
||||
}
|
||||
}
|
||||
|
||||
func fxBuild() *structs.Build {
|
||||
return &structs.Build{
|
||||
App: "app1",
|
||||
Id: "build1",
|
||||
Description: "desc",
|
||||
Ended: fxStarted.Add(2 * time.Minute),
|
||||
Manifest: "manifest1\nmanifest2\n",
|
||||
Release: "release1",
|
||||
Started: fxStarted,
|
||||
Status: "complete",
|
||||
}
|
||||
}
|
||||
|
||||
func fxBuildCreated() *structs.Build {
|
||||
return &structs.Build{
|
||||
Id: "build2",
|
||||
Status: "running",
|
||||
}
|
||||
}
|
||||
|
||||
func fxBuildFailed() *structs.Build {
|
||||
return &structs.Build{
|
||||
Id: "build3",
|
||||
Started: fxStarted,
|
||||
Status: "failed",
|
||||
}
|
||||
}
|
||||
|
||||
func fxBuildRunning() *structs.Build {
|
||||
return &structs.Build{
|
||||
Id: "build4",
|
||||
Started: fxStarted,
|
||||
Status: "running",
|
||||
}
|
||||
}
|
||||
|
||||
func fxCertificate() *structs.Certificate {
|
||||
return &structs.Certificate{
|
||||
Id: "cert1",
|
||||
Domain: "example.org",
|
||||
Domains: []string{"example.net", "example.com"},
|
||||
Expiration: time.Now().Add(49 * time.Hour).UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
func fxInstance() *structs.Instance {
|
||||
return &structs.Instance{
|
||||
Agent: true,
|
||||
Cpu: 0.423,
|
||||
Id: "instance1",
|
||||
Memory: 0.718,
|
||||
PrivateIp: "private",
|
||||
Processes: 3,
|
||||
PublicIp: "public",
|
||||
Status: "status",
|
||||
Started: time.Now().UTC().Add(-48 * time.Hour),
|
||||
}
|
||||
}
|
||||
|
||||
func fxLogs() []string {
|
||||
return []string{
|
||||
"log1",
|
||||
"log2",
|
||||
}
|
||||
}
|
||||
|
||||
func fxLogsLonger() []string {
|
||||
return []string{
|
||||
"log1",
|
||||
"log2",
|
||||
"log3",
|
||||
}
|
||||
}
|
||||
|
||||
func fxLogsSystem() []string {
|
||||
return []string{
|
||||
"TIME system/aws/component log1",
|
||||
"TIME system/aws/component log2",
|
||||
}
|
||||
}
|
||||
|
||||
func fxParameters() map[string]string {
|
||||
return map[string]string{
|
||||
"ParamFoo": "value1",
|
||||
"ParamOther": "value2",
|
||||
"ParamPassword": "****",
|
||||
}
|
||||
}
|
||||
|
||||
func fxProcess() *structs.Process {
|
||||
return &structs.Process{
|
||||
Id: "pid1",
|
||||
App: "app1",
|
||||
Command: "command",
|
||||
Cpu: 1.0,
|
||||
Host: "host",
|
||||
Image: "image",
|
||||
Instance: "instance",
|
||||
Memory: 2.0,
|
||||
Name: "name",
|
||||
Ports: []string{"1000", "2000"},
|
||||
Release: "release1",
|
||||
Started: time.Now().UTC().Add(-49 * time.Hour),
|
||||
Status: "running",
|
||||
}
|
||||
}
|
||||
|
||||
func fxProcessPending() *structs.Process {
|
||||
return &structs.Process{
|
||||
Id: "pid1",
|
||||
App: "app1",
|
||||
Command: "command",
|
||||
Cpu: 1.0,
|
||||
Host: "host",
|
||||
Image: "image",
|
||||
Instance: "instance",
|
||||
Memory: 2.0,
|
||||
Name: "name",
|
||||
Ports: []string{"1000", "2000"},
|
||||
Release: "release1",
|
||||
Started: time.Now().UTC().Add(-49 * time.Hour),
|
||||
Status: "pending",
|
||||
}
|
||||
}
|
||||
|
||||
func fxRegistry() *structs.Registry {
|
||||
return &structs.Registry{
|
||||
Server: "registry1",
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
}
|
||||
}
|
||||
|
||||
func fxRelease() *structs.Release {
|
||||
return &structs.Release{
|
||||
Id: "release1",
|
||||
App: "app1",
|
||||
Build: "build1",
|
||||
Env: "FOO=bar\nBAZ=quux",
|
||||
Manifest: "services:\n web:\n build: .\n test: make test",
|
||||
Created: time.Now().UTC().Add(-49 * time.Hour),
|
||||
Description: "description1",
|
||||
}
|
||||
}
|
||||
|
||||
func fxRelease2() *structs.Release {
|
||||
return &structs.Release{
|
||||
Id: "release2",
|
||||
App: "app1",
|
||||
Build: "build1",
|
||||
Env: "FOO=bar\nBAZ=quux",
|
||||
Manifest: "manifest",
|
||||
Created: time.Now().UTC().Add(-49 * time.Hour),
|
||||
}
|
||||
}
|
||||
|
||||
func fxRelease3() *structs.Release {
|
||||
return &structs.Release{
|
||||
Id: "release3",
|
||||
App: "app1",
|
||||
Build: "build1",
|
||||
Env: "FOO=bar\nBAZ=quux",
|
||||
Manifest: "manifest",
|
||||
Created: time.Now().UTC().Add(-49 * time.Hour),
|
||||
}
|
||||
}
|
||||
|
||||
func fxResource() *structs.Resource {
|
||||
return &structs.Resource{
|
||||
Name: "resource1",
|
||||
Parameters: map[string]string{"k1": "v1", "k2": "v2", "Url": "https://other.example.org/path"},
|
||||
Status: "status",
|
||||
Type: "type",
|
||||
Url: "https://example.org/path",
|
||||
Apps: structs.Apps{*fxApp(), *fxApp()},
|
||||
}
|
||||
}
|
||||
|
||||
func fxResourceType() structs.ResourceType {
|
||||
return structs.ResourceType{
|
||||
Name: "type1",
|
||||
Parameters: structs.ResourceParameters{
|
||||
{Default: "def1", Description: "desc1", Name: "Param1"},
|
||||
{Default: "def2", Description: "desc2", Name: "Param2"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func fxService() *structs.Service {
|
||||
return &structs.Service{
|
||||
Name: "service1",
|
||||
Count: 1,
|
||||
Cpu: 2,
|
||||
Domain: "domain",
|
||||
Memory: 3,
|
||||
Ports: []structs.ServicePort{
|
||||
{Balancer: 1, Certificate: "cert1", Container: 2},
|
||||
{Balancer: 1, Certificate: "cert1", Container: 2},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func fxSystem() *structs.System {
|
||||
return &structs.System{
|
||||
Count: 1,
|
||||
Domain: "domain",
|
||||
Name: "name",
|
||||
Outputs: map[string]string{"k1": "v1", "k2": "v2"},
|
||||
Parameters: map[string]string{"Autoscale": "Yes", "ParamFoo": "value1", "ParamOther": "value2"},
|
||||
Provider: "provider",
|
||||
Region: "region",
|
||||
Status: "running",
|
||||
Type: "type",
|
||||
Version: "21000101000000",
|
||||
}
|
||||
}
|
||||
|
||||
func fxSystemClassic() *structs.System {
|
||||
return &structs.System{
|
||||
Count: 1,
|
||||
Domain: "domain",
|
||||
Name: "name",
|
||||
Outputs: map[string]string{"k1": "v1", "k2": "v2"},
|
||||
Parameters: map[string]string{"ParamFoo": "value1", "ParamOther": "value2"},
|
||||
Provider: "provider",
|
||||
Region: "region",
|
||||
Status: "running",
|
||||
Type: "type",
|
||||
Version: "20180101000000",
|
||||
}
|
||||
}
|
||||
|
||||
func fxSystemLocal() *structs.System {
|
||||
return &structs.System{
|
||||
Name: "convox",
|
||||
Provider: "local",
|
||||
Status: "running",
|
||||
Version: "dev",
|
||||
}
|
||||
}
|
||||
|
||||
func fxSystemInternal() *structs.System {
|
||||
return &structs.System{
|
||||
Count: 1,
|
||||
Domain: "domain",
|
||||
Name: "name",
|
||||
Outputs: map[string]string{"DomainInternal": "domain-internal"},
|
||||
Parameters: map[string]string{"Autoscale": "Yes", "ParamFoo": "value1", "ParamOther": "value2"},
|
||||
Provider: "provider",
|
||||
Region: "region",
|
||||
Status: "running",
|
||||
Type: "type",
|
||||
Version: "20180901000000",
|
||||
}
|
||||
}
|
||||
369
pkg/cli/helpers.go
Normal file
369
pkg/cli/helpers.go
Normal file
@ -0,0 +1,369 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/convox/convox/pkg/common"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
"github.com/convox/stdsdk"
|
||||
)
|
||||
|
||||
type rack struct {
|
||||
Name string
|
||||
Status string
|
||||
}
|
||||
|
||||
func app(c *stdcli.Context) string {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return coalesce(c.String("app"), c.LocalSetting("app"), filepath.Base(wd))
|
||||
}
|
||||
|
||||
func coalesce(ss ...string) string {
|
||||
for _, s := range ss {
|
||||
if s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func currentHost(c *stdcli.Context) (string, error) {
|
||||
if h := os.Getenv("CONVOX_HOST"); h != "" {
|
||||
return h, nil
|
||||
}
|
||||
|
||||
if h, _ := c.SettingRead("host"); h != "" {
|
||||
return h, nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func currentPassword(c *stdcli.Context, host string) (string, error) {
|
||||
if pw := os.Getenv("CONVOX_PASSWORD"); pw != "" {
|
||||
return pw, nil
|
||||
}
|
||||
|
||||
return c.SettingReadKey("auth", host)
|
||||
}
|
||||
|
||||
func currentEndpoint(c *stdcli.Context, rack_ string) (string, error) {
|
||||
if e := os.Getenv("RACK_URL"); e != "" {
|
||||
return e, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(rack_, "local/") {
|
||||
return fmt.Sprintf("https://rack.%s", strings.SplitN(rack_, "/", 2)[1]), nil
|
||||
}
|
||||
|
||||
host, err := currentHost(c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
if !localRackRunning(c) {
|
||||
return "", fmt.Errorf("no racks found, try `convox login`")
|
||||
}
|
||||
|
||||
var r *rack
|
||||
|
||||
if cr := currentRack(c, ""); cr != "" {
|
||||
r, err = matchRack(c, cr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
r, err = matchRack(c, "local/")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if r == nil {
|
||||
return "", fmt.Errorf("no racks found, try `convox login`")
|
||||
}
|
||||
|
||||
return fmt.Sprintf("https://rack.%s", strings.SplitN(r.Name, "/", 2)[1]), nil
|
||||
}
|
||||
|
||||
pw, err := currentPassword(c, host)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("https://convox:%s@%s", url.QueryEscape(pw), host), nil
|
||||
}
|
||||
|
||||
func currentRack(c *stdcli.Context, host string) string {
|
||||
if r := c.String("rack"); r != "" {
|
||||
return r
|
||||
}
|
||||
|
||||
if r := os.Getenv("CONVOX_RACK"); r != "" {
|
||||
return r
|
||||
}
|
||||
|
||||
if r := c.LocalSetting("rack"); r != "" {
|
||||
return r
|
||||
}
|
||||
|
||||
if r := hostRacks(c)[host]; r != "" {
|
||||
return r
|
||||
}
|
||||
|
||||
if r, _ := c.SettingRead("rack"); r != "" {
|
||||
return r
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func executableName() string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return "convox.exe"
|
||||
default:
|
||||
return "convox"
|
||||
}
|
||||
}
|
||||
|
||||
func generateTempKey() (string, error) {
|
||||
data := make([]byte, 1024)
|
||||
|
||||
if _, err := rand.Read(data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(data)
|
||||
|
||||
return fmt.Sprintf("tmp/%s", hex.EncodeToString(hash[:])[0:30]), nil
|
||||
}
|
||||
|
||||
func hostRacks(c *stdcli.Context) map[string]string {
|
||||
data, err := c.SettingRead("racks")
|
||||
if err != nil {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
var rs map[string]string
|
||||
|
||||
if err := json.Unmarshal([]byte(data), &rs); err != nil {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
return rs
|
||||
}
|
||||
|
||||
func localRackRunning(c *stdcli.Context) bool {
|
||||
rs, err := localRacks(c)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return len(rs) > 0
|
||||
}
|
||||
|
||||
func localRacks(c *stdcli.Context) ([]rack, error) {
|
||||
if os.Getenv("CONVOX_LOCAL") == "disable" {
|
||||
return []rack{}, nil
|
||||
}
|
||||
|
||||
racks := []rack{}
|
||||
|
||||
data, err := c.Execute("kubectl", "get", "ns", "--selector=system=convox,type=rack", "--output=name")
|
||||
if err == nil {
|
||||
nsrs := strings.Split(strings.TrimSpace(string(data)), "\n")
|
||||
|
||||
for _, nsr := range nsrs {
|
||||
if strings.HasPrefix(nsr, "namespace/") {
|
||||
racks = append(racks, rack{
|
||||
Name: fmt.Sprintf("local/%s", strings.TrimPrefix(nsr, "namespace/")),
|
||||
Status: "running",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return racks, nil
|
||||
}
|
||||
|
||||
func matchRack(c *stdcli.Context, name string) (*rack, error) {
|
||||
rs, err := racks(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
matches := []rack{}
|
||||
|
||||
for _, r := range rs {
|
||||
if r.Name == name {
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
if strings.Index(r.Name, name) != -1 {
|
||||
matches = append(matches, r)
|
||||
}
|
||||
}
|
||||
|
||||
if len(matches) > 1 {
|
||||
return nil, fmt.Errorf("ambiguous rack name: %s", name)
|
||||
}
|
||||
|
||||
if len(matches) == 1 {
|
||||
return &matches[0], nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("could not find rack: %s", name)
|
||||
}
|
||||
|
||||
func racks(c *stdcli.Context) ([]rack, error) {
|
||||
rs := []rack{}
|
||||
|
||||
rrs, err := remoteRacks(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rs = append(rs, rrs...)
|
||||
|
||||
lrs, err := localRacks(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rs = append(rs, lrs...)
|
||||
|
||||
sort.Slice(rs, func(i, j int) bool {
|
||||
return rs[i].Name < rs[j].Name
|
||||
})
|
||||
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
func remoteRacks(c *stdcli.Context) ([]rack, error) {
|
||||
h, err := currentHost(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if h == "" {
|
||||
return []rack{}, nil
|
||||
}
|
||||
|
||||
racks := []rack{}
|
||||
|
||||
var rs []struct {
|
||||
Name string
|
||||
Organization struct {
|
||||
Name string
|
||||
}
|
||||
Status string
|
||||
}
|
||||
|
||||
// override local rack to get remote rack list
|
||||
endpoint, err := currentEndpoint(c, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p, err := sdk.New(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.Authenticator = authenticator(c)
|
||||
p.Session = currentSession(c)
|
||||
|
||||
if err := p.Get("/racks", stdsdk.RequestOptions{}, &rs); err != nil {
|
||||
if _, ok := err.(AuthenticationError); ok {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if rs != nil {
|
||||
for _, r := range rs {
|
||||
racks = append(racks, rack{
|
||||
Name: fmt.Sprintf("%s/%s", r.Organization.Name, r.Name),
|
||||
Status: r.Status,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return racks, nil
|
||||
}
|
||||
|
||||
func tag(name, value string) string {
|
||||
return fmt.Sprintf("<%s>%s</%s>", name, value, name)
|
||||
}
|
||||
|
||||
func waitForResourceDeleted(rack sdk.Interface, c *stdcli.Context, resource string) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(WaitDuration) // give the stack time to start updating
|
||||
|
||||
return common.Wait(WaitDuration, 30*time.Minute, 2, func() (bool, error) {
|
||||
var err error
|
||||
if s.Version <= "20190111211123" {
|
||||
_, err = rack.SystemResourceGetClassic(resource)
|
||||
} else {
|
||||
_, err = rack.SystemResourceGet(resource)
|
||||
}
|
||||
if err == nil {
|
||||
return false, nil
|
||||
}
|
||||
if strings.Contains(err.Error(), "no such resource") {
|
||||
return true, nil
|
||||
}
|
||||
if strings.Contains(err.Error(), "does not exist") {
|
||||
return true, nil
|
||||
}
|
||||
return false, err
|
||||
})
|
||||
}
|
||||
|
||||
func waitForResourceRunning(rack sdk.Interface, c *stdcli.Context, resource string) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(WaitDuration) // give the stack time to start updating
|
||||
|
||||
return common.Wait(WaitDuration, 30*time.Minute, 2, func() (bool, error) {
|
||||
var r *structs.Resource
|
||||
var err error
|
||||
|
||||
if s.Version <= "20190111211123" {
|
||||
r, err = rack.SystemResourceGetClassic(resource)
|
||||
} else {
|
||||
r, err = rack.SystemResourceGet(resource)
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return r.Status == "running", nil
|
||||
})
|
||||
}
|
||||
116
pkg/cli/instances.go
Normal file
116
pkg/cli/instances.go
Normal file
@ -0,0 +1,116 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/convox/convox/pkg/common"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("instances", "list instances", Instances, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("instances keyroll", "roll ssh key on instances", InstancesKeyroll, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack, flagWait},
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("instances ssh", "run a shell on an instance", InstancesSsh, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Validate: stdcli.ArgsMin(1),
|
||||
})
|
||||
|
||||
register("instances terminate", "terminate an instance", InstancesTerminate, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Validate: stdcli.ArgsMin(1),
|
||||
})
|
||||
}
|
||||
|
||||
func Instances(rack sdk.Interface, c *stdcli.Context) error {
|
||||
is, err := rack.InstanceList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := c.Table("ID", "STATUS", "STARTED", "PS", "CPU", "MEM", "PUBLIC", "PRIVATE")
|
||||
|
||||
for _, i := range is {
|
||||
t.AddRow(i.Id, i.Status, common.Ago(i.Started), fmt.Sprintf("%d", i.Processes), common.Percent(i.Cpu), common.Percent(i.Memory), i.PublicIp, i.PrivateIp)
|
||||
}
|
||||
|
||||
return t.Print()
|
||||
}
|
||||
|
||||
func InstancesKeyroll(rack sdk.Interface, c *stdcli.Context) error {
|
||||
c.Startf("Rolling instance key")
|
||||
|
||||
if err := rack.InstanceKeyroll(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("wait") {
|
||||
c.Writef("\n")
|
||||
|
||||
if err := common.WaitForRackWithLogs(rack, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
func InstancesSsh(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts := structs.InstanceShellOptions{}
|
||||
|
||||
if w, h, err := c.TerminalSize(); err == nil {
|
||||
opts.Height = options.Int(h)
|
||||
opts.Width = options.Int(w)
|
||||
}
|
||||
|
||||
restore := c.TerminalRaw()
|
||||
defer restore()
|
||||
|
||||
command := strings.Join(c.Args[1:], " ")
|
||||
|
||||
if command != "" {
|
||||
opts.Command = options.String(command)
|
||||
}
|
||||
|
||||
if s.Version <= "20180708231844" {
|
||||
code, err := rack.InstanceShellClassic(c.Arg(0), c, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return stdcli.Exit(code)
|
||||
}
|
||||
|
||||
code, err := rack.InstanceShell(c.Arg(0), c, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return stdcli.Exit(code)
|
||||
}
|
||||
|
||||
func InstancesTerminate(rack sdk.Interface, c *stdcli.Context) error {
|
||||
c.Startf("Terminating instance")
|
||||
|
||||
if err := rack.InstanceTerminate(c.Arg(0)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
143
pkg/cli/instances_test.go
Normal file
143
pkg/cli/instances_test.go
Normal file
@ -0,0 +1,143 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInstances(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("InstanceList").Return(structs.Instances{*fxInstance(), *fxInstance()}, nil)
|
||||
|
||||
res, err := testExecute(e, "instances", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"ID STATUS STARTED PS CPU MEM PUBLIC PRIVATE",
|
||||
"instance1 status 2 days ago 3 42.30% 71.80% public private",
|
||||
"instance1 status 2 days ago 3 42.30% 71.80% public private",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestInstancesError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("InstanceList").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "instances", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestInstancesKeyroll(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("InstanceKeyroll").Return(nil)
|
||||
|
||||
res, err := testExecute(e, "instances keyroll", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Rolling instance key... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestInstancesKeyrollError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("InstanceKeyroll").Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "instances keyroll", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Rolling instance key... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestInstancesSsh(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
opts := structs.InstanceShellOptions{}
|
||||
i.On("InstanceShell", "instance1", mock.Anything, opts).Return(4, nil).Run(func(args mock.Arguments) {
|
||||
data, err := ioutil.ReadAll(args.Get(1).(io.Reader))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "in", string(data))
|
||||
args.Get(1).(io.Writer).Write([]byte("out"))
|
||||
})
|
||||
|
||||
res, err := testExecute(e, "instances ssh instance1", strings.NewReader("in"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 4, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
require.Equal(t, "out", res.Stdout)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInstancesSshError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
opts := structs.InstanceShellOptions{}
|
||||
i.On("InstanceShell", "instance1", mock.Anything, opts).Return(0, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "instances ssh instance1", strings.NewReader("in"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestInstancesSshClassic(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
opts := structs.InstanceShellOptions{}
|
||||
i.On("InstanceShellClassic", "instance1", mock.Anything, opts).Return(4, nil).Run(func(args mock.Arguments) {
|
||||
data, err := ioutil.ReadAll(args.Get(1).(io.Reader))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "in", string(data))
|
||||
args.Get(1).(io.Writer).Write([]byte("out"))
|
||||
})
|
||||
|
||||
res, err := testExecute(e, "instances ssh instance1", strings.NewReader("in"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 4, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
require.Equal(t, "out", res.Stdout)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInstancesTerminate(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("InstanceTerminate", "instance1").Return(nil)
|
||||
|
||||
res, err := testExecute(e, "instances terminate instance1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Terminating instance... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestInstancesTerminateError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("InstanceTerminate", "instance1").Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "instances terminate instance1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Terminating instance... "})
|
||||
})
|
||||
}
|
||||
74
pkg/cli/login.go
Normal file
74
pkg/cli/login.go
Normal file
@ -0,0 +1,74 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerWithoutProvider("login", "authenticate with a rack", Login, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{
|
||||
stdcli.StringFlag("password", "p", "password"),
|
||||
},
|
||||
Usage: "[hostname]",
|
||||
Validate: stdcli.ArgsMax(1),
|
||||
})
|
||||
}
|
||||
|
||||
func Login(rack sdk.Interface, c *stdcli.Context) error {
|
||||
hostname := coalesce(c.Arg(0), "console.convox.com")
|
||||
|
||||
auth, err := c.SettingReadKey("auth", hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
password := coalesce(c.String("password"), os.Getenv("CONVOX_PASSWORD"), auth)
|
||||
|
||||
if password == "" {
|
||||
c.Writef("Password: ")
|
||||
|
||||
password, err = c.ReadSecret()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Writef("\n")
|
||||
}
|
||||
|
||||
c.Startf("Authenticating with <info>%s</info>", hostname)
|
||||
|
||||
cl, err := sdk.New(fmt.Sprintf("https://convox:%s@%s", url.QueryEscape(password), hostname))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := cl.Auth()
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid login")
|
||||
}
|
||||
|
||||
if err := c.SettingWriteKey("auth", hostname, password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.SettingWrite("host", hostname); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if id != "" {
|
||||
if err := c.SettingWrite("id", id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.SettingDelete("rack"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
60
pkg/cli/login_test.go
Normal file
60
pkg/cli/login_test.go
Normal file
@ -0,0 +1,60 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/auth", r.URL.Path)
|
||||
user, pass, _ := r.BasicAuth()
|
||||
require.Equal(t, "convox", user)
|
||||
require.Equal(t, "password", pass)
|
||||
}))
|
||||
|
||||
tsu, err := url.Parse(ts.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := testExecute(e, fmt.Sprintf("login %s -p password", tsu.Host), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{fmt.Sprintf("Authenticating with %s... OK", tsu.Host)})
|
||||
|
||||
data, err := ioutil.ReadFile(filepath.Join(e.Settings, "auth"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fmt.Sprintf("{\n \"%s\": \"password\"\n}", tsu.Host), string(data))
|
||||
|
||||
data, err = ioutil.ReadFile(filepath.Join(e.Settings, "host"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tsu.Host, string(data))
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoginError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(401)
|
||||
}))
|
||||
|
||||
tsu, err := url.Parse(ts.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := testExecute(e, fmt.Sprintf("login %s -p password", tsu.Host), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: invalid login"})
|
||||
res.RequireStdout(t, []string{fmt.Sprintf("Authenticating with %s... ", tsu.Host)})
|
||||
})
|
||||
}
|
||||
40
pkg/cli/logs.go
Normal file
40
pkg/cli/logs.go
Normal file
@ -0,0 +1,40 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("logs", "get logs for an app", Logs, stdcli.CommandOptions{
|
||||
Flags: append(stdcli.OptionFlags(structs.LogsOptions{}), flagApp, flagNoFollow, flagRack),
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
}
|
||||
|
||||
func Logs(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var opts structs.LogsOptions
|
||||
|
||||
if err := c.Options(&opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("no-follow") {
|
||||
opts.Follow = options.Bool(false)
|
||||
}
|
||||
|
||||
opts.Prefix = options.Bool(true)
|
||||
|
||||
r, err := rack.AppLogs(app(c), opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(c, r)
|
||||
|
||||
return nil
|
||||
}
|
||||
39
pkg/cli/logs_test.go
Normal file
39
pkg/cli/logs_test.go
Normal file
@ -0,0 +1,39 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLogs(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppLogs", "app1", structs.LogsOptions{Prefix: options.Bool(true)}).Return(testLogs(fxLogs()), nil)
|
||||
|
||||
res, err := testExecute(e, "logs -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
fxLogs()[0],
|
||||
fxLogs()[1],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestLogsError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppLogs", "app1", structs.LogsOptions{Prefix: options.Bool(true)}).Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "logs -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
125
pkg/cli/proxy.go
Normal file
125
pkg/cli/proxy.go
Normal file
@ -0,0 +1,125 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("proxy", "proxy a connection inside the rack", Proxy, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{
|
||||
flagRack,
|
||||
stdcli.BoolFlag("tls", "t", "wrap connection in tls"),
|
||||
},
|
||||
Usage: "<[port:]host:hostport> [[port:]host:hostport]...",
|
||||
Validate: stdcli.ArgsMin(1),
|
||||
})
|
||||
}
|
||||
|
||||
// var ProxyCloser = make(chan error)
|
||||
|
||||
func Proxy(rack sdk.Interface, c *stdcli.Context) error {
|
||||
for _, arg := range c.Args {
|
||||
parts := strings.SplitN(arg, ":", 3)
|
||||
|
||||
var host string
|
||||
var port, hostport int
|
||||
|
||||
switch len(parts) {
|
||||
case 2:
|
||||
host = parts[0]
|
||||
|
||||
p, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
port = p
|
||||
hostport = p
|
||||
case 3:
|
||||
host = parts[1]
|
||||
|
||||
p, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
port = p
|
||||
|
||||
p, err = strconv.Atoi(parts[2])
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hostport = p
|
||||
default:
|
||||
return fmt.Errorf("invalid argument: %s", arg)
|
||||
}
|
||||
|
||||
go proxy(rack, c, port, host, hostport, c.Bool("tls"))
|
||||
}
|
||||
|
||||
<-c.Done()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func proxy(rack sdk.Interface, c *stdcli.Context, localport int, remotehost string, remoteport int, secure bool) {
|
||||
c.Writef("proxying localhost:%d to %s:%d\n", localport, remotehost, remoteport)
|
||||
|
||||
lc := &net.ListenConfig{}
|
||||
|
||||
ln, err := lc.Listen(c.Context, "tcp4", fmt.Sprintf("127.0.0.1:%d", localport))
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
ch := make(chan net.Conn)
|
||||
|
||||
go proxyAccept(c, ln, ch)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.Done():
|
||||
return
|
||||
case cn := <-ch:
|
||||
c.Writef("connect: %d\n", localport)
|
||||
go proxyConnection(c, cn, rack, remotehost, remoteport, secure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func proxyAccept(c *stdcli.Context, ln net.Listener, ch chan net.Conn) {
|
||||
for {
|
||||
select {
|
||||
case <-c.Done():
|
||||
return
|
||||
default:
|
||||
if cn, _ := ln.Accept(); cn != nil {
|
||||
ch <- cn
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func proxyConnection(c *stdcli.Context, cn net.Conn, rack sdk.Interface, remotehost string, remoteport int, secure bool) {
|
||||
defer cn.Close()
|
||||
|
||||
opts := structs.ProxyOptions{
|
||||
TLS: options.Bool(secure),
|
||||
}
|
||||
|
||||
if err := rack.WithContext(c.Context).Proxy(remotehost, remoteport, cn, opts); err != nil {
|
||||
c.Error(err)
|
||||
}
|
||||
}
|
||||
122
pkg/cli/proxy_test.go
Normal file
122
pkg/cli/proxy_test.go
Normal file
@ -0,0 +1,122 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProxy(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
i.On("WithContext", ctx).Return(i)
|
||||
opts := structs.ProxyOptions{
|
||||
TLS: options.Bool(false),
|
||||
}
|
||||
i.On("Proxy", "test.example.org", 5000, mock.Anything, opts).Return(nil).Run(func(args mock.Arguments) {
|
||||
buf := make([]byte, 2)
|
||||
rwc := args.Get(2).(io.ReadWriteCloser)
|
||||
n, err := rwc.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, n)
|
||||
require.Equal(t, "in", string(buf))
|
||||
n, err = rwc.Write([]byte("out"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, n)
|
||||
rwc.Close()
|
||||
})
|
||||
|
||||
port := rand.Intn(30000) + 10000
|
||||
|
||||
ch := make(chan *result)
|
||||
|
||||
go func() {
|
||||
res, err := testExecuteContext(ctx, e, fmt.Sprintf("proxy %d:test.example.org:5000", port), nil)
|
||||
require.NoError(t, err)
|
||||
ch <- res
|
||||
}()
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
cn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port))
|
||||
require.NoError(t, err)
|
||||
|
||||
cn.Write([]byte("in"))
|
||||
|
||||
data, err := ioutil.ReadAll(cn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "out", string(data))
|
||||
|
||||
cancel()
|
||||
|
||||
res := <-ch
|
||||
|
||||
require.NotNil(t, res)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
fmt.Sprintf("proxying localhost:%d to test.example.org:5000", port),
|
||||
fmt.Sprintf("connect: %d", port),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestProxyError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
i.On("WithContext", ctx).Return(i)
|
||||
opts := structs.ProxyOptions{
|
||||
TLS: options.Bool(false),
|
||||
}
|
||||
i.On("Proxy", "test.example.org", 5000, mock.Anything, opts).Return(fmt.Errorf("err1"))
|
||||
|
||||
port := rand.Intn(30000) + 10000
|
||||
|
||||
ch := make(chan *result)
|
||||
|
||||
go func() {
|
||||
res, err := testExecuteContext(ctx, e, fmt.Sprintf("proxy %d:test.example.org:5000", port), nil)
|
||||
require.NoError(t, err)
|
||||
ch <- res
|
||||
}()
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
cn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port))
|
||||
require.NoError(t, err)
|
||||
|
||||
cn.Write([]byte("in"))
|
||||
|
||||
data, _ := ioutil.ReadAll(cn)
|
||||
require.Len(t, data, 0)
|
||||
|
||||
cancel()
|
||||
// cli.ProxyCloser <- nil
|
||||
|
||||
res := <-ch
|
||||
|
||||
require.NotNil(t, res)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{
|
||||
fmt.Sprintf("proxying localhost:%d to test.example.org:5000", port),
|
||||
fmt.Sprintf("connect: %d", port),
|
||||
})
|
||||
})
|
||||
}
|
||||
76
pkg/cli/ps.go
Normal file
76
pkg/cli/ps.go
Normal file
@ -0,0 +1,76 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/convox/convox/pkg/common"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("ps", "list app processes", Ps, stdcli.CommandOptions{
|
||||
Flags: append(stdcli.OptionFlags(structs.ProcessListOptions{}), flagApp, flagRack),
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("ps info", "get information about a process", PsInfo, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagApp, flagRack},
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("ps stop", "stop a process", PsStop, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagApp, flagRack},
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
}
|
||||
|
||||
func Ps(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var opts structs.ProcessListOptions
|
||||
|
||||
if err := c.Options(&opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ps, err := rack.ProcessList(app(c), opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := c.Table("ID", "SERVICE", "STATUS", "RELEASE", "STARTED", "COMMAND")
|
||||
|
||||
for _, p := range ps {
|
||||
t.AddRow(p.Id, p.Name, p.Status, p.Release, common.Ago(p.Started), p.Command)
|
||||
}
|
||||
|
||||
return t.Print()
|
||||
}
|
||||
|
||||
func PsInfo(rack sdk.Interface, c *stdcli.Context) error {
|
||||
i := c.Info()
|
||||
|
||||
ps, err := rack.ProcessGet(app(c), c.Arg(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i.Add("Id", ps.Id)
|
||||
i.Add("App", ps.App)
|
||||
i.Add("Command", ps.Command)
|
||||
i.Add("Instance", ps.Instance)
|
||||
i.Add("Release", ps.Release)
|
||||
i.Add("Service", ps.Name)
|
||||
i.Add("Started", common.Ago(ps.Started))
|
||||
i.Add("Status", ps.Status)
|
||||
|
||||
return i.Print()
|
||||
}
|
||||
|
||||
func PsStop(rack sdk.Interface, c *stdcli.Context) error {
|
||||
c.Startf("Stopping <process>%s</process>", c.Arg(0))
|
||||
|
||||
if err := rack.ProcessStop(app(c), c.Arg(0)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
96
pkg/cli/ps_test.go
Normal file
96
pkg/cli/ps_test.go
Normal file
@ -0,0 +1,96 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPs(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("ProcessList", "app1", structs.ProcessListOptions{}).Return(structs.Processes{*fxProcess(), *fxProcessPending()}, nil)
|
||||
|
||||
res, err := testExecute(e, "ps -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"ID SERVICE STATUS RELEASE STARTED COMMAND",
|
||||
"pid1 name running release1 2 days ago command",
|
||||
"pid1 name pending release1 2 days ago command",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestPsError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("ProcessList", "app1", structs.ProcessListOptions{}).Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "ps -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestPsInfo(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("ProcessGet", "app1", "pid1").Return(fxProcess(), nil)
|
||||
|
||||
res, err := testExecute(e, "ps info pid1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Id pid1",
|
||||
"App app1",
|
||||
"Command command",
|
||||
"Instance instance",
|
||||
"Release release1",
|
||||
"Service name",
|
||||
"Started 2 days ago",
|
||||
"Status running",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestPsInfoError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("ProcessGet", "app1", "pid1").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "ps info pid1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestPsStop(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("ProcessStop", "app1", "pid1").Return(nil)
|
||||
|
||||
res, err := testExecute(e, "ps stop pid1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Stopping pid1... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestPsStopError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("ProcessStop", "app1", "pid1").Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "ps stop pid1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Stopping pid1... "})
|
||||
})
|
||||
}
|
||||
312
pkg/cli/rack.go
Normal file
312
pkg/cli/rack.go
Normal file
@ -0,0 +1,312 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/convox/convox/pkg/common"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
|
||||
cv "github.com/convox/version"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("rack", "get information about the rack", Rack, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("rack logs", "get logs for the rack", RackLogs, stdcli.CommandOptions{
|
||||
Flags: append(stdcli.OptionFlags(structs.LogsOptions{}), flagNoFollow, flagRack),
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("rack params", "display rack parameters", RackParams, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("rack params set", "set rack parameters", RackParamsSet, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack, flagWait},
|
||||
Usage: "<Key=Value> [Key=Value]...",
|
||||
Validate: stdcli.ArgsMin(1),
|
||||
})
|
||||
|
||||
register("rack ps", "list rack processes", RackPs, stdcli.CommandOptions{
|
||||
Flags: append(stdcli.OptionFlags(structs.SystemProcessesOptions{}), flagRack),
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("rack releases", "list rack version history", RackReleases, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("rack scale", "scale the rack", RackScale, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{
|
||||
flagRack,
|
||||
stdcli.IntFlag("count", "c", "instance count"),
|
||||
stdcli.StringFlag("type", "t", "instance type"),
|
||||
},
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("rack update", "update the rack", RackUpdate, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack, flagWait},
|
||||
Validate: stdcli.ArgsMax(1),
|
||||
})
|
||||
|
||||
register("rack wait", "wait for rack to finish updating", RackWait, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
}
|
||||
|
||||
func Rack(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i := c.Info()
|
||||
|
||||
i.Add("Name", s.Name)
|
||||
i.Add("Provider", s.Provider)
|
||||
|
||||
if s.Region != "" {
|
||||
i.Add("Region", s.Region)
|
||||
}
|
||||
|
||||
if s.Domain != "" {
|
||||
if ri := s.Outputs["DomainInternal"]; ri != "" {
|
||||
i.Add("Router", fmt.Sprintf("%s (external)\n%s (internal)", s.Domain, ri))
|
||||
} else {
|
||||
i.Add("Router", s.Domain)
|
||||
}
|
||||
}
|
||||
|
||||
i.Add("Status", s.Status)
|
||||
i.Add("Version", s.Version)
|
||||
|
||||
return i.Print()
|
||||
}
|
||||
|
||||
func RackLogs(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var opts structs.LogsOptions
|
||||
|
||||
if err := c.Options(&opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("no-follow") {
|
||||
opts.Follow = options.Bool(false)
|
||||
}
|
||||
|
||||
opts.Prefix = options.Bool(true)
|
||||
|
||||
r, err := rack.SystemLogs(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
io.Copy(c, r)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RackParams(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keys := []string{}
|
||||
|
||||
for k := range s.Parameters {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
i := c.Info()
|
||||
|
||||
for _, k := range keys {
|
||||
i.Add(k, s.Parameters[k])
|
||||
}
|
||||
|
||||
return i.Print()
|
||||
}
|
||||
|
||||
func RackParamsSet(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts := structs.SystemUpdateOptions{
|
||||
Parameters: map[string]string{},
|
||||
}
|
||||
|
||||
for _, arg := range c.Args {
|
||||
parts := strings.SplitN(arg, "=", 2)
|
||||
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("Key=Value expected: %s", arg)
|
||||
}
|
||||
|
||||
opts.Parameters[parts[0]] = parts[1]
|
||||
}
|
||||
|
||||
c.Startf("Updating parameters")
|
||||
|
||||
if s.Version <= "20180708231844" {
|
||||
if err := rack.AppParametersSet(s.Name, opts.Parameters); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := rack.SystemUpdate(opts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.Bool("wait") {
|
||||
c.Writef("\n")
|
||||
|
||||
if err := common.WaitForRackWithLogs(rack, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
func RackPs(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var opts structs.SystemProcessesOptions
|
||||
|
||||
if err := c.Options(&opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ps, err := rack.SystemProcesses(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := c.Table("ID", "APP", "SERVICE", "STATUS", "RELEASE", "STARTED", "COMMAND")
|
||||
|
||||
for _, p := range ps {
|
||||
t.AddRow(p.Id, p.App, p.Name, p.Status, p.Release, common.Ago(p.Started), p.Command)
|
||||
}
|
||||
|
||||
return t.Print()
|
||||
}
|
||||
|
||||
func RackReleases(rack sdk.Interface, c *stdcli.Context) error {
|
||||
rs, err := rack.SystemReleases()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := c.Table("VERSION", "UPDATED")
|
||||
|
||||
for _, r := range rs {
|
||||
t.AddRow(r.Id, common.Ago(r.Created))
|
||||
}
|
||||
|
||||
return t.Print()
|
||||
}
|
||||
|
||||
func RackScale(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var opts structs.SystemUpdateOptions
|
||||
update := false
|
||||
|
||||
if v, ok := c.Value("count").(int); ok {
|
||||
opts.Count = options.Int(v)
|
||||
update = true
|
||||
}
|
||||
|
||||
if v, ok := c.Value("type").(string); ok {
|
||||
opts.Type = options.String(v)
|
||||
update = true
|
||||
}
|
||||
|
||||
if update {
|
||||
c.Startf("Scaling rack")
|
||||
|
||||
if err := rack.SystemUpdate(opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
i := c.Info()
|
||||
|
||||
i.Add("Autoscale", s.Parameters["Autoscale"])
|
||||
i.Add("Count", fmt.Sprintf("%d", s.Count))
|
||||
i.Add("Status", s.Status)
|
||||
i.Add("Type", s.Type)
|
||||
|
||||
return i.Print()
|
||||
}
|
||||
|
||||
func RackUpdate(rack sdk.Interface, c *stdcli.Context) error {
|
||||
target := c.Arg(0)
|
||||
|
||||
// if no version specified, find the next version
|
||||
if target == "" {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Version == "dev" {
|
||||
target = "dev"
|
||||
} else {
|
||||
v, err := cv.Next(s.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target = v
|
||||
}
|
||||
}
|
||||
|
||||
c.Startf("Updating to <release>%s</release>", target)
|
||||
|
||||
if err := rack.SystemUpdate(structs.SystemUpdateOptions{Version: options.String(target)}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("wait") {
|
||||
c.Writef("\n")
|
||||
|
||||
if err := common.WaitForRackWithLogs(rack, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
func RackWait(rack sdk.Interface, c *stdcli.Context) error {
|
||||
c.Startf("Waiting for rack")
|
||||
|
||||
c.Writef("\n")
|
||||
|
||||
if err := common.WaitForRackWithLogs(rack, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
492
pkg/cli/rack_test.go
Normal file
492
pkg/cli/rack_test.go
Normal file
@ -0,0 +1,492 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/provider"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRack(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
|
||||
res, err := testExecute(e, "rack", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Name name",
|
||||
"Provider provider",
|
||||
"Region region",
|
||||
"Router domain",
|
||||
"Status running",
|
||||
"Version 21000101000000",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackInternal(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemInternal(), nil)
|
||||
|
||||
res, err := testExecute(e, "rack", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Name name",
|
||||
"Provider provider",
|
||||
"Region region",
|
||||
"Router domain (external)",
|
||||
" domain-internal (internal)",
|
||||
"Status running",
|
||||
"Version 20180901000000",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackInstall(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/auth", r.URL.Path)
|
||||
user, pass, _ := r.BasicAuth()
|
||||
require.Equal(t, "convox", user)
|
||||
require.Equal(t, "password", pass)
|
||||
}))
|
||||
|
||||
tsu, err := url.Parse(ts.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
opts := structs.SystemInstallOptions{
|
||||
Name: options.String("foo"),
|
||||
Parameters: map[string]string{},
|
||||
Version: options.String("bar"),
|
||||
}
|
||||
provider.Mock.On("SystemInstall", mock.Anything, opts).Once().Return(fmt.Sprintf("https://convox:password@%s", tsu.Host), nil).Run(func(args mock.Arguments) {
|
||||
w := args.Get(0).(io.Writer)
|
||||
fmt.Fprintf(w, "line1\n")
|
||||
fmt.Fprintf(w, "line2\n")
|
||||
})
|
||||
|
||||
res, err := testExecute(e, "rack install test -n foo -v bar", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"line1",
|
||||
"line2",
|
||||
})
|
||||
|
||||
data, err := ioutil.ReadFile(filepath.Join(e.Settings, "auth"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fmt.Sprintf("{\n \"%s\": \"password\"\n}", tsu.Host), string(data))
|
||||
|
||||
data, err = ioutil.ReadFile(filepath.Join(e.Settings, "host"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tsu.Host, string(data))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackInstallError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.SystemInstallOptions{
|
||||
Name: options.String("foo"),
|
||||
Parameters: map[string]string{},
|
||||
Version: options.String("bar"),
|
||||
}
|
||||
provider.Mock.On("SystemInstall", mock.Anything, opts).Return("", fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack install test -n foo -v bar", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackLogs(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemLogs", structs.LogsOptions{Prefix: options.Bool(true)}).Return(testLogs(fxLogs()), nil)
|
||||
|
||||
res, err := testExecute(e, "rack logs", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
fxLogs()[0],
|
||||
fxLogs()[1],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackLogsError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemLogs", structs.LogsOptions{Prefix: options.Bool(true)}).Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack logs", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackParams(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
|
||||
res, err := testExecute(e, "rack params", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Autoscale Yes",
|
||||
"ParamFoo value1",
|
||||
"ParamOther value2",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackParamsError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack params", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackParamsSet(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
opts := structs.SystemUpdateOptions{
|
||||
Parameters: map[string]string{
|
||||
"Foo": "bar",
|
||||
"Baz": "qux",
|
||||
},
|
||||
}
|
||||
i.On("SystemUpdate", opts).Return(nil)
|
||||
|
||||
res, err := testExecute(e, "rack params set Foo=bar Baz=qux", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Updating parameters... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackParamsSetError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
opts := structs.SystemUpdateOptions{
|
||||
Parameters: map[string]string{
|
||||
"Foo": "bar",
|
||||
"Baz": "qux",
|
||||
},
|
||||
}
|
||||
i.On("SystemUpdate", opts).Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack params set Foo=bar Baz=qux", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Updating parameters... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackParamsSetClassic(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
i.On("AppParametersSet", "name", map[string]string{"Foo": "bar", "Baz": "qux"}).Return(nil)
|
||||
|
||||
res, err := testExecute(e, "rack params set Foo=bar Baz=qux", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Updating parameters... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackParamsSetClassicError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
i.On("AppParametersSet", "name", map[string]string{"Foo": "bar", "Baz": "qux"}).Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack params set Foo=bar Baz=qux", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Updating parameters... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackPs(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemProcesses", structs.SystemProcessesOptions{}).Return(structs.Processes{*fxProcess(), *fxProcessPending()}, nil)
|
||||
|
||||
res, err := testExecute(e, "rack ps", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"ID APP SERVICE STATUS RELEASE STARTED COMMAND",
|
||||
"pid1 app1 name running release1 2 days ago command",
|
||||
"pid1 app1 name pending release1 2 days ago command",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackPsError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemProcesses", structs.SystemProcessesOptions{}).Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack ps", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackPsAll(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemProcesses", structs.SystemProcessesOptions{All: options.Bool(true)}).Return(structs.Processes{*fxProcess(), *fxProcessPending()}, nil)
|
||||
|
||||
res, err := testExecute(e, "rack ps -a", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"ID APP SERVICE STATUS RELEASE STARTED COMMAND",
|
||||
"pid1 app1 name running release1 2 days ago command",
|
||||
"pid1 app1 name pending release1 2 days ago command",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackReleases(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemReleases").Return(structs.Releases{*fxRelease(), *fxRelease()}, nil)
|
||||
|
||||
res, err := testExecute(e, "rack releases", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"VERSION UPDATED ",
|
||||
"release1 2 days ago",
|
||||
"release1 2 days ago",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackReleasesError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemReleases").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack releases", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackScale(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
|
||||
res, err := testExecute(e, "rack scale", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Autoscale Yes",
|
||||
"Count 1",
|
||||
"Status running",
|
||||
"Type type",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackScaleError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack scale", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackScaleUpdate(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("SystemUpdate", structs.SystemUpdateOptions{Count: options.Int(5), Type: options.String("type1")}).Return(nil)
|
||||
|
||||
res, err := testExecute(e, "rack scale -c 5 -t type1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Scaling rack... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackScaleUpdateError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("SystemUpdate", structs.SystemUpdateOptions{Count: options.Int(5), Type: options.String("type1")}).Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack scale -c 5 -t type1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Scaling rack... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackUninstall(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.SystemUninstallOptions{
|
||||
Force: options.Bool(true),
|
||||
}
|
||||
provider.Mock.On("SystemUninstall", "foo", mock.Anything, opts).Once().Return(nil).Run(func(args mock.Arguments) {
|
||||
w := args.Get(1).(io.Writer)
|
||||
fmt.Fprintf(w, "line1\n")
|
||||
fmt.Fprintf(w, "line2\n")
|
||||
})
|
||||
|
||||
res, err := testExecute(e, "rack uninstall test foo --force", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"line1",
|
||||
"line2",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackUninstallError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.SystemUninstallOptions{
|
||||
Force: options.Bool(true),
|
||||
}
|
||||
provider.Mock.On("SystemUninstall", "foo", mock.Anything, opts).Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack uninstall test foo --force", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackUninstallWithoutForce(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
res, err := testExecute(e, "rack uninstall test foo", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: must use --force for non-interactive uninstall"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackUpdate(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemUpdate", structs.SystemUpdateOptions{Version: options.String("version1")}).Return(nil)
|
||||
|
||||
res, err := testExecute(e, "rack update version1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Updating to version1... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackUpdateError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemUpdate", structs.SystemUpdateOptions{Version: options.String("version1")}).Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack update version1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Updating to version1... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackWait(t *testing.T) {
|
||||
testClientWait(t, 100*time.Millisecond, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.LogsOptions{
|
||||
Prefix: options.Bool(true),
|
||||
Since: options.Duration(5 * time.Second),
|
||||
}
|
||||
i.On("SystemLogs", opts).Return(testLogs(fxLogsSystem()), nil).Once()
|
||||
i.On("SystemGet").Return(&structs.System{Status: "updating"}, nil).Twice()
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
|
||||
res, err := testExecute(e, "rack wait", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Waiting for rack... ",
|
||||
fxLogsSystem()[0],
|
||||
fxLogsSystem()[1],
|
||||
"OK",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackWaitError(t *testing.T) {
|
||||
testClientWait(t, 100*time.Millisecond, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
opts := structs.LogsOptions{
|
||||
Prefix: options.Bool(true),
|
||||
Since: options.Duration(5 * time.Second),
|
||||
}
|
||||
i.On("SystemLogs", opts).Return(testLogs(fxLogsSystem()), nil).Once()
|
||||
i.On("SystemGet").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack wait", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{
|
||||
"Waiting for rack... ",
|
||||
fxLogsSystem()[0],
|
||||
fxLogsSystem()[1],
|
||||
})
|
||||
})
|
||||
}
|
||||
27
pkg/cli/racks.go
Normal file
27
pkg/cli/racks.go
Normal file
@ -0,0 +1,27 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("racks", "list available racks", Racks, stdcli.CommandOptions{
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
}
|
||||
|
||||
func Racks(rack sdk.Interface, c *stdcli.Context) error {
|
||||
rs, err := racks(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := c.Table("NAME", "STATUS")
|
||||
|
||||
for _, r := range rs {
|
||||
t.AddRow(r.Name, r.Status)
|
||||
}
|
||||
|
||||
return t.Print()
|
||||
}
|
||||
127
pkg/cli/racks_test.go
Normal file
127
pkg/cli/racks_test.go
Normal file
@ -0,0 +1,127 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
mockstdcli "github.com/convox/convox/pkg/mock/stdcli"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRacks(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
r := mux.NewRouter()
|
||||
|
||||
r.HandleFunc("/racks", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`[
|
||||
{"name":"foo","organization":{"name":"test"},"status":"running"},
|
||||
{"name":"other","organization":{"name":"test"},"status":"updating"}
|
||||
]`))
|
||||
}).Methods("GET")
|
||||
|
||||
ts := httptest.NewTLSServer(r)
|
||||
|
||||
tsu, err := url.Parse(ts.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(e.Settings, "host"), []byte(tsu.Host), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
me := &mockstdcli.Executor{}
|
||||
me.On("Execute", "kubectl", "get", "ns", "--selector=system=convox,type=rack", "--output=name").Return([]byte("namespace/dev\n"), nil)
|
||||
e.Executor = me
|
||||
|
||||
res, err := testExecute(e, "racks", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"NAME STATUS ",
|
||||
"local/dev running ",
|
||||
"test/foo running ",
|
||||
"test/other updating",
|
||||
})
|
||||
|
||||
me.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRacksLocalDisable(t *testing.T) {
|
||||
orig := os.Getenv("CONVOX_LOCAL")
|
||||
os.Setenv("CONVOX_LOCAL", "disable")
|
||||
defer os.Setenv("CONVOX_LOCAL", orig)
|
||||
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
r := mux.NewRouter()
|
||||
|
||||
r.HandleFunc("/racks", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`[
|
||||
{"name":"foo","organization":{"name":"test"},"status":"running"},
|
||||
{"name":"other","organization":{"name":"test"},"status":"updating"}
|
||||
]`))
|
||||
}).Methods("GET")
|
||||
|
||||
ts := httptest.NewTLSServer(r)
|
||||
|
||||
tsu, err := url.Parse(ts.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(e.Settings, "host"), []byte(tsu.Host), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
me := &mockstdcli.Executor{}
|
||||
e.Executor = me
|
||||
|
||||
res, err := testExecute(e, "racks", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"NAME STATUS ",
|
||||
"test/foo running ",
|
||||
"test/other updating",
|
||||
})
|
||||
|
||||
me.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRacksError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
r := mux.NewRouter()
|
||||
|
||||
r.HandleFunc("/racks", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte("test"))
|
||||
}).Methods("GET")
|
||||
|
||||
ts := httptest.NewTLSServer(r)
|
||||
|
||||
tsu, err := url.Parse(ts.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(e.Settings, "host"), []byte(tsu.Host), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
me := &mockstdcli.Executor{}
|
||||
me.On("Execute", "kubectl", "get", "ns", "--selector=system=convox,type=rack", "--output=name").Return(nil, fmt.Errorf("err1"))
|
||||
e.Executor = me
|
||||
|
||||
res, err := testExecute(e, "racks", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"NAME STATUS",
|
||||
})
|
||||
})
|
||||
}
|
||||
70
pkg/cli/registries.go
Normal file
70
pkg/cli/registries.go
Normal file
@ -0,0 +1,70 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("registries", "list private registries", Registries, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("registries add", "add a private registry", RegistriesAdd, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Usage: "<server> <username> <password>",
|
||||
Validate: stdcli.Args(3),
|
||||
})
|
||||
|
||||
register("registries remove", "remove private registry", RegistriesRemove, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
}
|
||||
|
||||
func Registries(rack sdk.Interface, c *stdcli.Context) error {
|
||||
rs, err := rack.RegistryList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := c.Table("SERVER", "USERNAME")
|
||||
|
||||
for _, r := range rs {
|
||||
t.AddRow(r.Server, r.Username)
|
||||
}
|
||||
|
||||
return t.Print()
|
||||
}
|
||||
|
||||
func RegistriesAdd(rack sdk.Interface, c *stdcli.Context) error {
|
||||
c.Startf("Adding registry")
|
||||
|
||||
if _, err := rack.RegistryAdd(c.Arg(0), c.Arg(1), c.Arg(2)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
func RegistriesRemove(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Startf("Removing registry")
|
||||
|
||||
if s.Version <= "20180708231844" {
|
||||
if err := rack.RegistryRemoveClassic(c.Arg(0)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := rack.RegistryRemove(c.Arg(0)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
102
pkg/cli/registries_test.go
Normal file
102
pkg/cli/registries_test.go
Normal file
@ -0,0 +1,102 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRegistries(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("RegistryList").Return(structs.Registries{*fxRegistry(), *fxRegistry()}, nil)
|
||||
|
||||
res, err := testExecute(e, "registries", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"SERVER USERNAME",
|
||||
"registry1 username",
|
||||
"registry1 username",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRegistriesError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("RegistryList").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "registries", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRegistriesAdd(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("RegistryAdd", "foo", "bar", "baz").Return(fxRegistry(), nil)
|
||||
|
||||
res, err := testExecute(e, "registries add foo bar baz", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Adding registry... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRegistriesAddError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("RegistryAdd", "foo", "bar", "baz").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "registries add foo bar baz", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Adding registry... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRegistriesRemove(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("RegistryRemove", "foo").Return(nil)
|
||||
|
||||
res, err := testExecute(e, "registries remove foo", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Removing registry... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRegistriesRemoveError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("RegistryRemove", "foo").Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "registries remove foo", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Removing registry... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRegistriesRemoveClassic(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
i.On("RegistryRemoveClassic", "foo").Return(nil)
|
||||
|
||||
res, err := testExecute(e, "registries remove foo", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Removing registry... OK"})
|
||||
})
|
||||
}
|
||||
234
pkg/cli/releases.go
Normal file
234
pkg/cli/releases.go
Normal file
@ -0,0 +1,234 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/convox/convox/pkg/common"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("releases", "list releases for an app", Releases, stdcli.CommandOptions{
|
||||
Flags: append(stdcli.OptionFlags(structs.ReleaseListOptions{}), flagRack, flagApp),
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("releases info", "get information about a release", ReleasesInfo, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagApp, flagRack},
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("releases manifest", "get manifest for a release", ReleasesManifest, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagApp, flagRack},
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("releases promote", "promote a release", ReleasesPromote, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagApp, flagRack, flagWait},
|
||||
Validate: stdcli.ArgsMax(1),
|
||||
})
|
||||
|
||||
register("releases rollback", "copy an old release forward and promote it", ReleasesRollback, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagApp, flagId, flagRack, flagWait},
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
}
|
||||
|
||||
func Releases(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var opts structs.ReleaseListOptions
|
||||
|
||||
if err := c.Options(&opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a, err := rack.AppGet(app(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rs, err := rack.ReleaseList(app(c), opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := c.Table("ID", "STATUS", "BUILD", "CREATED", "DESCRIPTION")
|
||||
|
||||
for _, r := range rs {
|
||||
status := ""
|
||||
|
||||
if a.Release == r.Id {
|
||||
status = "active"
|
||||
}
|
||||
|
||||
t.AddRow(r.Id, status, r.Build, common.Ago(r.Created), r.Description)
|
||||
}
|
||||
|
||||
return t.Print()
|
||||
}
|
||||
|
||||
func ReleasesInfo(rack sdk.Interface, c *stdcli.Context) error {
|
||||
r, err := rack.ReleaseGet(app(c), c.Arg(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i := c.Info()
|
||||
|
||||
i.Add("Id", r.Id)
|
||||
i.Add("Build", r.Build)
|
||||
i.Add("Created", r.Created.Format(time.RFC3339))
|
||||
i.Add("Description", r.Description)
|
||||
i.Add("Env", r.Env)
|
||||
|
||||
return i.Print()
|
||||
}
|
||||
|
||||
func ReleasesManifest(rack sdk.Interface, c *stdcli.Context) error {
|
||||
release := c.Arg(0)
|
||||
|
||||
r, err := rack.ReleaseGet(app(c), release)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.Build == "" {
|
||||
return fmt.Errorf("no build for release: %s", release)
|
||||
}
|
||||
|
||||
b, err := rack.BuildGet(app(c), r.Build)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(c, "%s\n", strings.TrimSpace(b.Manifest))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReleasesPromote(rack sdk.Interface, c *stdcli.Context) error {
|
||||
release := c.Arg(0)
|
||||
|
||||
if release == "" {
|
||||
rs, err := rack.ReleaseList(app(c), structs.ReleaseListOptions{Limit: options.Int(1)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(rs) == 0 {
|
||||
return fmt.Errorf("no releases to promote")
|
||||
}
|
||||
|
||||
release = rs[0].Id
|
||||
}
|
||||
|
||||
return releasePromote(rack, c, app(c), release)
|
||||
}
|
||||
|
||||
func releasePromote(rack sdk.Interface, c *stdcli.Context, app, id string) error {
|
||||
if id == "" {
|
||||
return fmt.Errorf("no release to promote")
|
||||
}
|
||||
|
||||
a, err := rack.AppGet(app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a.Status != "running" {
|
||||
c.Startf("Waiting for app to be ready")
|
||||
|
||||
if err := common.WaitForAppRunning(rack, app); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.OK()
|
||||
}
|
||||
|
||||
c.Startf("Promoting <release>%s</release>", id)
|
||||
|
||||
if err := rack.ReleasePromote(app, id, structs.ReleasePromoteOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("wait") {
|
||||
c.Writef("\n")
|
||||
|
||||
if err := common.WaitForAppWithLogs(rack, c, app); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a, err = rack.AppGet(app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a.Release != id {
|
||||
return fmt.Errorf("rollback")
|
||||
}
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
func ReleasesRollback(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var stdout io.Writer
|
||||
|
||||
if c.Bool("id") {
|
||||
stdout = c.Writer().Stdout
|
||||
c.Writer().Stdout = c.Writer().Stderr
|
||||
}
|
||||
|
||||
release := c.Arg(0)
|
||||
|
||||
c.Startf("Rolling back to <release>%s</release>", release)
|
||||
|
||||
ro, err := rack.ReleaseGet(app(c), release)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rn, err := rack.ReleaseCreate(app(c), structs.ReleaseCreateOptions{
|
||||
Build: options.String(ro.Build),
|
||||
Env: options.String(ro.Env),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.OK(rn.Id)
|
||||
|
||||
c.Startf("Promoting <release>%s</release>", rn.Id)
|
||||
|
||||
if err := rack.ReleasePromote(app(c), rn.Id, structs.ReleasePromoteOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("wait") {
|
||||
c.Writef("\n")
|
||||
|
||||
if err := common.WaitForAppWithLogs(rack, c, app(c)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a, err := rack.AppGet(app(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a.Release != rn.Id {
|
||||
return fmt.Errorf("rollback")
|
||||
}
|
||||
}
|
||||
|
||||
if c.Bool("id") {
|
||||
fmt.Fprintf(stdout, rn.Id)
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
192
pkg/cli/releases_test.go
Normal file
192
pkg/cli/releases_test.go
Normal file
@ -0,0 +1,192 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReleases(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil)
|
||||
i.On("ReleaseList", "app1", structs.ReleaseListOptions{}).Return(structs.Releases{*fxRelease(), *fxRelease2()}, nil)
|
||||
|
||||
res, err := testExecute(e, "releases -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"ID STATUS BUILD CREATED DESCRIPTION ",
|
||||
"release1 active build1 2 days ago description1",
|
||||
"release2 build1 2 days ago ",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestReleasesError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil)
|
||||
i.On("ReleaseList", "app1", structs.ReleaseListOptions{}).Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "releases -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestReleasesInfo(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("ReleaseGet", "app1", "release1").Return(fxRelease(), nil)
|
||||
|
||||
res, err := testExecute(e, "releases info release1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Id release1",
|
||||
"Build build1",
|
||||
fmt.Sprintf("Created %s", fxRelease().Created.Format(time.RFC3339)),
|
||||
"Description description1",
|
||||
"Env FOO=bar",
|
||||
" BAZ=quux",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestReleasesInfoError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("ReleaseGet", "app1", "release1").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "releases info release1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestReleasesManifest(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("ReleaseGet", "app1", "release1").Return(fxRelease(), nil)
|
||||
i.On("BuildGet", "app1", "build1").Return(fxBuild(), nil)
|
||||
|
||||
res, err := testExecute(e, "releases manifest release1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"manifest1",
|
||||
"manifest2",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestReleasesManifestError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("ReleaseGet", "app1", "release1").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "releases manifest release1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestReleasesPromote(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil)
|
||||
i.On("ReleasePromote", "app1", "release1", structs.ReleasePromoteOptions{}).Return(nil)
|
||||
|
||||
res, err := testExecute(e, "releases promote release1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Promoting release1... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestReleasesPromoteError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil)
|
||||
i.On("ReleasePromote", "app1", "release1", structs.ReleasePromoteOptions{}).Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "releases promote release1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Promoting release1... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestReleasesPromoteAlreadyUpdating(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppGet", "app1").Return(fxAppUpdating(), nil).Twice()
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil)
|
||||
i.On("ReleasePromote", "app1", "release1", structs.ReleasePromoteOptions{}).Return(nil)
|
||||
|
||||
res, err := testExecute(e, "releases promote release1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Waiting for app to be ready... OK",
|
||||
"Promoting release1... OK",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestReleasesRollback(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("ReleaseGet", "app1", "release2").Return(fxRelease2(), nil)
|
||||
i.On("ReleaseCreate", "app1", structs.ReleaseCreateOptions{Build: options.String(fxRelease2().Build), Env: options.String(fxRelease2().Env)}).Return(fxRelease3(), nil)
|
||||
i.On("ReleasePromote", "app1", "release3", structs.ReleasePromoteOptions{}).Return(nil)
|
||||
|
||||
res, err := testExecute(e, "releases rollback release2 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Rolling back to release2... OK, release3",
|
||||
"Promoting release3... OK",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestReleasesRollbackErrorCreate(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("ReleaseGet", "app1", "release2").Return(fxRelease2(), nil)
|
||||
i.On("ReleaseCreate", "app1", structs.ReleaseCreateOptions{Build: options.String(fxRelease2().Build), Env: options.String(fxRelease2().Env)}).Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "releases rollback release2 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Rolling back to release2... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestReleasesRollbackErrorPromote(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("ReleaseGet", "app1", "release2").Return(fxRelease2(), nil)
|
||||
i.On("ReleaseCreate", "app1", structs.ReleaseCreateOptions{Build: options.String(fxRelease2().Build), Env: options.String(fxRelease2().Env)}).Return(fxRelease3(), nil)
|
||||
i.On("ReleasePromote", "app1", "release3", structs.ReleasePromoteOptions{}).Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "releases rollback release2 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{
|
||||
"Rolling back to release2... OK, release3",
|
||||
"Promoting release3... ",
|
||||
})
|
||||
})
|
||||
}
|
||||
770
pkg/cli/resources.go
Normal file
770
pkg/cli/resources.go
Normal file
@ -0,0 +1,770 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("resources", "list resources", Resources, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack, flagApp},
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("resources console", "start a console for a resource", ResourcesConsole, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack, flagApp},
|
||||
Usage: "<resource>",
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("resources export", "export data from a resource", ResourcesExport, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{
|
||||
flagRack,
|
||||
flagApp,
|
||||
stdcli.StringFlag("file", "f", "export to file"),
|
||||
},
|
||||
Usage: "<resource>",
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("resources import", "import data to a resource", ResourcesImport, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{
|
||||
flagRack,
|
||||
flagApp,
|
||||
stdcli.StringFlag("file", "f", "import from a file"),
|
||||
},
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("resources info", "get information about a resource", ResourcesInfo, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack, flagApp},
|
||||
Usage: "<resource>",
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("resources proxy", "proxy a local port to a resource", ResourcesProxy, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{
|
||||
flagRack,
|
||||
flagApp,
|
||||
stdcli.IntFlag("port", "p", "local port"),
|
||||
stdcli.BoolFlag("tls", "t", "wrap connection in tls"),
|
||||
},
|
||||
Usage: "<resource>",
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("resources url", "get url for a resource", ResourcesUrl, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack, flagApp},
|
||||
Usage: "<resource>",
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("rack resources", "list resources", RackResources, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Invisible: true,
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("rack resources create", "create a resource", RackResourcesCreate, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{
|
||||
flagRack,
|
||||
flagWait,
|
||||
stdcli.StringFlag("name", "n", "resource name"),
|
||||
},
|
||||
Invisible: true,
|
||||
Usage: "<type> [Option=Value]...",
|
||||
Validate: stdcli.ArgsMin(1),
|
||||
})
|
||||
|
||||
register("rack resources delete", "delete a resource", RackResourcesDelete, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack, flagWait},
|
||||
Invisible: true,
|
||||
Usage: "<name>",
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("rack resources info", "get information about a resource", RackResourcesInfo, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Invisible: true,
|
||||
Usage: "<resource>",
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("rack resources link", "link a resource to an app", RackResourcesLink, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagApp, flagRack, flagWait},
|
||||
Invisible: true,
|
||||
Usage: "<resource>",
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("rack resources options", "list options for a resource type", RackResourcesOptions, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Invisible: true,
|
||||
Usage: "<resource>",
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("rack resources proxy", "proxy a local port to a rack resource", RackResourcesProxy, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{
|
||||
flagRack,
|
||||
stdcli.IntFlag("port", "p", "local port"),
|
||||
stdcli.BoolFlag("tls", "t", "wrap connection in tls"),
|
||||
},
|
||||
Invisible: true,
|
||||
Usage: "<resource>",
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("rack resources types", "list resource types", RackResourcesTypes, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Invisible: true,
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("rack resources update", "update resource options", RackResourcesUpdate, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack, flagWait},
|
||||
Invisible: true,
|
||||
Usage: "<name> [Option=Value]...",
|
||||
Validate: stdcli.ArgsMin(1),
|
||||
})
|
||||
|
||||
register("rack resources unlink", "unlink a resource from an app", RackResourcesUnlink, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagApp, flagRack, flagWait},
|
||||
Invisible: true,
|
||||
Usage: "<resource>",
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
|
||||
register("rack resources url", "get url for a resource", RackResourcesUrl, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Invisible: true,
|
||||
Usage: "<resource>",
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
}
|
||||
|
||||
func Resources(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Version <= "20190111211123" {
|
||||
return fmt.Errorf("command unavailable, please upgrade this rack")
|
||||
}
|
||||
|
||||
rs, err := rack.ResourceList(app(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := c.Table("NAME", "TYPE", "URL")
|
||||
|
||||
for _, r := range rs {
|
||||
t.AddRow(r.Name, r.Type, r.Url)
|
||||
}
|
||||
|
||||
return t.Print()
|
||||
}
|
||||
|
||||
func ResourcesConsole(rack sdk.Interface, c *stdcli.Context) error {
|
||||
opts := structs.ResourceConsoleOptions{}
|
||||
|
||||
if w, h, err := c.TerminalSize(); err == nil {
|
||||
opts.Height = options.Int(h)
|
||||
opts.Width = options.Int(w)
|
||||
}
|
||||
|
||||
fmt.Printf("opts: %+v\n", opts)
|
||||
|
||||
restore := c.TerminalRaw()
|
||||
defer restore()
|
||||
|
||||
if err := rack.ResourceConsole(app(c), c.Arg(0), c, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ResourcesExport(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var w io.Writer
|
||||
|
||||
if file := c.String("file"); file != "" {
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
w = f
|
||||
} else {
|
||||
w = c.Writer().Stdout
|
||||
c.Writer().Stdout = c.Writer().Stderr
|
||||
}
|
||||
|
||||
r, err := rack.ResourceExport(app(c), c.Arg(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(w, r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ResourcesImport(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var r io.Reader
|
||||
|
||||
if file := c.String("file"); file != "" {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
r = f
|
||||
} else {
|
||||
r = c.Reader()
|
||||
}
|
||||
|
||||
c.Startf("Importing data")
|
||||
|
||||
if err := rack.ResourceImport(app(c), c.Arg(0), r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
func ResourcesInfo(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Version <= "20190111211123" {
|
||||
return fmt.Errorf("command unavailable, please upgrade this rack")
|
||||
}
|
||||
|
||||
r, err := rack.ResourceGet(app(c), c.Arg(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i := c.Info()
|
||||
|
||||
i.Add("Name", r.Name)
|
||||
i.Add("Type", r.Type)
|
||||
|
||||
if r.Url != "" {
|
||||
i.Add("URL", r.Url)
|
||||
}
|
||||
|
||||
return i.Print()
|
||||
}
|
||||
|
||||
func ResourcesProxy(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Version <= "20190111211123" {
|
||||
return fmt.Errorf("command unavailable, please upgrade this rack")
|
||||
}
|
||||
|
||||
r, err := rack.ResourceGet(app(c), c.Arg(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.Url == "" {
|
||||
return fmt.Errorf("no url for resource: %s", r.Name)
|
||||
}
|
||||
|
||||
u, err := url.Parse(r.Url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
remotehost := u.Hostname()
|
||||
remoteport := u.Port()
|
||||
|
||||
if remoteport == "" {
|
||||
switch u.Scheme {
|
||||
case "http":
|
||||
remoteport = "80"
|
||||
case "https":
|
||||
remoteport = "443"
|
||||
default:
|
||||
return fmt.Errorf("unknown port for url: %s", r.Url)
|
||||
}
|
||||
}
|
||||
|
||||
rpi, err := strconv.Atoi(remoteport)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
port := rpi
|
||||
|
||||
if p := c.Int("port"); p != 0 {
|
||||
port = p
|
||||
}
|
||||
|
||||
go proxy(rack, c, port, remotehost, rpi, c.Bool("tls"))
|
||||
|
||||
<-c.Done()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ResourcesUrl(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Version <= "20190111211123" {
|
||||
return fmt.Errorf("command unavailable, please upgrade this rack")
|
||||
}
|
||||
|
||||
r, err := rack.ResourceGet(app(c), c.Arg(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.Url == "" {
|
||||
return fmt.Errorf("no url for resource: %s", r.Name)
|
||||
}
|
||||
|
||||
fmt.Fprintf(c, "%s\n", r.Url)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RackResources(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var rs structs.Resources
|
||||
|
||||
if s.Version <= "20190111211123" {
|
||||
rs, err = rack.SystemResourceListClassic()
|
||||
} else {
|
||||
rs, err = rack.SystemResourceList()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := c.Table("NAME", "TYPE", "STATUS")
|
||||
|
||||
for _, r := range rs {
|
||||
t.AddRow(r.Name, r.Type, r.Status)
|
||||
}
|
||||
|
||||
return t.Print()
|
||||
}
|
||||
|
||||
func RackResourcesCreate(rack sdk.Interface, c *stdcli.Context) error {
|
||||
var opts structs.ResourceCreateOptions
|
||||
|
||||
if err := c.Options(&opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v := c.String("name"); v != "" {
|
||||
opts.Name = options.String(v)
|
||||
}
|
||||
|
||||
opts.Parameters = map[string]string{}
|
||||
|
||||
for _, arg := range c.Args[1:] {
|
||||
parts := strings.SplitN(arg, "=", 2)
|
||||
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("Name=Value expected: %s", arg)
|
||||
}
|
||||
|
||||
opts.Parameters[parts[0]] = parts[1]
|
||||
}
|
||||
|
||||
c.Startf("Creating resource")
|
||||
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var r *structs.Resource
|
||||
|
||||
if s.Version <= "20180708231844" {
|
||||
r, err = rack.ResourceCreateClassic(c.Arg(0), opts)
|
||||
} else if s.Version <= "20190111211123" {
|
||||
r, err = rack.SystemResourceCreateClassic(c.Arg(0), opts)
|
||||
} else {
|
||||
r, err = rack.SystemResourceCreate(c.Arg(0), opts)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("wait") {
|
||||
if err := waitForResourceRunning(rack, c, r.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.OK(r.Name)
|
||||
}
|
||||
|
||||
func RackResourcesDelete(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Startf("Deleting resource")
|
||||
|
||||
if s.Version <= "20190111211123" {
|
||||
err = rack.SystemResourceDeleteClassic(c.Arg(0))
|
||||
} else {
|
||||
err = rack.SystemResourceDelete(c.Arg(0))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("wait") {
|
||||
if err := waitForResourceDeleted(rack, c, c.Arg(0)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
func RackResourcesInfo(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var r *structs.Resource
|
||||
|
||||
if s.Version <= "20190111211123" {
|
||||
r, err = rack.SystemResourceGetClassic(c.Arg(0))
|
||||
} else {
|
||||
r, err = rack.SystemResourceGet(c.Arg(0))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// fmt.Printf("r = %+v\n", r)
|
||||
|
||||
i := c.Info()
|
||||
|
||||
apps := []string{}
|
||||
|
||||
for _, a := range r.Apps {
|
||||
apps = append(apps, a.Name)
|
||||
}
|
||||
|
||||
sort.Strings(apps)
|
||||
|
||||
options := []string{}
|
||||
|
||||
for k, v := range r.Parameters {
|
||||
options = append(options, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
|
||||
sort.Strings(options)
|
||||
|
||||
i.Add("Name", r.Name)
|
||||
i.Add("Type", r.Type)
|
||||
i.Add("Status", r.Status)
|
||||
i.Add("Options", strings.Join(options, "\n"))
|
||||
|
||||
if r.Url != "" {
|
||||
i.Add("URL", r.Url)
|
||||
}
|
||||
|
||||
if len(apps) > 0 {
|
||||
i.Add("Apps", strings.Join(apps, ", "))
|
||||
}
|
||||
|
||||
return i.Print()
|
||||
}
|
||||
|
||||
func RackResourcesLink(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Startf("Linking to <app>%s</app>", app(c))
|
||||
|
||||
resource := c.Arg(0)
|
||||
|
||||
if s.Version <= "20190111211123" {
|
||||
_, err = rack.SystemResourceLinkClassic(resource, app(c))
|
||||
} else {
|
||||
_, err = rack.SystemResourceLink(resource, app(c))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("wait") {
|
||||
if err := waitForResourceRunning(rack, c, resource); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
func RackResourcesOptions(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var rts structs.ResourceTypes
|
||||
|
||||
if s.Version <= "20190111211123" {
|
||||
rts, err = rack.SystemResourceTypesClassic()
|
||||
} else {
|
||||
rts, err = rack.SystemResourceTypes()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var rt *structs.ResourceType
|
||||
|
||||
for _, t := range rts {
|
||||
if t.Name == c.Arg(0) {
|
||||
rt = &t
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if rt == nil {
|
||||
return fmt.Errorf("no such resource type: %s", c.Arg(0))
|
||||
}
|
||||
|
||||
t := c.Table("NAME", "DEFAULT", "DESCRIPTION")
|
||||
|
||||
sort.Slice(rt.Parameters, rt.Parameters.Less)
|
||||
|
||||
for _, p := range rt.Parameters {
|
||||
t.AddRow(p.Name, p.Default, p.Description)
|
||||
}
|
||||
|
||||
return t.Print()
|
||||
}
|
||||
|
||||
func RackResourcesProxy(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var r *structs.Resource
|
||||
|
||||
if s.Version <= "20190111211123" {
|
||||
r, err = rack.SystemResourceGetClassic(c.Arg(0))
|
||||
} else {
|
||||
r, err = rack.SystemResourceGet(c.Arg(0))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.Url == "" {
|
||||
return fmt.Errorf("no url for resource: %s", r.Name)
|
||||
}
|
||||
|
||||
u, err := url.Parse(r.Url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
remotehost := u.Hostname()
|
||||
remoteport := u.Port()
|
||||
|
||||
if remoteport == "" {
|
||||
switch u.Scheme {
|
||||
case "http":
|
||||
remoteport = "80"
|
||||
case "https":
|
||||
remoteport = "443"
|
||||
default:
|
||||
return fmt.Errorf("unknown port for url: %s", r.Url)
|
||||
}
|
||||
}
|
||||
|
||||
rpi, err := strconv.Atoi(remoteport)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
port := rpi
|
||||
|
||||
if p := c.Int("port"); p != 0 {
|
||||
port = p
|
||||
}
|
||||
|
||||
go proxy(rack, c, port, remotehost, rpi, c.Bool("tls"))
|
||||
|
||||
<-c.Done()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RackResourcesTypes(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var rts structs.ResourceTypes
|
||||
|
||||
if s.Version <= "20190111211123" {
|
||||
rts, err = rack.SystemResourceTypesClassic()
|
||||
} else {
|
||||
rts, err = rack.SystemResourceTypes()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := c.Table("TYPE")
|
||||
|
||||
for _, rt := range rts {
|
||||
t.AddRow(rt.Name)
|
||||
}
|
||||
|
||||
return t.Print()
|
||||
}
|
||||
|
||||
func RackResourcesUnlink(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Startf("Unlinking from <app>%s</app>", app(c))
|
||||
|
||||
resource := c.Arg(0)
|
||||
|
||||
if s.Version <= "20190111211123" {
|
||||
_, err = rack.SystemResourceUnlinkClassic(resource, app(c))
|
||||
} else {
|
||||
_, err = rack.SystemResourceUnlink(resource, app(c))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("wait") {
|
||||
if err := waitForResourceRunning(rack, c, resource); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
func RackResourcesUpdate(rack sdk.Interface, c *stdcli.Context) error {
|
||||
opts := structs.ResourceUpdateOptions{
|
||||
Parameters: map[string]string{},
|
||||
}
|
||||
|
||||
for _, arg := range c.Args[1:] {
|
||||
parts := strings.SplitN(arg, "=", 2)
|
||||
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("Key=Value expected: %s", arg)
|
||||
}
|
||||
|
||||
opts.Parameters[parts[0]] = parts[1]
|
||||
}
|
||||
|
||||
c.Startf("Updating resource")
|
||||
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resource := c.Arg(0)
|
||||
|
||||
if s.Version <= "20180708231844" {
|
||||
_, err = rack.ResourceUpdateClassic(resource, opts)
|
||||
} else if s.Version <= "20190111211123" {
|
||||
_, err = rack.SystemResourceUpdateClassic(resource, opts)
|
||||
} else {
|
||||
_, err = rack.SystemResourceUpdate(resource, opts)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("wait") {
|
||||
if err := waitForResourceRunning(rack, c, resource); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
func RackResourcesUrl(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var r *structs.Resource
|
||||
|
||||
if s.Version <= "20190111211123" {
|
||||
r, err = rack.SystemResourceGetClassic(c.Arg(0))
|
||||
} else {
|
||||
r, err = rack.SystemResourceGet(c.Arg(0))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Version <= "20180708231844" {
|
||||
if u := r.Parameters["Url"]; u != "" {
|
||||
fmt.Fprintf(c, "%s\n", u)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if r.Url == "" {
|
||||
return fmt.Errorf("no url for resource: %s", r.Name)
|
||||
}
|
||||
|
||||
fmt.Fprintf(c, "%s\n", r.Url)
|
||||
|
||||
return nil
|
||||
}
|
||||
541
pkg/cli/resources_test.go
Normal file
541
pkg/cli/resources_test.go
Normal file
@ -0,0 +1,541 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResources(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ResourceList", "app1").Return(structs.Resources{*fxResource(), *fxResource()}, nil)
|
||||
|
||||
res, err := testExecute(e, "resources -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"NAME TYPE URL ",
|
||||
"resource1 type https://example.org/path",
|
||||
"resource1 type https://example.org/path",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestResourcesError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ResourceList", "app1").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "resources -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestResourcesInfo(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ResourceGet", "app1", "resource1").Return(fxResource(), nil)
|
||||
|
||||
res, err := testExecute(e, "resources info resource1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Name resource1",
|
||||
"Type type",
|
||||
"URL https://example.org/path",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestResourcesInfoError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ResourceGet", "app1", "resource1").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "resources info resource1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestResourcesProxy(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
i.On("WithContext", ctx).Return(i)
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ResourceGet", "app1", "resource1").Return(fxResource(), nil)
|
||||
i.On("Proxy", "example.org", 443, mock.Anything, structs.ProxyOptions{TLS: options.Bool(false)}).Return(nil).Run(func(args mock.Arguments) {
|
||||
buf := make([]byte, 2)
|
||||
rwc := args.Get(2).(io.ReadWriteCloser)
|
||||
n, err := rwc.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, n)
|
||||
require.Equal(t, "in", string(buf))
|
||||
n, err = rwc.Write([]byte("out"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, n)
|
||||
rwc.Close()
|
||||
})
|
||||
|
||||
port := rand.Intn(30000) + 10000
|
||||
|
||||
ch := make(chan *result)
|
||||
|
||||
go func() {
|
||||
res, _ := testExecuteContext(ctx, e, fmt.Sprintf("resources proxy resource1 -a app1 -p %d", port), nil)
|
||||
ch <- res
|
||||
}()
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
cn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port))
|
||||
require.NoError(t, err)
|
||||
|
||||
cn.Write([]byte("in"))
|
||||
|
||||
data, err := ioutil.ReadAll(cn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "out", string(data))
|
||||
|
||||
cancel()
|
||||
|
||||
res := <-ch
|
||||
|
||||
require.NotNil(t, res)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
fmt.Sprintf("proxying localhost:%d to example.org:443", port),
|
||||
fmt.Sprintf("connect: %d", port),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestResourcesUrl(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ResourceGet", "app1", "resource1").Return(fxResource(), nil)
|
||||
|
||||
res, err := testExecute(e, "resources url resource1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"https://example.org/path"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestResourcesUrlError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ResourceGet", "app1", "resource1").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "resources url resource1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResources(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("SystemResourceList").Return(structs.Resources{*fxResource(), *fxResource()}, nil)
|
||||
|
||||
res, err := testExecute(e, "rack resources", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"NAME TYPE STATUS",
|
||||
"resource1 type status",
|
||||
"resource1 type status",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("SystemResourceList").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack resources", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesCreate(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
opts := structs.ResourceCreateOptions{Name: options.String("name1"), Parameters: map[string]string{"Foo": "bar", "Baz": "quux"}}
|
||||
i.On("SystemResourceCreate", "type1", opts).Return(fxResource(), nil)
|
||||
|
||||
res, err := testExecute(e, "rack resources create type1 -n name1 Foo=bar Baz=quux", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Creating resource... OK, resource1"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesCreateError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
opts := structs.ResourceCreateOptions{Name: options.String("name1"), Parameters: map[string]string{"Foo": "bar", "Baz": "quux"}}
|
||||
i.On("SystemResourceCreate", "type1", opts).Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack resources create type1 -n name1 Foo=bar Baz=quux", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Creating resource... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesCreateClassic(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
opts := structs.ResourceCreateOptions{Name: options.String("name1"), Parameters: map[string]string{"Foo": "bar", "Baz": "quux"}}
|
||||
i.On("ResourceCreateClassic", "type1", opts).Return(fxResource(), nil)
|
||||
|
||||
res, err := testExecute(e, "rack resources create type1 -n name1 Foo=bar Baz=quux", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Creating resource... OK, resource1"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesDelete(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("SystemResourceDelete", "resource1").Return(nil)
|
||||
|
||||
res, err := testExecute(e, "rack resources delete resource1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Deleting resource... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesDeleteError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("SystemResourceDelete", "resource1").Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack resources delete resource1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Deleting resource... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesInfo(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("SystemResourceGet", "resource1").Return(fxResource(), nil)
|
||||
|
||||
res, err := testExecute(e, "rack resources info resource1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Name resource1",
|
||||
"Type type",
|
||||
"Status status",
|
||||
"Options Url=https://other.example.org/path",
|
||||
" k1=v1",
|
||||
" k2=v2",
|
||||
"URL https://example.org/path",
|
||||
"Apps app1, app1",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesInfoError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("SystemResourceGet", "resource1").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack resources info resource1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesLink(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("SystemResourceLink", "resource1", "app1").Return(fxResource(), nil)
|
||||
|
||||
res, err := testExecute(e, "rack resources link resource1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Linking to app1... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesLinkError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("SystemResourceLink", "resource1", "app1").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack resources link resource1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Linking to app1... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesOptions(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("SystemResourceTypes").Return(structs.ResourceTypes{fxResourceType()}, nil)
|
||||
|
||||
res, err := testExecute(e, "rack resources options type1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"NAME DEFAULT DESCRIPTION",
|
||||
"Param1 def1 desc1 ",
|
||||
"Param2 def2 desc2 ",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesOptionsError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("SystemResourceTypes").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack resources options type1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesProxy(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
i.On("WithContext", ctx).Return(i)
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("SystemResourceGet", "resource1").Return(fxResource(), nil)
|
||||
i.On("Proxy", "example.org", 443, mock.Anything, structs.ProxyOptions{TLS: options.Bool(false)}).Return(nil).Run(func(args mock.Arguments) {
|
||||
buf := make([]byte, 2)
|
||||
rwc := args.Get(2).(io.ReadWriteCloser)
|
||||
n, err := rwc.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, n)
|
||||
require.Equal(t, "in", string(buf))
|
||||
n, err = rwc.Write([]byte("out"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, n)
|
||||
rwc.Close()
|
||||
})
|
||||
|
||||
port := rand.Intn(30000) + 10000
|
||||
|
||||
ch := make(chan *result)
|
||||
|
||||
go func() {
|
||||
res, _ := testExecuteContext(ctx, e, fmt.Sprintf("rack resources proxy resource1 -p %d", port), nil)
|
||||
ch <- res
|
||||
}()
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
cn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port))
|
||||
require.NoError(t, err)
|
||||
|
||||
cn.Write([]byte("in"))
|
||||
|
||||
data, err := ioutil.ReadAll(cn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "out", string(data))
|
||||
|
||||
cancel()
|
||||
|
||||
res := <-ch
|
||||
|
||||
require.NotNil(t, res)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
fmt.Sprintf("proxying localhost:%d to example.org:443", port),
|
||||
fmt.Sprintf("connect: %d", port),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesTypes(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("SystemResourceTypes").Return(structs.ResourceTypes{fxResourceType(), fxResourceType()}, nil)
|
||||
|
||||
res, err := testExecute(e, "rack resources types", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"TYPE ",
|
||||
"type1",
|
||||
"type1",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesTypesError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("SystemResourceTypes").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack resources types", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesUnlink(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("SystemResourceUnlink", "resource1", "app1").Return(fxResource(), nil)
|
||||
|
||||
res, err := testExecute(e, "rack resources unlink resource1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Unlinking from app1... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesUnlinkError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("SystemResourceUnlink", "resource1", "app1").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack resources unlink resource1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Unlinking from app1... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesUpdate(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
opts := structs.ResourceUpdateOptions{Parameters: map[string]string{"Foo": "bar", "Baz": "quux"}}
|
||||
i.On("SystemResourceUpdate", "resource1", opts).Return(fxResource(), nil)
|
||||
|
||||
res, err := testExecute(e, "rack resources update resource1 Foo=bar Baz=quux", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Updating resource... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesUpdateError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
opts := structs.ResourceUpdateOptions{Parameters: map[string]string{"Foo": "bar", "Baz": "quux"}}
|
||||
i.On("SystemResourceUpdate", "resource1", opts).Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack resources update resource1 Foo=bar Baz=quux", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Updating resource... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesUpdateClassic(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
opts := structs.ResourceUpdateOptions{Parameters: map[string]string{"Foo": "bar", "Baz": "quux"}}
|
||||
i.On("ResourceUpdateClassic", "resource1", opts).Return(fxResource(), nil)
|
||||
|
||||
res, err := testExecute(e, "rack resources update resource1 Foo=bar Baz=quux", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Updating resource... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesUrl(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("SystemResourceGet", "resource1").Return(fxResource(), nil)
|
||||
|
||||
res, err := testExecute(e, "rack resources url resource1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"https://example.org/path"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesUrlError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("SystemResourceGet", "resource1").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "rack resources url resource1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRackResourcesUrlClassic(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
i.On("SystemResourceGetClassic", "resource1").Return(fxResource(), nil)
|
||||
|
||||
res, err := testExecute(e, "rack resources url resource1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"https://other.example.org/path"})
|
||||
})
|
||||
}
|
||||
36
pkg/cli/restart.go
Normal file
36
pkg/cli/restart.go
Normal file
@ -0,0 +1,36 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("restart", "restart an app", Restart, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagApp, flagRack},
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
}
|
||||
|
||||
func Restart(rack sdk.Interface, c *stdcli.Context) error {
|
||||
ss, err := rack.ServiceList(app(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Slice(ss, func(i, j int) bool { return ss[i].Name < ss[j].Name })
|
||||
|
||||
for _, s := range ss {
|
||||
c.Startf("Restarting <service>%s</service>", s.Name)
|
||||
|
||||
if err := rack.ServiceRestart(app(c), s.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.OK()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
52
pkg/cli/restart_test.go
Normal file
52
pkg/cli/restart_test.go
Normal file
@ -0,0 +1,52 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRestart(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("ServiceList", "app1").Return(structs.Services{*fxService(), *fxService()}, nil)
|
||||
i.On("ServiceRestart", "app1", "service1").Return(nil).Twice()
|
||||
|
||||
res, err := testExecute(e, "restart -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Restarting service1... OK",
|
||||
"Restarting service1... OK",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRestartErrorList(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("ServiceList", "app1").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "restart -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRestartErrorRestart(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("ServiceList", "app1").Return(structs.Services{*fxService(), *fxService()}, nil)
|
||||
i.On("ServiceRestart", "app1", "service1").Return(fmt.Errorf("err1")).Once()
|
||||
|
||||
res, err := testExecute(e, "restart -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Restarting service1... "})
|
||||
})
|
||||
}
|
||||
120
pkg/cli/run.go
Normal file
120
pkg/cli/run.go
Normal file
@ -0,0 +1,120 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/convox/convox/pkg/common"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("run", "execute a command in a new process", Run, stdcli.CommandOptions{
|
||||
Flags: append(
|
||||
stdcli.OptionFlags(structs.ProcessRunOptions{}),
|
||||
flagRack,
|
||||
flagApp,
|
||||
stdcli.BoolFlag("detach", "d", "run process in the background"),
|
||||
stdcli.IntFlag("timeout", "t", "timeout"),
|
||||
),
|
||||
Usage: "<service> <command>",
|
||||
Validate: stdcli.ArgsMin(2),
|
||||
})
|
||||
}
|
||||
|
||||
func Run(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service := c.Arg(0)
|
||||
command := strings.Join(c.Args[1:], " ")
|
||||
|
||||
var opts structs.ProcessRunOptions
|
||||
|
||||
if err := c.Options(&opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.Command = options.String(command)
|
||||
|
||||
timeout := 3600
|
||||
|
||||
if t := c.Int("timeout"); t > 0 {
|
||||
timeout = t
|
||||
}
|
||||
|
||||
if w, h, err := c.TerminalSize(); err == nil {
|
||||
opts.Height = options.Int(h)
|
||||
opts.Width = options.Int(w)
|
||||
}
|
||||
|
||||
restore := c.TerminalRaw()
|
||||
defer restore()
|
||||
|
||||
if s.Version <= "20180708231844" {
|
||||
if c.Bool("detach") {
|
||||
c.Startf("Running detached process")
|
||||
|
||||
pid, err := rack.ProcessRunDetached(app(c), service, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OK(pid)
|
||||
}
|
||||
|
||||
code, err := rack.ProcessRunAttached(app(c), service, c, timeout, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return stdcli.Exit(code)
|
||||
}
|
||||
|
||||
if c.Bool("detach") {
|
||||
c.Startf("Running detached process")
|
||||
|
||||
ps, err := rack.ProcessRun(app(c), service, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OK(ps.Id)
|
||||
}
|
||||
|
||||
opts.Command = options.String(fmt.Sprintf("sleep %d", timeout))
|
||||
|
||||
ps, err := rack.ProcessRun(app(c), c.Arg(0), opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer rack.ProcessStop(app(c), ps.Id)
|
||||
|
||||
if err := common.WaitForProcessRunning(rack, c, app(c), ps.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eopts := structs.ProcessExecOptions{
|
||||
Entrypoint: options.Bool(true),
|
||||
Height: opts.Height,
|
||||
Width: opts.Width,
|
||||
}
|
||||
|
||||
if !stdcli.IsTerminal(os.Stdin) {
|
||||
eopts.Tty = options.Bool(false)
|
||||
}
|
||||
|
||||
code, err := rack.ProcessExec(app(c), ps.Id, command, c, eopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return stdcli.Exit(code)
|
||||
}
|
||||
96
pkg/cli/run_test.go
Normal file
96
pkg/cli/run_test.go
Normal file
@ -0,0 +1,96 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ProcessRun", "app1", "web", structs.ProcessRunOptions{Command: options.String("sleep 7200")}).Return(fxProcess(), nil)
|
||||
i.On("ProcessGet", "app1", "pid1").Return(fxProcessPending(), nil).Twice()
|
||||
i.On("ProcessGet", "app1", "pid1").Return(fxProcess(), nil)
|
||||
opts := structs.ProcessExecOptions{Entrypoint: options.Bool(true), Tty: options.Bool(false)}
|
||||
i.On("ProcessExec", "app1", "pid1", "bash", mock.Anything, opts).Return(4, nil).Run(func(args mock.Arguments) {
|
||||
data, err := ioutil.ReadAll(args.Get(3).(io.Reader))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "in", string(data))
|
||||
args.Get(3).(io.Writer).Write([]byte("out"))
|
||||
})
|
||||
i.On("ProcessStop", "app1", "pid1").Return(nil)
|
||||
|
||||
res, err := testExecute(e, "run web bash -a app1 -t 7200", strings.NewReader("in"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 4, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
require.Equal(t, "out", res.Stdout)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ProcessRun", "app1", "web", structs.ProcessRunOptions{Command: options.String("sleep 7200")}).Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "run web bash -a app1 -t 7200", strings.NewReader("in"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunClassic(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
i.On("ProcessRunAttached", "app1", "web", mock.Anything, 7200, structs.ProcessRunOptions{Command: options.String("bash")}).Return(4, nil).Run(func(args mock.Arguments) {
|
||||
data, err := ioutil.ReadAll(args.Get(2).(io.Reader))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "in", string(data))
|
||||
args.Get(2).(io.Writer).Write([]byte("out"))
|
||||
})
|
||||
|
||||
res, err := testExecute(e, "run web bash -a app1 -t 7200", strings.NewReader("in"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 4, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
require.Equal(t, "out", res.Stdout)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunClassicDetached(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
i.On("ProcessRunDetached", "app1", "web", structs.ProcessRunOptions{Command: options.String("bash")}).Return("pid1", nil)
|
||||
|
||||
res, err := testExecute(e, "run web bash -a app1 -d", strings.NewReader("in"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Running detached process... OK, pid1"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunDetached(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ProcessRun", "app1", "web", structs.ProcessRunOptions{Command: options.String("bash")}).Return(fxProcess(), nil)
|
||||
|
||||
res, err := testExecute(e, "run web bash -a app1 -d", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Running detached process... OK, pid1"})
|
||||
})
|
||||
}
|
||||
102
pkg/cli/scale.go
Normal file
102
pkg/cli/scale.go
Normal file
@ -0,0 +1,102 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/convox/convox/pkg/common"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("scale", "scale a service", Scale, stdcli.CommandOptions{
|
||||
Flags: append(stdcli.OptionFlags(structs.ServiceUpdateOptions{}), flagApp, flagRack, flagWait),
|
||||
Usage: "<service>",
|
||||
Validate: func(c *stdcli.Context) error {
|
||||
if c.Value("count") != nil || c.Value("cpu") != nil || c.Value("memory") != nil {
|
||||
if len(c.Args) < 1 {
|
||||
return fmt.Errorf("service name required")
|
||||
} else {
|
||||
return stdcli.Args(1)(c)
|
||||
}
|
||||
} else {
|
||||
return stdcli.Args(0)(c)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func Scale(rack sdk.Interface, c *stdcli.Context) error {
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var opts structs.ServiceUpdateOptions
|
||||
|
||||
if err := c.Options(&opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.Count != nil || opts.Cpu != nil || opts.Memory != nil {
|
||||
service := c.Arg(0)
|
||||
|
||||
c.Startf("Scaling <service>%s</service>", service)
|
||||
|
||||
if s.Version <= "20180708231844" {
|
||||
if err := rack.FormationUpdate(app(c), service, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := rack.ServiceUpdate(app(c), service, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.Bool("wait") {
|
||||
c.Writef("\n")
|
||||
|
||||
if err := common.WaitForAppWithLogs(rack, c, app(c)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
|
||||
var ss structs.Services
|
||||
running := map[string]int{}
|
||||
|
||||
if s.Version < "20180708231844" {
|
||||
ss, err = rack.FormationGet(app(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
ss, err = rack.ServiceList(app(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(ss, func(i, j int) bool { return ss[i].Name < ss[j].Name })
|
||||
|
||||
ps, err := rack.ProcessList(app(c), structs.ProcessListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range ps {
|
||||
running[p.Name] += 1
|
||||
}
|
||||
|
||||
t := c.Table("SERVICE", "DESIRED", "RUNNING", "CPU", "MEMORY")
|
||||
|
||||
for _, s := range ss {
|
||||
t.AddRow(s.Name, fmt.Sprintf("%d", s.Count), fmt.Sprintf("%d", running[s.Name]), fmt.Sprintf("%d", s.Cpu), fmt.Sprintf("%d", s.Memory))
|
||||
}
|
||||
|
||||
return t.Print()
|
||||
}
|
||||
100
pkg/cli/scale_test.go
Normal file
100
pkg/cli/scale_test.go
Normal file
@ -0,0 +1,100 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestScale(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ServiceList", "app1").Return(structs.Services{*fxService(), *fxService()}, nil)
|
||||
i.On("ProcessList", "app1", structs.ProcessListOptions{}).Return(structs.Processes{*fxProcess(), *fxProcess()}, nil)
|
||||
|
||||
res, err := testExecute(e, "scale -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"SERVICE DESIRED RUNNING CPU MEMORY",
|
||||
"service1 1 0 2 3 ",
|
||||
"service1 1 0 2 3 ",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestScaleError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ServiceList", "app1").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "scale -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestScaleClassic(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
i.On("FormationGet", "app1").Return(structs.Services{*fxService(), *fxService()}, nil)
|
||||
i.On("ProcessList", "app1", structs.ProcessListOptions{}).Return(structs.Processes{*fxProcess(), *fxProcess()}, nil)
|
||||
|
||||
res, err := testExecute(e, "scale -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"SERVICE DESIRED RUNNING CPU MEMORY",
|
||||
"service1 1 0 2 3 ",
|
||||
"service1 1 0 2 3 ",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestScaleUpdate(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ServiceUpdate", "app1", "web", structs.ServiceUpdateOptions{Count: options.Int(3), Cpu: options.Int(5), Memory: options.Int(10)}).Return(nil)
|
||||
|
||||
res, err := testExecute(e, "scale web --cpu 5 --memory 10 --count 3 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Scaling web... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestScaleUpdateError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ServiceUpdate", "app1", "web", structs.ServiceUpdateOptions{Count: options.Int(3), Cpu: options.Int(5), Memory: options.Int(10)}).Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "scale web --cpu 5 --memory 10 --count 3 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Scaling web... "})
|
||||
})
|
||||
}
|
||||
|
||||
func TestScaleUpdateClassic(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
i.On("FormationUpdate", "app1", "web", structs.ServiceUpdateOptions{Count: options.Int(3), Cpu: options.Int(5), Memory: options.Int(10)}).Return(nil)
|
||||
|
||||
res, err := testExecute(e, "scale web --cpu 5 --memory 10 --count 3 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Scaling web... OK"})
|
||||
})
|
||||
}
|
||||
75
pkg/cli/services.go
Normal file
75
pkg/cli/services.go
Normal file
@ -0,0 +1,75 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("services", "list services for an app", Services, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagApp, flagRack},
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("services restart", "restart a service", ServicesRestart, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagApp, flagRack},
|
||||
Validate: stdcli.Args(1),
|
||||
})
|
||||
}
|
||||
|
||||
func Services(rack sdk.Interface, c *stdcli.Context) error {
|
||||
sys, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ss structs.Services
|
||||
|
||||
if sys.Version < "20180708231844" {
|
||||
ss, err = rack.FormationGet(app(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
ss, err = rack.ServiceList(app(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
t := c.Table("SERVICE", "DOMAIN", "PORTS")
|
||||
|
||||
for _, s := range ss {
|
||||
ports := []string{}
|
||||
|
||||
for _, p := range s.Ports {
|
||||
port := fmt.Sprintf("%d", p.Balancer)
|
||||
|
||||
if p.Container != 0 {
|
||||
port = fmt.Sprintf("%d:%d", p.Balancer, p.Container)
|
||||
}
|
||||
|
||||
ports = append(ports, port)
|
||||
}
|
||||
|
||||
t.AddRow(s.Name, s.Domain, strings.Join(ports, " "))
|
||||
}
|
||||
|
||||
return t.Print()
|
||||
}
|
||||
|
||||
func ServicesRestart(rack sdk.Interface, c *stdcli.Context) error {
|
||||
name := c.Arg(0)
|
||||
|
||||
c.Startf("Restarting <service>%s</service>", name)
|
||||
|
||||
if err := rack.ServiceRestart(app(c), name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
82
pkg/cli/services_test.go
Normal file
82
pkg/cli/services_test.go
Normal file
@ -0,0 +1,82 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServices(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ServiceList", "app1").Return(structs.Services{*fxService(), *fxService()}, nil)
|
||||
|
||||
res, err := testExecute(e, "services -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"SERVICE DOMAIN PORTS ",
|
||||
"service1 domain 1:2 1:2",
|
||||
"service1 domain 1:2 1:2",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestServicesError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ServiceList", "app1").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "services -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestServicesClassic(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
i.On("FormationGet", "app1").Return(structs.Services{*fxService(), *fxService()}, nil)
|
||||
|
||||
res, err := testExecute(e, "services -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"SERVICE DOMAIN PORTS ",
|
||||
"service1 domain 1:2 1:2",
|
||||
"service1 domain 1:2 1:2",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestServicesRestart(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("ServiceRestart", "app1", "service1").Return(nil)
|
||||
|
||||
res, err := testExecute(e, "services restart service1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Restarting service1... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestServicesRestartError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("ServiceRestart", "app1", "service1").Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "services restart service1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Restarting service1... "})
|
||||
})
|
||||
}
|
||||
105
pkg/cli/ssl.go
Normal file
105
pkg/cli/ssl.go
Normal file
@ -0,0 +1,105 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/convox/convox/pkg/common"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("ssl", "list certificate associates for an app", Ssl, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack, flagApp},
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
|
||||
register("ssl update", "update certificate for an app", SslUpdate, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack, flagApp, flagWait},
|
||||
Usage: "<process:port> <certificate>",
|
||||
Validate: stdcli.Args(2),
|
||||
})
|
||||
}
|
||||
|
||||
func Ssl(rack sdk.Interface, c *stdcli.Context) error {
|
||||
sys, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ss structs.Services
|
||||
|
||||
if sys.Version < "20180708231844" {
|
||||
ss, err = rack.FormationGet(app(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
ss, err = rack.ServiceList(app(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
t := c.Table("ENDPOINT", "CERTIFICATE", "DOMAIN", "EXPIRES")
|
||||
|
||||
certs := map[string]structs.Certificate{}
|
||||
|
||||
cs, err := rack.CertificateList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, c := range cs {
|
||||
certs[c.Id] = c
|
||||
}
|
||||
|
||||
for _, s := range ss {
|
||||
for _, p := range s.Ports {
|
||||
if p.Certificate != "" {
|
||||
t.AddRow(fmt.Sprintf("%s:%d", s.Name, p.Balancer), p.Certificate, certs[p.Certificate].Domain, common.Ago(certs[p.Certificate].Expiration))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return t.Print()
|
||||
}
|
||||
|
||||
func SslUpdate(rack sdk.Interface, c *stdcli.Context) error {
|
||||
a, err := rack.AppGet(app(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a.Generation == "2" {
|
||||
return fmt.Errorf("command not valid for generation 2 applications")
|
||||
}
|
||||
|
||||
parts := strings.SplitN(c.Arg(0), ":", 2)
|
||||
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("process:port required as first argument")
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Startf("Updating certificate")
|
||||
|
||||
if err := rack.CertificateApply(app(c), parts[0], port, c.Arg(1)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("wait") {
|
||||
if err := common.WaitForAppRunning(rack, app(c)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
102
pkg/cli/ssl_test.go
Normal file
102
pkg/cli/ssl_test.go
Normal file
@ -0,0 +1,102 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSsl(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ServiceList", "app1").Return(structs.Services{*fxService(), *fxService()}, nil)
|
||||
i.On("CertificateList").Return(structs.Certificates{*fxCertificate()}, nil)
|
||||
|
||||
res, err := testExecute(e, "ssl -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"ENDPOINT CERTIFICATE DOMAIN EXPIRES ",
|
||||
"service1:1 cert1 example.org 2 days from now",
|
||||
"service1:1 cert1 example.org 2 days from now",
|
||||
"service1:1 cert1 example.org 2 days from now",
|
||||
"service1:1 cert1 example.org 2 days from now",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSslError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ServiceList", "app1").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "ssl -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSslClassic(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystemClassic(), nil)
|
||||
i.On("FormationGet", "app1").Return(structs.Services{*fxService(), *fxService()}, nil)
|
||||
i.On("CertificateList").Return(structs.Certificates{*fxCertificate()}, nil)
|
||||
|
||||
res, err := testExecute(e, "ssl -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"ENDPOINT CERTIFICATE DOMAIN EXPIRES ",
|
||||
"service1:1 cert1 example.org 2 days from now",
|
||||
"service1:1 cert1 example.org 2 days from now",
|
||||
"service1:1 cert1 example.org 2 days from now",
|
||||
"service1:1 cert1 example.org 2 days from now",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSslUpdate(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppGet", "app1").Return(fxApp(), nil)
|
||||
|
||||
res, err := testExecute(e, "ssl update web:5000 cert1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: command not valid for generation 2 applications"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSslUpdateGeneration1(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppGet", "app1").Return(fxAppGeneration1(), nil)
|
||||
i.On("CertificateApply", "app1", "web", 5000, "cert1").Return(nil)
|
||||
|
||||
res, err := testExecute(e, "ssl update web:5000 cert1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Updating certificate... OK"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSslUpdateGeneration1Error(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("AppGet", "app1").Return(fxAppGeneration1(), nil)
|
||||
i.On("CertificateApply", "app1", "web", 5000, "cert1").Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "ssl update web:5000 cert1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{"Updating certificate... "})
|
||||
})
|
||||
}
|
||||
102
pkg/cli/start.go
Normal file
102
pkg/cli/start.go
Normal file
@ -0,0 +1,102 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/convox/convox/pkg/start"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("start", "start an application for local development", Start, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{
|
||||
flagRack,
|
||||
flagApp,
|
||||
stdcli.StringFlag("manifest", "m", "manifest file"),
|
||||
stdcli.StringFlag("generation", "g", "generation"),
|
||||
stdcli.BoolFlag("no-build", "", "skip build"),
|
||||
stdcli.BoolFlag("no-cache", "", "build withoit layer cache"),
|
||||
stdcli.BoolFlag("no-sync", "", "do not sync local changes into the running containers"),
|
||||
stdcli.IntFlag("shift", "s", "shift local port numbers (generation 1 only)"),
|
||||
},
|
||||
Usage: "[service] [service...]",
|
||||
})
|
||||
}
|
||||
|
||||
func Start(rack sdk.Interface, c *stdcli.Context) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
go handleInterrupt(cancel)
|
||||
|
||||
if c.String("generation") == "1" || c.LocalSetting("generation") == "1" || filepath.Base(c.String("manifest")) == "docker-compose.yml" {
|
||||
return fmt.Errorf("gen1 is no longer supported")
|
||||
}
|
||||
|
||||
var p structs.Provider
|
||||
|
||||
if rack != nil {
|
||||
p = rack
|
||||
// s, err := rack.SystemGet()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if s.Provider == "local" || s.Provider == "kaws" {
|
||||
// p = rack
|
||||
// }
|
||||
}
|
||||
|
||||
if p == nil {
|
||||
if !localRackRunning(c) {
|
||||
return fmt.Errorf("local rack not found, try `sudo convox rack install local`")
|
||||
}
|
||||
|
||||
r, err := matchRack(c, "local/")
|
||||
if err != nil {
|
||||
if strings.HasPrefix(err.Error(), "ambiguous rack name") {
|
||||
return fmt.Errorf("multiple local racks detected, use `convox switch` to select one")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
cl, err := sdk.New(fmt.Sprintf("https://rack.%s", strings.TrimPrefix(r.Name, "local/")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p = cl
|
||||
}
|
||||
|
||||
if p == nil {
|
||||
return fmt.Errorf("could not find local rack")
|
||||
}
|
||||
|
||||
opts := start.Options2{
|
||||
App: app(c),
|
||||
Build: !c.Bool("no-build"),
|
||||
Cache: !c.Bool("no-cache"),
|
||||
Manifest: c.String("manifest"),
|
||||
Provider: p,
|
||||
Sync: !c.Bool("no-sync"),
|
||||
}
|
||||
|
||||
if len(c.Args) > 0 {
|
||||
opts.Services = c.Args
|
||||
}
|
||||
|
||||
return Starter.Start2(ctx, c, opts)
|
||||
}
|
||||
|
||||
func handleInterrupt(cancel context.CancelFunc) {
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, os.Interrupt, os.Kill)
|
||||
<-ch
|
||||
fmt.Println("")
|
||||
cancel()
|
||||
}
|
||||
236
pkg/cli/start_test.go
Normal file
236
pkg/cli/start_test.go
Normal file
@ -0,0 +1,236 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
mockstart "github.com/convox/convox/pkg/mock/start"
|
||||
mockstdcli "github.com/convox/convox/pkg/mock/stdcli"
|
||||
"github.com/convox/convox/pkg/start"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestStart1(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
ms := &mockstart.Interface{}
|
||||
cli.Starter = ms
|
||||
|
||||
opts := start.Options1{
|
||||
App: "app1",
|
||||
Build: true,
|
||||
Cache: true,
|
||||
Sync: true,
|
||||
}
|
||||
|
||||
ms.On("Start1", mock.Anything, opts).Return(nil)
|
||||
|
||||
res, err := testExecute(e, "start -g 1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestStart1Error(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
ms := &mockstart.Interface{}
|
||||
cli.Starter = ms
|
||||
|
||||
opts := start.Options1{
|
||||
App: "app1",
|
||||
Build: true,
|
||||
Cache: true,
|
||||
Sync: true,
|
||||
}
|
||||
|
||||
ms.On("Start1", mock.Anything, opts).Return(fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "start -g 1 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestStart1Options(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
ms := &mockstart.Interface{}
|
||||
cli.Starter = ms
|
||||
|
||||
opts := start.Options1{
|
||||
App: "app1",
|
||||
Build: false,
|
||||
Cache: false,
|
||||
Command: []string{"bin/command", "args"},
|
||||
Manifest: "manifest1",
|
||||
Service: "service1",
|
||||
Shift: 3000,
|
||||
Sync: false,
|
||||
}
|
||||
|
||||
ms.On("Start1", mock.Anything, opts).Return(nil)
|
||||
|
||||
res, err := testExecute(e, "start -g 1 -a app1 -m manifest1 --no-build --no-cache --no-sync -s 3000 service1 bin/command args", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestStart2(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
me := &mockstdcli.Executor{}
|
||||
me.On("Execute", "kubectl", "get", "ns", "--selector=system=convox,type=rack", "--output=name").Return([]byte("namespace/dev\n"), nil)
|
||||
e.Executor = me
|
||||
|
||||
ms := &mockstart.Interface{}
|
||||
cli.Starter = ms
|
||||
|
||||
opts := start.Options2{
|
||||
App: "app1",
|
||||
Build: true,
|
||||
Cache: true,
|
||||
Provider: i,
|
||||
Sync: true,
|
||||
}
|
||||
|
||||
ms.On("Start2", mock.Anything, mock.Anything, opts).Return(nil)
|
||||
|
||||
i.On("SystemGet").Return(fxSystemLocal, nil)
|
||||
|
||||
res, err := testExecute(e, "start -g 2 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestStart2Error(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
me := &mockstdcli.Executor{}
|
||||
me.On("Execute", "kubectl", "get", "ns", "--selector=system=convox,type=rack", "--output=name").Return([]byte("namespace/dev\n"), nil)
|
||||
e.Executor = me
|
||||
|
||||
ms := &mockstart.Interface{}
|
||||
cli.Starter = ms
|
||||
|
||||
opts := start.Options2{
|
||||
App: "app1",
|
||||
Build: true,
|
||||
Cache: true,
|
||||
Provider: i,
|
||||
Sync: true,
|
||||
}
|
||||
|
||||
ms.On("Start2", mock.Anything, mock.Anything, opts).Return(fmt.Errorf("err1"))
|
||||
|
||||
i.On("SystemGet").Return(fxSystemLocal, nil)
|
||||
|
||||
res, err := testExecute(e, "start -g 2 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestStart2Options(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
me := &mockstdcli.Executor{}
|
||||
me.On("Execute", "kubectl", "get", "ns", "--selector=system=convox,type=rack", "--output=name").Return([]byte("namespace/dev\n"), nil)
|
||||
e.Executor = me
|
||||
|
||||
ms := &mockstart.Interface{}
|
||||
cli.Starter = ms
|
||||
|
||||
opts := start.Options2{
|
||||
App: "app1",
|
||||
Build: false,
|
||||
Cache: false,
|
||||
Manifest: "manifest1",
|
||||
Provider: i,
|
||||
Services: []string{"service1", "service2"},
|
||||
Sync: false,
|
||||
}
|
||||
|
||||
ms.On("Start2", mock.Anything, mock.Anything, opts).Return(nil)
|
||||
|
||||
i.On("SystemGet").Return(fxSystemLocal(), nil)
|
||||
|
||||
res, err := testExecute(e, "start -g 2 -a app1 -m manifest1 --no-build --no-cache --no-sync service1 service2", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestStart2Remote(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
me := &mockstdcli.Executor{}
|
||||
me.On("Execute", "kubectl", "get", "ns", "--selector=system=convox,type=rack", "--output=name").Return([]byte("namespace/dev"), nil)
|
||||
e.Executor = me
|
||||
|
||||
ms := &mockstart.Interface{}
|
||||
cli.Starter = ms
|
||||
|
||||
ms.On("Start2", mock.Anything, mock.Anything, mock.Anything).Return(nil).Run(func(args mock.Arguments) {
|
||||
opts := args.Get(2).(start.Options2)
|
||||
require.Equal(t, "app1", opts.App)
|
||||
require.Equal(t, true, opts.Build)
|
||||
require.Equal(t, true, opts.Cache)
|
||||
require.Equal(t, true, opts.Sync)
|
||||
p := opts.Provider.(*sdk.Client)
|
||||
require.Equal(t, "https", p.Client.Endpoint.Scheme)
|
||||
require.Equal(t, "rack.dev", p.Client.Endpoint.Host)
|
||||
})
|
||||
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
|
||||
res, err := testExecute(e, "start -g 2 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
func TestStart2RemoteMultiple(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
me := &mockstdcli.Executor{}
|
||||
me.On("Execute", "kubectl", "get", "ns", "--selector=system=convox,type=rack", "--output=name").Return([]byte("namespace/dev\nnamespace/dev2\n"), nil)
|
||||
e.Executor = me
|
||||
|
||||
ms := &mockstart.Interface{}
|
||||
cli.Starter = ms
|
||||
|
||||
opts := start.Options2{
|
||||
App: "app1",
|
||||
Build: true,
|
||||
Cache: true,
|
||||
Sync: true,
|
||||
}
|
||||
|
||||
ms.On("Start2", mock.Anything, opts).Return(nil).Run(func(args mock.Arguments) {
|
||||
s := args.Get(0).(*sdk.Client)
|
||||
require.Equal(t, "https", s.Client.Endpoint.Scheme)
|
||||
require.Equal(t, "rack.classic", s.Client.Endpoint.Host)
|
||||
})
|
||||
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
|
||||
res, err := testExecute(e, "start -g 2 -a app1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: multiple local racks detected, use `convox switch` to select one"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
49
pkg/cli/switch.go
Normal file
49
pkg/cli/switch.go
Normal file
@ -0,0 +1,49 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerWithoutProvider("switch", "switch current rack", Switch, stdcli.CommandOptions{
|
||||
Validate: stdcli.ArgsMax(1),
|
||||
})
|
||||
}
|
||||
|
||||
func Switch(rack sdk.Interface, c *stdcli.Context) error {
|
||||
host, err := currentHost(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rack := c.Arg(0); rack != "" {
|
||||
r, err := matchRack(c, rack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rs := hostRacks(c)
|
||||
|
||||
rs[host] = r.Name
|
||||
|
||||
data, err := json.MarshalIndent(rs, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.SettingWrite("racks", string(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Writef("Switched to <rack>%s</rack>\n", r.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
c.Writef("%s\n", currentRack(c, host))
|
||||
|
||||
return nil
|
||||
}
|
||||
74
pkg/cli/switch_test.go
Normal file
74
pkg/cli/switch_test.go
Normal file
@ -0,0 +1,74 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSwitch(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
r := mux.NewRouter()
|
||||
|
||||
r.HandleFunc("/racks", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`[
|
||||
{"name":"foo","organization":{"name":"test"},"status":"running"},
|
||||
{"name":"other","organization":{"name":"test"},"status":"updating"}
|
||||
]`))
|
||||
}).Methods("GET")
|
||||
|
||||
ts := httptest.NewTLSServer(r)
|
||||
|
||||
tsu, err := url.Parse(ts.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(e.Settings, "host"), []byte(tsu.Host), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := testExecute(e, "switch foo", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{"Switched to test/foo"})
|
||||
|
||||
data, err := ioutil.ReadFile(filepath.Join(e.Settings, "racks"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fmt.Sprintf("{\n \"%s\": \"test/foo\"\n}", tsu.Host), string(data))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSwitchUnknown(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
r := mux.NewRouter()
|
||||
|
||||
r.HandleFunc("/racks", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`[
|
||||
{"name":"foo","organization":{"name":"test"},"status":"running"},
|
||||
{"name":"other","organization":{"name":"test"},"status":"updating"}
|
||||
]`))
|
||||
}).Methods("GET")
|
||||
|
||||
ts := httptest.NewTLSServer(r)
|
||||
|
||||
tsu, err := url.Parse(ts.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(e.Settings, "host"), []byte(tsu.Host), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := testExecute(e, "switch rack1", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: could not find rack: rack1"})
|
||||
res.RequireStdout(t, []string{""})
|
||||
})
|
||||
}
|
||||
98
pkg/cli/test.go
Normal file
98
pkg/cli/test.go
Normal file
@ -0,0 +1,98 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/convox/convox/pkg/common"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("test", "run tests", Test, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{
|
||||
flagApp,
|
||||
flagRack,
|
||||
stdcli.StringFlag("description", "d", "description"),
|
||||
stdcli.StringFlag("release", "", "use existing release to run tests"),
|
||||
stdcli.IntFlag("timeout", "t", "timeout"),
|
||||
},
|
||||
Usage: "[dir]",
|
||||
Validate: stdcli.ArgsMax(1),
|
||||
})
|
||||
}
|
||||
|
||||
func Test(rack sdk.Interface, c *stdcli.Context) error {
|
||||
release := c.String("release")
|
||||
|
||||
if release == "" {
|
||||
b, err := build(rack, c, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
release = b.Release
|
||||
}
|
||||
|
||||
m, _, err := common.ReleaseManifest(rack, app(c), release)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
timeout := 3600
|
||||
|
||||
if t := c.Int("timeout"); t > 0 {
|
||||
timeout = t
|
||||
}
|
||||
|
||||
for _, s := range m.Services {
|
||||
if s.Test == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
c.Writef("Running <command>%s</command> on <service>%s</service>\n", s.Test, s.Name)
|
||||
|
||||
ropts := structs.ProcessRunOptions{
|
||||
Command: options.String(fmt.Sprintf("sleep %d", timeout)),
|
||||
Release: options.String(release),
|
||||
}
|
||||
|
||||
ps, err := rack.ProcessRun(app(c), s.Name, ropts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer rack.ProcessStop(app(c), ps.Id)
|
||||
|
||||
if err := common.WaitForProcessRunning(rack, c, app(c), ps.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eopts := structs.ProcessExecOptions{
|
||||
Entrypoint: options.Bool(true),
|
||||
}
|
||||
|
||||
if w, h, err := c.TerminalSize(); err == nil {
|
||||
eopts.Height = options.Int(h)
|
||||
eopts.Width = options.Int(w)
|
||||
}
|
||||
|
||||
if !stdcli.IsTerminal(os.Stdin) {
|
||||
eopts.Tty = options.Bool(false)
|
||||
}
|
||||
|
||||
code, err := rack.ProcessExec(app(c), ps.Id, s.Test, c, eopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if code != 0 {
|
||||
return fmt.Errorf("exit %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
93
pkg/cli/test_test.go
Normal file
93
pkg/cli/test_test.go
Normal file
@ -0,0 +1,93 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTest(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ObjectStore", "app1", mock.AnythingOfType("string"), mock.Anything, structs.ObjectStoreOptions{}).Return(&fxObject, nil).Run(func(args mock.Arguments) {
|
||||
require.Regexp(t, `tmp/[0-9a-f]{30}\.tgz`, args.Get(1).(string))
|
||||
})
|
||||
i.On("BuildCreate", "app1", "object://test", structs.BuildCreateOptions{Development: options.Bool(true), Description: options.String("foo")}).Return(fxBuild(), nil)
|
||||
i.On("BuildLogs", "app1", "build1", structs.LogsOptions{}).Return(testLogs(fxLogs()), nil)
|
||||
i.On("BuildGet", "app1", "build1").Return(fxBuildRunning(), nil).Once()
|
||||
i.On("BuildGet", "app1", "build4").Return(fxBuild(), nil)
|
||||
i.On("ReleaseGet", "app1", "release1").Return(fxRelease(), nil)
|
||||
i.On("ProcessRun", "app1", "web", structs.ProcessRunOptions{Command: options.String("sleep 7200"), Release: options.String("release1")}).Return(fxProcess(), nil)
|
||||
i.On("ProcessGet", "app1", "pid1").Return(fxProcessPending(), nil).Twice()
|
||||
i.On("ProcessGet", "app1", "pid1").Return(fxProcess(), nil)
|
||||
opts := structs.ProcessExecOptions{Entrypoint: options.Bool(true), Tty: options.Bool(false)}
|
||||
i.On("ProcessExec", "app1", "pid1", "make test", mock.Anything, opts).Return(0, nil).Run(func(args mock.Arguments) {
|
||||
data, err := ioutil.ReadAll(args.Get(3).(io.Reader))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "in", string(data))
|
||||
args.Get(3).(io.Writer).Write([]byte("out"))
|
||||
})
|
||||
i.On("ProcessStop", "app1", "pid1").Return(nil)
|
||||
|
||||
res, err := testExecute(e, "test ./testdata/httpd -a app1 -d foo -t 7200", strings.NewReader("in"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"Packaging source... OK",
|
||||
"Uploading source... OK",
|
||||
"Starting build... OK",
|
||||
"log1",
|
||||
"log2",
|
||||
"Running make test on web",
|
||||
"out",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestTestFail(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
i.On("ObjectStore", "app1", mock.AnythingOfType("string"), mock.Anything, structs.ObjectStoreOptions{}).Return(&fxObject, nil).Run(func(args mock.Arguments) {
|
||||
require.Regexp(t, `tmp/[0-9a-f]{30}\.tgz`, args.Get(1).(string))
|
||||
})
|
||||
i.On("BuildCreate", "app1", "object://test", structs.BuildCreateOptions{Development: options.Bool(true), Description: options.String("foo")}).Return(fxBuild(), nil)
|
||||
i.On("BuildLogs", "app1", "build1", structs.LogsOptions{}).Return(testLogs(fxLogs()), nil)
|
||||
i.On("BuildGet", "app1", "build1").Return(fxBuildRunning(), nil).Once()
|
||||
i.On("BuildGet", "app1", "build4").Return(fxBuild(), nil)
|
||||
i.On("ReleaseGet", "app1", "release1").Return(fxRelease(), nil)
|
||||
i.On("ProcessRun", "app1", "web", structs.ProcessRunOptions{Command: options.String("sleep 7200"), Release: options.String("release1")}).Return(fxProcess(), nil)
|
||||
i.On("ProcessGet", "app1", "pid1").Return(fxProcessPending(), nil).Twice()
|
||||
i.On("ProcessGet", "app1", "pid1").Return(fxProcess(), nil)
|
||||
opts := structs.ProcessExecOptions{Entrypoint: options.Bool(true), Tty: options.Bool(false)}
|
||||
i.On("ProcessExec", "app1", "pid1", "make test", mock.Anything, opts).Return(4, nil).Run(func(args mock.Arguments) {
|
||||
data, err := ioutil.ReadAll(args.Get(3).(io.Reader))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "in", string(data))
|
||||
args.Get(3).(io.Writer).Write([]byte("out"))
|
||||
})
|
||||
i.On("ProcessStop", "app1", "pid1").Return(nil)
|
||||
|
||||
res, err := testExecute(e, "test ./testdata/httpd -a app1 -d foo -t 7200", strings.NewReader("in"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: exit 4"})
|
||||
res.RequireStdout(t, []string{
|
||||
"Packaging source... OK",
|
||||
"Uploading source... OK",
|
||||
"Starting build... OK",
|
||||
"log1",
|
||||
"log2",
|
||||
"Running make test on web",
|
||||
"out",
|
||||
})
|
||||
})
|
||||
}
|
||||
BIN
pkg/cli/testdata/app.nobuild.tgz
vendored
Normal file
BIN
pkg/cli/testdata/app.nobuild.tgz
vendored
Normal file
Binary file not shown.
BIN
pkg/cli/testdata/app.noparams.tgz
vendored
Normal file
BIN
pkg/cli/testdata/app.noparams.tgz
vendored
Normal file
Binary file not shown.
BIN
pkg/cli/testdata/app.sameparams.tgz
vendored
Normal file
BIN
pkg/cli/testdata/app.sameparams.tgz
vendored
Normal file
Binary file not shown.
BIN
pkg/cli/testdata/app.tgz
vendored
Normal file
BIN
pkg/cli/testdata/app.tgz
vendored
Normal file
Binary file not shown.
1
pkg/cli/testdata/base/file
vendored
Normal file
1
pkg/cli/testdata/base/file
vendored
Normal file
@ -0,0 +1 @@
|
||||
file
|
||||
BIN
pkg/cli/testdata/build.tgz
vendored
Normal file
BIN
pkg/cli/testdata/build.tgz
vendored
Normal file
Binary file not shown.
1
pkg/cli/testdata/cert.pem
vendored
Normal file
1
pkg/cli/testdata/cert.pem
vendored
Normal file
@ -0,0 +1 @@
|
||||
cert
|
||||
1
pkg/cli/testdata/chain.pem
vendored
Normal file
1
pkg/cli/testdata/chain.pem
vendored
Normal file
@ -0,0 +1 @@
|
||||
chain
|
||||
1
pkg/cli/testdata/file
vendored
Normal file
1
pkg/cli/testdata/file
vendored
Normal file
@ -0,0 +1 @@
|
||||
file
|
||||
BIN
pkg/cli/testdata/file.tar
vendored
Normal file
BIN
pkg/cli/testdata/file.tar
vendored
Normal file
Binary file not shown.
1
pkg/cli/testdata/httpd/Dockerfile
vendored
Normal file
1
pkg/cli/testdata/httpd/Dockerfile
vendored
Normal file
@ -0,0 +1 @@
|
||||
FROM httpd
|
||||
4
pkg/cli/testdata/httpd/convox-dockerfile.yml
vendored
Normal file
4
pkg/cli/testdata/httpd/convox-dockerfile.yml
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
port: 80
|
||||
4
pkg/cli/testdata/httpd/convox.yml
vendored
Normal file
4
pkg/cli/testdata/httpd/convox.yml
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
services:
|
||||
web:
|
||||
image: httpd
|
||||
port: 80
|
||||
7
pkg/cli/testdata/httpd/docker-compose.yml
vendored
Normal file
7
pkg/cli/testdata/httpd/docker-compose.yml
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
web:
|
||||
image: httpd
|
||||
labels:
|
||||
- convox.port.443.protocol=tls
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:80
|
||||
1
pkg/cli/testdata/key.pem
vendored
Normal file
1
pkg/cli/testdata/key.pem
vendored
Normal file
@ -0,0 +1 @@
|
||||
key
|
||||
54
pkg/cli/update.go
Normal file
54
pkg/cli/update.go
Normal file
@ -0,0 +1,54 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
cv "github.com/convox/version"
|
||||
update "github.com/inconshreveable/go-update"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerWithoutProvider("update", "update the cli", Update, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Validate: stdcli.ArgsMax(1),
|
||||
})
|
||||
}
|
||||
|
||||
func Update(rack sdk.Interface, c *stdcli.Context) error {
|
||||
target := c.Arg(0)
|
||||
|
||||
// if no version specified, find the latest version
|
||||
if target == "" {
|
||||
v, err := cv.Latest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target = v
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://s3.amazonaws.com/convox/release/%s/cli/%s/%s", target, runtime.GOOS, executableName())
|
||||
|
||||
res, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("invalid version")
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
c.Startf("Updating to <release>%s</release>", target)
|
||||
|
||||
if err := update.Apply(res.Body, update.Options{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OK()
|
||||
}
|
||||
45
pkg/cli/version.go
Normal file
45
pkg/cli/version.go
Normal file
@ -0,0 +1,45 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/convox/convox/sdk"
|
||||
"github.com/convox/stdcli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("version", "display version information", Version, stdcli.CommandOptions{
|
||||
Flags: []stdcli.Flag{flagRack},
|
||||
Validate: stdcli.Args(0),
|
||||
})
|
||||
}
|
||||
|
||||
func Version(rack sdk.Interface, c *stdcli.Context) error {
|
||||
c.Writef("client: <info>%s</info>\n", c.Version())
|
||||
|
||||
host, err := currentHost(c)
|
||||
if err != nil {
|
||||
c.Writef("server: <info>none</info>\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
ep, err := currentEndpoint(c, currentRack(c, host))
|
||||
if err != nil {
|
||||
c.Writef("server: <info>none</info>\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
s, err := rack.SystemGet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eu, err := url.Parse(ep)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Writef("server: <info>%s</info> (<info>%s</info>)\n", s.Version, eu.Host)
|
||||
|
||||
return nil
|
||||
}
|
||||
109
pkg/cli/version_test.go
Normal file
109
pkg/cli/version_test.go
Normal file
@ -0,0 +1,109 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/convox/convox/pkg/cli"
|
||||
mocksdk "github.com/convox/convox/pkg/mock/sdk"
|
||||
mockstdcli "github.com/convox/convox/pkg/mock/stdcli"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
me := &mockstdcli.Executor{}
|
||||
me.On("Execute", "kubectl", "get", "ns", "--selector=system=convox,type=rack", "--output=name").Return([]byte("namespace/dev\n"), nil)
|
||||
e.Executor = me
|
||||
|
||||
err := ioutil.WriteFile(filepath.Join(e.Settings, "host"), []byte("host1"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
i.On("SystemGet").Return(fxSystem(), nil)
|
||||
|
||||
res, err := testExecute(e, "version", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"client: test",
|
||||
"server: 21000101000000 (host1)",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestVersionError(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
me := &mockstdcli.Executor{}
|
||||
me.On("Execute", "kubectl", "get", "ns", "--selector=system=convox,type=rack", "--output=name").Return([]byte("namespace/dev\n"), nil)
|
||||
e.Executor = me
|
||||
|
||||
err := ioutil.WriteFile(filepath.Join(e.Settings, "host"), []byte("host1"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
i.On("SystemGet").Return(nil, fmt.Errorf("err1"))
|
||||
|
||||
res, err := testExecute(e, "version", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, res.Code)
|
||||
res.RequireStderr(t, []string{"ERROR: err1"})
|
||||
res.RequireStdout(t, []string{
|
||||
"client: test",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestVersionNoSystem(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
me := &mockstdcli.Executor{}
|
||||
me.On("Execute", "kubectl", "get", "ns", "--selector=system=convox,type=rack", "--output=name").Return([]byte(""), nil)
|
||||
e.Executor = me
|
||||
|
||||
res, err := testExecute(e, "version", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"client: test",
|
||||
"server: none",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestVersionNoSystemMultipleLocal(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
me := &mockstdcli.Executor{}
|
||||
me.On("Execute", "kubectl", "get", "ns", "--selector=system=convox,type=rack", "--output=name").Return([]byte("namespace/dev\nnamespace/dev2\n"), nil)
|
||||
e.Executor = me
|
||||
|
||||
res, err := testExecute(e, "version", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"client: test",
|
||||
"server: none",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestVersionNoSystemSingleLocal(t *testing.T) {
|
||||
testClient(t, func(e *cli.Engine, i *mocksdk.Interface) {
|
||||
me := &mockstdcli.Executor{}
|
||||
me.On("Execute", "kubectl", "get", "ns", "--selector=system=convox,type=rack", "--output=name").Return([]byte("namespace/dev\n"), nil)
|
||||
e.Executor = me
|
||||
|
||||
i.On("SystemGet").Return(fxSystemLocal(), nil)
|
||||
|
||||
res, err := testExecute(e, "version", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, res.Code)
|
||||
res.RequireStderr(t, []string{""})
|
||||
res.RequireStdout(t, []string{
|
||||
"client: test",
|
||||
"server: dev (rack.dev)",
|
||||
})
|
||||
})
|
||||
}
|
||||
2042
pkg/mock/sdk/interface.go
Normal file
2042
pkg/mock/sdk/interface.go
Normal file
File diff suppressed because it is too large
Load Diff
31
pkg/mock/start/interface.go
Normal file
31
pkg/mock/start/interface.go
Normal file
@ -0,0 +1,31 @@
|
||||
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||
|
||||
package sdk
|
||||
|
||||
import (
|
||||
context "context"
|
||||
io "io"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
start "github.com/convox/convox/pkg/start"
|
||||
)
|
||||
|
||||
// Interface is an autogenerated mock type for the Interface type
|
||||
type Interface struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Start2 provides a mock function with given fields: _a0, _a1, _a2
|
||||
func (_m *Interface) Start2(_a0 context.Context, _a1 io.Writer, _a2 start.Options2) error {
|
||||
ret := _m.Called(_a0, _a1, _a2)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, io.Writer, start.Options2) error); ok {
|
||||
r0 = rf(_a0, _a1, _a2)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
86
pkg/mock/stdcli/executor.go
Normal file
86
pkg/mock/stdcli/executor.go
Normal file
@ -0,0 +1,86 @@
|
||||
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||
|
||||
package stdcli
|
||||
|
||||
import (
|
||||
io "io"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Executor is an autogenerated mock type for the Executor type
|
||||
type Executor struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Execute provides a mock function with given fields: cmd, args
|
||||
func (_m *Executor) Execute(cmd string, args ...string) ([]byte, error) {
|
||||
_va := make([]interface{}, len(args))
|
||||
for _i := range args {
|
||||
_va[_i] = args[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, cmd)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
var r0 []byte
|
||||
if rf, ok := ret.Get(0).(func(string, ...string) []byte); ok {
|
||||
r0 = rf(cmd, args...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, ...string) error); ok {
|
||||
r1 = rf(cmd, args...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Run provides a mock function with given fields: w, cmd, args
|
||||
func (_m *Executor) Run(w io.Writer, cmd string, args ...string) error {
|
||||
_va := make([]interface{}, len(args))
|
||||
for _i := range args {
|
||||
_va[_i] = args[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, w, cmd)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(io.Writer, string, ...string) error); ok {
|
||||
r0 = rf(w, cmd, args...)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Terminal provides a mock function with given fields: cmd, args
|
||||
func (_m *Executor) Terminal(cmd string, args ...string) error {
|
||||
_va := make([]interface{}, len(args))
|
||||
for _i := range args {
|
||||
_va[_i] = args[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, cmd)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, ...string) error); ok {
|
||||
r0 = rf(cmd, args...)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
75
pkg/prefix/writer.go
Normal file
75
pkg/prefix/writer.go
Normal file
@ -0,0 +1,75 @@
|
||||
package prefix
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
ScannerStartSize = 4096
|
||||
ScannerMaxSize = 20 * 1024 * 1024
|
||||
)
|
||||
|
||||
type Writer struct {
|
||||
lock sync.Mutex
|
||||
max int
|
||||
prefixes map[string]string
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
func NewWriter(w io.Writer, prefixes map[string]string) Writer {
|
||||
max := 0
|
||||
|
||||
for k := range prefixes {
|
||||
if l := len(k); l > max {
|
||||
max = l
|
||||
}
|
||||
}
|
||||
|
||||
return Writer{max: max, prefixes: prefixes, writer: w}
|
||||
}
|
||||
|
||||
func (w Writer) Write(prefix string, r io.Reader) {
|
||||
s := bufio.NewScanner(r)
|
||||
|
||||
s.Buffer(make([]byte, ScannerStartSize), ScannerMaxSize)
|
||||
|
||||
for s.Scan() {
|
||||
w.Writef(prefix, "%s\n", s.Text())
|
||||
}
|
||||
|
||||
if err := s.Err(); err != nil {
|
||||
w.Writef(prefix, "scan error: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (w Writer) Writer(prefix string) io.Writer {
|
||||
rr, ww := io.Pipe()
|
||||
|
||||
go w.Write(prefix, rr)
|
||||
|
||||
return ww
|
||||
}
|
||||
|
||||
func (w Writer) Writef(prefix string, format string, args ...interface{}) {
|
||||
w.lock.Lock()
|
||||
defer w.lock.Unlock()
|
||||
|
||||
line := fmt.Sprintf(w.format(prefix), prefix, fmt.Sprintf(format, args...))
|
||||
|
||||
fmt.Fprintf(w.writer, "%s", line)
|
||||
}
|
||||
|
||||
func (w Writer) format(prefix string) string {
|
||||
ot := ""
|
||||
ct := ""
|
||||
|
||||
if t := w.prefixes[prefix]; t != "" {
|
||||
ot = fmt.Sprintf("<%s>", t)
|
||||
ct = fmt.Sprintf("</%s>", t)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%%-%ds%s | %%s", ot, w.max, ct)
|
||||
}
|
||||
806
pkg/start/gen2.go
Normal file
806
pkg/start/gen2.go
Normal file
@ -0,0 +1,806 @@
|
||||
package start
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/convox/changes"
|
||||
"github.com/convox/convox/pkg/common"
|
||||
"github.com/convox/convox/pkg/manifest"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/prefix"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/docker/docker/builder/dockerignore"
|
||||
)
|
||||
|
||||
const (
|
||||
ScannerStartSize = 4096
|
||||
ScannerMaxSize = 20 * 1024 * 1024
|
||||
)
|
||||
|
||||
var (
|
||||
reAppLog = regexp.MustCompile(`^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})Z ([^/]+)/([^/]+)/([^ ]+) (.*)$`)
|
||||
reDockerOption = regexp.MustCompile("--([a-z]+)")
|
||||
)
|
||||
|
||||
type Options2 struct {
|
||||
App string
|
||||
Build bool
|
||||
Cache bool
|
||||
Manifest string
|
||||
Provider structs.Provider
|
||||
Services []string
|
||||
Sync bool
|
||||
Test bool
|
||||
}
|
||||
|
||||
type buildSource struct {
|
||||
Local string
|
||||
Remote string
|
||||
}
|
||||
|
||||
func (s *Start) Start2(ctx context.Context, w io.Writer, opts Options2) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
if opts.App == "" {
|
||||
return errors.WithStack(fmt.Errorf("app required"))
|
||||
}
|
||||
|
||||
a, err := opts.Provider.AppGet(opts.App)
|
||||
if err != nil {
|
||||
if _, err := opts.Provider.AppCreate(opts.App, structs.AppCreateOptions{Generation: options.String("2")}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
} else {
|
||||
if a.Generation != "2" && a.Generation != "3" {
|
||||
return errors.WithStack(fmt.Errorf("invalid generation: %s", a.Generation))
|
||||
}
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(common.CoalesceString(opts.Manifest, "convox.yml"))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
env, err := common.AppEnvironment(opts.Provider, opts.App)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
m, err := manifest.Load(data, env)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
services := map[string]bool{}
|
||||
|
||||
if opts.Services == nil {
|
||||
for _, s := range m.Services {
|
||||
services[s.Name] = true
|
||||
}
|
||||
} else {
|
||||
for _, s := range opts.Services {
|
||||
services[s] = true
|
||||
}
|
||||
}
|
||||
|
||||
pw := prefixWriter(w, services)
|
||||
|
||||
if opts.Build {
|
||||
pw.Writef("build", "uploading source\n")
|
||||
|
||||
data, err := common.Tarball(".")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
o, err := opts.Provider.ObjectStore(opts.App, "", bytes.NewReader(data), structs.ObjectStoreOptions{})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
pw.Writef("build", "starting build\n")
|
||||
|
||||
bopts := structs.BuildCreateOptions{Development: options.Bool(true)}
|
||||
|
||||
if opts.Manifest != "" {
|
||||
bopts.Manifest = options.String(opts.Manifest)
|
||||
}
|
||||
|
||||
b, err := opts.Provider.BuildCreate(opts.App, o.Url, bopts)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
logs, err := opts.Provider.BuildLogs(opts.App, b.Id, structs.LogsOptions{})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
bo := pw.Writer("build")
|
||||
|
||||
go io.Copy(bo, logs)
|
||||
|
||||
if err := opts.waitForBuild(ctx, b.Id); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
b, err = opts.Provider.BuildGet(opts.App, b.Id)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
popts := structs.ReleasePromoteOptions{
|
||||
Development: options.Bool(true),
|
||||
Force: options.Bool(true),
|
||||
Idle: options.Bool(false),
|
||||
Min: options.Int(0),
|
||||
Timeout: options.Int(300),
|
||||
}
|
||||
|
||||
if err := opts.Provider.ReleasePromote(opts.App, b.Release, popts); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
go opts.streamLogs(ctx, pw, services)
|
||||
|
||||
errch := make(chan error)
|
||||
defer close(errch)
|
||||
|
||||
go handleErrors(ctx, pw, errch)
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
for _, s := range m.Services {
|
||||
if !services[s.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
if s.Build.Path != "" {
|
||||
go opts.watchChanges(ctx, pw, m, s.Name, wd, errch)
|
||||
}
|
||||
}
|
||||
|
||||
if err := common.WaitForAppRunningContext(ctx, opts.Provider, opts.App); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
a, err = opts.Provider.AppGet(opts.App)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
pw.Writef("convox", "stopping\n")
|
||||
|
||||
if a.Release != "" {
|
||||
popts := structs.ReleasePromoteOptions{
|
||||
Development: options.Bool(false),
|
||||
Force: options.Bool(true),
|
||||
}
|
||||
|
||||
if err := opts.Provider.ReleasePromote(opts.App, a.Release, popts); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (opts Options2) handleAdds(pid, remote string, adds []changes.Change) error {
|
||||
if len(adds) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(remote) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
if _, err := opts.Provider.ProcessExec(opts.App, pid, "pwd", &buf, structs.ProcessExecOptions{}); err != nil {
|
||||
return errors.WithStack(fmt.Errorf("%s pwd: %s", pid, err))
|
||||
}
|
||||
|
||||
wd := strings.TrimSpace(buf.String())
|
||||
|
||||
remote = filepath.Join(wd, remote)
|
||||
}
|
||||
|
||||
rp, wp := io.Pipe()
|
||||
|
||||
ch := make(chan error)
|
||||
|
||||
go func() {
|
||||
ch <- opts.Provider.FilesUpload(opts.App, pid, rp)
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
tw := tar.NewWriter(wp)
|
||||
|
||||
for _, add := range adds {
|
||||
local := filepath.Join(add.Base, add.Path)
|
||||
|
||||
stat, err := os.Stat(local)
|
||||
if err != nil {
|
||||
// skip transient files like '.git/.COMMIT_EDITMSG.swp'
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
tw.WriteHeader(&tar.Header{
|
||||
Name: filepath.Join(remote, add.Path),
|
||||
Mode: int64(stat.Mode()),
|
||||
Size: stat.Size(),
|
||||
ModTime: stat.ModTime(),
|
||||
})
|
||||
|
||||
fd, err := os.Open(local)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer fd.Close()
|
||||
|
||||
if _, err := io.Copy(tw, fd); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
fd.Close()
|
||||
}
|
||||
|
||||
if err := tw.Close(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := wp.Close(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return <-ch
|
||||
}
|
||||
|
||||
func (opts Options2) handleRemoves(pid string, removes []changes.Change) error {
|
||||
if len(removes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return opts.Provider.FilesDelete(opts.App, pid, changes.Files(removes))
|
||||
}
|
||||
|
||||
func (opts Options2) healthCheck(ctx context.Context, pw prefix.Writer, s manifest.Service, errch chan error, wg *sync.WaitGroup) {
|
||||
rss, err := opts.Provider.ServiceList(opts.App)
|
||||
if err != nil {
|
||||
errch <- err
|
||||
return
|
||||
}
|
||||
|
||||
hostname := ""
|
||||
|
||||
for _, rs := range rss {
|
||||
if rs.Name == s.Name {
|
||||
hostname = rs.Domain
|
||||
}
|
||||
}
|
||||
|
||||
if hostname == "" {
|
||||
errch <- fmt.Errorf("could not find hostname for service: %s", s.Name)
|
||||
return
|
||||
}
|
||||
|
||||
pw.Writef("convox", "starting health check for <service>%s</service> on path <setting>%s</setting> with <setting>%d</setting>s interval, <setting>%d</setting>s grace\n", s.Name, s.Health.Path, s.Health.Interval, s.Health.Grace)
|
||||
|
||||
wg.Done()
|
||||
|
||||
hcu := fmt.Sprintf("https://%s%s", hostname, s.Health.Path)
|
||||
|
||||
grace := time.Duration(s.Health.Grace) * time.Second
|
||||
interval := time.Duration(s.Health.Interval) * time.Second
|
||||
|
||||
if opts.Test {
|
||||
grace = 5 * time.Millisecond
|
||||
interval = 5 * time.Millisecond
|
||||
}
|
||||
|
||||
time.Sleep(grace)
|
||||
|
||||
tick := time.Tick(interval)
|
||||
|
||||
c := &http.Client{
|
||||
Timeout: time.Duration(s.Health.Timeout) * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// previous status code
|
||||
var ps int
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-tick:
|
||||
res, err := c.Get(hcu)
|
||||
if err != nil {
|
||||
pw.Writef("convox", "health check <service>%s</service>: <fail>%s</fail>\n", s.Name, err.Error())
|
||||
continue
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode < 200 || res.StatusCode > 399 {
|
||||
pw.Writef("convox", "health check <service>%s</service>: <fail>%d</fail>\n", s.Name, res.StatusCode)
|
||||
} else if res.StatusCode != ps {
|
||||
pw.Writef("convox", "health check <service>%s</service>: <ok>%d</ok>\n", s.Name, res.StatusCode)
|
||||
}
|
||||
|
||||
ps = res.StatusCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (opts Options2) stopProcess(pid string, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
opts.Provider.ProcessStop(opts.App, pid)
|
||||
}
|
||||
|
||||
func (opts Options2) streamLogs(ctx context.Context, pw prefix.Writer, services map[string]bool) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
logs, err := opts.Provider.AppLogs(opts.App, structs.LogsOptions{Prefix: options.Bool(true), Since: options.Duration(1 * time.Second)})
|
||||
if err == nil {
|
||||
writeLogs(ctx, pw, logs, services)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (opts Options2) waitForBuild(ctx context.Context, id string) error {
|
||||
tick := time.Tick(1 * time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-tick:
|
||||
b, err := opts.Provider.BuildGet(opts.App, id)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
switch b.Status {
|
||||
case "created", "running":
|
||||
break
|
||||
case "complete":
|
||||
return nil
|
||||
case "failed":
|
||||
return errors.WithStack(fmt.Errorf("build failed"))
|
||||
default:
|
||||
return errors.WithStack(fmt.Errorf("unknown build status: %s", b.Status))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (opts Options2) watchChanges(ctx context.Context, pw prefix.Writer, m *manifest.Manifest, service, root string, ch chan error) {
|
||||
bss, err := buildSources(m, root, service)
|
||||
if err != nil {
|
||||
ch <- fmt.Errorf("sync error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
ignores, err := buildIgnores(root, service)
|
||||
if err != nil {
|
||||
ch <- fmt.Errorf("sync error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, bs := range bss {
|
||||
go opts.watchPath(ctx, pw, service, root, bs, ignores, ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (opts Options2) watchPath(ctx context.Context, pw prefix.Writer, service, root string, bs buildSource, ignores []string, ch chan error) {
|
||||
cch := make(chan changes.Change, 1)
|
||||
|
||||
abs, err := filepath.Abs(bs.Local)
|
||||
if err != nil {
|
||||
ch <- fmt.Errorf("sync error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
ch <- fmt.Errorf("sync error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(wd, bs.Local)
|
||||
if err != nil {
|
||||
ch <- fmt.Errorf("sync error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
pw.Writef("convox", "starting sync from <dir>%s</dir> to <dir>%s</dir> on <service>%s</service>\n", rel, common.CoalesceString(bs.Remote, "."), service)
|
||||
|
||||
go changes.Watch(abs, cch, changes.WatchOptions{
|
||||
Ignores: ignores,
|
||||
})
|
||||
|
||||
tick := time.Tick(1000 * time.Millisecond)
|
||||
chgs := []changes.Change{}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case c := <-cch:
|
||||
chgs = append(chgs, c)
|
||||
case <-tick:
|
||||
if len(chgs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
pss, err := opts.Provider.ProcessList(opts.App, structs.ProcessListOptions{Service: options.String(service)})
|
||||
if err != nil {
|
||||
pw.Writef("convox", "sync error: %s\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
adds, removes := changes.Partition(chgs)
|
||||
|
||||
for _, ps := range pss {
|
||||
switch {
|
||||
case len(adds) > 3:
|
||||
pw.Writef("convox", "sync: %d files to <dir>%s</dir> on <service>%s</service>\n", len(adds), common.CoalesceString(bs.Remote, "."), service)
|
||||
case len(adds) > 0:
|
||||
for _, a := range adds {
|
||||
pw.Writef("convox", "sync: <dir>%s</dir> to <dir>%s</dir> on <service>%s</service>\n", a.Path, common.CoalesceString(bs.Remote, "."), service)
|
||||
}
|
||||
}
|
||||
|
||||
if err := opts.handleAdds(ps.Id, bs.Remote, adds); err != nil {
|
||||
pw.Writef("convox", "sync add error: %s\n", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(removes) > 3:
|
||||
pw.Writef("convox", "remove: %d files from <dir>%s</dir> to <service>%s</service>\n", len(removes), common.CoalesceString(bs.Remote, "."), service)
|
||||
case len(removes) > 0:
|
||||
for _, r := range removes {
|
||||
pw.Writef("convox", "remove: <dir>%s</dir> from <dir>%s</dir> on <service>%s</service>\n", r.Path, common.CoalesceString(bs.Remote, "."), service)
|
||||
}
|
||||
}
|
||||
|
||||
if err := opts.handleRemoves(ps.Id, removes); err != nil {
|
||||
pw.Writef("convox", "sync remove error: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
chgs = []changes.Change{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildDockerfile(m *manifest.Manifest, root, service string) ([]byte, error) {
|
||||
s, err := m.Service(service)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if s.Image != "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
path, err := filepath.Abs(filepath.Join(root, s.Build.Path, s.Build.Manifest))
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil, errors.WithStack(fmt.Errorf("no such file: %s", filepath.Join(s.Build.Path, s.Build.Manifest)))
|
||||
}
|
||||
|
||||
return ioutil.ReadFile(path)
|
||||
}
|
||||
|
||||
func buildIgnores(root, service string) ([]string, error) {
|
||||
fd, err := os.Open(filepath.Join(root, ".dockerignore"))
|
||||
if os.IsNotExist(err) {
|
||||
return []string{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return dockerignore.ReadAll(fd)
|
||||
}
|
||||
|
||||
func buildSources(m *manifest.Manifest, root, service string) ([]buildSource, error) {
|
||||
data, err := buildDockerfile(m, root, service)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
if data == nil {
|
||||
return []buildSource{}, nil
|
||||
}
|
||||
|
||||
svc, err := m.Service(service)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
bs := []buildSource{}
|
||||
env := map[string]string{}
|
||||
wd := ""
|
||||
|
||||
s := bufio.NewScanner(bytes.NewReader(data))
|
||||
|
||||
lines:
|
||||
for s.Scan() {
|
||||
parts := strings.Fields(s.Text())
|
||||
|
||||
if len(parts) < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch strings.ToUpper(parts[0]) {
|
||||
case "ADD", "COPY":
|
||||
for i, p := range parts {
|
||||
if m := reDockerOption.FindStringSubmatch(p); len(m) > 1 {
|
||||
switch strings.ToLower(m[1]) {
|
||||
case "from":
|
||||
continue lines
|
||||
default:
|
||||
parts = append(parts[:i], parts[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(parts) > 2 {
|
||||
u, err := url.Parse(parts[1])
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(parts[1], "--from") {
|
||||
continue
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "http", "https":
|
||||
// do nothing
|
||||
default:
|
||||
local := filepath.Join(svc.Build.Path, parts[1])
|
||||
remote := replaceEnv(parts[2], env)
|
||||
|
||||
if wd != "" && !filepath.IsAbs(remote) {
|
||||
remote = filepath.Join(wd, remote)
|
||||
}
|
||||
|
||||
bs = append(bs, buildSource{Local: local, Remote: remote})
|
||||
}
|
||||
}
|
||||
case "ENV":
|
||||
if len(parts) > 2 {
|
||||
env[parts[1]] = parts[2]
|
||||
}
|
||||
case "FROM":
|
||||
if len(parts) > 1 {
|
||||
var ee []string
|
||||
|
||||
data, err := Exec.Execute("docker", "inspect", parts[1], "--format", "{{json .Config.Env}}")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &ee); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
for _, e := range ee {
|
||||
parts := strings.SplitN(e, "=", 2)
|
||||
|
||||
if len(parts) == 2 {
|
||||
env[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
data, err = Exec.Execute("docker", "inspect", parts[1], "--format", "{{.Config.WorkingDir}}")
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
wd = strings.TrimSpace(string(data))
|
||||
}
|
||||
case "WORKDIR":
|
||||
if len(parts) > 1 {
|
||||
wd = replaceEnv(parts[1], env)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := range bs {
|
||||
abs, err := filepath.Abs(bs[i].Local)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
stat, err := os.Stat(abs)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if stat.IsDir() && !strings.HasSuffix(abs, "/") {
|
||||
abs = abs + "/"
|
||||
}
|
||||
|
||||
bs[i].Local = abs
|
||||
|
||||
if bs[i].Remote == "." {
|
||||
bs[i].Remote = wd
|
||||
}
|
||||
}
|
||||
|
||||
bss := []buildSource{}
|
||||
|
||||
for i := range bs {
|
||||
contained := false
|
||||
|
||||
for j := i + 1; j < len(bs); j++ {
|
||||
if strings.HasPrefix(bs[i].Local, bs[j].Local) {
|
||||
if bs[i].Remote == bs[j].Remote {
|
||||
contained = true
|
||||
break
|
||||
}
|
||||
|
||||
rl, err := filepath.Rel(bs[j].Local, bs[i].Local)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
rr, err := filepath.Rel(bs[j].Remote, bs[i].Remote)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if rl == rr {
|
||||
contained = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !contained {
|
||||
bss = append(bss, bs[i])
|
||||
}
|
||||
}
|
||||
|
||||
return bss, nil
|
||||
}
|
||||
|
||||
type stackTracer interface {
|
||||
StackTrace() errors.StackTrace
|
||||
}
|
||||
|
||||
func handleErrors(ctx context.Context, pw prefix.Writer, errch chan error) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case err := <-errch:
|
||||
if err != nil {
|
||||
pw.Writef("convox", "<error>error: %s</error>\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replaceEnv(s string, env map[string]string) string {
|
||||
for k, v := range env {
|
||||
s = strings.Replace(s, fmt.Sprintf("${%s}", k), v, -1)
|
||||
s = strings.Replace(s, fmt.Sprintf("$%s", k), v, -1)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
var ansiScreenSequences = []*regexp.Regexp{
|
||||
regexp.MustCompile("\033\\[\\d+;\\d+H"),
|
||||
}
|
||||
|
||||
func stripANSIScreenCommands(data string) string {
|
||||
for _, r := range ansiScreenSequences {
|
||||
data = r.ReplaceAllString(data, "")
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func writeLogs(ctx context.Context, pw prefix.Writer, r io.Reader, services map[string]bool) {
|
||||
ls := bufio.NewScanner(r)
|
||||
|
||||
ls.Buffer(make([]byte, ScannerStartSize), ScannerMaxSize)
|
||||
|
||||
for ls.Scan() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
match := reAppLog.FindStringSubmatch(ls.Text())
|
||||
|
||||
if len(match) != 7 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch match[3] {
|
||||
case "service":
|
||||
service := match[4]
|
||||
|
||||
if !services[service] {
|
||||
continue
|
||||
}
|
||||
|
||||
stripped := stripANSIScreenCommands(match[6])
|
||||
|
||||
pw.Writef(service, "%s\n", stripped)
|
||||
case "system":
|
||||
service := strings.Split(match[5], "-")[0]
|
||||
|
||||
if !services[service] {
|
||||
continue
|
||||
}
|
||||
|
||||
pw.Writef(service, "%s\n", match[6])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := ls.Err(); err != nil {
|
||||
pw.Writef("convox", "scan error: %s\n", err)
|
||||
}
|
||||
}
|
||||
141
pkg/start/gen2_test.go
Normal file
141
pkg/start/gen2_test.go
Normal file
@ -0,0 +1,141 @@
|
||||
package start_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/convox/exec"
|
||||
"github.com/convox/convox/pkg/common"
|
||||
"github.com/convox/convox/pkg/options"
|
||||
"github.com/convox/convox/pkg/start"
|
||||
"github.com/convox/convox/pkg/structs"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestStart2(t *testing.T) {
|
||||
p := &structs.MockProvider{}
|
||||
|
||||
mhc := mockHealthCheck(func(n int) int {
|
||||
return 200
|
||||
})
|
||||
defer mhc.Close()
|
||||
|
||||
logs := "0000-00-00T00:00:00Z service/web/pid1 log1\n0000-00-00T00:00:00Z service/web/pid1 log2\n"
|
||||
|
||||
p.On("AppGet", "app1").Return(&structs.App{Name: "app1", Generation: "2"}, nil)
|
||||
p.On("ReleaseList", "app1", structs.ReleaseListOptions{Limit: options.Int(1)}).Return(structs.Releases{{Id: "release1"}}, nil)
|
||||
p.On("ReleaseGet", "app1", "release1").Return(&structs.Release{}, nil)
|
||||
p.On("AppLogs", "app1", structs.LogsOptions{Prefix: options.Bool(true), Since: options.Duration(1 * time.Second)}).Return(ioutil.NopCloser(strings.NewReader(logs)), nil)
|
||||
|
||||
e := &exec.MockInterface{}
|
||||
start.Exec = e
|
||||
|
||||
e.On("Execute", "docker", "inspect", "httpd", "--format", "{{json .Config.Env}}").Return([]byte(`["FOO=bar","BAZ=qux"]`), nil)
|
||||
e.On("Execute", "docker", "inspect", "httpd", "--format", "{{.Config.WorkingDir}}").Return([]byte(`/app/foo`), nil)
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
os.Chdir("testdata/httpd")
|
||||
defer os.Chdir(cwd)
|
||||
|
||||
s := start.New()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
|
||||
opts := start.Options2{
|
||||
App: "app1",
|
||||
Provider: p,
|
||||
Test: true,
|
||||
}
|
||||
|
||||
err = s.Start2(ctx, &buf, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t,
|
||||
[]string{
|
||||
"<color3>web </color3> | log1",
|
||||
"<color3>web </color3> | log2",
|
||||
"<system>convox</system> | stopping",
|
||||
},
|
||||
strings.Split(strings.TrimSuffix(buf.String(), "\n"), "\n"),
|
||||
)
|
||||
|
||||
p.AssertExpectations(t)
|
||||
e.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestStart2Options(t *testing.T) {
|
||||
helpers.ProviderWaitDuration = 1
|
||||
|
||||
p := &structs.MockProvider{}
|
||||
|
||||
appLogs := "0000-00-00T00:00:00Z service/web/pid1 log1\n0000-00-00T00:00:00Z service/web/pid1 log2\n"
|
||||
buildLogs := "build1\nbuild2\n"
|
||||
|
||||
p.On("AppGet", "app1").Return(&structs.App{Name: "app1", Generation: "2", Release: "old", Status: "running"}, nil)
|
||||
p.On("ReleaseList", "app1", structs.ReleaseListOptions{Limit: options.Int(1)}).Return(structs.Releases{{Id: "release1"}}, nil)
|
||||
p.On("ReleaseGet", "app1", "release1").Return(&structs.Release{}, nil)
|
||||
p.On("ObjectStore", "app1", "", mock.Anything, structs.ObjectStoreOptions{}).Return(&structs.Object{Url: "object://app1/object1.tgz"}, nil)
|
||||
p.On("BuildCreate", "app1", "object://app1/object1.tgz", structs.BuildCreateOptions{Development: options.Bool(true), Manifest: options.String("convox2.yml")}).Return(&structs.Build{Id: "build1"}, nil)
|
||||
p.On("BuildLogs", "app1", "build1", structs.LogsOptions{}).Return(ioutil.NopCloser(strings.NewReader(buildLogs)), nil)
|
||||
p.On("BuildGet", "app1", "build1").Return(&structs.Build{Id: "build1", Release: "release1", Status: "complete"}, nil)
|
||||
p.On("ReleasePromote", "app1", "release1", structs.ReleasePromoteOptions{Development: options.Bool(true), Force: options.Bool(true), Idle: options.Bool(false), Min: options.Int(0), Timeout: options.Int(300)}).Return(nil)
|
||||
p.On("AppLogs", "app1", structs.LogsOptions{Prefix: options.Bool(true), Since: options.Duration(1 * time.Second)}).Return(ioutil.NopCloser(strings.NewReader(appLogs)), nil).Once()
|
||||
p.On("ReleasePromote", "app1", "old", structs.ReleasePromoteOptions{Development: options.Bool(false), Force: options.Bool(true)}).Return(nil)
|
||||
|
||||
e := &exec.MockInterface{}
|
||||
start.Exec = e
|
||||
|
||||
e.On("Execute", "docker", "inspect", "httpd", "--format", "{{json .Config.Env}}").Return([]byte(`["FOO=bar","BAZ=qux"]`), nil)
|
||||
e.On("Execute", "docker", "inspect", "httpd", "--format", "{{.Config.WorkingDir}}").Return([]byte(`/app/foo`), nil)
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
os.Chdir("testdata/httpd")
|
||||
defer os.Chdir(cwd)
|
||||
|
||||
s := start.New()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
|
||||
opts := start.Options2{
|
||||
App: "app1",
|
||||
Build: true,
|
||||
Cache: true,
|
||||
Manifest: "convox2.yml",
|
||||
Provider: p,
|
||||
Sync: true,
|
||||
Test: true,
|
||||
}
|
||||
|
||||
err = s.Start2(ctx, &buf, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t,
|
||||
[]string{
|
||||
"<system>build </system> | uploading source",
|
||||
"<system>build </system> | starting build",
|
||||
"<system>build </system> | build1",
|
||||
"<system>build </system> | build2",
|
||||
"<color3>web </color3> | log1",
|
||||
"<color3>web </color3> | log2",
|
||||
"<system>convox</system> | stopping",
|
||||
},
|
||||
strings.Split(strings.TrimSuffix(buf.String(), "\n"), "\n"),
|
||||
)
|
||||
|
||||
p.AssertExpectations(t)
|
||||
e.AssertExpectations(t)
|
||||
}
|
||||
57
pkg/start/start.go
Normal file
57
pkg/start/start.go
Normal file
@ -0,0 +1,57 @@
|
||||
package start
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/convox/convox/pkg/prefix"
|
||||
"github.com/convox/exec"
|
||||
)
|
||||
|
||||
var (
|
||||
Exec exec.Interface = &exec.Exec{}
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
Start2(context.Context, io.Writer, Options2) error
|
||||
}
|
||||
|
||||
type Start struct{}
|
||||
|
||||
func New() Interface {
|
||||
return &Start{}
|
||||
}
|
||||
|
||||
func prefixHash(prefix string) int {
|
||||
sum := 0
|
||||
|
||||
for c := range prefix {
|
||||
sum += int(c)
|
||||
}
|
||||
|
||||
return sum % 18
|
||||
}
|
||||
|
||||
func prefixWriter(w io.Writer, services map[string]bool) prefix.Writer {
|
||||
prefixes := map[string]string{
|
||||
"build": "system",
|
||||
"convox": "system",
|
||||
}
|
||||
|
||||
for s := range services {
|
||||
prefixes[s] = fmt.Sprintf("color%d", prefixHash(s))
|
||||
}
|
||||
|
||||
return prefix.NewWriter(w, prefixes)
|
||||
}
|
||||
|
||||
func handleInterrupt(fn func()) {
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, os.Interrupt, os.Kill)
|
||||
<-ch
|
||||
fmt.Println("")
|
||||
fn()
|
||||
}
|
||||
27
pkg/start/start_test.go
Normal file
27
pkg/start/start_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
package start_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
)
|
||||
|
||||
type MockHealthCheck struct {
|
||||
*httptest.Server
|
||||
count int
|
||||
handler func(int) int
|
||||
}
|
||||
|
||||
func (mhc *MockHealthCheck) Count() int {
|
||||
return mhc.count
|
||||
}
|
||||
|
||||
func (mhc *MockHealthCheck) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(mhc.handler(mhc.count))
|
||||
mhc.count++
|
||||
}
|
||||
|
||||
func mockHealthCheck(fn func(n int) int) *MockHealthCheck {
|
||||
m := &MockHealthCheck{handler: fn}
|
||||
m.Server = httptest.NewTLSServer(m)
|
||||
return m
|
||||
}
|
||||
1
pkg/start/testdata/httpd/Dockerfile
vendored
Normal file
1
pkg/start/testdata/httpd/Dockerfile
vendored
Normal file
@ -0,0 +1 @@
|
||||
FROM httpd
|
||||
3
pkg/start/testdata/httpd/Dockerfile2
vendored
Normal file
3
pkg/start/testdata/httpd/Dockerfile2
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
FROM httpd
|
||||
|
||||
ARG FOO=qux
|
||||
4
pkg/start/testdata/httpd/convox.yml
vendored
Normal file
4
pkg/start/testdata/httpd/convox.yml
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
port: 80
|
||||
8
pkg/start/testdata/httpd/convox2.yml
vendored
Normal file
8
pkg/start/testdata/httpd/convox2.yml
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
services:
|
||||
web:
|
||||
image: httpd
|
||||
port: 80
|
||||
web2:
|
||||
build:
|
||||
manifest: Dockerfile2
|
||||
path: .
|
||||
@ -982,6 +982,43 @@ func (_m *MockProvider) ReleasePromote(app string, id string, opts ReleasePromot
|
||||
return r0
|
||||
}
|
||||
|
||||
// ResourceConsole provides a mock function with given fields: app, name, rw, opts
|
||||
func (_m *MockProvider) ResourceConsole(app string, name string, rw io.ReadWriter, opts ResourceConsoleOptions) error {
|
||||
ret := _m.Called(app, name, rw, opts)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, io.ReadWriter, ResourceConsoleOptions) error); ok {
|
||||
r0 = rf(app, name, rw, opts)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// ResourceExport provides a mock function with given fields: app, name
|
||||
func (_m *MockProvider) ResourceExport(app string, name string) (io.ReadCloser, error) {
|
||||
ret := _m.Called(app, name)
|
||||
|
||||
var r0 io.ReadCloser
|
||||
if rf, ok := ret.Get(0).(func(string, string) io.ReadCloser); ok {
|
||||
r0 = rf(app, name)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(io.ReadCloser)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
||||
r1 = rf(app, name)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ResourceGet provides a mock function with given fields: app, name
|
||||
func (_m *MockProvider) ResourceGet(app string, name string) (*Resource, error) {
|
||||
ret := _m.Called(app, name)
|
||||
@ -1005,6 +1042,20 @@ func (_m *MockProvider) ResourceGet(app string, name string) (*Resource, error)
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ResourceImport provides a mock function with given fields: app, name, r
|
||||
func (_m *MockProvider) ResourceImport(app string, name string, r io.Reader) error {
|
||||
ret := _m.Called(app, name, r)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, io.Reader) error); ok {
|
||||
r0 = rf(app, name, r)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// ResourceList provides a mock function with given fields: app
|
||||
func (_m *MockProvider) ResourceList(app string) (Resources, error) {
|
||||
ret := _m.Called(app)
|
||||
|
||||
@ -69,7 +69,10 @@ type Provider interface {
|
||||
ReleaseList(app string, opts ReleaseListOptions) (Releases, error)
|
||||
ReleasePromote(app, id string, opts ReleasePromoteOptions) error
|
||||
|
||||
ResourceConsole(app, name string, rw io.ReadWriter, opts ResourceConsoleOptions) error
|
||||
ResourceExport(app, name string) (io.ReadCloser, error)
|
||||
ResourceGet(app, name string) (*Resource, error)
|
||||
ResourceImport(app, name string, r io.Reader) error
|
||||
ResourceList(app string) (Resources, error)
|
||||
|
||||
ServiceList(app string) (Services, error)
|
||||
|
||||
@ -39,6 +39,11 @@ func (rps ResourceParameters) Less(i, j int) bool {
|
||||
return rps[i].Name < rps[j].Name
|
||||
}
|
||||
|
||||
type ResourceConsoleOptions struct {
|
||||
Height *int `header:"Height"`
|
||||
Width *int `header:"Width"`
|
||||
}
|
||||
|
||||
type ResourceCreateOptions struct {
|
||||
Name *string `param:"name"`
|
||||
Parameters map[string]string `param:"parameters"`
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user