legacy.go raw

   1  // Package lwApi is a minimalist API client to LiquidWeb's (https://www.liquidweb.com) API:
   2  //
   3  // https://cart.liquidweb.com/storm/api/docs/v1
   4  //
   5  // https://cart.liquidweb.com/storm/api/docs/bleed
   6  //
   7  // As you might have guessed from the above API documentation links, there are API versions:
   8  // "v1" and "bleed". As the name suggests, if you always want the latest features and abilities,
   9  // use "bleed". If you want long term compatibility (at the cost of being a little further behind
  10  // sometimes), use "v1".
  11  package lwApi
  12  
  13  import (
  14  	"bytes"
  15  	"crypto/tls"
  16  	"encoding/json"
  17  	"fmt"
  18  	"io/ioutil"
  19  	"net/http"
  20  	"sync"
  21  	"time"
  22  )
  23  
  24  // A LWAPIConfig holds the configuration details used to call
  25  // the API with the client.
  26  type LWAPIConfig struct {
  27  	Username *string
  28  	Password *string
  29  	Token    *string
  30  	Url      string
  31  	Timeout  uint
  32  	Insecure bool
  33  }
  34  
  35  // A Client holds the packages *LWAPIConfig and *http.Client. To get a *Client, call New.
  36  type Client struct {
  37  	Headers    http.Header
  38  	config     *LWAPIConfig
  39  	httpClient *http.Client
  40  	mutex      sync.Mutex
  41  }
  42  
  43  // A LWAPIError is used to identify error responses when JSON unmarshalling json from a
  44  // byte slice.
  45  type LWAPIError struct {
  46  	ErrorMsg     string `json:"error,omitempty"`
  47  	ErrorClass   string `json:"error_class,omitempty"`
  48  	ErrorFullMsg string `json:"full_message,omitempty"`
  49  }
  50  
  51  // Given a LWAPIError, returns a string containing the ErrorClass and ErrorFullMsg.
  52  func (e LWAPIError) Error() string {
  53  	return fmt.Sprintf("%v: %v", e.ErrorClass, e.ErrorFullMsg)
  54  }
  55  
  56  // Given a LWAPIError, returns boolean if ErrorClass was present or not. You can
  57  // use this function to determine if a LWAPIRes response indicates an error or not.
  58  func (e LWAPIError) HadError() bool {
  59  	return e.ErrorClass != ""
  60  }
  61  
  62  // LWAPIRes is a convenient interface used (for example) by CallInto to ensure a passed
  63  // struct knows how to indicate whether or not it had an error.
  64  type LWAPIRes interface {
  65  	Error() string
  66  	HadError() bool
  67  }
  68  
  69  // New takes a *LWAPIConfig, and gives you a *Client. If there's an error, it is returned.
  70  // When using this package, this should be the first function you call. Below is an example
  71  // that demonstrates creating the config and passing it to New.
  72  //
  73  // Example:
  74  //
  75  //	username := "ExampleUsername"
  76  //	password := "ExamplePassword"
  77  //
  78  //	config := lwApi.LWAPIConfig{
  79  //		Username: &username,
  80  //		Password: &password,
  81  //		Url:      "api.liquidweb.com",
  82  //	}
  83  //	apiClient, newErr := lwApi.New(&config)
  84  //	if newErr != nil {
  85  //		panic(newErr)
  86  //	}
  87  func New(config *LWAPIConfig) (*Client, error) {
  88  	if err := processConfig(config); err != nil {
  89  		return nil, err
  90  	}
  91  
  92  	httpClient := &http.Client{Timeout: time.Duration(time.Duration(config.Timeout) * time.Second)}
  93  
  94  	if config.Insecure {
  95  		tr := &http.Transport{
  96  			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
  97  		}
  98  		httpClient.Transport = tr
  99  	}
 100  
 101  	headers := make(http.Header)
 102  	client := Client{
 103  		config:     config,
 104  		httpClient: httpClient,
 105  		Headers:    headers,
 106  		mutex:      sync.Mutex{},
 107  	}
 108  	return &client, nil
 109  }
 110  
 111  // Call takes a path, such as "network/zone/details" and a params structure.
 112  // It is recommended that the params be a map[string]interface{}, but you can use
 113  // anything that serializes to the right json structure.
 114  // A `interface{}` and an error are returned, in typical go fasion.
 115  //
 116  // Example:
 117  //
 118  //	args := map[string]interface{}{
 119  //		"uniq_id": "ABC123",
 120  //	}
 121  //	got, gotErr := apiClient.Call("bleed/asset/details", args)
 122  //	if gotErr != nil {
 123  //		panic(gotErr)
 124  //	}
 125  func (client *Client) Call(method string, params interface{}) (interface{}, error) {
 126  	bsRb, err := client.CallRaw(method, params)
 127  	if err != nil {
 128  		return nil, err
 129  	}
 130  
 131  	var resp interface{}
 132  	resp, err = client.callRawRespToInterface(bsRb)
 133  
 134  	return resp, err
 135  }
 136  
 137  // CallInto is like call, but instead of returning an interface you pass it a
 138  // struct which is filled, much like the json.Unmarshal function.  The struct
 139  // you pass must satisfy the LWAPIRes interface.  If you embed the LWAPIError
 140  // struct from this package into your struct, this will be taken care of for you.
 141  //
 142  // Example:
 143  //
 144  //	type ZoneDetails struct {
 145  //		lwApi.LWAPIError
 146  //		AvlZone     string   `json:"availability_zone"`
 147  //		Desc        string   `json:"description"`
 148  //		GatewayDevs []string `json:"gateway_devices"`
 149  //		HvType      string   `json:"hv_type"`
 150  //		ID          int      `json:"id"`
 151  //		Legacy      int      `json:"legacy"`
 152  //		Name        string   `json:"name"`
 153  //		Status      string   `json:"status"`
 154  //		SourceHVs   []string `json:"valid_source_hvs"`
 155  //	}
 156  //	var zone ZoneDetails
 157  //	err = apiClient.CallInto("network/zone/details", paramers, &zone)
 158  //	if err != nil {
 159  //		log.Fatal(err)
 160  //	}
 161  //	fmt.Printf("Got struct %#v\n", zone)
 162  func (client *Client) CallInto(method string, params interface{}, into LWAPIRes) error {
 163  	bsRb, err := client.CallRaw(method, params)
 164  	if err != nil {
 165  		return err
 166  	}
 167  
 168  	err = json.Unmarshal(bsRb, into)
 169  	if err != nil {
 170  		return err
 171  	}
 172  
 173  	if into.HadError() {
 174  		// the LWAPIRes satisfies the Error interface, so we can just return it on
 175  		// error.
 176  		return into
 177  	}
 178  
 179  	return nil
 180  }
 181  
 182  // Similar to CallInto(), but populates an interface without needing to satisfy
 183  // the LWAPIRes interface.
 184  // Example:
 185  //
 186  //	type ZoneDetails struct {
 187  //	        AvlZone     string   `json:"availability_zone"`
 188  //	        Desc        string   `json:"description"`
 189  //	        GatewayDevs []string `json:"gateway_devices"`
 190  //	        HvType      string   `json:"hv_type"`
 191  //	        ID          int      `json:"id"`
 192  //	        Legacy      int      `json:"legacy"`
 193  //	        Name        string   `json:"name"`
 194  //	        Status      string   `json:"status"`
 195  //	        SourceHVs   []string `json:"valid_source_hvs"`
 196  //	}
 197  //	var zone ZoneDetails
 198  //	err = apiClient.CallIntoInterface("network/zone/details", params, &zone)
 199  func (client *Client) CallIntoInterface(method string, params interface{}, into interface{}) error {
 200  	bsRb, err := client.CallRaw(method, params)
 201  	if err != nil {
 202  		return err
 203  	}
 204  
 205  	if _, err = client.callRawRespToInterface(bsRb, &into); err != nil {
 206  		return err
 207  	}
 208  
 209  	return nil
 210  }
 211  
 212  // CallRaw is just like Call, except it returns the raw json as a byte slice. However, in contrast to
 213  // Call, CallRaw does *not* check the API response for LiquidWeb specific exceptions as defined in
 214  // the type LWAPIError. As such, if calling this function directly, you must check for LiquidWeb specific
 215  // exceptions yourself.
 216  //
 217  // Example:
 218  //
 219  //	args := map[string]interface{}{
 220  //		"uniq_id": "ABC123",
 221  //	}
 222  //	got, gotErr := apiClient.CallRaw("bleed/asset/details", args)
 223  //	if gotErr != nil {
 224  //		panic(gotErr)
 225  //	}
 226  //	// Check got now for LiquidWeb specific exceptions, as described above.
 227  func (client *Client) CallRaw(method string, params interface{}) ([]byte, error) {
 228  	config := client.config
 229  	//  api wants the "params" prefix key. Do it here so consumers dont have
 230  	// to do this everytime.
 231  	args := map[string]interface{}{
 232  		"params": params,
 233  	}
 234  	encodedArgs, encodeErr := json.Marshal(args)
 235  	if encodeErr != nil {
 236  		return nil, encodeErr
 237  	}
 238  	// formulate the HTTP POST request
 239  	url := fmt.Sprintf("%s/%s", config.Url, method)
 240  	req, reqErr := http.NewRequest("POST", url, bytes.NewReader(encodedArgs))
 241  	if reqErr != nil {
 242  		return nil, reqErr
 243  	}
 244  
 245  	// We need a unique copy of the headers map in each request struct, otherwise
 246  	// we can end up with a concurrent map access and a panic.
 247  	client.mutex.Lock()
 248  	for name, value := range client.Headers {
 249  		newvalue := make([]string, len(value))
 250  		copy(newvalue, value)
 251  		req.Header[name] = newvalue
 252  	}
 253  	client.mutex.Unlock()
 254  
 255  	if config.Token != nil {
 256  		// Oauth2 token
 257  		req.Header.Add("Authorization", "Bearer "+*config.Token)
 258  	} else if config.Username != nil && config.Password != nil {
 259  		// HTTP basic auth
 260  		req.SetBasicAuth(*config.Username, *config.Password)
 261  	} else {
 262  		return nil, fmt.Errorf("No valid credential provided")
 263  	}
 264  
 265  	// make the POST request
 266  	resp, doErr := client.httpClient.Do(req)
 267  	if doErr != nil {
 268  		return nil, doErr
 269  	}
 270  	defer resp.Body.Close()
 271  	if resp.StatusCode != 200 {
 272  		return nil, fmt.Errorf("Bad HTTP response code [%d] from [%s]", resp.StatusCode, url)
 273  	}
 274  	// read the response body into a byte slice
 275  	bsRb, readErr := ioutil.ReadAll(resp.Body)
 276  	if readErr != nil {
 277  		return nil, readErr
 278  	}
 279  
 280  	return bsRb, nil
 281  }
 282  
 283  /* private */
 284  
 285  func processConfig(config *LWAPIConfig) error {
 286  	if config.Url == "" {
 287  		return fmt.Errorf("url is missing from config")
 288  	}
 289  	if config.Timeout == 0 {
 290  		config.Timeout = 20
 291  	}
 292  
 293  	if config.Token != nil {
 294  		// Oauth2 token
 295  		if *config.Token == "" {
 296  			return fmt.Errorf("Bearer token provided, but empty")
 297  		}
 298  	} else if config.Username != nil && config.Password != nil {
 299  		// HTTP basic auth
 300  		if *config.Username == "" {
 301  			return fmt.Errorf("provided username is empty")
 302  		}
 303  		if *config.Password == "" {
 304  			return fmt.Errorf("provided password is empty")
 305  		}
 306  	} else {
 307  		return fmt.Errorf("No valid credential provided")
 308  	}
 309  
 310  	return nil
 311  }
 312  
 313  func (client *Client) callRawRespToInterface(dataBytes []byte, into ...interface{}) (dataInter interface{}, err error) {
 314  	var raw map[string]interface{}
 315  	if err = json.Unmarshal(dataBytes, &raw); err == nil {
 316  		if errorClass, exists := raw["error_class"]; exists {
 317  			errorClassStr := fmt.Sprintf("%s", errorClass)
 318  			if errorClassStr != "" {
 319  				err = LWAPIError{
 320  					ErrorClass:   errorClassStr,
 321  					ErrorFullMsg: fmt.Sprintf("%s", raw["full_message"]),
 322  					ErrorMsg:     fmt.Sprintf("%s", raw["error"]),
 323  				}
 324  				return
 325  			}
 326  		}
 327  	} else {
 328  		return
 329  	}
 330  
 331  	if len(into) > 0 {
 332  		dataInter = &into[0]
 333  	}
 334  
 335  	err = json.Unmarshal(dataBytes, &dataInter)
 336  
 337  	return
 338  }
 339