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  }