add resources console/export/import

This commit is contained in:
David Dollar 2019-11-05 11:03:42 -05:00
parent 53d3b03763
commit dc93453d28
No known key found for this signature in database
GPG Key ID: AFAF263FB45B2124
389 changed files with 32104 additions and 1691 deletions

View File

@ -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}

View File

@ -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

View File

@ -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
View 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
View 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
View File

@ -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
View File

@ -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=

View File

@ -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

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

BIN
pkg/cli/testdata/app.noparams.tgz vendored Normal file

Binary file not shown.

BIN
pkg/cli/testdata/app.sameparams.tgz vendored Normal file

Binary file not shown.

BIN
pkg/cli/testdata/app.tgz vendored Normal file

Binary file not shown.

1
pkg/cli/testdata/base/file vendored Normal file
View File

@ -0,0 +1 @@
file

BIN
pkg/cli/testdata/build.tgz vendored Normal file

Binary file not shown.

1
pkg/cli/testdata/cert.pem vendored Normal file
View File

@ -0,0 +1 @@
cert

1
pkg/cli/testdata/chain.pem vendored Normal file
View File

@ -0,0 +1 @@
chain

1
pkg/cli/testdata/file vendored Normal file
View File

@ -0,0 +1 @@
file

BIN
pkg/cli/testdata/file.tar vendored Normal file

Binary file not shown.

1
pkg/cli/testdata/httpd/Dockerfile vendored Normal file
View File

@ -0,0 +1 @@
FROM httpd

View File

@ -0,0 +1,4 @@
services:
web:
build: .
port: 80

4
pkg/cli/testdata/httpd/convox.yml vendored Normal file
View File

@ -0,0 +1,4 @@
services:
web:
image: httpd
port: 80

View 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
View File

@ -0,0 +1 @@
key

54
pkg/cli/update.go Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
FROM httpd

3
pkg/start/testdata/httpd/Dockerfile2 vendored Normal file
View File

@ -0,0 +1,3 @@
FROM httpd
ARG FOO=qux

4
pkg/start/testdata/httpd/convox.yml vendored Normal file
View File

@ -0,0 +1,4 @@
services:
web:
build: .
port: 80

8
pkg/start/testdata/httpd/convox2.yml vendored Normal file
View File

@ -0,0 +1,8 @@
services:
web:
image: httpd
port: 80
web2:
build:
manifest: Dockerfile2
path: .

View File

@ -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)

View File

@ -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)

View File

@ -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