github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/cmd/juju/commands/publish.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package commands
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/juju/cmd"
    13  	"gopkg.in/juju/charm.v5"
    14  	"gopkg.in/juju/charm.v5/charmrepo"
    15  	"launchpad.net/gnuflag"
    16  
    17  	"github.com/juju/juju/bzr"
    18  	"github.com/juju/juju/cmd/envcmd"
    19  )
    20  
    21  type PublishCommand struct {
    22  	envcmd.EnvCommandBase
    23  	URL       string
    24  	CharmPath string
    25  
    26  	// changePushLocation allows translating the branch location
    27  	// for testing purposes.
    28  	changePushLocation func(loc string) string
    29  
    30  	pollDelay time.Duration
    31  }
    32  
    33  const publishDoc = `
    34  <charm url> can be a charm URL, or an unambiguously condensed form of it;
    35  the following forms are accepted:
    36  
    37  For cs:precise/mysql
    38    cs:precise/mysql
    39    precise/mysql
    40  
    41  For cs:~user/precise/mysql
    42    cs:~user/precise/mysql
    43  
    44  There is no default series, so one must be provided explicitly when
    45  informing a charm URL. If the URL isn't provided, an attempt will be
    46  made to infer it from the current branch push URL.
    47  `
    48  
    49  func (c *PublishCommand) Info() *cmd.Info {
    50  	return &cmd.Info{
    51  		Name:    "publish",
    52  		Args:    "[<charm url>]",
    53  		Purpose: "publish charm to the store",
    54  		Doc:     publishDoc,
    55  	}
    56  }
    57  
    58  func (c *PublishCommand) SetFlags(f *gnuflag.FlagSet) {
    59  	f.StringVar(&c.CharmPath, "from", ".", "path for charm to be published")
    60  }
    61  
    62  func (c *PublishCommand) Init(args []string) error {
    63  	if len(args) == 0 {
    64  		return nil
    65  	}
    66  	c.URL = args[0]
    67  	return cmd.CheckEmpty(args[1:])
    68  }
    69  
    70  func (c *PublishCommand) ChangePushLocation(change func(string) string) {
    71  	c.changePushLocation = change
    72  }
    73  
    74  func (c *PublishCommand) SetPollDelay(delay time.Duration) {
    75  	c.pollDelay = delay
    76  }
    77  
    78  // Wording guideline to avoid confusion: charms have *URLs*, branches have *locations*.
    79  
    80  func (c *PublishCommand) Run(ctx *cmd.Context) (err error) {
    81  	branch := bzr.New(ctx.AbsPath(c.CharmPath))
    82  	if _, err := os.Stat(branch.Join(".bzr")); err != nil {
    83  		return fmt.Errorf("not a charm branch: %s", branch.Location())
    84  	}
    85  	if err := branch.CheckClean(); err != nil {
    86  		return err
    87  	}
    88  
    89  	var curl *charm.URL
    90  	if c.URL == "" {
    91  		if err == nil {
    92  			loc, err := branch.PushLocation()
    93  			if err != nil {
    94  				return fmt.Errorf("no charm URL provided and cannot infer from current directory (no push location)")
    95  			}
    96  			curl, err = charmrepo.LegacyStore.CharmURL(loc)
    97  			if err != nil {
    98  				return fmt.Errorf("cannot infer charm URL from branch location: %q", loc)
    99  			}
   100  		}
   101  	} else {
   102  		curl, err = charm.InferURL(c.URL, "")
   103  		if err != nil {
   104  			return err
   105  		}
   106  	}
   107  
   108  	pushLocation := charmrepo.LegacyStore.BranchLocation(curl)
   109  	if c.changePushLocation != nil {
   110  		pushLocation = c.changePushLocation(pushLocation)
   111  	}
   112  
   113  	repo, err := charmrepo.LegacyInferRepository(curl.Reference(), "/not/important")
   114  	if err != nil {
   115  		return err
   116  	}
   117  	if repo != charmrepo.LegacyStore {
   118  		return fmt.Errorf("charm URL must reference the juju charm store")
   119  	}
   120  
   121  	localDigest, err := branch.RevisionId()
   122  	if err != nil {
   123  		return fmt.Errorf("cannot obtain local digest: %v", err)
   124  	}
   125  	logger.Infof("local digest is %s", localDigest)
   126  
   127  	ch, err := charm.ReadCharmDir(branch.Location())
   128  	if err != nil {
   129  		return err
   130  	}
   131  	if ch.Meta().Name != curl.Name {
   132  		return fmt.Errorf("charm name in metadata must match name in URL: %q != %q", ch.Meta().Name, curl.Name)
   133  	}
   134  
   135  	oldEvent, err := charmrepo.LegacyStore.Event(curl, localDigest)
   136  	if _, ok := err.(*charmrepo.NotFoundError); ok {
   137  		oldEvent, err = charmrepo.LegacyStore.Event(curl, "")
   138  		if _, ok := err.(*charmrepo.NotFoundError); ok {
   139  			logger.Infof("charm %s is not yet in the store", curl)
   140  			err = nil
   141  		}
   142  	}
   143  	if err != nil {
   144  		return fmt.Errorf("cannot obtain event details from the store: %s", err)
   145  	}
   146  
   147  	if oldEvent != nil && oldEvent.Digest == localDigest {
   148  		return handleEvent(ctx, curl, oldEvent)
   149  	}
   150  
   151  	logger.Infof("sending charm to the charm store...")
   152  
   153  	err = branch.Push(&bzr.PushAttr{Location: pushLocation, Remember: true})
   154  	if err != nil {
   155  		return err
   156  	}
   157  	logger.Infof("charm sent; waiting for it to be published...")
   158  	for {
   159  		time.Sleep(c.pollDelay)
   160  		newEvent, err := charmrepo.LegacyStore.Event(curl, "")
   161  		if _, ok := err.(*charmrepo.NotFoundError); ok {
   162  			continue
   163  		}
   164  		if err != nil {
   165  			return fmt.Errorf("cannot obtain event details from the store: %s", err)
   166  		}
   167  		if oldEvent != nil && oldEvent.Digest == newEvent.Digest {
   168  			continue
   169  		}
   170  		if newEvent.Digest != localDigest {
   171  			// TODO Check if the published digest is in the local history.
   172  			return fmt.Errorf("charm changed but not to local charm digest; publishing race?")
   173  		}
   174  		return handleEvent(ctx, curl, newEvent)
   175  	}
   176  }
   177  
   178  func handleEvent(ctx *cmd.Context, curl *charm.URL, event *charmrepo.EventResponse) error {
   179  	switch event.Kind {
   180  	case "published":
   181  		curlRev := curl.WithRevision(event.Revision)
   182  		logger.Infof("charm published at %s as %s", event.Time, curlRev)
   183  		fmt.Fprintln(ctx.Stdout, curlRev)
   184  	case "publish-error":
   185  		return fmt.Errorf("charm could not be published: %s", strings.Join(event.Errors, "; "))
   186  	default:
   187  		return fmt.Errorf("unknown event kind %q for charm %s", event.Kind, curl)
   188  	}
   189  	return nil
   190  }