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  }