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 }