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 }