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