client: Add client helper functions for API testing and future cli tools

This package will contain functions for communicating with the API that
can be used in both the integration tests as well as in a future cmdline
tool similar to composer-cli

When possible the client functions will return the same structures used
by weldr/api.go which have been exported in weldr/json.go
This commit is contained in:
Brian C. Lane 2020-02-06 17:12:27 -08:00 committed by Tom Gundersen
parent 87e9c39532
commit cfa4233db5
3 changed files with 419 additions and 0 deletions

View file

@ -0,0 +1,167 @@
// Package client - blueprints contains functions for the blueprint API
// Copyright (C) 2020 by Red Hat, Inc.
package client
import (
"encoding/json"
"fmt"
"strings"
)
// PostTOMLBlueprintV0 sends a TOML blueprint string to the API
// and returns an APIResponse
func PostTOMLBlueprintV0(socket, blueprint string) *APIResponse {
body, err := PostTOML(socket, "/api/v0/blueprints/new", blueprint)
if err != nil {
return err
}
return NewAPIResponse(body)
}
// PostTOMLWorkspaceV0 sends a TOML blueprint string to the API
// and returns an APIResponse
func PostTOMLWorkspaceV0(socket, blueprint string) *APIResponse {
body, err := PostTOML(socket, "/api/v0/blueprints/workspace", blueprint)
if err != nil {
return err
}
return NewAPIResponse(body)
}
// PostJSONBlueprintV0 sends a JSON blueprint string to the API
// and returns an APIResponse
func PostJSONBlueprintV0(socket, blueprint string) *APIResponse {
body, err := PostJSON(socket, "/api/v0/blueprints/new", blueprint)
if err != nil {
return err
}
return NewAPIResponse(body)
}
// PostJSONWorkspaceV0 sends a JSON blueprint string to the API
// and returns an APIResponse
func PostJSONWorkspaceV0(socket, blueprint string) *APIResponse {
body, err := PostJSON(socket, "/api/v0/blueprints/workspace", blueprint)
if err != nil {
return err
}
return NewAPIResponse(body)
}
// DeleteBlueprintV0 deletes the named blueprint and returns an APIResponse
func DeleteBlueprintV0(socket, bpName string) *APIResponse {
body, err := DeleteRaw(socket, "/api/v0/blueprints/delete/"+bpName)
if err != nil {
return err
}
return NewAPIResponse(body)
}
// DeleteWorkspaceV0 deletes the named blueprint's workspace and returns an APIResponse
func DeleteWorkspaceV0(socket, bpName string) *APIResponse {
body, err := DeleteRaw(socket, "/api/v0/blueprints/workspace/"+bpName)
if err != nil {
return err
}
return NewAPIResponse(body)
}
// ListBlueprintsV0 returns a list of blueprint names
func ListBlueprintsV0(socket string) ([]string, *APIResponse) {
body, err := GetJSONAll(socket, "/api/v0/blueprints/list")
if err != nil {
return nil, err
}
var resp BlueprintsListV0
jerr := json.Unmarshal(body, &resp)
if jerr != nil {
return nil, clientError(err)
}
return resp.Blueprints, nil
}
// GetBlueprintInfoTOMLV0 returns the requested blueprint as a TOML string
func GetBlueprintInfoTOMLV0(socket, bpName string) (string, *APIResponse) {
body, err := GetRaw(socket, "GET", "/api/v0/blueprints/info/"+bpName+"?format=toml")
if err != nil {
return "", err
}
return string(body), nil
}
// GetBlueprintsInfoJSONV0 returns the requested blueprints and their changed state
func GetBlueprintsInfoJSONV0(socket, bpName string) (BlueprintsInfoV0, *APIResponse) {
body, err := GetRaw(socket, "GET", "/api/v0/blueprints/info/"+bpName)
if err != nil {
return BlueprintsInfoV0{}, err
}
var resp BlueprintsInfoV0
jerr := json.Unmarshal(body, &resp)
if jerr != nil {
return BlueprintsInfoV0{}, clientError(err)
}
return resp, nil
}
// GetBlueprintsChangesV0 returns the changes to the listed blueprints
func GetBlueprintsChangesV0(socket string, bpNames []string) (BlueprintsChangesV0, *APIResponse) {
names := strings.Join(bpNames, ",")
body, err := GetRaw(socket, "GET", "/api/v0/blueprints/changes/"+names)
if err != nil {
return BlueprintsChangesV0{}, err
}
var resp BlueprintsChangesV0
jerr := json.Unmarshal(body, &resp)
if jerr != nil {
return BlueprintsChangesV0{}, clientError(err)
}
return resp, nil
}
// UndoBlueprintChangeV0 reverts a blueprint to a previous commit
func UndoBlueprintChangeV0(socket, blueprint, commit string) *APIResponse {
request := fmt.Sprintf("/api/v0/blueprints/undo/%s/%s", blueprint, commit)
body, err := PostRaw(socket, request, "", nil)
if err != nil {
return err
}
return NewAPIResponse(body)
}
// TagBlueprintV0 tags the current blueprint commit as a new revision
func TagBlueprintV0(socket, blueprint string) *APIResponse {
body, err := PostRaw(socket, "/api/v0/blueprints/tag/"+blueprint, "", nil)
if err != nil {
return err
}
return NewAPIResponse(body)
}
// DepsolveBlueprintV0 depsolves the listed blueprint
func DepsolveBlueprintV0(socket, blueprint string) (BlueprintsDepsolveV0, *APIResponse) {
body, err := GetRaw(socket, "GET", "/api/v0/blueprints/depsolve/"+blueprint)
if err != nil {
return BlueprintsDepsolveV0{}, err
}
var resp BlueprintsDepsolveV0
jerr := json.Unmarshal(body, &resp)
if jerr != nil {
return BlueprintsDepsolveV0{}, clientError(err)
}
return resp, nil
}
// FreezeBlueprintV0 depsolves the listed blueprint and returns the blueprint with frozen package
// versions
func FreezeBlueprintV0(socket, blueprint string) (BlueprintsFreezeV0, *APIResponse) {
body, err := GetRaw(socket, "GET", "/api/v0/blueprints/freeze/"+blueprint)
if err != nil {
return BlueprintsFreezeV0{}, err
}
var resp BlueprintsFreezeV0
jerr := json.Unmarshal(body, &resp)
if jerr != nil {
return BlueprintsFreezeV0{}, clientError(err)
}
return resp, nil
}

