debian-forge-composer/internal/client/client.go
Brian C. Lane 7a4bb863dd Update deprecated io/ioutil functions
ioutil has been deprecated since go 1.16, this fixes all of the
deprecated functions we are using:

ioutil.ReadFile -> os.ReadFile
ioutil.ReadAll -> io.ReadAll
ioutil.WriteFile -> os.WriteFile
ioutil.TempFile -> os.CreateTemp
ioutil.TempDir -> os.MkdirTemp

All of the above are a simple name change, the function arguments and
results are exactly the same as before.

ioutil.ReadDir -> os.ReadDir

now returns a os.DirEntry but the IsDir and Name functions work the
same. The difference is that the FileInfo must be retrieved with the
Info() function which can also return an error.

These were identified by running:
golangci-lint run --build-tags=integration ./...
2023-03-07 09:22:23 -08:00

224 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
}