Skip to content
Snippets Groups Projects
  • Christoph Glaubitz's avatar
    fb7848d1
    Prepared client for POST/PUT/PATCH/DELETE · fb7848d1
    Christoph Glaubitz authored
    1. Fixed issue with concating urls, removed use of path.Join
    
    The problem here: With path.Join, the trailing slash has been removed on
    each call. Even when using `NewRequest(..., "/api/XXX/", nil)`. As a result,
    Do always queries `http://nebox.example.com/api/XXX`, getting back a 301
    and retries the new Location. While this works well with GET, it breaks
    all the pushing methods. To not override too much, or too less, of
    `Client.u`, I append/prepend slashes if necessary.
    
    2. Added two new functions to construct requests
    
    * c.NewDataRequest to create a request with body
    * c.NewJSONRequest to make posts more convenient
    * Changed c.NewRequest to be a wrap for NewJSONRequest
    
    As a result, using NewRequest stays the same, but NewJSONRequest can be
    used to create "writing" functions, without the need of repeatedly
    checking for errors. e.g. for `tenant-groups`:
    
    ```
    func (s *TenantGroupsService) Update(group *TenantGroup) (*TenantGroup, error) {
    	req, err := s.c.NewJSONRequest(
    		http.MethodPatch,
    		fmt.Sprintf("api/tenancy/tenant-groups/%d/", group.ID),
    		nil,
    		group)
    	if err != nil {
    		return nil, err
    	}
    
    	g := new(TenantGroup)
    	err = s.c.Do(req, g)
    	return g, err
    }
    
    func (s *TenantGroupsService) Delete(group *TenantGroup) error {
    	req, err := s.c.NewRequest(
    		http.MethodDelete,
    		fmt.Sprintf("api/tenancy/tenant-groups/%d/", group.ID),
    		nil)
    	if err != nil {
    		return err
    	}
    
    	return s.c.Do(req, nil)
    }
    ```
    fb7848d1
    History
    Prepared client for POST/PUT/PATCH/DELETE
    Christoph Glaubitz authored
    1. Fixed issue with concating urls, removed use of path.Join
    
    The problem here: With path.Join, the trailing slash has been removed on
    each call. Even when using `NewRequest(..., "/api/XXX/", nil)`. As a result,
    Do always queries `http://nebox.example.com/api/XXX`, getting back a 301
    and retries the new Location. While this works well with GET, it breaks
    all the pushing methods. To not override too much, or too less, of
    `Client.u`, I append/prepend slashes if necessary.
    
    2. Added two new functions to construct requests
    
    * c.NewDataRequest to create a request with body
    * c.NewJSONRequest to make posts more convenient
    * Changed c.NewRequest to be a wrap for NewJSONRequest
    
    As a result, using NewRequest stays the same, but NewJSONRequest can be
    used to create "writing" functions, without the need of repeatedly
    checking for errors. e.g. for `tenant-groups`:
    
    ```
    func (s *TenantGroupsService) Update(group *TenantGroup) (*TenantGroup, error) {
    	req, err := s.c.NewJSONRequest(
    		http.MethodPatch,
    		fmt.Sprintf("api/tenancy/tenant-groups/%d/", group.ID),
    		nil,
    		group)
    	if err != nil {
    		return nil, err
    	}
    
    	g := new(TenantGroup)
    	err = s.c.Do(req, g)
    	return g, err
    }
    
    func (s *TenantGroupsService) Delete(group *TenantGroup) error {
    	req, err := s.c.NewRequest(
    		http.MethodDelete,
    		fmt.Sprintf("api/tenancy/tenant-groups/%d/", group.ID),
    		nil)
    	if err != nil {
    		return err
    	}
    
    	return s.c.Do(req, nil)
    }
    ```
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
client.go 5.89 KiB
// Copyright 2017 The go-netbox Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package netbox

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"strings"
)

// A Client is a NetBox client.  It can be used to retrieve network and
// datacenter infrastructure information from a NetBox server.
type Client struct {
	// DCIM provides access to methods in NetBox's DCIM API.
	DCIM *DCIMService

	// IPAM provides access to methods in NetBox's IPAM API.
	IPAM *IPAMService

	u      *url.URL
	client *http.Client
}

