github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/resource/deploy.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package resource
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"os"
    13  	"strings"
    14  
    15  	"github.com/juju/errors"
    16  	charmresource "gopkg.in/juju/charm.v6/resource"
    17  	"gopkg.in/macaroon.v2-unstable"
    18  	"gopkg.in/yaml.v2"
    19  
    20  	"github.com/juju/juju/charmstore"
    21  	resources "github.com/juju/juju/core/resources"
    22  )
    23  
    24  // DeployClient exposes the functionality of the resources API needed
    25  // for deploy.
    26  type DeployClient interface {
    27  	// AddPendingResources adds pending metadata for store-based resources.
    28  	AddPendingResources(applicationID string, chID charmstore.CharmID, csMac *macaroon.Macaroon, resources []charmresource.Resource) (ids []string, err error)
    29  
    30  	// UploadPendingResource uploads data and metadata for a pending resource for the given application.
    31  	UploadPendingResource(applicationID string, resource charmresource.Resource, filename string, r io.ReadSeeker) (id string, err error)
    32  }
    33  
    34  // DeployResourcesArgs holds the arguments to DeployResources().
    35  type DeployResourcesArgs struct {
    36  	// ApplicationID identifies the application being deployed.
    37  	ApplicationID string
    38  
    39  	// CharmID identifies the application's charm.
    40  	CharmID charmstore.CharmID
    41  
    42  	// CharmStoreMacaroon is the macaroon to use for the charm when
    43  	// interacting with the charm store.
    44  	CharmStoreMacaroon *macaroon.Macaroon
    45  
    46  	// ResourceValues is the set of resources for which a value
    47  	// was provided at the command-line.
    48  	ResourceValues map[string]string
    49  
    50  	// Revisions is the set of resources for which a revision
    51  	// was provided at the command-line.
    52  	Revisions map[string]int
    53  
    54  	// ResourcesMeta holds the charm metadata for each of the resources
    55  	// that should be added/updated on the controller.
    56  	ResourcesMeta map[string]charmresource.Meta
    57  
    58  	// Client is the resources API client to use during deploy.
    59  	Client DeployClient
    60  }
    61  
    62  // DeployResources uploads the bytes for the given files to the server and
    63  // creates pending resource metadata for the all resource mentioned in the
    64  // metadata. It returns a map of resource name to pending resource IDs.
    65  func DeployResources(args DeployResourcesArgs) (ids map[string]string, err error) {
    66  	d := deployUploader{
    67  		applicationID: args.ApplicationID,
    68  		chID:          args.CharmID,
    69  		csMac:         args.CharmStoreMacaroon,
    70  		client:        args.Client,
    71  		resources:     args.ResourcesMeta,
    72  		osOpen:        func(s string) (ReadSeekCloser, error) { return os.Open(s) },
    73  		osStat:        func(s string) error { _, err := os.Stat(s); return err },
    74  	}
    75  
    76  	ids, err = d.upload(args.ResourceValues, args.Revisions)
    77  	if err != nil {
    78  		return nil, errors.Trace(err)
    79  	}
    80  	return ids, nil
    81  }
    82  
    83  type deployUploader struct {
    84  	applicationID string
    85  	chID          charmstore.CharmID
    86  	csMac         *macaroon.Macaroon
    87  	resources     map[string]charmresource.Meta
    88  	client        DeployClient
    89  	osOpen        func(path string) (ReadSeekCloser, error)
    90  	osStat        func(path string) error
    91  }
    92  
    93  func (d deployUploader) upload(resourceValues map[string]string, revisions map[string]int) (map[string]string, error) {
    94  	if err := d.validateResources(); err != nil {
    95  		return nil, errors.Trace(err)
    96  	}
    97  
    98  	if err := d.checkExpectedResources(resourceValues, revisions); err != nil {
    99  		return nil, errors.Trace(err)
   100  	}
   101  
   102  	if err := d.validateResourceDetails(resourceValues); err != nil {
   103  		return nil, errors.Trace(err)
   104  	}
   105  
   106  	storeResources := d.charmStoreResources(resourceValues, revisions)
   107  	pending := map[string]string{}
   108  	if len(storeResources) > 0 {
   109  		ids, err := d.client.AddPendingResources(d.applicationID, d.chID, d.csMac, storeResources)
   110  		if err != nil {
   111  			return nil, errors.Trace(err)
   112  		}
   113  		// guaranteed 1:1 correlation between ids and resources.
   114  		for i, res := range storeResources {
   115  			pending[res.Name] = ids[i]
   116  		}
   117  	}
   118  
   119  	for name, resValue := range resourceValues {
   120  		var (
   121  			id  string
   122  			err error
   123  		)
   124  		switch d.resources[name].Type {
   125  		case charmresource.TypeFile:
   126  			id, err = d.uploadFile(name, resValue)
   127  		case charmresource.TypeContainerImage:
   128  			id, err = d.uploadDockerDetails(name, resValue)
   129  		default:
   130  			err = errors.New("unknown resource type to upload")
   131  		}
   132  
   133  		if err != nil {
   134  			return nil, errors.Trace(err)
   135  		}
   136  		pending[name] = id
   137  	}
   138  
   139  	return pending, nil
   140  }
   141  
   142  func (d deployUploader) validateResourceDetails(res map[string]string) error {
   143  	for name, value := range res {
   144  		var err error
   145  		switch d.resources[name].Type {
   146  		case charmresource.TypeFile:
   147  			err = d.checkFile(name, value)
   148  		case charmresource.TypeContainerImage:
   149  			dockerDetails, err := getDockerDetailsData(value)
   150  			if err != nil {
   151  				return err
   152  			}
   153  			// At the moment this is the same validation that occurs in getDockerDetailsData
   154  			err = resources.CheckDockerDetails(name, dockerDetails)
   155  		default:
   156  			return fmt.Errorf("unknown resource: %s", name)
   157  		}
   158  		if err != nil {
   159  			return err
   160  		}
   161  	}
   162  	return nil
   163  }
   164  
   165  func (d deployUploader) checkFile(name, path string) error {
   166  	err := d.osStat(path)
   167  	if os.IsNotExist(err) {
   168  		return errors.Annotatef(err, "file for resource %q", name)
   169  	}
   170  	if err != nil {
   171  		return errors.Annotatef(err, "can't read file for resource %q", name)
   172  	}
   173  	return nil
   174  }
   175  
   176  func (d deployUploader) validateResources() error {
   177  	var errs []error
   178  	for _, meta := range d.resources {
   179  		if err := meta.Validate(); err != nil {
   180  			errs = append(errs, err)
   181  		}
   182  	}
   183  	if len(errs) == 1 {
   184  		return errors.Trace(errs[0])
   185  	}
   186  	if len(errs) > 1 {
   187  		msgs := make([]string, len(errs))
   188  		for i, err := range errs {
   189  			msgs[i] = err.Error()
   190  		}
   191  		return errors.NewNotValid(nil, strings.Join(msgs, ", "))
   192  	}
   193  	return nil
   194  }
   195  
   196  // charmStoreResources returns which resources revisions will need to be retrieved
   197  // either as they where explicitly requested by the user for that rev or they
   198  // weren't provided by the user.
   199  func (d deployUploader) charmStoreResources(uploads map[string]string, revisions map[string]int) []charmresource.Resource {
   200  	var resources []charmresource.Resource
   201  	for name, meta := range d.resources {
   202  		if _, ok := uploads[name]; ok {
   203  			continue
   204  		}
   205  
   206  		revision := -1
   207  		if rev, ok := revisions[name]; ok {
   208  			revision = rev
   209  		}
   210  
   211  		resources = append(resources, charmresource.Resource{
   212  			Meta:     meta,
   213  			Origin:   charmresource.OriginStore,
   214  			Revision: revision,
   215  			// Fingerprint and Size will be added server-side in
   216  			// the AddPendingResources() API call.
   217  		})
   218  	}
   219  	return resources
   220  }
   221  
   222  func (d deployUploader) uploadPendingResource(resourcename, resourcevalue string, data io.ReadSeeker) (id string, err error) {
   223  	res := charmresource.Resource{
   224  		Meta:   d.resources[resourcename],
   225  		Origin: charmresource.OriginUpload,
   226  	}
   227  
   228  	return d.client.UploadPendingResource(d.applicationID, res, resourcevalue, data)
   229  }
   230  
   231  func (d deployUploader) uploadFile(resourcename, filename string) (id string, err error) {
   232  	f, err := d.osOpen(filename)
   233  	if err != nil {
   234  		return "", errors.Trace(err)
   235  	}
   236  	defer f.Close()
   237  
   238  	id, err = d.uploadPendingResource(resourcename, filename, f)
   239  	if err != nil {
   240  		return "", errors.Trace(err)
   241  	}
   242  	return id, err
   243  }
   244  
   245  func (d deployUploader) uploadDockerDetails(resourcename, registryPath string) (id string, error error) {
   246  	dockerDetails, err := getDockerDetailsData(registryPath)
   247  	if err != nil {
   248  		return "", errors.Trace(err)
   249  	}
   250  	data, err := json.Marshal(dockerDetails)
   251  	if err != nil {
   252  		return "", errors.Trace(err)
   253  	}
   254  	dr := bytes.NewReader(data)
   255  
   256  	id, err = d.uploadPendingResource(resourcename, registryPath, dr)
   257  	if err != nil {
   258  		return "", errors.Trace(err)
   259  	}
   260  	return id, nil
   261  }
   262  
   263  func (d deployUploader) checkExpectedResources(filenames map[string]string, revisions map[string]int) error {
   264  	var unknown []string
   265  	for name := range filenames {
   266  		if _, ok := d.resources[name]; !ok {
   267  			unknown = append(unknown, name)
   268  		}
   269  	}
   270  	for name := range revisions {
   271  		if _, ok := d.resources[name]; !ok {
   272  			unknown = append(unknown, name)
   273  		}
   274  	}
   275  	if len(unknown) == 1 {
   276  		return errors.Errorf("unrecognized resource %q", unknown[0])
   277  	}
   278  	if len(unknown) > 1 {
   279  		return errors.Errorf("unrecognized resources: %s", strings.Join(unknown, ", "))
   280  	}
   281  	return nil
   282  }
   283  
   284  // getDockerDetailsData determines if path is a local file path and extracts the
   285  // details from that otherwise path is considered to be a registry path.
   286  func getDockerDetailsData(path string) (resources.DockerImageDetails, error) {
   287  	f, err := os.Open(path)
   288  	if err == nil {
   289  		defer f.Close()
   290  		details, err := unMarshalDockerDetails(f)
   291  		if err != nil {
   292  			return details, errors.Trace(err)
   293  		}
   294  		return details, nil
   295  	} else if err := resources.ValidateDockerRegistryPath(path); err == nil {
   296  		return resources.DockerImageDetails{
   297  			RegistryPath: path,
   298  		}, nil
   299  	}
   300  	return resources.DockerImageDetails{}, errors.NotValidf("filepath or registry path: %s", path)
   301  
   302  }
   303  
   304  func unMarshalDockerDetails(data io.Reader) (resources.DockerImageDetails, error) {
   305  	var details resources.DockerImageDetails
   306  	contents, err := ioutil.ReadAll(data)
   307  	if err != nil {
   308  		return details, errors.Trace(err)
   309  	}
   310  
   311  	if err := json.Unmarshal(contents, &details); err != nil {
   312  		if err := yaml.Unmarshal(contents, &details); err != nil {
   313  			return details, errors.Annotate(err, "file neither valid json or yaml")
   314  		}
   315  	}
   316  	if err := resources.ValidateDockerRegistryPath(details.RegistryPath); err != nil {
   317  		return resources.DockerImageDetails{}, err
   318  	}
   319  	return details, nil
   320  }