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