// NewClient returns a new instance of a NetBox client.  addr specifies the address
// of the NetBox server, and client specifies an optional HTTP client to use
// for requests.
//
// If client is nil, a default HTTP client will be used.
func NewClient(addr string, client *http.Client) (*Client, error) {
	if client == nil {
		client = &http.Client{}
	}

	// Append trailing slash there is none. This is necessary
	// to be able to concat url parts in a correct manner.
	// See NewRequest
	if !strings.HasSuffix(addr, "/") {
		addr = addr + "/"
	}

	u, err := url.Parse(addr)
	if err != nil {
		return nil, err
	}

	c := &Client{
		u:      u,
		client: client,
	}

	c.DCIM = &DCIMService{c: c}
	c.IPAM = &IPAMService{c: c}

	return c, nil
}

// NewRequest creates a HTTP request using the input HTTP method, URL
// endpoint, and a Valuer which creates URL parameters for the request.
//
// If a nil Valuer is specified, no query parameters will be sent with the
// request.
func (c *Client) NewRequest(method string, endpoint string, options Valuer) (*http.Request, error) {
	return c.NewDataRequest(method, endpoint, options, nil)
}

// NewDataRequest creates a HTTP request using the input HTTP method, URL
// endpoint, a Valuer which creates URL parameters for the request, and
// a io.Reader as the body of the request.
//
// If a nil Valuer is specified, no query parameters will be sent with the
// request.
// If a nil io.Reader, no body will be sent with the request.
func (c *Client) NewDataRequest(method string, endpoint string, options Valuer, body io.Reader) (*http.Request, error) {
	// Allow specifying a base path for API requests, so if a NetBox server
	// resides at a path like http://example.com/netbox/, API requests will
	// be sent to http://example.com/netbox/api/...
	//
	// Enables support of: https://github.com/digitalocean/netbox/issues/212.
	//
	// Remove leading slash if there is one. This is necessary to be able to
	// concat url parts in a correct manner. We can not use path.Join here,
	// because this always trims the trailing slash, which causes the
	// Do function to always run into 301 and then retrying the correct
	// Location. With GET, it does work with one useless request, but it breaks
	// each other http method.
	// Doing this, because out-of-tree extensions are more robust.
	rel, err := url.Parse(strings.TrimLeft(endpoint, "/"))
	if err != nil {
		return nil, err
	}

	u := c.u.ResolveReference(rel)

	// If no valuer specified, create a request with no query parameters
	if options == nil {
		return http.NewRequest(method, u.String(), body)
	}

	values, err := options.Values()
	if err != nil {
		return nil, err
	}
	u.RawQuery = values.Encode()

	return http.NewRequest(method, u.String(), body)
}

// NewJSONRequest creates a HTTP request using the input HTTP method, URL
// endpoint, a Valuer which creates URL parameters for the request, and
// a io.Reader as the body of the request. For body, expecting some
// json.Marshal-able struct. nil body is not allowed.
// NewJSONRequest also sets HTTP Header
// "Content-Type: application/json; utf-8"
//
// If a nil Valuer is specified, no query parameters will be sent with the
// request.
func (c *Client) NewJSONRequest(method string, endpoint string, options Valuer, body interface{}) (*http.Request, error) {
	if body == nil {
		return nil, errors.New("expected body to be not nil")
	}
	b := new(bytes.Buffer)
	err := json.NewEncoder(b).Encode(body)
	if err != nil {
		return nil, err
	}
	req, err := c.NewDataRequest(
		method,
		endpoint,
		options,
		b,
	)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json; charset=utf-8")
	return req, nil
}

// Do executes an HTTP request and if v is not nil, Do unmarshals result
// JSON onto v.
func (c *Client) Do(req *http.Request, v interface{}) error {
	res, err := c.client.Do(req)
	if err != nil {
		return err
	}
	defer func() {
		_ = res.Body.Close()
	}()
	// Test if the request was successful.
	// If not, do not try to decode the body. This would result in
	// misleading error messages
	err = httpStatusOK(res)
	if err != nil {
		return err
	}

	if v == nil {
		return nil
	}

	return json.NewDecoder(res.Body).Decode(v)
}

// httpStatusOK tests if the StatusCode of res is smaller than 300. Tries to
// Unmarshal the response into json, and returns only the detail
// if this exists. Otherwise returns the status code with the raw Body data.
func httpStatusOK(res *http.Response) error {
	if res.StatusCode >= http.StatusMultipleChoices {
		errDetail := struct {
			Detail string `json:"detail"`
		}{}
		bodyData, err := ioutil.ReadAll(res.Body)
		if err != nil {
			return fmt.Errorf("%d - %v", res.StatusCode, err)
		}
		err = json.Unmarshal(bodyData, &errDetail)
		if err == nil && errDetail.Detail != "" {
			return fmt.Errorf("%d - %s", res.StatusCode, errDetail.Detail)
		}

		return fmt.Errorf("%d - %s", res.StatusCode, bodyData)
	}
	return nil
}