github.com/jdhenke/godel@v0.0.0-20161213181855-abeb3861bf0d/apps/distgo/cmd/publish/almanac.go (about)

     1  // Copyright 2016 Palantir Technologies, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package publish
    16  
    17  import (
    18  	"bytes"
    19  	"crypto/hmac"
    20  	"crypto/sha1"
    21  	"encoding/base64"
    22  	"encoding/json"
    23  	"fmt"
    24  	"io/ioutil"
    25  	"net/http"
    26  	"net/url"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/pkg/errors"
    31  )
    32  
    33  type AlmanacUnit struct {
    34  	Product  string            `json:"product"`
    35  	Branch   string            `json:"branch"`
    36  	Revision string            `json:"revision"`
    37  	URL      string            `json:"url"`
    38  	Tags     []string          `json:"tags,omitempty"`
    39  	Metadata map[string]string `json:"metadata,omitempty"`
    40  }
    41  
    42  type AlmanacInfo struct {
    43  	URL      string
    44  	AccessID string
    45  	Secret   string
    46  	Release  bool
    47  }
    48  
    49  func (a AlmanacInfo) CheckConnectivity(client *http.Client) error {
    50  	_, err := a.get(client, "/v1/units")
    51  	return err
    52  }
    53  
    54  func (a AlmanacInfo) CheckProduct(client *http.Client, product string) error {
    55  	_, err := a.get(client, strings.Join([]string{"/v1/units", product}, "/"))
    56  	return err
    57  }
    58  
    59  func (a AlmanacInfo) CreateProduct(client *http.Client, product string) error {
    60  	_, err := a.do(client, http.MethodPost, "/v1/units/products", fmt.Sprintf(`{"name":"%v"}`, product))
    61  	return err
    62  }
    63  
    64  func (a AlmanacInfo) CheckProductBranch(client *http.Client, product, branch string) error {
    65  	_, err := a.get(client, strings.Join([]string{"/v1/units", product, branch}, "/"))
    66  	return err
    67  }
    68  
    69  func (a AlmanacInfo) CreateProductBranch(client *http.Client, product, branch string) error {
    70  	_, err := a.do(client, http.MethodPost, strings.Join([]string{"/v1/units", product}, "/"), fmt.Sprintf(`{"name":"%v"}`, branch))
    71  	return err
    72  }
    73  
    74  func (a AlmanacInfo) GetUnit(client *http.Client, product, branch, revision string) ([]byte, error) {
    75  	return a.get(client, strings.Join([]string{"/v1/units", product, branch, revision}, "/"))
    76  }
    77  
    78  func (a AlmanacInfo) CreateUnit(client *http.Client, unit AlmanacUnit, version string) error {
    79  	endpoint := "/v1/units"
    80  
    81  	// set version field of metadata to be version
    82  	if unit.Metadata == nil {
    83  		unit.Metadata = make(map[string]string)
    84  	}
    85  	unit.Metadata["version"] = version
    86  
    87  	jsonBytes, err := json.Marshal(unit)
    88  	if err != nil {
    89  		return errors.Wrapf(err, "Failed to marshal %v as JSON", unit)
    90  	}
    91  
    92  	_, err = a.do(client, http.MethodPost, endpoint, string(jsonBytes))
    93  	return err
    94  }
    95  
    96  func (a AlmanacInfo) ReleaseProduct(client *http.Client, product, branch, revision string) error {
    97  	gaBody := map[string]string{
    98  		"name": "GA",
    99  	}
   100  	jsonBytes, err := json.Marshal(gaBody)
   101  	if err != nil {
   102  		return errors.Wrapf(err, "Failed to marshal %v as JSON", gaBody)
   103  	}
   104  
   105  	_, err = a.do(client, http.MethodPost, strings.Join([]string{"/v1/units", product, branch, revision, "releases"}, "/"), string(jsonBytes))
   106  	return err
   107  }
   108  
   109  func (a AlmanacInfo) get(client *http.Client, endpoint string) ([]byte, error) {
   110  	return a.do(client, http.MethodGet, endpoint, "")
   111  }
   112  
   113  func (a AlmanacInfo) do(client *http.Client, method, endpoint, body string) (rBody []byte, rErr error) {
   114  	destURL, err := url.Parse(a.URL + endpoint)
   115  	if err != nil {
   116  		return nil, errors.Wrapf(err, "Failed")
   117  	}
   118  
   119  	req := http.Request{
   120  		Method: method,
   121  		URL:    destURL,
   122  	}
   123  
   124  	if body != "" {
   125  		req.Header = http.Header{
   126  			"Content-Type": []string{"application/json"},
   127  		}
   128  		req.Body = ioutil.NopCloser(bytes.NewReader([]byte(body)))
   129  		req.ContentLength = int64(len([]byte(body)))
   130  	}
   131  
   132  	if err := addAlmanacAuthForRequest(a.AccessID, a.Secret, body, &req); err != nil {
   133  		return nil, errors.Wrapf(err, "Failed to add Almanac authorization info to header for request %v", req)
   134  	}
   135  
   136  	resp, err := client.Do(&req)
   137  	if err != nil {
   138  		// remove authorization information from header before returning as part of error
   139  		req.Header.Del("X-authorization")
   140  		return nil, errors.Wrapf(err, "Almanac request failed: %v", req)
   141  	}
   142  
   143  	defer func() {
   144  		if err := resp.Body.Close(); err != nil && rErr == nil {
   145  			rErr = errors.Wrapf(err, "failed to close response body for %s", destURL.String())
   146  		}
   147  	}()
   148  	responseBytes, err := ioutil.ReadAll(resp.Body)
   149  	if err != nil {
   150  		return nil, errors.Wrapf(err, "Failed to read response body for %v", destURL.String())
   151  	}
   152  
   153  	if resp.StatusCode >= http.StatusBadRequest {
   154  		return responseBytes, errors.Errorf("Received non-success status code: %v. Response: %s", resp.Status, string(responseBytes))
   155  	}
   156  
   157  	return responseBytes, nil
   158  }
   159  
   160  // Adds the X-timestamp and X-authorization header entries to the provided http.Request. Assumes that the body of the
   161  // request will be the byte representation of the "body" string.
   162  func addAlmanacAuthForRequest(accessID, secret, body string, req *http.Request) error {
   163  	// if request Header is nil, initialize to empty object so that map assignment can be done
   164  	if req.Header == nil {
   165  		req.Header = http.Header{}
   166  	}
   167  
   168  	timestamp := time.Now().Unix()
   169  	req.Header.Add("X-timestamp", fmt.Sprintf("%d", timestamp))
   170  
   171  	hmac, err := hmacSHA1(fmt.Sprintf("%v%d%v", req.URL.String(), timestamp, body), secret)
   172  	if err != nil {
   173  		return err
   174  	}
   175  	req.Header.Add("X-authorization", fmt.Sprintf("%v:%v", accessID, hmac))
   176  	return nil
   177  }
   178  
   179  func hmacSHA1(message string, secret string) (string, error) {
   180  	h := hmac.New(sha1.New, []byte(secret))
   181  	if _, err := h.Write([]byte(message)); err != nil {
   182  		return "", errors.Wrapf(err, "Failed to compute HMAC-SHA1")
   183  	}
   184  	return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
   185  }