UBI and the oldest support Fedora (37) now all have go 1.19, so we are cleared to switch. gofmt now reformats comments in certain cases, so that explains the formatting changes in this commit. See https://go.dev/doc/go1.19#go-doc Signed-off-by: Ondřej Budai <ondrej@budai.cz>
225 lines
6.9 KiB
Go
225 lines
6.9 KiB
Go
// Package client contains functions for communicating with the API server
|
|
// Copyright (C) 2020 by Red Hat, Inc.
|
|
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
)
|
|
|
|
// 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 *http.Client, method, path, body string, headers map[string]string) (*http.Response, error) {
|
|
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 := socket.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 by some requests to indicate success or failure.
|
|
// It is always returned when the status code is 400, indicating some kind of error with the request.
|
|
// 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.
|
|
type APIResponse struct {
|
|
Status bool `json:"status"`
|
|
Errors []APIErrorMsg `json:"errors,omitempty"`
|
|
}
|
|
|
|
// String returns the description of the first error, if there is one
|
|
func (r *APIResponse) String() 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
|
|
}
|
|
|
|
// NewAPIResponse converts the response body to a status response
|
|
func NewAPIResponse(body []byte) (*APIResponse, error) {
|
|
var status APIResponse
|
|
err := json.Unmarshal(body, &status)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &status, nil
|
|
}
|
|
|
|
// 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, error) {
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewAPIResponse(body)
|
|
}
|
|
|
|
// GetRawBody returns the resp.Body io.ReadCloser to the caller
|
|
// NOTE: The caller is responsible for closing the Body when finished
|
|
func GetRawBody(socket *http.Client, method, path string) (io.ReadCloser, *APIResponse, error) {
|
|
resp, err := Request(socket, method, path, "", map[string]string{})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Convert the API's JSON error response to an error type and return it
|
|
// lorax-composer (wrongly) returns 404 for some of its json responses
|
|
if resp.StatusCode == 400 || resp.StatusCode == 404 {
|
|
apiResponse, err := apiError(resp)
|
|
return nil, apiResponse, err
|
|
}
|
|
return resp.Body, nil, nil
|
|
}
|
|
|
|
// GetRaw returns raw data from a GET request
|
|
// Errors from the API are returned as an APIResponse, client errors are returned as error
|
|
func GetRaw(socket *http.Client, method, path string) ([]byte, *APIResponse, error) {
|
|
body, resp, err := GetRawBody(socket, method, path)
|
|
if err != nil || resp != nil {
|
|
return nil, resp, err
|
|
}
|
|
defer body.Close()
|
|
|
|
bodyBytes, err := io.ReadAll(body)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return bodyBytes, nil, 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 API are returned as an APIResponse, client errors are returned as error
|
|
func GetJSONAll(socket *http.Client, path string) ([]byte, *APIResponse, error) {
|
|
body, api, err := GetRaw(socket, "GET", path+"?limit=0")
|
|
if api != nil || err != nil {
|
|
return nil, api, err
|
|
}
|
|
|
|
// We just want the total
|
|
var j interface{}
|
|
err = json.Unmarshal(body, &j)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
m := j.(map[string]interface{})
|
|
var v interface{}
|
|
var ok bool
|
|
if v, ok = m["total"]; !ok {
|
|
return nil, nil, 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, nil, errors.New("Response 'total' is not a float64")
|
|
}
|
|
|
|
// PostRaw sends a POST with raw data and returns the raw response body
|
|
// Errors from the API are returned as an APIResponse, client errors are returned as error
|
|
func PostRaw(socket *http.Client, path, body string, headers map[string]string) ([]byte, *APIResponse, error) {
|
|
resp, err := Request(socket, "POST", path, body, headers)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Convert the API's JSON error response to an APIResponse
|
|
if resp.StatusCode == 400 {
|
|
apiResponse, err := apiError(resp)
|
|
return nil, apiResponse, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
responseBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return responseBody, nil, nil
|
|
}
|
|
|
|
// PostTOML sends a POST with TOML data and the Content-Type header set to "text/x-toml"
|
|
// Errors from the API are returned as an APIResponse, client errors are returned as error
|
|
func PostTOML(socket *http.Client, path, body string) ([]byte, *APIResponse, error) {
|
|
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"
|
|
// Errors from the API are returned as an APIResponse, client errors are returned as error
|
|
func PostJSON(socket *http.Client, path, body string) ([]byte, *APIResponse, error) {
|
|
headers := map[string]string{"Content-Type": "application/json"}
|
|
return PostRaw(socket, path, body, headers)
|
|
}
|
|
|
|
// DeleteRaw sends a DELETE request
|
|
// Errors from the API are returned as an APIResponse, client errors are returned as error
|
|
func DeleteRaw(socket *http.Client, path string) ([]byte, *APIResponse, error) {
|
|
resp, err := Request(socket, "DELETE", path, "", nil)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Convert the API's JSON error response to an APIResponse
|
|
if resp.StatusCode == 400 {
|
|
apiResponse, err := apiError(resp)
|
|
return nil, apiResponse, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
responseBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return responseBody, nil, nil
|
|
}
|