232
internal/client/client.go Normal file
View file

@ -0,0 +1,232 @@
// Package client contains functions for communicating with the API server
// Copyright (C) 2020 by Red Hat, Inc.
package client
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"time"
)
// Request handles sending the request, handling errors, returning the response
// socket is the path to a Unix Domain socket
// path is the full URL path, including query strings
// body is the data to send with POST
// headers is a map of header:value to add to the request
//
// If it is successful a http.Response will be returned. If there is an error, the response will be
// nil and error will be returned.
func Request(socket, method, path, body string, headers map[string]string) (*http.Response, error) {
client := http.Client{
// TODO This may be too short/simple for downloading images
Timeout: 60 * time.Second,
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", socket)
},
},
}
req, err := http.NewRequest(method, "http://localhost"+path, bytes.NewReader([]byte(body)))
if err != nil {
return nil, err
}
for h, v := range headers {
req.Header.Set(h, v)
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}
// APIErrorMsg is an individual API error with an ID and a message string
type APIErrorMsg struct {
ID string `json:"id"`
Msg string `json:"msg"`
}
// String returns the error id and message as a string
func (r *APIErrorMsg) String() string {
return fmt.Sprintf("%s: %s", r.ID, r.Msg)
}
// APIResponse is returned with status code 400 and may contain a list of errors
// If Status is true the Errors list will not be included or will be empty.
// When Status is false it will include at least one APIErrorMsg with details about the error.
// It also implements the error interface so that it can be used in place of error
type APIResponse struct {
Status bool `json:"status"`
Errors []APIErrorMsg `json:"errors,omitempty"`
}
// Error returns the description of the first error
func (r *APIResponse) Error() string {
if len(r.Errors) == 0 {
return ""
}
return r.Errors[0].String()
}
// AllErrors returns a list of error description strings
func (r *APIResponse) AllErrors() (all []string) {
for i := range r.Errors {
all = append(all, r.Errors[i].String())
}
return all
}
// clientError converts an error into an APIResponse with ID set to ClientError
// This is used to return golang function errors to callers of the client functions
func clientError(err error) *APIResponse {
return &APIResponse{
Status: false,
Errors: []APIErrorMsg{{ID: "ClientError", Msg: err.Error()}},
}
}
// NewAPIResponse converts the response body to a status response
func NewAPIResponse(body []byte) *APIResponse {
var status APIResponse
err := json.Unmarshal(body, &status)
if err != nil {
return clientError(err)
}
return &status
}
// apiError converts an API error 400 JSON to a status response
//
// The response body should alway be of the form:
// {"status": false, "errors": [{"id": ERROR_ID, "msg": ERROR_MESSAGE}, ...]}
func apiError(resp *http.Response) *APIResponse {
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return clientError(err)
}
return NewAPIResponse(body)
}
// GetRaw returns raw data from a GET request
// Errors from the client and from the API are returned as an APIResponse
func GetRaw(socket, method, path string) ([]byte, *APIResponse) {
resp, err := Request(socket, method, path, "", map[string]string{})
if err != nil {
return nil, clientError(err)
}
// Convert the API's JSON error response to an error type and return it
if resp.StatusCode == 400 {
return nil, apiError(resp)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, clientError(err)
}
return body, nil
}
// GetJSONAll returns all JSON results from a GET request using offset/limit
// This function makes 2 requests, the first with limit=0 to get the total number of results,
// and then with limit=TOTAL to fetch all of the results.
// The path passed to GetJSONAll should not include the limit or offset query parameters
// Errors from the client and from the API are returned as an APIResponse
func GetJSONAll(socket, path string) ([]byte, *APIResponse) {
body, err := GetRaw(socket, "GET", path+"?limit=0")
if err != nil {
return nil, err
}
// We just want the total
var j interface{}
jerr := json.Unmarshal(body, &j)
if jerr != nil {
return nil, clientError(jerr)
}
m := j.(map[string]interface{})
var v interface{}
var ok bool
if v, ok = m["total"]; !ok {
return nil, clientError(errors.New("Response is missing the total value"))
}
switch total := v.(type) {
case float64:
allResults := fmt.Sprintf("%s?limit=%v", path, total)
return GetRaw(socket, "GET", allResults)
}
return nil, clientError(errors.New("Response 'total' is wrong type"))
}
// PostRaw sends a POST with raw data and returns the raw response body
// Errors from the client and from the API are returned as an APIResponse
func PostRaw(socket, path, body string, headers map[string]string) ([]byte, *APIResponse) {
resp, err := Request(socket, "POST", path, body, headers)
if err != nil {
return nil, clientError(err)
}
// Convert the API's JSON error response to an error type and return it
if resp.StatusCode == 400 {
return nil, apiError(resp)
}
defer resp.Body.Close()
responseBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, clientError(err)
}
return responseBody, nil
}
// PostTOML sends a POST with TOML data and the Content-Type header set to "text/x-toml"
// It returns the raw response data or errors as an APIResponse
func PostTOML(socket, path, body string) ([]byte, *APIResponse) {
headers := map[string]string{"Content-Type": "text/x-toml"}
return PostRaw(socket, path, body, headers)
}
// PostJSON sends a POST with JSON data and the Content-Type header set to "application/json"
// It returns the raw response data or errors as an APIResponse
func PostJSON(socket, path, body string) ([]byte, *APIResponse) {
headers := map[string]string{"Content-Type": "application/json"}
return PostRaw(socket, path, body, headers)
}
// DeleteRaw sends a DELETE request
// It returns the raw response data or errors as an APIResponse
func DeleteRaw(socket, path string) ([]byte, *APIResponse) {
resp, err := Request(socket, "DELETE", path, "", nil)
if err != nil {
return nil, clientError(err)
}
// Convert the API's JSON error response to an error type and return it
if resp.StatusCode == 400 {
return nil, apiError(resp)
}
defer resp.Body.Close()
responseBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, clientError(err)
}
return responseBody, nil
}

20
internal/client/weldr.go Normal file
View file

@ -0,0 +1,20 @@
// Package client - weldr contains functions to return API structures
// Copyright (C) 2020 by Red Hat, Inc.
package client
import (
"encoding/json"
)
// GetStatusV0 makes a GET request to /api/status and returns the v0 response as a StatusResponseV0
func GetStatusV0(socket string) (reply StatusV0, err *APIResponse) {
body, err := GetRaw(socket, "GET", "/api/status")
if err != nil {
return reply, err
}
jerr := json.Unmarshal(body, &reply)
if jerr != nil {
return reply, clientError(jerr)
}
return reply, nil
}