github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/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  	"launchpad.net/gnuflag"
    13  
    14  	"launchpad.net/juju-core/bzr"
    15  	"launchpad.net/juju-core/charm"
    16  	"launchpad.net/juju-core/cmd"
    17  	"launchpad.net/juju-core/log"
    18  )
    19  
    20  type PublishCommand struct {
    21  	cmd.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  	c.EnvCommandBase.SetFlags(f)
    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 = charm.Store.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 := charm.Store.BranchLocation(curl)
   109  	if c.changePushLocation != nil {
   110  		pushLocation = c.changePushLocation(pushLocation)
   111  	}
   112  
   113  	repo, err := charm.InferRepository(curl, "/not/important")
   114  	if err != nil {
   115  		return err
   116  	}
   117  	if repo != charm.Store {
   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  	log.Infof("local digest is %s", localDigest)
   126  
   127  	ch, err := charm.ReadDir(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 := charm.Store.Event(curl, localDigest)
   136  	if _, ok := err.(*charm.NotFoundError); ok {
   137  		oldEvent, err = charm.Store.Event(curl, "")
   138  		if _, ok := err.(*charm.NotFoundError); ok {
   139  			log.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  	log.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  	log.Infof("charm sent; waiting for it to be published...")
   158  	for {
   159  		time.Sleep(c.pollDelay)
   160  		newEvent, err := charm.Store.Event(curl, "")
   161  		if _, ok := err.(*charm.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  	return nil
   177  }
   178  
   179  func handleEvent(ctx *cmd.Context, curl *charm.URL, event *charm.EventResponse) error {
   180  	switch event.Kind {
   181  	case "published":
   182  		curlRev := curl.WithRevision(event.Revision)
   183  		log.Infof("charm published at %s as %s", event.Time, curlRev)
   184  		fmt.Fprintln(ctx.Stdout, curlRev)
   185  	case "publish-error":
   186  		return fmt.Errorf("charm could not be published: %s", strings.Join(event.Errors, "; "))
   187  	default:
   188  		return fmt.Errorf("unknown event kind %q for charm %s", event.Kind, curl)
   189  	}
   190  	return nil
   191  }