github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/cmd/juju/cloud/updateclouds.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package cloud 5 6 import ( 7 "bytes" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "net/http" 12 "sort" 13 "strings" 14 15 "github.com/juju/cmd" 16 "github.com/juju/errors" 17 "github.com/juju/utils" 18 "github.com/juju/utils/set" 19 "golang.org/x/crypto/openpgp" 20 "golang.org/x/crypto/openpgp/clearsign" 21 22 jujucloud "github.com/juju/juju/cloud" 23 "github.com/juju/juju/juju" 24 ) 25 26 type updateCloudsCommand struct { 27 cmd.CommandBase 28 29 publicSigningKey string 30 publicCloudURL string 31 } 32 33 var updateCloudsDoc = ` 34 If any new information for public clouds (such as regions and connection 35 endpoints) are available this command will update Juju accordingly. It is 36 suggested to run this command periodically. 37 38 Examples: 39 40 juju update-clouds 41 42 See also: list-clouds 43 ` 44 45 // NewUpdateCloudsCommand returns a command to update cloud information. 46 func NewUpdateCloudsCommand() cmd.Command { 47 return &updateCloudsCommand{ 48 publicSigningKey: juju.JujuPublicKey, 49 publicCloudURL: "https://streams.canonical.com/juju/public-clouds.syaml", 50 } 51 } 52 53 func (c *updateCloudsCommand) Info() *cmd.Info { 54 return &cmd.Info{ 55 Name: "update-clouds", 56 Purpose: "Updates public cloud information available to Juju.", 57 Doc: updateCloudsDoc, 58 } 59 } 60 61 func (c *updateCloudsCommand) Run(ctxt *cmd.Context) error { 62 fmt.Fprint(ctxt.Stdout, "Fetching latest public cloud list...\n") 63 client := utils.GetHTTPClient(utils.VerifySSLHostnames) 64 resp, err := client.Get(c.publicCloudURL) 65 if err != nil { 66 return err 67 } 68 defer resp.Body.Close() 69 70 if resp.StatusCode != http.StatusOK { 71 switch resp.StatusCode { 72 case http.StatusNotFound: 73 fmt.Fprintln(ctxt.Stdout, "Public cloud list is unavailable right now.") 74 return nil 75 case http.StatusUnauthorized: 76 return errors.Unauthorizedf("unauthorised access to URL %q", c.publicCloudURL) 77 } 78 return fmt.Errorf("cannot read public cloud information at URL %q, %q", c.publicCloudURL, resp.Status) 79 } 80 81 cloudData, err := decodeCheckSignature(resp.Body, c.publicSigningKey) 82 if err != nil { 83 return errors.Annotate(err, "error receiving updated cloud data") 84 } 85 newPublicClouds, err := jujucloud.ParseCloudMetadata(cloudData) 86 if err != nil { 87 return errors.Annotate(err, "invalid cloud data received when updating clouds") 88 } 89 currentPublicClouds, _, err := jujucloud.PublicCloudMetadata(jujucloud.JujuPublicCloudsPath()) 90 if err != nil { 91 return errors.Annotate(err, "invalid local public cloud data") 92 } 93 sameCloudInfo, err := jujucloud.IsSameCloudMetadata(newPublicClouds, currentPublicClouds) 94 if err != nil { 95 // Should never happen. 96 return err 97 } 98 if sameCloudInfo { 99 fmt.Fprintln(ctxt.Stdout, "Your list of public clouds is up to date, see juju list-clouds.") 100 return nil 101 } 102 if err := jujucloud.WritePublicCloudMetadata(newPublicClouds); err != nil { 103 return errors.Annotate(err, "error writing new local public cloud data") 104 } 105 updateDetails := diffClouds(newPublicClouds, currentPublicClouds) 106 fmt.Fprintln(ctxt.Stdout, fmt.Sprintf("Updated your list of public clouds with %s", updateDetails)) 107 return nil 108 } 109 110 func decodeCheckSignature(r io.Reader, publicKey string) ([]byte, error) { 111 data, err := ioutil.ReadAll(r) 112 if err != nil { 113 return nil, err 114 } 115 b, _ := clearsign.Decode(data) 116 if b == nil { 117 return nil, errors.New("no PGP signature embedded in plain text data") 118 } 119 keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(publicKey)) 120 if err != nil { 121 return nil, fmt.Errorf("failed to parse public key: %v", err) 122 } 123 124 _, err = openpgp.CheckDetachedSignature(keyring, bytes.NewBuffer(b.Bytes), b.ArmoredSignature.Body) 125 if err != nil { 126 return nil, err 127 } 128 return b.Plaintext, nil 129 } 130 131 func diffClouds(newClouds, oldClouds map[string]jujucloud.Cloud) string { 132 diff := newChanges() 133 // added and updated clouds 134 for cloudName, cloud := range newClouds { 135 oldCloud, ok := oldClouds[cloudName] 136 if !ok { 137 diff.addChange(addChange, cloudScope, cloudName) 138 continue 139 } 140 141 if cloudChanged(cloudName, cloud, oldCloud) { 142 diffCloudDetails(cloudName, cloud, oldCloud, diff) 143 } 144 } 145 146 // deleted clouds 147 for cloudName, _ := range oldClouds { 148 if _, ok := newClouds[cloudName]; !ok { 149 diff.addChange(deleteChange, cloudScope, cloudName) 150 } 151 } 152 return diff.summary() 153 } 154 155 func cloudChanged(cloudName string, new, old jujucloud.Cloud) bool { 156 same, _ := jujucloud.IsSameCloudMetadata( 157 map[string]jujucloud.Cloud{cloudName: new}, 158 map[string]jujucloud.Cloud{cloudName: old}, 159 ) 160 // If both old and new version are the same the cloud is not changed. 161 return !same 162 } 163 164 func diffCloudDetails(cloudName string, new, old jujucloud.Cloud, diff *changes) { 165 sameAuthTypes := func() bool { 166 if len(old.AuthTypes) != len(new.AuthTypes) { 167 return false 168 } 169 newAuthTypes := set.NewStrings() 170 for _, one := range new.AuthTypes { 171 newAuthTypes.Add(string(one)) 172 } 173 174 for _, anOldOne := range old.AuthTypes { 175 if !newAuthTypes.Contains(string(anOldOne)) { 176 return false 177 } 178 } 179 return true 180 } 181 182 endpointChanged := new.Endpoint != old.Endpoint 183 storageEndpointChanged := new.StorageEndpoint != old.StorageEndpoint 184 185 if endpointChanged || storageEndpointChanged || new.Type != old.Type || !sameAuthTypes() { 186 diff.addChange(updateChange, attributeScope, cloudName) 187 } 188 189 formatCloudRegion := func(rName string) string { 190 return fmt.Sprintf("%v/%v", cloudName, rName) 191 } 192 193 oldRegions := mapRegions(old.Regions) 194 newRegions := mapRegions(new.Regions) 195 // added & modified regions 196 for newName, newRegion := range newRegions { 197 oldRegion, ok := oldRegions[newName] 198 if !ok { 199 diff.addChange(addChange, regionScope, formatCloudRegion(newName)) 200 continue 201 202 } 203 if (oldRegion.Endpoint != newRegion.Endpoint) || (oldRegion.StorageEndpoint != newRegion.StorageEndpoint) { 204 diff.addChange(updateChange, regionScope, formatCloudRegion(newName)) 205 } 206 } 207 208 // deleted regions 209 for oldName, _ := range oldRegions { 210 if _, ok := newRegions[oldName]; !ok { 211 diff.addChange(deleteChange, regionScope, formatCloudRegion(oldName)) 212 } 213 } 214 } 215 216 func mapRegions(regions []jujucloud.Region) map[string]jujucloud.Region { 217 result := make(map[string]jujucloud.Region) 218 for _, region := range regions { 219 result[region.Name] = region 220 } 221 return result 222 } 223 224 type changeType string 225 226 const ( 227 addChange changeType = "added" 228 deleteChange changeType = "deleted" 229 updateChange changeType = "changed" 230 ) 231 232 type scope string 233 234 const ( 235 cloudScope scope = "cloud" 236 regionScope scope = "cloud region" 237 attributeScope scope = "cloud attribute" 238 ) 239 240 type changes struct { 241 all map[changeType]map[scope][]string 242 } 243 244 func newChanges() *changes { 245 return &changes{make(map[changeType]map[scope][]string)} 246 } 247 248 func (c *changes) addChange(aType changeType, entity scope, details string) { 249 byType, ok := c.all[aType] 250 if !ok { 251 byType = make(map[scope][]string) 252 c.all[aType] = byType 253 } 254 byType[entity] = append(byType[entity], details) 255 } 256 257 func (c *changes) summary() string { 258 if len(c.all) == 0 { 259 return "" 260 } 261 262 // Sort by change types 263 types := []string{} 264 for one, _ := range c.all { 265 types = append(types, string(one)) 266 } 267 sort.Strings(types) 268 269 msgs := []string{} 270 details := "" 271 tabSpace := " " 272 detailsSeparator := fmt.Sprintf("\n%v%v- ", tabSpace, tabSpace) 273 for _, aType := range types { 274 typeGroup := c.all[changeType(aType)] 275 entityMsgs := []string{} 276 277 // Sort by change scopes 278 scopes := []string{} 279 for one, _ := range typeGroup { 280 scopes = append(scopes, string(one)) 281 } 282 sort.Strings(scopes) 283 284 for _, aScope := range scopes { 285 scopeGroup := typeGroup[scope(aScope)] 286 sort.Strings(scopeGroup) 287 entityMsgs = append(entityMsgs, adjustPlurality(aScope, len(scopeGroup))) 288 details += fmt.Sprintf("\n%v%v %v:%v%v", 289 tabSpace, 290 aType, 291 aScope, 292 detailsSeparator, 293 strings.Join(scopeGroup, detailsSeparator)) 294 } 295 typeMsg := formatSlice(entityMsgs, ", ", " and ") 296 msgs = append(msgs, fmt.Sprintf("%v %v", typeMsg, aType)) 297 } 298 299 result := formatSlice(msgs, "; ", " as well as ") 300 return fmt.Sprintf("%v:\n%v", result, details) 301 } 302 303 // TODO(anastasiamac 2014-04-13) Move this to 304 // juju/utils (eg. Pluralize). Added tech debt card. 305 func adjustPlurality(entity string, count int) string { 306 switch count { 307 case 0: 308 return "" 309 case 1: 310 return fmt.Sprintf("%d %v", count, entity) 311 default: 312 return fmt.Sprintf("%d %vs", count, entity) 313 } 314 } 315 316 func formatSlice(slice []string, itemSeparator, lastSeparator string) string { 317 switch len(slice) { 318 case 0: 319 return "" 320 case 1: 321 return slice[0] 322 default: 323 return fmt.Sprintf("%v%v%v", 324 strings.Join(slice[:len(slice)-1], itemSeparator), 325 lastSeparator, 326 slice[len(slice)-1]) 327 } 328 }