github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/state/cloudcredentials.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package state
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"github.com/juju/errors"
    10  	jujutxn "github.com/juju/txn"
    11  	"github.com/juju/utils/set"
    12  	"gopkg.in/juju/names.v2"
    13  	mgo "gopkg.in/mgo.v2"
    14  	"gopkg.in/mgo.v2/bson"
    15  	"gopkg.in/mgo.v2/txn"
    16  
    17  	"github.com/juju/juju/cloud"
    18  )
    19  
    20  // cloudCredentialDoc records information about a user's cloud credentials.
    21  type cloudCredentialDoc struct {
    22  	DocID      string            `bson:"_id"`
    23  	Owner      string            `bson:"owner"`
    24  	Cloud      string            `bson:"cloud"`
    25  	Name       string            `bson:"name"`
    26  	Revoked    bool              `bson:"revoked"`
    27  	AuthType   string            `bson:"auth-type"`
    28  	Attributes map[string]string `bson:"attributes,omitempty"`
    29  }
    30  
    31  // CloudCredential returns the cloud credential for the given tag.
    32  func (st *State) CloudCredential(tag names.CloudCredentialTag) (cloud.Credential, error) {
    33  	coll, cleanup := st.getCollection(cloudCredentialsC)
    34  	defer cleanup()
    35  
    36  	var doc cloudCredentialDoc
    37  	err := coll.FindId(cloudCredentialDocID(tag)).One(&doc)
    38  	if err == mgo.ErrNotFound {
    39  		return cloud.Credential{}, errors.NotFoundf(
    40  			"cloud credential %q", tag.Id(),
    41  		)
    42  	} else if err != nil {
    43  		return cloud.Credential{}, errors.Annotatef(
    44  			err, "getting cloud credential %q", tag.Id(),
    45  		)
    46  	}
    47  	return doc.toCredential(), nil
    48  }
    49  
    50  // CloudCredentials returns the user's cloud credentials for a given cloud,
    51  // keyed by credential name.
    52  func (st *State) CloudCredentials(user names.UserTag, cloudName string) (map[string]cloud.Credential, error) {
    53  	coll, cleanup := st.getCollection(cloudCredentialsC)
    54  	defer cleanup()
    55  
    56  	var doc cloudCredentialDoc
    57  	credentials := make(map[string]cloud.Credential)
    58  	iter := coll.Find(bson.D{
    59  		{"owner", user.Canonical()},
    60  		{"cloud", cloudName},
    61  	}).Iter()
    62  	for iter.Next(&doc) {
    63  		tag, err := doc.cloudCredentialTag()
    64  		if err != nil {
    65  			return nil, errors.Trace(err)
    66  		}
    67  		credentials[tag.Canonical()] = doc.toCredential()
    68  	}
    69  	if err := iter.Err(); err != nil {
    70  		return nil, errors.Annotatef(
    71  			err, "cannot get cloud credentials for user %q, cloud %q",
    72  			user.Canonical(), cloudName,
    73  		)
    74  	}
    75  	return credentials, nil
    76  }
    77  
    78  // UpdateCloudCredential adds or updates a cloud credential with the given tag.
    79  func (st *State) UpdateCloudCredential(tag names.CloudCredentialTag, credential cloud.Credential) error {
    80  	credentials := map[names.CloudCredentialTag]cloud.Credential{tag: credential}
    81  	buildTxn := func(attempt int) ([]txn.Op, error) {
    82  		cloudName := tag.Cloud().Id()
    83  		cloud, err := st.Cloud(cloudName)
    84  		if err != nil {
    85  			return nil, errors.Trace(err)
    86  		}
    87  		ops, err := validateCloudCredentials(cloud, cloudName, credentials)
    88  		if err != nil {
    89  			return nil, errors.Annotate(err, "validating cloud credentials")
    90  		}
    91  		_, err = st.CloudCredential(tag)
    92  		if err != nil && !errors.IsNotFound(err) {
    93  			return nil, errors.Maskf(err, "fetching cloud credentials")
    94  		}
    95  		if err == nil {
    96  			ops = append(ops, updateCloudCredentialOp(tag, credential))
    97  		} else {
    98  			ops = append(ops, createCloudCredentialOp(tag, credential))
    99  		}
   100  		return ops, nil
   101  	}
   102  	if err := st.run(buildTxn); err != nil {
   103  		return errors.Annotate(err, "updating cloud credentials")
   104  	}
   105  	return nil
   106  }
   107  
   108  // RemoveCloudCredential removes a cloud credential with the given tag.
   109  func (st *State) RemoveCloudCredential(tag names.CloudCredentialTag) error {
   110  	buildTxn := func(attempt int) ([]txn.Op, error) {
   111  		_, err := st.CloudCredential(tag)
   112  		if errors.IsNotFound(err) {
   113  			return nil, jujutxn.ErrNoOperations
   114  		}
   115  		if err != nil {
   116  			return nil, errors.Trace(err)
   117  		}
   118  		return removeCloudCredentialOps(tag), nil
   119  	}
   120  	if err := st.run(buildTxn); err != nil {
   121  		return errors.Annotate(err, "removing cloud credential")
   122  	}
   123  	return nil
   124  }
   125  
   126  // createCloudCredentialOp returns a txn.Op that will create
   127  // a cloud credential.
   128  func createCloudCredentialOp(tag names.CloudCredentialTag, cred cloud.Credential) txn.Op {
   129  	return txn.Op{
   130  		C:      cloudCredentialsC,
   131  		Id:     cloudCredentialDocID(tag),
   132  		Assert: txn.DocMissing,
   133  		Insert: &cloudCredentialDoc{
   134  			Owner:      tag.Owner().Canonical(),
   135  			Cloud:      tag.Cloud().Id(),
   136  			Name:       tag.Name(),
   137  			AuthType:   string(cred.AuthType()),
   138  			Attributes: cred.Attributes(),
   139  			Revoked:    cred.Revoked,
   140  		},
   141  	}
   142  }
   143  
   144  // updateCloudCredentialOp returns a txn.Op that will update
   145  // a cloud credential.
   146  func updateCloudCredentialOp(tag names.CloudCredentialTag, cred cloud.Credential) txn.Op {
   147  	return txn.Op{
   148  		C:      cloudCredentialsC,
   149  		Id:     cloudCredentialDocID(tag),
   150  		Assert: txn.DocExists,
   151  		Update: bson.D{{"$set", bson.D{
   152  			{"auth-type", string(cred.AuthType())},
   153  			{"attributes", cred.Attributes()},
   154  			{"revoked", cred.Revoked},
   155  		}}},
   156  	}
   157  }
   158  
   159  // removeCloudCredentialOp returns a txn.Op that will remove
   160  // a cloud credential.
   161  func removeCloudCredentialOps(tag names.CloudCredentialTag) []txn.Op {
   162  	return []txn.Op{{
   163  		C:      cloudCredentialsC,
   164  		Id:     cloudCredentialDocID(tag),
   165  		Assert: txn.DocExists,
   166  		Remove: true,
   167  	}}
   168  }
   169  
   170  func cloudCredentialDocID(tag names.CloudCredentialTag) string {
   171  	return fmt.Sprintf("%s#%s#%s", tag.Cloud().Id(), tag.Owner().Canonical(), tag.Name())
   172  }
   173  
   174  func (c cloudCredentialDoc) cloudCredentialTag() (names.CloudCredentialTag, error) {
   175  	ownerTag := names.NewUserTag(c.Owner)
   176  	id := fmt.Sprintf("%s/%s/%s", c.Cloud, ownerTag.Canonical(), c.Name)
   177  	if !names.IsValidCloudCredential(id) {
   178  		return names.CloudCredentialTag{}, errors.NotValidf("cloud credential ID")
   179  	}
   180  	return names.NewCloudCredentialTag(id), nil
   181  }
   182  
   183  func (c cloudCredentialDoc) toCredential() cloud.Credential {
   184  	out := cloud.NewCredential(cloud.AuthType(c.AuthType), c.Attributes)
   185  	out.Revoked = c.Revoked
   186  	out.Label = c.Name
   187  	return out
   188  }
   189  
   190  // validateCloudCredentials checks that the supplied cloud credentials are
   191  // valid for use with the controller's cloud, and returns a set of txn.Ops
   192  // to assert the same in a transaction. The map keys are the cloud credential
   193  // IDs.
   194  //
   195  // TODO(rogpeppe) We're going to a lot of effort here to assert that a
   196  // cloud's auth types haven't changed since we looked at them a moment
   197  // ago, but we don't support changing a cloud's definition currently and
   198  // it's not clear that doing so would be a good idea, as changing a
   199  // cloud's auth type would invalidate all existing credentials and would
   200  // usually involve a new provider version and juju binary too, so
   201  // perhaps all this code is unnecessary.
   202  func validateCloudCredentials(
   203  	cloud cloud.Cloud,
   204  	cloudName string,
   205  	credentials map[names.CloudCredentialTag]cloud.Credential,
   206  ) ([]txn.Op, error) {
   207  	requiredAuthTypes := make(set.Strings)
   208  	for tag, credential := range credentials {
   209  		if tag.Cloud().Id() != cloudName {
   210  			return nil, errors.NewNotValid(nil, fmt.Sprintf(
   211  				"credential %q for non-matching cloud is not valid (expected %q)",
   212  				tag.Id(), cloudName,
   213  			))
   214  		}
   215  		var found bool
   216  		for _, authType := range cloud.AuthTypes {
   217  			if credential.AuthType() == authType {
   218  				found = true
   219  				break
   220  			}
   221  		}
   222  		if !found {
   223  			return nil, errors.NewNotValid(nil, fmt.Sprintf(
   224  				"credential %q with auth-type %q is not supported (expected one of %q)",
   225  				tag.Id(), credential.AuthType(), cloud.AuthTypes,
   226  			))
   227  		}
   228  		requiredAuthTypes.Add(string(credential.AuthType()))
   229  	}
   230  	ops := make([]txn.Op, len(requiredAuthTypes))
   231  	for i, authType := range requiredAuthTypes.SortedValues() {
   232  		ops[i] = txn.Op{
   233  			C:      cloudsC,
   234  			Id:     cloudName,
   235  			Assert: bson.D{{"auth-types", authType}},
   236  		}
   237  	}
   238  	return ops, nil
   239  }
   240  
   241  // WatchCredential returns a new NotifyWatcher watching for
   242  // changes to the specified credential.
   243  func (st *State) WatchCredential(cred names.CloudCredentialTag) NotifyWatcher {
   244  	filter := func(rawId interface{}) bool {
   245  		id, ok := rawId.(string)
   246  		if !ok {
   247  			return false
   248  		}
   249  		return id == cloudCredentialDocID(cred)
   250  	}
   251  	return newNotifyCollWatcher(st, cloudCredentialsC, filter)
   252  }