diff --git a/internal/client/blueprints.go b/internal/client/blueprints.go new file mode 100644 index 000000000..8356d1435 --- /dev/null +++ b/internal/client/blueprints.go @@ -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 +} diff --git a/internal/client/client.go b/internal/client/client.go new file mode 100644 index 000000000..8753b76dd --- /dev/null +++ b/internal/client/client.go @@ -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 +} diff --git a/internal/client/weldr.go b/internal/client/weldr.go new file mode 100644 index 000000000..c815b649e --- /dev/null +++ b/internal/client/weldr.go @@ -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 +}