From 055ea1bfabd2390b432cfb998745109c2028a0f9 Mon Sep 17 00:00:00 2001 From: Andrew Reed Date: Thu, 29 Jun 2017 11:16:08 -0700 Subject: [PATCH 1/2] Client for release and channel management --- .gitignore | 3 ++ Dockerfile | 11 ++++++ Makefile | 44 ++++++++++++++++++++++++ README.md | 11 +++++- client/channel.go | 53 +++++++++++++++++++++++++++++ client/channel_test.go | 60 ++++++++++++++++++++++++++++++++ client/client.go | 56 ++++++++++++++++++++++++++++++ client/client_test.go | 8 +++++ client/release.go | 77 ++++++++++++++++++++++++++++++++++++++++++ client/release_test.go | 34 +++++++++++++++++++ glide.lock | 10 ++++++ glide.yaml | 4 +++ 12 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 client/channel.go create mode 100644 client/channel_test.go create mode 100644 client/client.go create mode 100644 client/client_test.go create mode 100644 client/release.go create mode 100644 client/release_test.go create mode 100644 glide.lock create mode 100644 glide.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..74bfa1712 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +gen/ +vendor/ +.glide diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..fbfaf06c2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.8 + +RUN curl https://glide.sh/get | sh + +ENV PROJECTPATH=/go/src/github.com/replicatedhq/replicated + +RUN go get golang.org/x/tools/cmd/goimports + +WORKDIR $PROJECTPATH + +CMD ["/bin/bash"] diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..3abfcfacc --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +docker: + docker build -t replicatedhq.replicated . + +shell: + docker run --rm -it \ + --volume `pwd`:/go/src/github.com/replicatedhq/replicated \ + replicatedhq.replicated + +clean: + rm -rf gen + +deps: + docker run --rm \ + --volume `pwd`:/go/src/github.com/replicatedhq/replicated \ + replicatedhq.replicated glide install + +test: + go test ./client + +gen: + docker run --rm \ + --volume `pwd`:/local \ + swaggerapi/swagger-codegen-cli generate \ + -Dmodels -DmodelsDocs=false \ + -i https://api.replicated.com/vendor/v1/spec/channels.json \ + -l go \ + -o /local/gen/go/channels + docker run --rm \ + --volume `pwd`:/local \ + swaggerapi/swagger-codegen-cli generate \ + -Dmodels -DmodelsDocs=false \ + -i https://api.replicated.com/vendor/v1/spec/releases.json \ + -l go \ + -o /local/gen/go/releases + sudo chown -R ${USER}:${USER} gen/ + # fix time.Time fields. Codegen generates empty Time struct. + rm gen/go/releases/time.go + sed -i 's/Time/time.Time/' gen/go/releases/app_release_info.go + # import "time" + docker run --rm \ + --volume `pwd`:/go/src/github.com/replicatedhq/replicated \ + replicatedhq.replicated goimports -w gen/go/releases + +build: deps gen diff --git a/README.md b/README.md index dd907a20b..cf8711011 100644 --- a/README.md +++ b/README.md @@ -1 +1,10 @@ -# replicated \ No newline at end of file +# replicated + +This repository provides a client and CLI for interacting with the Replicated Vendor API. +The models are generated from the API's swagger spec. + +## Tests +Set the following env vars to run integration tests against the Vendor API. + * VENDOR_API_KEY + * VENDOR_API_ORIGIN + * VENDOR_APP_ID diff --git a/client/channel.go b/client/channel.go new file mode 100644 index 000000000..1aaa4f01e --- /dev/null +++ b/client/channel.go @@ -0,0 +1,53 @@ +package client + +import ( + "fmt" + "net/http" + + channels "github.com/replicatedhq/replicated/gen/go/channels" +) + +// ListChannels returns all channels for an app. +func (c *Client) ListChannels(appID string) ([]channels.AppChannel, error) { + path := fmt.Sprintf("/v1/app/%s/channels", appID) + appChannels := make([]channels.AppChannel, 0) + err := c.doJSON("GET", path, http.StatusOK, nil, &appChannels) + if err != nil { + return nil, fmt.Errorf("ListChannels: %v", err) + } + return appChannels, nil +} + +// CreateChannel adds a channel to an app. +func (c *Client) CreateChannel(appID, name, desc string) ([]channels.AppChannel, error) { + path := fmt.Sprintf("/v1/app/%s/channel", appID) + body := &channels.Body{ + Name: name, + Description: desc, + } + appChannels := make([]channels.AppChannel, 0) + err := c.doJSON("POST", path, http.StatusOK, body, &appChannels) + if err != nil { + return nil, fmt.Errorf("CreateChannel: %v", err) + } + return appChannels, nil +} + +// ArchiveChannel archives a channel. +func (c *Client) ArchiveChannel(appID, channelID string) error { + endpoint := fmt.Sprintf("%s/v1/app/%s/channel/%s/archive", c.apiOrigin, appID, channelID) + req, err := http.NewRequest("POST", endpoint, nil) + if err != nil { + return err + } + req.Header.Add("Authorization", c.apiKey) + resp, err := (&http.Client{}).Do(req) + if err != nil { + return fmt.Errorf("ArchiveChannel (%s %s): %v", req.Method, endpoint, err) + } + resp.Body.Close() + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("ArchiveChannel (%s %s): status %d", req.Method, endpoint, resp.StatusCode) + } + return nil +} diff --git a/client/channel_test.go b/client/channel_test.go new file mode 100644 index 000000000..8ce779ccd --- /dev/null +++ b/client/channel_test.go @@ -0,0 +1,60 @@ +package client + +import ( + "testing" +) + +func TestListChannels(t *testing.T) { + client := New(apiOrigin, apiKey) + appChannels, err := client.ListChannels(appID) + if err != nil { + t.Fatal(err) + } + if len(appChannels) == 0 { + t.Error("No channels returned from ListChannels") + } +} + +func TestCreateChannel(t *testing.T) { + client := New(apiOrigin, apiKey) + name := "New Channel" + description := "TestCreateChanel" + appChannels, err := client.CreateChannel(appID, name, description) + if err != nil { + t.Fatal(err) + } + if len(appChannels) == 0 { + t.Error("No channels returned from CreateChannel") + } +} + +func TestArchiveChannel(t *testing.T) { + client := New(apiOrigin, apiKey) + // ensure channel exists to delete + name := "Delete me" + description := "TestDeleteChannel" + appChannels, err := client.CreateChannel(appID, name, description) + if err != nil { + t.Fatal(err) + } + var channelID string + for _, appChannel := range appChannels { + if appChannel.Name == name { + channelID = appChannel.Id + break + } + } + err = client.ArchiveChannel(appID, channelID) + if err != nil { + t.Fatal(err) + } + appChannels, err = client.ListChannels(appID) + if err != nil { + t.Fatal(err) + } + for _, appChannel := range appChannels { + if appChannel.Id == channelID { + t.Errorf("Channel %s not successfully archived", channelID) + } + } +} diff --git a/client/client.go b/client/client.go new file mode 100644 index 000000000..5f35fb6d8 --- /dev/null +++ b/client/client.go @@ -0,0 +1,56 @@ +// Package client manages channels and releases through the Replicated Vendor API. +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +// A Client communicates with the Replicated Vendor API. +type Client struct { + apiKey string + apiOrigin string +} + +// New returns a new client. +func New(origin string, apiKey string) *Client { + c := &Client{ + apiKey: apiKey, + apiOrigin: origin, + } + + return c +} + +func (c *Client) doJSON(method, path string, successStatus int, reqBody, respBody interface{}) error { + endpoint := fmt.Sprintf("%s%s", c.apiOrigin, path) + var buf bytes.Buffer + if reqBody != nil { + if err := json.NewEncoder(&buf).Encode(reqBody); err != nil { + return fmt.Errorf("%s %s: %v", method, endpoint, err) + } + } + req, err := http.NewRequest(method, endpoint, &buf) + if err != nil { + return err + } + req.Header.Set("Authorization", c.apiKey) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("%s %s: %v", method, endpoint, err) + } + defer resp.Body.Close() + if resp.StatusCode != successStatus { + return fmt.Errorf("%s %s: status %d", method, endpoint, resp.StatusCode) + } + if respBody != nil { + if err := json.NewDecoder(resp.Body).Decode(respBody); err != nil { + return fmt.Errorf("%s %s response decoding: %v", method, endpoint, err) + } + } + return nil +} diff --git a/client/client_test.go b/client/client_test.go new file mode 100644 index 000000000..800ba8588 --- /dev/null +++ b/client/client_test.go @@ -0,0 +1,8 @@ +// These are integration tests that generate garbage in the Vendor API. +package client + +import "os" + +var apiKey = os.Getenv("VENDOR_API_KEY") +var apiOrigin = os.Getenv("VENDOR_API_ORIGIN") +var appID = os.Getenv("VENDOR_APP_ID") diff --git a/client/release.go b/client/release.go new file mode 100644 index 000000000..2b4248cb4 --- /dev/null +++ b/client/release.go @@ -0,0 +1,77 @@ +package client + +import ( + "fmt" + "net/http" + "strings" + + releases "github.com/replicatedhq/replicated/gen/go/releases" +) + +// ListReleases lists all releases for an app. +func (c *Client) ListReleases(appID string) ([]releases.AppReleaseInfo, error) { + path := fmt.Sprintf("/v1/app/%s/releases", appID) + releases := make([]releases.AppReleaseInfo, 0) + if err := c.doJSON("GET", path, http.StatusOK, nil, &releases); err != nil { + return nil, fmt.Errorf("ListReleases: %v", err) + } + return releases, nil +} + +// CreateRelease adds a release to an app. +func (c *Client) CreateRelease(appID string) (*releases.AppReleaseInfo, error) { + path := fmt.Sprintf("/v1/app/%s/release", appID) + body := &releases.Body{ + Source: "latest", + } + release := &releases.AppReleaseInfo{} + if err := c.doJSON("POST", path, http.StatusCreated, body, release); err != nil { + return nil, fmt.Errorf("CreateRelease: %v", err) + } + return release, nil +} + +// UpdateRelease updates a release's yaml. +func (c *Client) UpdateRelease(appID string, sequence int64, yaml string) error { + endpoint := fmt.Sprintf("/v1/app/%s/%d/raw", appID, sequence) + req, err := http.NewRequest("PUT", endpoint, strings.NewReader(yaml)) + if err != nil { + return err + } + req.Header.Set("Authorization", c.apiKey) + req.Header.Set("Content-Type", "application/yaml") + resp, err := (&http.Client{}).Do(req) + if err != nil { + return fmt.Errorf("UpdateRelease (%s %s): %v", req.Method, endpoint, err) + } + resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("UpdateRelease (%s %s): status %d", req.Method, endpoint, resp.StatusCode) + } + return nil +} + +// GetRelease returns a release's properties. +func (c *Client) GetRelease(appID string, sequence int64) (*releases.AppReleaseInfo, error) { + path := fmt.Sprintf("%s/v1/app/%s/release/%d/properties", c.apiOrigin, appID, sequence) + release := &releases.AppReleaseInfo{} + if err := c.doJSON("GET", path, http.StatusOK, nil, release); err != nil { + return nil, fmt.Errorf("GetRelease: %v", err) + } + return release, nil +} + +// PromoteRelease points the specified channels at a release sequence. +func (c *Client) PromoteRelease(appID string, sequence int64, label, notes string, required bool, channelIDs ...string) error { + path := fmt.Sprintf("/v1/app/%s/%d/promote", appID, sequence) + body := &releases.Body1{ + Label: label, + ReleaseNotes: notes, + Required: required, + Channels: channelIDs, + } + if err := c.doJSON("POST", path, http.StatusNoContent, body, nil); err != nil { + return fmt.Errorf("PromoteRelease: %v", err) + } + return nil +} diff --git a/client/release_test.go b/client/release_test.go new file mode 100644 index 000000000..db28a1e5b --- /dev/null +++ b/client/release_test.go @@ -0,0 +1,34 @@ +package client + +import ( + "testing" +) + +func TestListReleases(t *testing.T) { + client := New(apiOrigin, apiKey) + _, err := client.ListReleases(appID) + if err != nil { + t.Fatal(err) + } +} + +func TestCreateRelease(t *testing.T) { + client := New(apiOrigin, apiKey) + _, err := client.CreateRelease(appID) + if err != nil { + t.Fatal(err) + } +} + +func TestPromoteRelease(t *testing.T) { + client := New(apiOrigin, apiKey) + release, err := client.CreateRelease(appID) + if err != nil { + t.Fatal(err) + } + appChannels, err := client.CreateChannel(appID, "name", "Description") + err = client.PromoteRelease(appID, release.Sequence, "v1-labelx", "bug fixx", false, appChannels[0].Id) + if err != nil { + t.Fatal(err) + } +} diff --git a/glide.lock b/glide.lock new file mode 100644 index 000000000..2d13f9cfb --- /dev/null +++ b/glide.lock @@ -0,0 +1,10 @@ +hash: 7401793483743807d35d4a921e17dbab2bc293da606fa0b3b7729d9663576ffc +updated: 2017-06-27T17:18:03.865507609-07:00 +imports: +- name: golang.org/x/net + version: 455220fa52c866a8aa14ff5e8cc68cde16b8395e + subpackages: + - publicsuffix +- name: gopkg.in/go-resty/resty.v0 + version: cf81ed0a604d373be63b4c036c6b05c06520615f +testImports: [] diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 000000000..6547cbf7b --- /dev/null +++ b/glide.yaml @@ -0,0 +1,4 @@ +package: github.com/replicatedhq/replicated +import: +- package: gopkg.in/go-resty/resty.v0 + version: ^0.13.0 From 90cbbb46e029ef46bbfc11a45636df948714fdfd Mon Sep 17 00:00:00 2001 From: Andrew Reed Date: Thu, 29 Jun 2017 12:20:49 -0700 Subject: [PATCH 2/2] HTTPClient implements Client interface --- client/channel.go | 8 ++++---- client/client.go | 33 +++++++++++++++++++++++++++------ client/release.go | 12 ++++++------ 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/client/channel.go b/client/channel.go index 1aaa4f01e..ca183db27 100644 --- a/client/channel.go +++ b/client/channel.go @@ -8,7 +8,7 @@ import ( ) // ListChannels returns all channels for an app. -func (c *Client) ListChannels(appID string) ([]channels.AppChannel, error) { +func (c *HTTPClient) ListChannels(appID string) ([]channels.AppChannel, error) { path := fmt.Sprintf("/v1/app/%s/channels", appID) appChannels := make([]channels.AppChannel, 0) err := c.doJSON("GET", path, http.StatusOK, nil, &appChannels) @@ -19,7 +19,7 @@ func (c *Client) ListChannels(appID string) ([]channels.AppChannel, error) { } // CreateChannel adds a channel to an app. -func (c *Client) CreateChannel(appID, name, desc string) ([]channels.AppChannel, error) { +func (c *HTTPClient) CreateChannel(appID, name, desc string) ([]channels.AppChannel, error) { path := fmt.Sprintf("/v1/app/%s/channel", appID) body := &channels.Body{ Name: name, @@ -34,14 +34,14 @@ func (c *Client) CreateChannel(appID, name, desc string) ([]channels.AppChannel, } // ArchiveChannel archives a channel. -func (c *Client) ArchiveChannel(appID, channelID string) error { +func (c *HTTPClient) ArchiveChannel(appID, channelID string) error { endpoint := fmt.Sprintf("%s/v1/app/%s/channel/%s/archive", c.apiOrigin, appID, channelID) req, err := http.NewRequest("POST", endpoint, nil) if err != nil { return err } req.Header.Add("Authorization", c.apiKey) - resp, err := (&http.Client{}).Do(req) + resp, err := http.DefaultClient.Do(req) if err != nil { return fmt.Errorf("ArchiveChannel (%s %s): %v", req.Method, endpoint, err) } diff --git a/client/client.go b/client/client.go index 5f35fb6d8..bc640be99 100644 --- a/client/client.go +++ b/client/client.go @@ -6,17 +6,38 @@ import ( "encoding/json" "fmt" "net/http" + + channels "github.com/replicatedhq/replicated/gen/go/channels" + releases "github.com/replicatedhq/replicated/gen/go/releases" ) -// A Client communicates with the Replicated Vendor API. -type Client struct { +type Client interface { + ListChannels(appID string) ([]channels.AppChannel, error) + CreateChannel(appID, name, desc string) ([]channels.AppChannel, error) + ArchiveChannel(appID, channelID string) error + + ListReleases(appID string) ([]releases.AppReleaseInfo, error) + CreateRelease(appID string) (*releases.AppReleaseInfo, error) + UpdateRelease(appID string, sequence int64, yaml string) error + GetRelease(appID string, sequence int64) (*releases.AppReleaseInfo, error) + PromoteRelease( + appID string, + sequence int64, + label string, + notes string, + required bool, + channelIDs ...string) error +} + +// A Client communicates with the Replicated Vendor HTTP API. +type HTTPClient struct { apiKey string apiOrigin string } -// New returns a new client. -func New(origin string, apiKey string) *Client { - c := &Client{ +// New returns a new HTTP client. +func New(origin string, apiKey string) Client { + c := &HTTPClient{ apiKey: apiKey, apiOrigin: origin, } @@ -24,7 +45,7 @@ func New(origin string, apiKey string) *Client { return c } -func (c *Client) doJSON(method, path string, successStatus int, reqBody, respBody interface{}) error { +func (c *HTTPClient) doJSON(method, path string, successStatus int, reqBody, respBody interface{}) error { endpoint := fmt.Sprintf("%s%s", c.apiOrigin, path) var buf bytes.Buffer if reqBody != nil { diff --git a/client/release.go b/client/release.go index 2b4248cb4..afb63b118 100644 --- a/client/release.go +++ b/client/release.go @@ -9,7 +9,7 @@ import ( ) // ListReleases lists all releases for an app. -func (c *Client) ListReleases(appID string) ([]releases.AppReleaseInfo, error) { +func (c *HTTPClient) ListReleases(appID string) ([]releases.AppReleaseInfo, error) { path := fmt.Sprintf("/v1/app/%s/releases", appID) releases := make([]releases.AppReleaseInfo, 0) if err := c.doJSON("GET", path, http.StatusOK, nil, &releases); err != nil { @@ -19,7 +19,7 @@ func (c *Client) ListReleases(appID string) ([]releases.AppReleaseInfo, error) { } // CreateRelease adds a release to an app. -func (c *Client) CreateRelease(appID string) (*releases.AppReleaseInfo, error) { +func (c *HTTPClient) CreateRelease(appID string) (*releases.AppReleaseInfo, error) { path := fmt.Sprintf("/v1/app/%s/release", appID) body := &releases.Body{ Source: "latest", @@ -32,7 +32,7 @@ func (c *Client) CreateRelease(appID string) (*releases.AppReleaseInfo, error) { } // UpdateRelease updates a release's yaml. -func (c *Client) UpdateRelease(appID string, sequence int64, yaml string) error { +func (c *HTTPClient) UpdateRelease(appID string, sequence int64, yaml string) error { endpoint := fmt.Sprintf("/v1/app/%s/%d/raw", appID, sequence) req, err := http.NewRequest("PUT", endpoint, strings.NewReader(yaml)) if err != nil { @@ -40,7 +40,7 @@ func (c *Client) UpdateRelease(appID string, sequence int64, yaml string) error } req.Header.Set("Authorization", c.apiKey) req.Header.Set("Content-Type", "application/yaml") - resp, err := (&http.Client{}).Do(req) + resp, err := http.DefaultClient.Do(req) if err != nil { return fmt.Errorf("UpdateRelease (%s %s): %v", req.Method, endpoint, err) } @@ -52,7 +52,7 @@ func (c *Client) UpdateRelease(appID string, sequence int64, yaml string) error } // GetRelease returns a release's properties. -func (c *Client) GetRelease(appID string, sequence int64) (*releases.AppReleaseInfo, error) { +func (c *HTTPClient) GetRelease(appID string, sequence int64) (*releases.AppReleaseInfo, error) { path := fmt.Sprintf("%s/v1/app/%s/release/%d/properties", c.apiOrigin, appID, sequence) release := &releases.AppReleaseInfo{} if err := c.doJSON("GET", path, http.StatusOK, nil, release); err != nil { @@ -62,7 +62,7 @@ func (c *Client) GetRelease(appID string, sequence int64) (*releases.AppReleaseI } // PromoteRelease points the specified channels at a release sequence. -func (c *Client) PromoteRelease(appID string, sequence int64, label, notes string, required bool, channelIDs ...string) error { +func (c *HTTPClient) PromoteRelease(appID string, sequence int64, label, notes string, required bool, channelIDs ...string) error { path := fmt.Sprintf("/v1/app/%s/%d/promote", appID, sequence) body := &releases.Body1{ Label: label,