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  }