Skip to content

Commit

Permalink
daemon: JSON responses for API (#546)
Browse files Browse the repository at this point in the history
* daemon: switch over to chi

* update tests, clean up handler attachment

* convert all success responses into structured json messages via Render

* convert all error responses in auth to proper structured responses

* move base response type to package api

* update responses, fix tests

* reogranize package API

* add api.Unmarshal helper

* refactor as much of daemon as possible to respect JSON responses

* update client (partly) for new responses

* update package lock

* api package tests

* clean up res implementation, update tests

* more res api cleanup

* improve webhook responses

* export res.BaseResponse

* fix tests

* rename logger to streamer

* Fix ErrNotFound code

* minor fixes, cleanup

* more fixes to env

* fix request assignment for streamer, fix omitempty on data

* dont assign user on login

* Pretty print users
  • Loading branch information
bobheadxi authored and yaoharry committed Feb 12, 2019
1 parent 84b5b77 commit ce57052
Show file tree
Hide file tree
Showing 29 changed files with 903 additions and 509 deletions.
31 changes: 31 additions & 0 deletions Gopkg.lock

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

17 changes: 0 additions & 17 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,6 @@ type UserRequest struct {
Totp string `json:"totp"`
}

// TotpResponse is used for sending users their Totp secret and backup codes
type TotpResponse struct {
TotpSecret string `json:"key"`
BackupCodes []string
}

// EnvRequest represents a request to manage environment variables
type EnvRequest struct {
Name string `json:"name,omitempty"`
Expand All @@ -53,14 +47,3 @@ type EnvRequest struct {

Remove bool `json:"remove,omitempty"`
}

// DeploymentStatus lists details about the deployed project
type DeploymentStatus struct {
InertiaVersion string `json:"version"`
Branch string `json:"branch"`
CommitHash string `json:"commit_hash"`
CommitMessage string `json:"commit_message"`
BuildType string `json:"build_type"`
Containers []string `json:"containers"`
BuildContainerActive bool `json:"build_active"`
}
94 changes: 94 additions & 0 deletions api/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package api

import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
)

// BaseResponse is the underlying response structure to all responses.
type BaseResponse struct {
// Basic metadata
HTTPStatusCode int `json:"code"`
RequestID string `json:"request_id,omitempty"`

// Message is included in all responses, and is a summary of the server's response
Message string `json:"message"`

// Err contains additional context in the event of an error
Err string `json:"error,omitempty"`

// Data contains information the server wants to return
Data interface{} `json:"data,omitempty"`
}

// KV is used for defining specific values to be unmarshalled from BaseResponse
// data
type KV struct {
Key string
Value interface{}
}

// Unmarshal reads the response and unmarshalls the BaseResponse as well any
// requested key-value pairs.
// For example:
//
// var totpResp = &api.TotpResponse{}
// api.Unmarshal(resp.Body, api.KV{Key: "totp", Value: totpResp})
//
// Values provided in KV.Value MUST be explicit pointers, even if the value is
// a pointer type, ie maps and slices.
func Unmarshal(r io.Reader, kvs ...KV) (*BaseResponse, error) {
bytes, err := ioutil.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("could not read bytes from reader: %s", err.Error())
}

// Unmarshal data into a BaseResponse, replacing BaseResponse.Data with a
// map to preserve raw JSON data in the keys
var (
data = make(map[string]json.RawMessage)
resp = BaseResponse{Data: &data}
)
if err := json.Unmarshal(bytes, &resp); err != nil {
return nil, fmt.Errorf("could not unmarshal data from reader: %s", err.Error())
}

// Unmarshal all requested kv-pairs, silently ignoring errors
for _, kv := range kvs {
json.Unmarshal(data[kv.Key], kv.Value)
}

return &resp, nil
}

// Error returns a summary of an encountered error. For more details, you may
// want to interrogate Data. Returns nil if StatusCode is not an HTTP error
// code, ie if the code is in 1xx, 2xx, or 3xx
func (b *BaseResponse) Error() error {
if 100 <= b.HTTPStatusCode && b.HTTPStatusCode < 400 {
return nil
}
if b.Err == "" {
return fmt.Errorf("[error %d] %s", b.HTTPStatusCode, b.Message)
}
return fmt.Errorf("[error %d] %s: (%s)", b.HTTPStatusCode, b.Message, b.Err)
}

// TotpResponse is used for sending users their Totp secret and backup codes
type TotpResponse struct {
TotpSecret string `json:"secret"`
BackupCodes []string `json:"backup_codes"`
}

// DeploymentStatus lists details about the deployed project
type DeploymentStatus struct {
InertiaVersion string `json:"version"`
Branch string `json:"branch"`
CommitHash string `json:"commit_hash"`
CommitMessage string `json:"commit_message"`
BuildType string `json:"build_type"`
Containers []string `json:"containers"`
BuildContainerActive bool `json:"build_active"`
}
99 changes: 99 additions & 0 deletions api/response_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package api

import (
"bytes"
"testing"
)

