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 }