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