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 }