Skip to content

Instantly share code, notes, and snippets.

@jhoblitt
Created January 30, 2025 22:49
Show Gist options
  • Save jhoblitt/f87bb406344bdd2ce1770fb14299d8d7 to your computer and use it in GitHub Desktop.
Save jhoblitt/f87bb406344bdd2ce1770fb14299d8d7 to your computer and use it in GitHub Desktop.
Demo of idempotent reconcilation multiple of rgw user keys
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()
}
$ 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": ""
}
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