Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
gen/
vendor/
.glide
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
44 changes: 44 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
# replicated
# 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
53 changes: 53 additions & 0 deletions client/channel.go
Original file line number Diff line number Diff line change
@@ -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 *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)
if err != nil {
return nil, fmt.Errorf("ListChannels: %v", err)
}
return appChannels, nil
}

// CreateChannel adds a channel to an app.
func (c *HTTPClient) 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 *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.DefaultClient.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
}
60 changes: 60 additions & 0 deletions client/channel_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
77 changes: 77 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Package client manages channels and releases through the Replicated Vendor API.
package client

import (
"bytes"
"encoding/json"
"fmt"
"net/http"

channels "github.com/replicatedhq/replicated/gen/go/channels"
releases "github.com/replicatedhq/replicated/gen/go/releases"
)

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 HTTP client.
func New(origin string, apiKey string) Client {
c := &HTTPClient{
apiKey: apiKey,
apiOrigin: origin,
}

return c
}

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 {
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
}
8 changes: 8 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
@@ -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")
77 changes: 77 additions & 0 deletions client/release.go
Original file line number Diff line number Diff line change
@@ -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 *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 {
return nil, fmt.Errorf("ListReleases: %v", err)
}
return releases, nil
}

// CreateRelease adds a release to an app.
func (c *HTTPClient) 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 *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 {
return err
}
req.Header.Set("Authorization", c.apiKey)
req.Header.Set("Content-Type", "application/yaml")
resp, err := http.DefaultClient.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 *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 {
return nil, fmt.Errorf("GetRelease: %v", err)
}
return release, nil
}

// PromoteRelease points the specified channels at a release sequence.
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,
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
}
34 changes: 34 additions & 0 deletions client/release_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
10 changes: 10 additions & 0 deletions glide.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading