github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/cmd/juju/publish.go (about)

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