github.com/simonferquel/app@v0.6.1-0.20181012141724-68b7cccf26ac/pkg/resto/resto.go (about)

     1  package resto
     2  
     3  import (
     4  	"archive/tar"
     5  	"bytes"
     6  	"compress/gzip"
     7  	"context"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/docker/cli/cli/config"
    15  	"github.com/docker/distribution"
    16  	"github.com/docker/distribution/manifest"
    17  	"github.com/docker/distribution/manifest/schema2"
    18  	"github.com/docker/distribution/reference"
    19  	"github.com/docker/distribution/registry/client"
    20  	digest "github.com/opencontainers/go-digest"
    21  	ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
    22  	log "github.com/sirupsen/logrus"
    23  )
    24  
    25  // RegistryOptions contains optional configuration for Registry operations
    26  type RegistryOptions struct {
    27  	Username             string
    28  	Password             string
    29  	Insecure             bool
    30  	CleartextCredentials bool
    31  }
    32  
    33  type unsupportedMediaType struct{}
    34  
    35  func (u unsupportedMediaType) Error() string {
    36  	return "Unsupported media type"
    37  }
    38  
    39  // ManifestAny is a manifest type for arbitrary configuration data
    40  type ManifestAny struct {
    41  	manifest.Versioned
    42  	Payload string `json:"payload,omitempty"`
    43  }
    44  
    45  type parsedReference struct {
    46  	domain string
    47  	path   string
    48  	tag    string
    49  }
    50  
    51  func parseRef(repoTag string) (parsedReference, error) {
    52  	rawref, err := reference.ParseNormalizedNamed(repoTag)
    53  	if err != nil {
    54  		return parsedReference{}, err
    55  	}
    56  	ref, ok := rawref.(reference.Named)
    57  	if !ok {
    58  		return parseRef("docker.io/" + repoTag)
    59  	}
    60  	tag := "latest"
    61  	if rt, ok := ref.(reference.Tagged); ok {
    62  		tag = rt.Tag()
    63  	}
    64  	domain := reference.Domain(ref)
    65  	if domain == "docker.io" {
    66  		domain = "registry-1.docker.io"
    67  	}
    68  	return parsedReference{"https://" + domain, reference.Path(ref), tag}, nil
    69  }
    70  
    71  func getCredentials(domain string) (string, string, error) {
    72  	cfg, err := config.Load("")
    73  	if err != nil {
    74  		return "", "", err
    75  	}
    76  	switch domain {
    77  	case "https://registry-1.docker.io":
    78  		domain = "https://index.docker.io/v1/"
    79  	default:
    80  		domain = strings.TrimPrefix(domain, "https://")
    81  	}
    82  	auth, err := cfg.GetAuthConfig(domain)
    83  	if err != nil {
    84  		return "", "", err
    85  	}
    86  	return auth.Username, auth.Password, nil
    87  }
    88  
    89  func makeTarGz(content map[string]string) ([]byte, digest.Digest, error) {
    90  	buf := bytes.NewBuffer(nil)
    91  	err := func() error {
    92  		w := tar.NewWriter(buf)
    93  		defer w.Close()
    94  		for k, v := range content {
    95  			if err := w.WriteHeader(&tar.Header{
    96  				Typeflag: tar.TypeReg,
    97  				Name:     k,
    98  				Mode:     0600,
    99  				Size:     int64(len(v)),
   100  			}); err != nil {
   101  				return err
   102  			}
   103  			if _, err := w.Write([]byte(v)); err != nil {
   104  				return err
   105  			}
   106  		}
   107  		return nil
   108  	}()
   109  	if err != nil {
   110  		return nil, "", err
   111  	}
   112  	dgst := digest.SHA256.FromBytes(buf.Bytes())
   113  	gzbuf := bytes.NewBuffer(nil)
   114  	g := gzip.NewWriter(gzbuf)
   115  	if _, err := g.Write(buf.Bytes()); err != nil {
   116  		return nil, "", err
   117  	}
   118  	if err := g.Close(); err != nil {
   119  		return nil, "", err
   120  	}
   121  	return gzbuf.Bytes(), dgst, nil
   122  }
   123  
   124  const maxRepositoryCount = 10000
   125  
   126  // ListRepositories lists all the repositories in a registry
   127  func ListRepositories(ctx context.Context, endpoint string, opts RegistryOptions) ([]string, error) {
   128  	tr, err := NewTransportCatalog(endpoint, opts)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  	registry, err := client.NewRegistry(endpoint, tr)
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  	entries := make([]string, maxRepositoryCount)
   137  	count, err := registry.Repositories(ctx, entries, "")
   138  	if err != nil && err != io.EOF {
   139  		return nil, err
   140  	}
   141  	return entries[0:count], nil
   142  }
   143  
   144  // ListTags lists all the tags in a repository
   145  func ListTags(ctx context.Context, reponame string, opts RegistryOptions) ([]string, error) {
   146  	pr, err := parseRef(reponame)
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  	repo, err := NewRepository(ctx, pr.domain, pr.path, opts)
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  	tagService := repo.Tags(ctx)
   155  	return tagService.All(ctx)
   156  }
   157  
   158  // PullConfig pulls a configuration file from a registry
   159  func PullConfig(ctx context.Context, repoTag string, opts RegistryOptions) (string, error) {
   160  	res, err := PullConfigMulti(ctx, repoTag, opts)
   161  	if err != nil {
   162  		return "", err
   163  	}
   164  	return res["config"], nil
   165  }
   166  
   167  // PullConfigMulti pulls a set of configuration files from a registry
   168  func PullConfigMulti(ctx context.Context, repoTag string, opts RegistryOptions) (map[string]string, error) {
   169  	pr, err := parseRef(repoTag)
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  	if opts.Username == "" {
   174  		opts.Username, opts.Password, err = getCredentials(pr.domain)
   175  		if err != nil {
   176  			log.Debugf("failed to get credentials for %s: %s", pr.domain, err)
   177  		}
   178  	}
   179  	repo, err := NewRepository(ctx, pr.domain, pr.path, opts)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	tagService := repo.Tags(ctx)
   184  	dgst, err := tagService.Get(ctx, pr.tag)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  	manifestService, err := repo.Manifests(ctx)
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  	manifest, err := manifestService.Get(ctx, dgst.Digest)
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  	mediaType, payload, err := manifest.Payload()
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	if mediaType == MediaTypeConfig {
   201  		var ma ManifestAny
   202  		if err := json.Unmarshal(payload, &ma); err != nil {
   203  			return nil, err
   204  		}
   205  		res := make(map[string]string)
   206  		err = json.Unmarshal([]byte(ma.Payload), &res)
   207  		return res, err
   208  	}
   209  	// legacy image mode
   210  	return pullConfigImage(ctx, manifest, repo)
   211  }
   212  
   213  func pullConfigImage(ctx context.Context, manifest distribution.Manifest, repo distribution.Repository) (map[string]string, error) {
   214  	refs := manifest.References()
   215  	if len(refs) != 2 {
   216  		return nil, fmt.Errorf("expected 2 references, found %v", len(refs))
   217  	}
   218  	// assume second element is the layer (first being the image config)
   219  	r := refs[1]
   220  	rdgst := r.Digest
   221  	blobsService := repo.Blobs(ctx)
   222  	payloadGz, err := blobsService.Get(ctx, rdgst)
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	payloadBuf := bytes.NewBuffer(payloadGz)
   227  	gzf, err := gzip.NewReader(payloadBuf)
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  	tarReader := tar.NewReader(gzf)
   232  	return tarContent(tarReader)
   233  }
   234  
   235  func tarContent(tarReader *tar.Reader) (map[string]string, error) {
   236  	res := make(map[string]string)
   237  	for {
   238  		header, err := tarReader.Next()
   239  		if err == io.EOF {
   240  			break
   241  		}
   242  		if err != nil {
   243  			return res, err
   244  		}
   245  		if header.Typeflag == tar.TypeReg {
   246  			content := bytes.NewBuffer(nil)
   247  			io.Copy(content, tarReader)
   248  			res[header.Name] = content.String()
   249  		}
   250  	}
   251  	return res, nil
   252  }
   253  
   254  // PushConfig pushes a configuration file to a registry and returns its digest
   255  func PushConfig(ctx context.Context, payload, repoTag string, opts RegistryOptions, labels map[string]string) (string, error) {
   256  	return PushConfigMulti(ctx, map[string]string{
   257  		"config": payload,
   258  	}, repoTag, opts, labels)
   259  }
   260  
   261  // PushConfigMulti pushes a set of configuration files to a registry and returns its digest
   262  func PushConfigMulti(ctx context.Context, payload map[string]string, repoTag string, opts RegistryOptions, labels map[string]string) (string, error) {
   263  	pr, err := parseRef(repoTag)
   264  	if err != nil {
   265  		return "", err
   266  	}
   267  	if opts.Username == "" {
   268  		opts.Username, opts.Password, err = getCredentials(pr.domain)
   269  		if err != nil {
   270  			log.Debugf("failed to get credentials for %s: %s", pr.domain, err)
   271  		}
   272  	}
   273  	repo, err := NewRepository(ctx, pr.domain, pr.path, opts)
   274  	if err != nil {
   275  		return "", err
   276  	}
   277  	digest, err := pushConfigMediaType(ctx, payload, pr, repo)
   278  	if err == nil {
   279  		return digest, err
   280  	}
   281  	if _, ok := err.(unsupportedMediaType); ok {
   282  		return pushConfigLegacy(ctx, payload, pr, repo, labels)
   283  	}
   284  	return digest, err
   285  }
   286  
   287  func pushConfigMediaType(ctx context.Context, payload map[string]string, pr parsedReference, repo distribution.Repository) (string, error) {
   288  	j, err := json.Marshal(payload)
   289  	if err != nil {
   290  		return "", err
   291  	}
   292  	manifestAny := ManifestAny{
   293  		Versioned: manifest.Versioned{
   294  			SchemaVersion: 2,
   295  			MediaType:     MediaTypeConfig,
   296  		},
   297  		Payload: string(j),
   298  	}
   299  	raw, err := json.Marshal(manifestAny)
   300  	if err != nil {
   301  		return "", err
   302  	}
   303  	manifestService, err := repo.Manifests(ctx)
   304  	if err != nil {
   305  		return "", err
   306  	}
   307  	manifest := NewConfigManifest(MediaTypeConfig, raw)
   308  	dgst, err := manifestService.Put(ctx, manifest, distribution.WithTag(pr.tag))
   309  	if err == nil {
   310  		return dgst.String(), nil
   311  	}
   312  	switch {
   313  	case strings.Contains(err.Error(), "manifest invalid"):
   314  		return "", unsupportedMediaType{}
   315  	case strings.Contains(err.Error(), "manifest Unknown"):
   316  		return "", unsupportedMediaType{}
   317  	default:
   318  		return "", err
   319  	}
   320  }
   321  
   322  func pushConfigLegacy(ctx context.Context, payload map[string]string, pr parsedReference, repo distribution.Repository, labels map[string]string) (string, error) {
   323  	manifestService, err := repo.Manifests(ctx)
   324  	if err != nil {
   325  		return "", err
   326  	}
   327  	// try legacy mode
   328  	// create payload
   329  	payloadGz, payloadUncompressedDigest, err := makeTarGz(payload)
   330  	if err != nil {
   331  		return "", err
   332  	}
   333  	blobsService := repo.Blobs(ctx)
   334  	payloadDesc, err := blobsService.Put(ctx, schema2.MediaTypeLayer, payloadGz)
   335  	if err != nil {
   336  		return "", err
   337  	}
   338  	payloadDesc.MediaType = schema2.MediaTypeLayer
   339  	// create dummy image config
   340  	now := time.Now()
   341  	imageConfig := ociv1.Image{
   342  		Created:      &now,
   343  		Architecture: "config",
   344  		OS:           "config",
   345  		Config: ociv1.ImageConfig{
   346  			Labels: labels,
   347  		},
   348  		RootFS: ociv1.RootFS{
   349  			Type:    "layers",
   350  			DiffIDs: []digest.Digest{payloadUncompressedDigest},
   351  		},
   352  		History: []ociv1.History{
   353  			{CreatedBy: "COPY configfile /"},
   354  		},
   355  	}
   356  	icm, err := json.Marshal(imageConfig)
   357  	if err != nil {
   358  		return "", err
   359  	}
   360  	icDesc, err := blobsService.Put(ctx, schema2.MediaTypeImageConfig, icm)
   361  	if err != nil {
   362  		return "", err
   363  	}
   364  	icDesc.MediaType = schema2.MediaTypeImageConfig
   365  	man := schema2.Manifest{
   366  		Versioned: schema2.SchemaVersion,
   367  		Config:    icDesc,
   368  		Layers:    []distribution.Descriptor{payloadDesc},
   369  	}
   370  	dman, err := schema2.FromStruct(man)
   371  	if err != nil {
   372  		return "", err
   373  	}
   374  	dgst, err := manifestService.Put(ctx, dman, distribution.WithTag(pr.tag))
   375  	return dgst.String(), err
   376  }