github.com/uber/kraken@v0.1.4/build-index/tagclient/client.go (about) 1 // Copyright (c) 2016-2019 Uber 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 package tagclient 15 16 import ( 17 "bytes" 18 "crypto/tls" 19 "encoding/json" 20 "errors" 21 "fmt" 22 "io" 23 "io/ioutil" 24 "net/url" 25 "strconv" 26 "time" 27 28 "github.com/uber/kraken/build-index/tagmodels" 29 "github.com/uber/kraken/core" 30 "github.com/uber/kraken/lib/healthcheck" 31 "github.com/uber/kraken/utils/httputil" 32 ) 33 34 // Client errors. 35 var ( 36 ErrTagNotFound = errors.New("tag not found") 37 ) 38 39 // Client wraps tagserver endpoints. 40 type Client interface { 41 Put(tag string, d core.Digest) error 42 PutAndReplicate(tag string, d core.Digest) error 43 Get(tag string) (core.Digest, error) 44 Has(tag string) (bool, error) 45 List(prefix string) ([]string, error) 46 ListWithPagination(prefix string, filter ListFilter) (tagmodels.ListResponse, error) 47 ListRepository(repo string) ([]string, error) 48 ListRepositoryWithPagination(repo string, filter ListFilter) (tagmodels.ListResponse, error) 49 Replicate(tag string) error 50 Origin() (string, error) 51 52 DuplicateReplicate( 53 tag string, d core.Digest, dependencies core.DigestList, delay time.Duration) error 54 DuplicatePut(tag string, d core.Digest, delay time.Duration) error 55 } 56 57 type singleClient struct { 58 addr string 59 tls *tls.Config 60 } 61 62 // ListFilter contains filter request for list with pagination operations. 63 type ListFilter struct { 64 Offset string 65 Limit int 66 } 67 68 // NewSingleClient returns a Client scoped to a single tagserver instance. 69 func NewSingleClient(addr string, config *tls.Config) Client { 70 return &singleClient{addr, config} 71 } 72 73 func (c *singleClient) Put(tag string, d core.Digest) error { 74 _, err := httputil.Put( 75 fmt.Sprintf("http://%s/tags/%s/digest/%s", c.addr, url.PathEscape(tag), d.String()), 76 httputil.SendTimeout(30*time.Second), 77 httputil.SendTLS(c.tls)) 78 return err 79 } 80 81 func (c *singleClient) PutAndReplicate(tag string, d core.Digest) error { 82 _, err := httputil.Put( 83 fmt.Sprintf("http://%s/tags/%s/digest/%s?replicate=true", c.addr, url.PathEscape(tag), d.String()), 84 httputil.SendTimeout(30*time.Second), 85 httputil.SendTLS(c.tls)) 86 return err 87 } 88 89 func (c *singleClient) Get(tag string) (core.Digest, error) { 90 resp, err := httputil.Get( 91 fmt.Sprintf("http://%s/tags/%s", c.addr, url.PathEscape(tag)), 92 httputil.SendTimeout(10*time.Second), 93 httputil.SendTLS(c.tls)) 94 if err != nil { 95 if httputil.IsNotFound(err) { 96 return core.Digest{}, ErrTagNotFound 97 } 98 return core.Digest{}, err 99 } 100 defer resp.Body.Close() 101 b, err := ioutil.ReadAll(resp.Body) 102 if err != nil { 103 return core.Digest{}, fmt.Errorf("read body: %s", err) 104 } 105 d, err := core.ParseSHA256Digest(string(b)) 106 if err != nil { 107 return core.Digest{}, fmt.Errorf("new digest: %s", err) 108 } 109 return d, nil 110 } 111 112 func (c *singleClient) Has(tag string) (bool, error) { 113 _, err := httputil.Head( 114 fmt.Sprintf("http://%s/tags/%s", c.addr, url.PathEscape(tag)), 115 httputil.SendTimeout(10*time.Second), 116 httputil.SendTLS(c.tls)) 117 if err != nil { 118 if httputil.IsNotFound(err) { 119 return false, nil 120 } 121 return false, err 122 } 123 return true, nil 124 } 125 126 func (c *singleClient) doListPaginated(urlFormat string, pathSub string, 127 filter ListFilter) (tagmodels.ListResponse, error) { 128 129 // Build query. 130 reqVal := url.Values{} 131 if filter.Offset != "" { 132 reqVal.Add(tagmodels.OffsetQ, filter.Offset) 133 } 134 if filter.Limit != 0 { 135 reqVal.Add(tagmodels.LimitQ, strconv.Itoa(filter.Limit)) 136 } 137 138 // Fetch list response from server. 139 serverUrl := url.URL{ 140 Scheme: "http", 141 Host: c.addr, 142 Path: fmt.Sprintf(urlFormat, pathSub), 143 RawQuery: reqVal.Encode(), 144 } 145 var resp tagmodels.ListResponse 146 httpResp, err := httputil.Get( 147 serverUrl.String(), 148 httputil.SendTimeout(60*time.Second), 149 httputil.SendTLS(c.tls)) 150 if err != nil { 151 return resp, err 152 } 153 defer httpResp.Body.Close() 154 if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil { 155 return resp, fmt.Errorf("json decode: %s", err) 156 } 157 158 return resp, nil 159 } 160 161 func (c *singleClient) doList(pathSub string, 162 fn func(pathSub string, filter ListFilter) (tagmodels.ListResponse, error)) ( 163 []string, error) { 164 165 var names []string 166 167 offset := "" 168 for ok := true; ok; ok = (offset != "") { 169 filter := ListFilter{Offset: offset} 170 resp, err := fn(pathSub, filter) 171 if err != nil { 172 return nil, err 173 } 174 offset, err = resp.GetOffset() 175 if err != nil && err != io.EOF { 176 return nil, err 177 } 178 names = append(names, resp.Result...) 179 } 180 return names, nil 181 } 182 183 func (c *singleClient) List(prefix string) ([]string, error) { 184 return c.doList(prefix, func(prefix string, filter ListFilter) ( 185 tagmodels.ListResponse, error) { 186 187 return c.ListWithPagination(prefix, filter) 188 }) 189 } 190 191 func (c *singleClient) ListWithPagination(prefix string, filter ListFilter) ( 192 tagmodels.ListResponse, error) { 193 194 return c.doListPaginated("list/%s", prefix, filter) 195 } 196 197 // XXX: Deprecated. Use List instead. 198 func (c *singleClient) ListRepository(repo string) ([]string, error) { 199 return c.doList(repo, func(repo string, filter ListFilter) ( 200 tagmodels.ListResponse, error) { 201 202 return c.ListRepositoryWithPagination(repo, filter) 203 }) 204 } 205 206 func (c *singleClient) ListRepositoryWithPagination(repo string, 207 filter ListFilter) (tagmodels.ListResponse, error) { 208 209 return c.doListPaginated("repositories/%s/tags", url.PathEscape(repo), filter) 210 } 211 212 // ReplicateRequest defines a Replicate request body. 213 type ReplicateRequest struct { 214 Dependencies []core.Digest `json:"dependencies"` 215 } 216 217 func (c *singleClient) Replicate(tag string) error { 218 _, err := httputil.Post( 219 fmt.Sprintf("http://%s/remotes/tags/%s", c.addr, url.PathEscape(tag)), 220 httputil.SendTimeout(15*time.Second), 221 httputil.SendTLS(c.tls)) 222 return err 223 } 224 225 // DuplicateReplicateRequest defines a DuplicateReplicate request body. 226 type DuplicateReplicateRequest struct { 227 Dependencies core.DigestList `json:"dependencies"` 228 Delay time.Duration `json:"delay"` 229 } 230 231 func (c *singleClient) DuplicateReplicate( 232 tag string, d core.Digest, dependencies core.DigestList, delay time.Duration) error { 233 234 b, err := json.Marshal(DuplicateReplicateRequest{dependencies, delay}) 235 if err != nil { 236 return fmt.Errorf("json marshal: %s", err) 237 } 238 _, err = httputil.Post( 239 fmt.Sprintf( 240 "http://%s/internal/duplicate/remotes/tags/%s/digest/%s", 241 c.addr, url.PathEscape(tag), d.String()), 242 httputil.SendBody(bytes.NewReader(b)), 243 httputil.SendTimeout(10*time.Second), 244 httputil.SendRetry(), 245 httputil.SendTLS(c.tls)) 246 return err 247 } 248 249 // DuplicatePutRequest defines a DuplicatePut request body. 250 type DuplicatePutRequest struct { 251 Delay time.Duration `json:"delay"` 252 } 253 254 func (c *singleClient) DuplicatePut(tag string, d core.Digest, delay time.Duration) error { 255 b, err := json.Marshal(DuplicatePutRequest{delay}) 256 if err != nil { 257 return fmt.Errorf("json marshal: %s", err) 258 } 259 _, err = httputil.Put( 260 fmt.Sprintf( 261 "http://%s/internal/duplicate/tags/%s/digest/%s", 262 c.addr, url.PathEscape(tag), d.String()), 263 httputil.SendBody(bytes.NewReader(b)), 264 httputil.SendTimeout(10*time.Second), 265 httputil.SendRetry(), 266 httputil.SendTLS(c.tls)) 267 return err 268 } 269 270 func (c *singleClient) Origin() (string, error) { 271 resp, err := httputil.Get( 272 fmt.Sprintf("http://%s/origin", c.addr), 273 httputil.SendTimeout(5*time.Second), 274 httputil.SendTLS(c.tls)) 275 if err != nil { 276 return "", err 277 } 278 defer resp.Body.Close() 279 b, err := ioutil.ReadAll(resp.Body) 280 if err != nil { 281 return "", fmt.Errorf("read body: %s", err) 282 } 283 return string(b), nil 284 } 285 286 type clusterClient struct { 287 hosts healthcheck.List 288 tls *tls.Config 289 } 290 291 // NewClusterClient creates a Client which operates on tagserver instances as 292 // a cluster. 293 func NewClusterClient(hosts healthcheck.List, config *tls.Config) Client { 294 return &clusterClient{hosts, config} 295 } 296 297 func (cc *clusterClient) do(request func(c Client) error) error { 298 addrs := cc.hosts.Resolve().Sample(3) 299 if len(addrs) == 0 { 300 return errors.New("cluster client: no hosts could be resolved") 301 } 302 var err error 303 for addr := range addrs { 304 err = request(NewSingleClient(addr, cc.tls)) 305 if httputil.IsNetworkError(err) { 306 cc.hosts.Failed(addr) 307 continue 308 } 309 break 310 } 311 return err 312 } 313 314 func (cc *clusterClient) Put(tag string, d core.Digest) error { 315 return cc.do(func(c Client) error { return c.Put(tag, d) }) 316 } 317 318 func (cc *clusterClient) PutAndReplicate(tag string, d core.Digest) error { 319 return cc.do(func(c Client) error { return c.PutAndReplicate(tag, d) }) 320 } 321 322 func (cc *clusterClient) Get(tag string) (d core.Digest, err error) { 323 err = cc.do(func(c Client) error { 324 d, err = c.Get(tag) 325 return err 326 }) 327 return 328 } 329 330 func (cc *clusterClient) Has(tag string) (ok bool, err error) { 331 err = cc.do(func(c Client) error { 332 ok, err = c.Has(tag) 333 return err 334 }) 335 return 336 } 337 338 func (cc *clusterClient) List(prefix string) (tags []string, err error) { 339 err = cc.do(func(c Client) error { 340 tags, err = c.List(prefix) 341 return err 342 }) 343 return 344 } 345 346 func (cc *clusterClient) ListWithPagination(prefix string, filter ListFilter) ( 347 resp tagmodels.ListResponse, err error) { 348 349 err = cc.do(func(c Client) error { 350 resp, err = c.ListWithPagination(prefix, filter) 351 return err 352 }) 353 return 354 } 355 356 func (cc *clusterClient) ListRepository(repo string) (tags []string, err error) { 357 err = cc.do(func(c Client) error { 358 tags, err = c.ListRepository(repo) 359 return err 360 }) 361 return 362 } 363 364 func (cc *clusterClient) ListRepositoryWithPagination(repo string, 365 filter ListFilter) (resp tagmodels.ListResponse, err error) { 366 367 err = cc.do(func(c Client) error { 368 resp, err = c.ListRepositoryWithPagination(repo, filter) 369 return err 370 }) 371 return 372 } 373 374 func (cc *clusterClient) Replicate(tag string) error { 375 return cc.do(func(c Client) error { return c.Replicate(tag) }) 376 } 377 378 func (cc *clusterClient) Origin() (origin string, err error) { 379 err = cc.do(func(c Client) error { 380 origin, err = c.Origin() 381 return err 382 }) 383 return 384 } 385 386 func (cc *clusterClient) DuplicateReplicate( 387 tag string, d core.Digest, dependencies core.DigestList, delay time.Duration) error { 388 389 return errors.New("duplicate replicate not supported on cluster client") 390 } 391 392 func (cc *clusterClient) DuplicatePut(tag string, d core.Digest, delay time.Duration) error { 393 return errors.New("duplicate put not supported on cluster client") 394 }