func TestUnmarshal(t *testing.T) {
type args struct {
bytes []byte
}
tests := []struct {
name string
args args
wantCode int
wantErr bool
}{
{"invalid data",
args{nil},
0,
true},
{"invalid json",
args{
[]byte(`{
"code":200,
"request_id":"bobbook/2Mch7LMzhj-000001",
"mess`),
},
0,
true},
{"ok",
args{
[]byte(`{
"code":200,
"request_id":"bobbook/2Mch7LMzhj-000001",
"message":"session created",
"data":{
"token":"blah"
}
}`),
},
200,
false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var gotToken string
var gotKV = []KV{{Key: "token", Value: &gotToken}}
got, err := Unmarshal(bytes.NewReader(tt.args.bytes), gotKV...)
if (err != nil) != tt.wantErr {
t.Errorf("Unmarshal() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != nil && tt.wantCode != got.HTTPStatusCode {
t.Errorf("Unmarshal() code = %v, want %v", got.HTTPStatusCode, tt.wantCode)
}
for _, kv := range gotKV {
if kv.Value == "" {
t.Error("Unmarshal() kv is empty")
}
}
})
}
}

func TestBaseResponse_Error(t *testing.T) {
type fields struct {
HTTPStatusCode int
Message string
Err string
}
tests := []struct {
name string
fields fields
wantErr bool
}{
{"not an error",
fields{200, "hi", ""},
false},
{"error with only message",
fields{400, "hi", ""},
true},
{"error with message and error context",
fields{400, "hi", "oh no"},
true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := &BaseResponse{
HTTPStatusCode: tt.fields.HTTPStatusCode,
Message: tt.fields.Message,
Err: tt.fields.Err,
}
if err := b.Error(); (err != nil) != tt.wantErr {
t.Errorf("BaseResponse.Error() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
12 changes: 10 additions & 2 deletions cmd/host/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package hostcmd
import (
"fmt"
"io/ioutil"
"strings"

"github.com/spf13/cobra"
"github.com/ubclaunchpad/inertia/api"
"github.com/ubclaunchpad/inertia/cmd/printutil"
)

Expand Down Expand Up @@ -103,11 +105,17 @@ variables are not be decrypted.`,
printutil.Fatal(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
var variables = make([]string, 0)
b, err := api.Unmarshal(resp.Body, api.KV{Key: "variables", Value: &variables})
if err != nil {
printutil.Fatal(err)
}
fmt.Printf("(Status code %d) %s\n", resp.StatusCode, body)
if len(variables) == 0 {
fmt.Printf("(Status code %d) no variables configured", resp.StatusCode)
} else {
fmt.Printf("(Status code %d) %s: \n%s\n",
resp.StatusCode, b.Message, strings.Join(variables, "\n"))
}
},
}
root.AddCommand(list)
Expand Down
27 changes: 16 additions & 11 deletions cmd/host/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package hostcmd

import (
"bufio"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"strings"

"github.com/ubclaunchpad/inertia/client"
inertiacmd "github.com/ubclaunchpad/inertia/cmd/cmd"
Expand Down Expand Up @@ -228,7 +228,9 @@ Requires the Inertia daemon to be active on your remote - do this by running 'in
fmt.Printf("(Status code %d) Daemon at remote '%s' online at %s\n",
resp.StatusCode, root.client.Name, host)
var status = &api.DeploymentStatus{}
if err := json.NewDecoder(resp.Body).Decode(status); err != nil {
if _, err := api.Unmarshal(resp.Body, api.KV{
Key: "status", Value: status,
}); err != nil {
printutil.Fatal(err)
}
println(printutil.FormatStatus(status))
Expand Down Expand Up @@ -279,20 +281,22 @@ Use 'inertia [remote] status' to see which containers are active.`,
}
defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
var logs []string
b, err := api.Unmarshal(resp.Body, api.KV{Key: "logs", Value: &logs})
if err != nil {
printutil.Fatal(err)
}

switch resp.StatusCode {
case http.StatusOK:
fmt.Printf("(Status code %d) Logs: \n%s\n", resp.StatusCode, body)
fmt.Printf("(Status code %d) Logs: \n%s\n", resp.StatusCode, strings.Join(logs, "\n"))
case http.StatusUnauthorized:
fmt.Printf("(Status code %d) Bad auth:\n%s\n", resp.StatusCode, body)
fmt.Printf("(Status code %d) Bad auth:\n%s\n", resp.StatusCode, b.Message)
case http.StatusPreconditionFailed:
fmt.Printf("(Status code %d) Problem with deployment setup:\n%s\n", resp.StatusCode, body)
fmt.Printf("(Status code %d) Problem with deployment setup:\n%s\n", resp.StatusCode, b.Message)
default:
fmt.Printf("(Status code %d) Unknown response from daemon:\n%s\n",
resp.StatusCode, body)
resp.StatusCode, b.Message)
}
} else {
// if not short, open a websocket to stream logs
Expand Down Expand Up @@ -507,19 +511,20 @@ func (root *HostCmd) attachTokenCmd() {
}
defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
var token string
b, err := api.Unmarshal(resp.Body, api.KV{Key: "token", Value: &token})
if err != nil {
printutil.Fatal(err)
}

switch resp.StatusCode {
case http.StatusOK:
fmt.Printf("New token: %s\n", string(body))
fmt.Printf("New token: %s\n", token)
case http.StatusUnauthorized:
fmt.Printf("(Status code %d) Bad auth:\n%s\n", resp.StatusCode, string(body))
fmt.Printf("(Status code %d) Bad auth:\n%s\n", resp.StatusCode, b.Message)
default:
fmt.Printf("(Status code %d) Unknown response from daemon:\n%s\n",
resp.StatusCode, body)
resp.StatusCode, b.Message)
}
},
}
Expand Down

0 comments on commit ce57052

Please sign in to comment.