github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/resource/cmd/deploy.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package cmd
     5  
     6  import (
     7  	"io"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/juju/errors"
    12  	charmresource "gopkg.in/juju/charm.v6-unstable/resource"
    13  	"gopkg.in/macaroon.v1"
    14  
    15  	"github.com/juju/juju/charmstore"
    16  )
    17  
    18  // DeployClient exposes the functionality of the resources API needed
    19  // for deploy.
    20  type DeployClient interface {
    21  	// AddPendingResources adds pending metadata for store-based resources.
    22  	AddPendingResources(applicationID string, chID charmstore.CharmID, csMac *macaroon.Macaroon, resources []charmresource.Resource) (ids []string, err error)
    23  
    24  	// AddPendingResource uploads data and metadata for a pending resource for the given application.
    25  	AddPendingResource(applicationID string, resource charmresource.Resource, filename string, r io.ReadSeeker) (id string, err error)
    26  }
    27  
    28  // DeployResourcesArgs holds the arguments to DeployResources().
    29  type DeployResourcesArgs struct {
    30  	// ApplicationID identifies the application being deployed.
    31  	ApplicationID string
    32  
    33  	// CharmID identifies the application's charm.
    34  	CharmID charmstore.CharmID
    35  
    36  	// CharmStoreMacaroon is the macaroon to use for the charm when
    37  	// interacting with the charm store.
    38  	CharmStoreMacaroon *macaroon.Macaroon
    39  
    40  	// Filenames is the set of resources for which a filename
    41  	// was provided at the command-line.
    42  	Filenames map[string]string
    43  
    44  	// Revisions is the set of resources for which a revision
    45  	// was provided at the command-line.
    46  	Revisions map[string]int
    47  
    48  	// ResourcesMeta holds the charm metadata for each of the resources
    49  	// that should be added/updated on the controller.
    50  	ResourcesMeta map[string]charmresource.Meta
    51  
    52  	// Client is the resources API client to use during deploy.
    53  	Client DeployClient
    54  }
    55  
    56  // DeployResources uploads the bytes for the given files to the server and
    57  // creates pending resource metadata for the all resource mentioned in the
    58  // metadata. It returns a map of resource name to pending resource IDs.
    59  func DeployResources(args DeployResourcesArgs) (ids map[string]string, err error) {
    60  	d := deployUploader{
    61  		applicationID: args.ApplicationID,
    62  		chID:          args.CharmID,
    63  		csMac:         args.CharmStoreMacaroon,
    64  		client:        args.Client,
    65  		resources:     args.ResourcesMeta,
    66  		osOpen:        func(s string) (ReadSeekCloser, error) { return os.Open(s) },
    67  		osStat:        func(s string) error { _, err := os.Stat(s); return err },
    68  	}
    69  
    70  	ids, err = d.upload(args.Filenames, args.Revisions)
    71  	if err != nil {
    72  		return nil, errors.Trace(err)
    73  	}
    74  	return ids, nil
    75  }
    76  
    77  type deployUploader struct {
    78  	applicationID string
    79  	chID          charmstore.CharmID
    80  	csMac         *macaroon.Macaroon
    81  	resources     map[string]charmresource.Meta
    82  	client        DeployClient
    83  	osOpen        func(path string) (ReadSeekCloser, error)
    84  	osStat        func(path string) error
    85  }
    86  
    87  func (d deployUploader) upload(files map[string]string, revisions map[string]int) (map[string]string, error) {
    88  	if err := d.validateResources(); err != nil {
    89  		return nil, errors.Trace(err)
    90  	}
    91  
    92  	if err := d.checkExpectedResources(files, revisions); err != nil {
    93  		return nil, errors.Trace(err)
    94  	}
    95  
    96  	if err := d.checkFiles(files); err != nil {
    97  		return nil, errors.Trace(err)
    98  	}
    99  
   100  	storeResources := d.storeResources(files, revisions)
   101  	pending := map[string]string{}
   102  	if len(storeResources) > 0 {
   103  		ids, err := d.client.AddPendingResources(d.applicationID, d.chID, d.csMac, storeResources)
   104  		if err != nil {
   105  			return nil, errors.Trace(err)
   106  		}
   107  		// guaranteed 1:1 correlation between ids and resources.
   108  		for i, res := range storeResources {
   109  			pending[res.Name] = ids[i]
   110  		}
   111  	}
   112  
   113  	for name, filename := range files {
   114  		id, err := d.uploadFile(name, filename)
   115  		if err != nil {
   116  			return nil, errors.Trace(err)
   117  		}
   118  		pending[name] = id
   119  	}
   120  
   121  	return pending, nil
   122  }
   123  
   124  func (d deployUploader) checkFiles(files map[string]string) error {
   125  	for name, path := range files {
   126  		err := d.osStat(path)
   127  		if os.IsNotExist(err) {
   128  			return errors.Annotatef(err, "file for resource %q", name)
   129  		}
   130  		if err != nil {
   131  			return errors.Annotatef(err, "can't read file for resource %q", name)
   132  		}
   133  	}
   134  	return nil
   135  }
   136  
   137  func (d deployUploader) validateResources() error {
   138  	var errs []error
   139  	for _, meta := range d.resources {
   140  		if err := meta.Validate(); err != nil {
   141  			errs = append(errs, err)
   142  		}
   143  	}
   144  	if len(errs) == 1 {
   145  		return errors.Trace(errs[0])
   146  	}
   147  	if len(errs) > 1 {
   148  		msgs := make([]string, len(errs))
   149  		for i, err := range errs {
   150  			msgs[i] = err.Error()
   151  		}
   152  		return errors.NewNotValid(nil, strings.Join(msgs, ", "))
   153  	}
   154  	return nil
   155  }
   156  
   157  func (d deployUploader) storeResources(uploads map[string]string, revisions map[string]int) []charmresource.Resource {
   158  	var resources []charmresource.Resource
   159  	for name, meta := range d.resources {
   160  		if _, ok := uploads[name]; ok {
   161  			continue
   162  		}
   163  
   164  		revision := -1
   165  		if rev, ok := revisions[name]; ok {
   166  			revision = rev
   167  		}
   168  
   169  		resources = append(resources, charmresource.Resource{
   170  			Meta:     meta,
   171  			Origin:   charmresource.OriginStore,
   172  			Revision: revision,
   173  			// Fingerprint and Size will be added server-side in
   174  			// the AddPendingResources() API call.
   175  		})
   176  	}
   177  	return resources
   178  }
   179  
   180  func (d deployUploader) uploadFile(resourcename, filename string) (id string, err error) {
   181  	f, err := d.osOpen(filename)
   182  	if err != nil {
   183  		return "", errors.Trace(err)
   184  	}
   185  	defer f.Close()
   186  	res := charmresource.Resource{
   187  		Meta:   d.resources[resourcename],
   188  		Origin: charmresource.OriginUpload,
   189  	}
   190  
   191  	id, err = d.client.AddPendingResource(d.applicationID, res, filename, f)
   192  	if err != nil {
   193  		return "", errors.Trace(err)
   194  	}
   195  	return id, err
   196  }
   197  
   198  func (d deployUploader) checkExpectedResources(filenames map[string]string, revisions map[string]int) error {
   199  	var unknown []string
   200  	for name := range filenames {
   201  		if _, ok := d.resources[name]; !ok {
   202  			unknown = append(unknown, name)
   203  		}
   204  	}
   205  	for name := range revisions {
   206  		if _, ok := d.resources[name]; !ok {
   207  			unknown = append(unknown, name)
   208  		}
   209  	}
   210  	if len(unknown) == 1 {
   211  		return errors.Errorf("unrecognized resource %q", unknown[0])
   212  	}
   213  	if len(unknown) > 1 {
   214  		return errors.Errorf("unrecognized resources: %s", strings.Join(unknown, ", "))
   215  	}
   216  	return nil
   217  }