Created
January 30, 2025 22:49
-
-
Save jhoblitt/f87bb406344bdd2ce1770fb14299d8d7 to your computer and use it in GitHub Desktop.
Demo of idempotent reconcilation multiple of rgw user keys
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"context" | |
"encoding/json" | |
"fmt" | |
"log" | |
"github.com/ceph/go-ceph/rgw/admin" | |
"github.com/pkg/errors" | |
) | |
// reconcileUserKeys ensures the user's RGW keys match exactly the desiredKeys slice: | |
// 1. Removes any existing keys not present in desiredKeys | |
// 2. Ensures each key in desiredKeys exists with the correct secret | |
func reconcileUserKeys(ctx context.Context, client *admin.API, userID string, desiredKeys []admin.UserKeySpec) error { | |
// Fetch the current RGW user info (including the keys) | |
userInfo, err := client.GetUser(ctx, admin.User{ID: userID}) | |
if err != nil { | |
return errors.Wrapf(err, "failed to get user %q: %w", userID, err) | |
} | |
// Create a lookup for desired keys (by AccessKey) | |
desiredMap := make(map[string]admin.UserKeySpec, len(desiredKeys)) | |
for _, k := range desiredKeys { | |
desiredMap[k.AccessKey] = k | |
} | |
syncdKeys := make([]admin.UserKeySpec, len(desiredKeys)) | |
// Remove any keys in the user that aren't in desiredKeys | |
// userInfo.Keys is []admin.UserKey. Compare each key against desiredMap. | |
for _, existingKey := range userInfo.Keys { | |
k, found := desiredMap[existingKey.AccessKey] | |
if !found { | |
// RemoveKey requires the UID to be set but GetUser returns the list of keys with only .User set | |
existingKey.UID = userID | |
if err := client.RemoveKey(ctx, existingKey); err != nil { | |
return errors.Wrapf(err, "failed to remove key %q from user %q", existingKey.AccessKey, userID) | |
} | |
continue | |
} | |
if existingKey.KeyType != k.KeyType || existingKey.UID != k.UID || existingKey.SecretKey != k.SecretKey { | |
// Key exists but needs to be updated; delete it so it will be recreated | |
if err := client.RemoveKey(ctx, existingKey); err != nil { | |
return errors.Wrapf(err, "failed to remove key %q from user %q", existingKey.AccessKey, userID) | |
} | |
continue | |
} | |
// else key exists and is correct, no action needed | |
syncdKeys = append(syncdKeys, k) | |
} | |
// Remove any keys in desiredKeys that were already synced | |
for _, k := range syncdKeys { | |
delete(desiredMap, k.AccessKey) | |
} | |
// create each desired key | |
for _, k := range desiredKeys { | |
if _, err := client.CreateKey(ctx, k); err != nil { | |
return errors.Wrapf(err, "failed to create key %q for user %q", k.AccessKey, userID) | |
} | |
} | |
return nil | |
} | |
func main() { | |
ctx := context.Background() | |
s3endpoint := "https://example.org" | |
// rgw-admin-ops-user | |
accessKey := "XXX" | |
secretKey := "XXX | |
rgw, err := admin.New(s3endpoint, accessKey, secretKey, nil) | |
if err != nil { | |
log.Fatal(err) | |
} | |
id := "test18" | |
// it appears that | |
// https://docs.ceph.com/en/latest/radosgw/adminops/#create-user can create a | |
// user with a set of s3 credentials pre defined but i can only set 1 key | |
// pair as a limitation of the api. go-ceph silently ignores additional key | |
// pairs. | |
user := admin.User{ | |
ID: id, | |
DisplayName: id, | |
//Keys: []admin.UserKeySpec{ | |
// { | |
// //User: id, | |
// UID: id, | |
// AccessKey: id + "foo", | |
// SecretKey: id + "bar", | |
// KeyType: "s3", | |
// }, | |
// { | |
// //User: id, | |
// UID: id, | |
// AccessKey: id + "baz", | |
// SecretKey: id + "qux", | |
// KeyType: "s3", | |
// }, | |
//}, | |
} | |
liveUser, err := rgw.CreateUser(ctx, user) | |
if err != nil { | |
log.Fatal(err) | |
} | |
data, _ := json.MarshalIndent(liveUser, "", " ") | |
fmt.Println("User as reported by CreateUser:") | |
fmt.Println(string(data)) | |
fmt.Println() | |
liveKeys, err := rgw.CreateKey(ctx, admin.UserKeySpec{ | |
UID: id, | |
AccessKey: id + "quux", | |
SecretKey: id + "corge", | |
KeyType: "s3", | |
}) | |
if err != nil { | |
log.Fatal(err) | |
} | |
data, _ = json.MarshalIndent(liveKeys, "", " ") | |
fmt.Println("Keys as reported by CreateKey:") | |
fmt.Println(string(data)) | |
fmt.Println() | |
liveUser, err = rgw.GetUser(ctx, admin.User{ID: id}) | |
if err != nil { | |
log.Fatal(err) | |
} | |
data, _ = json.MarshalIndent(liveUser, "", " ") | |
fmt.Println("User after modification by CreateKey:") | |
fmt.Println(string(data)) | |
fmt.Println() | |
desiredKeys := []admin.UserKeySpec{ | |
{ | |
UID: id, | |
AccessKey: id + "foo", | |
SecretKey: id + "bar", | |
KeyType: "s3", | |
}, | |
{ | |
UID: id, | |
AccessKey: id + "baz", | |
SecretKey: id + "qux", | |
KeyType: "s3", | |
}, | |
} | |
err = reconcileUserKeys(ctx, rgw, id, desiredKeys) | |
if err != nil { | |
log.Fatal(err) | |
} | |
liveUser, err = rgw.GetUser(ctx, admin.User{ID: id}) | |
if err != nil { | |
log.Fatal(err) | |
} | |
data, _ = json.MarshalIndent(liveUser, "", " ") | |
fmt.Println("User after reconcileUserKeys:") | |
fmt.Println(string(data)) | |
fmt.Println() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$ go run main.go | |
User as reported by CreateUser: | |
{ | |
"user_id": "test18", | |
"display_name": "test18", | |
"email": "", | |
"suspended": 0, | |
"max_buckets": 1000, | |
"subusers": [], | |
"keys": [ | |
{ | |
"user": "test18", | |
"access_key": "M1MAQRHNH3Z3YMY0JL4D", | |
"secret_key": "Q8IjCNmWpGtH97TTAkXGQexKMR7pxHGEddMlDFKL", | |
"UID": "", | |
"SubUser": "", | |
"KeyType": "", | |
"GenerateKey": null | |
} | |
], | |
"swift_keys": [], | |
"caps": [], | |
"op_mask": "read, write, delete", | |
"default_placement": "", | |
"default_storage_class": "", | |
"placement_tags": [], | |
"bucket_quota": { | |
"user_id": "", | |
"bucket": "", | |
"QuotaType": "", | |
"enabled": false, | |
"check_on_raw": false, | |
"max_size": -1, | |
"max_size_kb": 0, | |
"max_objects": -1 | |
}, | |
"user_quota": { | |
"user_id": "", | |
"bucket": "", | |
"QuotaType": "", | |
"enabled": false, | |
"check_on_raw": false, | |
"max_size": -1, | |
"max_size_kb": 0, | |
"max_objects": -1 | |
}, | |
"temp_url_keys": [], | |
"type": "rgw", | |
"mfa_ids": [], | |
"KeyType": "", | |
"Tenant": "", | |
"GenerateKey": null, | |
"PurgeData": null, | |
"GenerateStat": null, | |
"stats": { | |
"size": null, | |
"size_rounded": null, | |
"num_objects": null | |
}, | |
"UserCaps": "" | |
} | |
Keys as reported by CreateKey: | |
[ | |
{ | |
"user": "test18", | |
"access_key": "M1MAQRHNH3Z3YMY0JL4D", | |
"secret_key": "Q8IjCNmWpGtH97TTAkXGQexKMR7pxHGEddMlDFKL", | |
"UID": "", | |
"SubUser": "", | |
"KeyType": "", | |
"GenerateKey": null | |
}, | |
{ | |
"user": "test18", | |
"access_key": "test18quux", | |
"secret_key": "test18corge", | |
"UID": "", | |
"SubUser": "", | |
"KeyType": "", | |
"GenerateKey": null | |
} | |
] | |
User after modification by CreateKey: | |
{ | |
"user_id": "test18", | |
"display_name": "test18", | |
"email": "", | |
"suspended": 0, | |
"max_buckets": 1000, | |
"subusers": [], | |
"keys": [ | |
{ | |
"user": "test18", | |
"access_key": "M1MAQRHNH3Z3YMY0JL4D", | |
"secret_key": "Q8IjCNmWpGtH97TTAkXGQexKMR7pxHGEddMlDFKL", | |
"UID": "", | |
"SubUser": "", | |
"KeyType": "", | |
"GenerateKey": null | |
}, | |
{ | |
"user": "test18", | |
"access_key": "test18quux", | |
"secret_key": "test18corge", | |
"UID": "", | |
"SubUser": "", | |
"KeyType": "", | |
"GenerateKey": null | |
} | |
], | |
"swift_keys": [], | |
"caps": [], | |
"op_mask": "read, write, delete", | |
"default_placement": "", | |
"default_storage_class": "", | |
"placement_tags": [], | |
"bucket_quota": { | |
"user_id": "", | |
"bucket": "", | |
"QuotaType": "", | |
"enabled": false, | |
"check_on_raw": false, | |
"max_size": -1, | |
"max_size_kb": 0, | |
"max_objects": -1 | |
}, | |
"user_quota": { | |
"user_id": "", | |
"bucket": "", | |
"QuotaType": "", | |
"enabled": false, | |
"check_on_raw": false, | |
"max_size": -1, | |
"max_size_kb": 0, | |
"max_objects": -1 | |
}, | |
"temp_url_keys": [], | |
"type": "rgw", | |
"mfa_ids": [], | |
"KeyType": "", | |
"Tenant": "", | |
"GenerateKey": null, | |
"PurgeData": null, | |
"GenerateStat": null, | |
"stats": { | |
"size": null, | |
"size_rounded": null, | |
"num_objects": null | |
}, | |
"UserCaps": "" | |
} | |
User after reconcileUserKeys: | |
{ | |
"user_id": "test18", | |
"display_name": "test18", | |
"email": "", | |
"suspended": 0, | |
"max_buckets": 1000, | |
"subusers": [], | |
"keys": [ | |
{ | |
"user": "test18", | |
"access_key": "test18baz", | |
"secret_key": "test18qux", | |
"UID": "", | |
"SubUser": "", | |
"KeyType": "", | |
"GenerateKey": null | |
}, | |
{ | |
"user": "test18", | |
"access_key": "test18foo", | |
"secret_key": "test18bar", | |
"UID": "", | |
"SubUser": "", | |
"KeyType": "", | |
"GenerateKey": null | |
} | |
], | |
"swift_keys": [], | |
"caps": [], | |
"op_mask": "read, write, delete", | |
"default_placement": "", | |
"default_storage_class": "", | |
"placement_tags": [], | |
"bucket_quota": { | |
"user_id": "", | |
"bucket": "", | |
"QuotaType": "", | |
"enabled": false, | |
"check_on_raw": false, | |
"max_size": -1, | |
"max_size_kb": 0, | |
"max_objects": -1 | |
}, | |
"user_quota": { | |
"user_id": "", | |
"bucket": "", | |
"QuotaType": "", | |
"enabled": false, | |
"check_on_raw": false, | |
"max_size": -1, | |
"max_size_kb": 0, | |
"max_objects": -1 | |
}, | |
"temp_url_keys": [], | |
"type": "rgw", | |
"mfa_ids": [], | |
"KeyType": "", | |
"Tenant": "", | |
"GenerateKey": null, | |
"PurgeData": null, | |
"GenerateStat": null, | |
"stats": { | |
"size": null, | |
"size_rounded": null, | |
"num_objects": null | |
}, | |
"UserCaps": "" | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
bash-5.1$ radosgw-admin user info --uid test18 | |
{ | |
"user_id": "test18", | |
"display_name": "test18", | |
"email": "", | |
"suspended": 0, | |
"max_buckets": 1000, | |
"subusers": [], | |
"keys": [ | |
{ | |
"user": "test18", | |
"access_key": "test18baz", | |
"secret_key": "test18qux" | |
}, | |
{ | |
"user": "test18", | |
"access_key": "test18foo", | |
"secret_key": "test18bar" | |
} | |
], | |
"swift_keys": [], | |
"caps": [], | |
"op_mask": "read, write, delete", | |
"default_placement": "", | |
"default_storage_class": "", | |
"placement_tags": [], | |
"bucket_quota": { | |
"enabled": false, | |
"check_on_raw": false, | |
"max_size": -1, | |
"max_size_kb": 0, | |
"max_objects": -1 | |
}, | |
"user_quota": { | |
"enabled": false, | |
"check_on_raw": false, | |
"max_size": -1, | |
"max_size_kb": 0, | |
"max_objects": -1 | |
}, | |
"temp_url_keys": [], | |
"type": "rgw", | |
"mfa_ids": [] | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment