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 }