Merge pull request #15642 from rook/mergify/bp/release-1.17/pr-15359
object: add CephObjectStoreUser.spec.keys (backport #15359)
This commit is contained in:
11 files changed
+1499
-42
No files matched your search
@@ -2086,6 +2086,21 @@ ObjectUserQuotaSpec
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>keys</code><br/>
|
||||
<em>
|
||||
<a href="#ceph.rook.io/v1.ObjectUserKey">
|
||||
[]ObjectUserKey
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>Allows specifying credentials for the user. If not provided, the operator
|
||||
will generate them.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>clusterNamespace</code><br/>
|
||||
<em>
|
||||
string
|
||||
@@ -10410,6 +10425,21 @@ ObjectUserQuotaSpec
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>keys</code><br/>
|
||||
<em>
|
||||
<a href="#ceph.rook.io/v1.ObjectUserKey">
|
||||
[]ObjectUserKey
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>Allows specifying credentials for the user. If not provided, the operator
|
||||
will generate them.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>clusterNamespace</code><br/>
|
||||
<em>
|
||||
string
|
||||
@@ -10472,6 +10502,19 @@ int64
|
||||
<p>ObservedGeneration is the latest generation observed by the controller.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>keys</code><br/>
|
||||
<em>
|
||||
<a href="#ceph.rook.io/v1.SecretReference">
|
||||
[]SecretReference
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="ceph.rook.io/v1.ObjectUserCapSpec">ObjectUserCapSpec
|
||||
@@ -10684,6 +10727,51 @@ string
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="ceph.rook.io/v1.ObjectUserKey">ObjectUserKey
|
||||
</h3>
|
||||
<p>
|
||||
(<em>Appears on:</em><a href="#ceph.rook.io/v1.ObjectStoreUserSpec">ObjectStoreUserSpec</a>)
|
||||
</p>
|
||||
<div>
|
||||
<p>ObjectUserKey defines a set of rgw user access credentials to be retrieved
|
||||
from secret resources.</p>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>accessKeyRef</code><br/>
|
||||
<em>
|
||||
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#secretkeyselector-v1-core">
|
||||
Kubernetes core/v1.SecretKeySelector
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>Secret key selector for the access_key (commonly referred to as AWS_ACCESS_KEY_ID).</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>secretKeyRef</code><br/>
|
||||
<em>
|
||||
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#secretkeyselector-v1-core">
|
||||
Kubernetes core/v1.SecretKeySelector
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>Secret key selector for the secret_key (commonly referred to as AWS_SECRET_ACCESS_KEY).</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="ceph.rook.io/v1.ObjectUserQuotaSpec">ObjectUserQuotaSpec
|
||||
</h3>
|
||||
<p>
|
||||
@@ -12396,6 +12484,55 @@ int32
|
||||
</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
<h3 id="ceph.rook.io/v1.SecretReference">SecretReference
|
||||
</h3>
|
||||
<p>
|
||||
(<em>Appears on:</em><a href="#ceph.rook.io/v1.ObjectStoreUserStatus">ObjectStoreUserStatus</a>)
|
||||
</p>
|
||||
<div>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>,secretReference</code><br/>
|
||||
<em>
|
||||
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#secretreference-v1-core">
|
||||
Kubernetes core/v1.SecretReference
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>uid</code><br/>
|
||||
<em>
|
||||
k8s.io/apimachinery/pkg/types.UID
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>resourceVersion</code><br/>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="ceph.rook.io/v1.SecuritySpec">SecuritySpec
|
||||
</h3>
|
||||
<p>
|
||||
|
||||
@@ -588,6 +588,70 @@ kubectl -n rook-ceph get secret rook-ceph-object-user-my-store-my-user -o jsonpa
|
||||
kubectl -n rook-ceph get secret rook-ceph-object-user-my-store-my-user -o jsonpath='{.data.SecretKey}' | base64 --decode
|
||||
```
|
||||
|
||||
### Managing User S3 Credentials
|
||||
|
||||
The default behavior of `CephObjectStoreUser` is to place the S3 credentials generated when an RGW user is created into a Kubernetes Secret.
|
||||
The s3 credential(s) may also be explicitly set via `.spec.keys`.
|
||||
This field requires at least one keypair must and there is no maximum limit.
|
||||
When `.spec.keys` is present, no `Secret` is created for the user.
|
||||
If an existing `CephObjectStoreUser` is being transited to using explicit keys, the existing `Secret` will be deleted.
|
||||
Conversely, if a `.spec.keys` is patched out of an existing `CephObjectStoreUser` all except one of keypairs set on the user will be purged and a `Secret` will be created holding the only remaining keypair.
|
||||
|
||||
!!! important
|
||||
When `.spec.keys` is not set, all but **one** keypair will be removed from the rgw user.
|
||||
When `.spec.keys` is set, any keypair not explicitly specified will be removed from the rgw user.
|
||||
|
||||
Example of explicitly managing the rgw user's keypairs:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: ceph.rook.io/v1
|
||||
kind: CephObjectStoreUser
|
||||
metadata:
|
||||
name: foo
|
||||
namespace: rook-ceph
|
||||
spec:
|
||||
store: my-store
|
||||
clusterNamespace: rook-ceph
|
||||
keys:
|
||||
- acccessKeyRef
|
||||
name: foo-s3
|
||||
key: AWS_ACCESS_KEY_ID
|
||||
secretKeyRef
|
||||
name: foo-s3
|
||||
key: AWS_SECRET_ACCESS_KEY
|
||||
- acccessKeyRef
|
||||
name: bar-s3
|
||||
key: AWS_ACCESS_KEY_ID
|
||||
secretKeyRef
|
||||
name: bar-s3
|
||||
key: AWS_SECRET_ACCESS_KEY
|
||||
```
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: foo-s3
|
||||
namespace: rook-ceph
|
||||
data:
|
||||
AWS_ACCESS_KEY_ID: baz
|
||||
AWS_SECRET_ACCESS_KEY:baz
|
||||
```
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: bar-s3
|
||||
namespace: rook-ceph
|
||||
data:
|
||||
AWS_ACCESS_KEY_ID: qux
|
||||
AWS_SECRET_ACCESS_KEY: quxbar
|
||||
```
|
||||
|
||||
## Enable TLS
|
||||
|
||||
TLS is critical for securing object storage data access, and it is assumed as a default by many S3
|
||||
|
||||
@@ -12,6 +12,12 @@ Object:
|
||||
new `ROOK_OBC_ALLOW_ADDITIONAL_CONFIG_FIELDS` to enable users to set potentially risky options.
|
||||
See https://github.com/rook/rook/pull/15376 for more information.
|
||||
|
||||
- Add credentials/keys management to CephObjectStoreUser. Note that this will
|
||||
cause manually created credentials/keys on an rgw user to be purged. This
|
||||
behavior is necessary to ensure that keys that were explicitly declared and
|
||||
then undeclared are purged. This could be a user observable regression.
|
||||
(see [#15359](https://github.com/rook/rook/issues/15359)
|
||||
|
||||
## Features
|
||||
|
||||
- Support external mons for local Rook cluster (see [#14733](https://github.com/rook/rook/issues/14733)).
|
||||
|
||||
@@ -13048,6 +13048,61 @@ spec:
|
||||
displayName:
|
||||
description: The display name for the ceph users
|
||||
type: string
|
||||
keys:
|
||||
description: |-
|
||||
Allows specifying credentials for the user. If not provided, the operator
|
||||
will generate them.
|
||||
items:
|
||||
description: |-
|
||||
ObjectUserKey defines a set of rgw user access credentials to be retrieved
|
||||
from secret resources.
|
||||
properties:
|
||||
accessKeyRef:
|
||||
description: Secret key selector for the access_key (commonly referred to as AWS_ACCESS_KEY_ID).
|
||||
properties:
|
||||
key:
|
||||
description: The key of the secret to select from. Must be a valid secret key.
|
||||
type: string
|
||||
name:
|
||||
default: ""
|
||||
description: |-
|
||||
Name of the referent.
|
||||
This field is effectively required, but due to backwards compatibility is
|
||||
allowed to be empty. Instances of this type with an empty value here are
|
||||
almost certainly wrong.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
type: string
|
||||
optional:
|
||||
description: Specify whether the Secret or its key must be defined
|
||||
type: boolean
|
||||
required:
|
||||
- key
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
secretKeyRef:
|
||||
description: Secret key selector for the secret_key (commonly referred to as AWS_SECRET_ACCESS_KEY).
|
||||
properties:
|
||||
key:
|
||||
description: The key of the secret to select from. Must be a valid secret key.
|
||||
type: string
|
||||
name:
|
||||
default: ""
|
||||
description: |-
|
||||
Name of the referent.
|
||||
This field is effectively required, but due to backwards compatibility is
|
||||
allowed to be empty. Instances of this type with an empty value here are
|
||||
almost certainly wrong.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
type: string
|
||||
optional:
|
||||
description: Specify whether the Secret or its key must be defined
|
||||
type: boolean
|
||||
required:
|
||||
- key
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
type: object
|
||||
type: array
|
||||
quotas:
|
||||
description: ObjectUserQuotaSpec can be used to set quotas for the object store user to limit their usage. See the [Ceph docs](https://docs.ceph.com/en/latest/radosgw/admin/?#quota-management) for more
|
||||
nullable: true
|
||||
@@ -13084,6 +13139,27 @@ spec:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
keys:
|
||||
items:
|
||||
properties:
|
||||
name:
|
||||
description: name is unique within a namespace to reference a secret resource.
|
||||
type: string
|
||||
namespace:
|
||||
description: namespace defines the space within which the secret name must be unique.
|
||||
type: string
|
||||
resourceVersion:
|
||||
type: string
|
||||
uid:
|
||||
description: |-
|
||||
UID is a type that holds unique ID values, including UUIDs. Because we
|
||||
don't ONLY use UUIDs, this is an alias to string. Being a type captures
|
||||
intent and helps make sure that UIDs and names do not get conflated.
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
nullable: true
|
||||
type: array
|
||||
observedGeneration:
|
||||
description: ObservedGeneration is the latest generation observed by the controller.
|
||||
format: int64
|
||||
|
||||
@@ -13038,6 +13038,61 @@ spec:
|
||||
displayName:
|
||||
description: The display name for the ceph users
|
||||
type: string
|
||||
keys:
|
||||
description: |-
|
||||
Allows specifying credentials for the user. If not provided, the operator
|
||||
will generate them.
|
||||
items:
|
||||
description: |-
|
||||
ObjectUserKey defines a set of rgw user access credentials to be retrieved
|
||||
from secret resources.
|
||||
properties:
|
||||
accessKeyRef:
|
||||
description: Secret key selector for the access_key (commonly referred to as AWS_ACCESS_KEY_ID).
|
||||
properties:
|
||||
key:
|
||||
description: The key of the secret to select from. Must be a valid secret key.
|
||||
type: string
|
||||
name:
|
||||
default: ""
|
||||
description: |-
|
||||
Name of the referent.
|
||||
This field is effectively required, but due to backwards compatibility is
|
||||
allowed to be empty. Instances of this type with an empty value here are
|
||||
almost certainly wrong.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
type: string
|
||||
optional:
|
||||
description: Specify whether the Secret or its key must be defined
|
||||
type: boolean
|
||||
required:
|
||||
- key
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
secretKeyRef:
|
||||
description: Secret key selector for the secret_key (commonly referred to as AWS_SECRET_ACCESS_KEY).
|
||||
properties:
|
||||
key:
|
||||
description: The key of the secret to select from. Must be a valid secret key.
|
||||
type: string
|
||||
name:
|
||||
default: ""
|
||||
description: |-
|
||||
Name of the referent.
|
||||
This field is effectively required, but due to backwards compatibility is
|
||||
allowed to be empty. Instances of this type with an empty value here are
|
||||
almost certainly wrong.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
type: string
|
||||
optional:
|
||||
description: Specify whether the Secret or its key must be defined
|
||||
type: boolean
|
||||
required:
|
||||
- key
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
type: object
|
||||
type: array
|
||||
quotas:
|
||||
description: ObjectUserQuotaSpec can be used to set quotas for the object store user to limit their usage. See the [Ceph docs](https://docs.ceph.com/en/latest/radosgw/admin/?#quota-management) for more
|
||||
nullable: true
|
||||
@@ -13074,6 +13129,27 @@ spec:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
keys:
|
||||
items:
|
||||
properties:
|
||||
name:
|
||||
description: name is unique within a namespace to reference a secret resource.
|
||||
type: string
|
||||
namespace:
|
||||
description: namespace defines the space within which the secret name must be unique.
|
||||
type: string
|
||||
resourceVersion:
|
||||
type: string
|
||||
uid:
|
||||
description: |-
|
||||
UID is a type that holds unique ID values, including UUIDs. Because we
|
||||
don't ONLY use UUIDs, this is an alias to string. Being a type captures
|
||||
intent and helps make sure that UIDs and names do not get conflated.
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
nullable: true
|
||||
type: array
|
||||
observedGeneration:
|
||||
description: ObservedGeneration is the latest generation observed by the controller.
|
||||
format: int64
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
// ***************************************************************************
|
||||
@@ -2002,6 +2003,15 @@ type ObjectStoreUserStatus struct {
|
||||
// ObservedGeneration is the latest generation observed by the controller.
|
||||
// +optional
|
||||
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||
// +optional
|
||||
// +nullable
|
||||
Keys []SecretReference `json:"keys,omitempty"`
|
||||
}
|
||||
|
||||
type SecretReference struct {
|
||||
v1.SecretReference `json:",secretReference"`
|
||||
UID types.UID `json:"uid,omitempty"`
|
||||
ResourceVersion string `json:"resourceVersion,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
@@ -2027,6 +2037,10 @@ type ObjectStoreUserSpec struct {
|
||||
// +optional
|
||||
// +nullable
|
||||
Quotas *ObjectUserQuotaSpec `json:"quotas,omitempty"`
|
||||
// Allows specifying credentials for the user. If not provided, the operator
|
||||
// will generate them.
|
||||
// +optional
|
||||
Keys []ObjectUserKey `json:"keys,omitempty"`
|
||||
// The namespace where the parent CephCluster and CephObjectStore are found
|
||||
// +optional
|
||||
ClusterNamespace string `json:"clusterNamespace,omitempty"`
|
||||
@@ -2117,6 +2131,15 @@ type ObjectUserQuotaSpec struct {
|
||||
MaxObjects *int64 `json:"maxObjects,omitempty"`
|
||||
}
|
||||
|
||||
// ObjectUserKey defines a set of rgw user access credentials to be retrieved
|
||||
// from secret resources.
|
||||
type ObjectUserKey struct {
|
||||
// Secret key selector for the access_key (commonly referred to as AWS_ACCESS_KEY_ID).
|
||||
AccessKeyRef *v1.SecretKeySelector `json:"accessKeyRef,omitempty"`
|
||||
// Secret key selector for the secret_key (commonly referred to as AWS_SECRET_ACCESS_KEY).
|
||||
SecretKeyRef *v1.SecretKeySelector `json:"secretKeyRef,omitempty"`
|
||||
}
|
||||
|
||||
// +genclient
|
||||
// +genclient:noStatus
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
@@ -3948,6 +3948,13 @@ func (in *ObjectStoreUserSpec) DeepCopyInto(out *ObjectStoreUserSpec) {
|
||||
*out = new(ObjectUserQuotaSpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Keys != nil {
|
||||
in, out := &in.Keys, &out.Keys
|
||||
*out = make([]ObjectUserKey, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3971,6 +3978,11 @@ func (in *ObjectStoreUserStatus) DeepCopyInto(out *ObjectStoreUserStatus) {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Keys != nil {
|
||||
in, out := &in.Keys, &out.Keys
|
||||
*out = make([]SecretReference, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -4000,6 +4012,32 @@ func (in *ObjectUserCapSpec) DeepCopy() *ObjectUserCapSpec {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ObjectUserKey) DeepCopyInto(out *ObjectUserKey) {
|
||||
*out = *in
|
||||
if in.AccessKeyRef != nil {
|
||||
in, out := &in.AccessKeyRef, &out.AccessKeyRef
|
||||
*out = new(corev1.SecretKeySelector)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.SecretKeyRef != nil {
|
||||
in, out := &in.SecretKeyRef, &out.SecretKeyRef
|
||||
*out = new(corev1.SecretKeySelector)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectUserKey.
|
||||
func (in *ObjectUserKey) DeepCopy() *ObjectUserKey {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ObjectUserKey)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ObjectUserQuotaSpec) DeepCopyInto(out *ObjectUserQuotaSpec) {
|
||||
*out = *in
|
||||
@@ -4660,6 +4698,23 @@ func (in *SanitizeDisksSpec) DeepCopy() *SanitizeDisksSpec {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SecretReference) DeepCopyInto(out *SecretReference) {
|
||||
*out = *in
|
||||
out.SecretReference = in.SecretReference
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretReference.
|
||||
func (in *SecretReference) DeepCopy() *SecretReference {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SecretReference)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SecuritySpec) DeepCopyInto(out *SecuritySpec) {
|
||||
*out = *in
|
||||
|
||||
@@ -21,31 +21,36 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"github.com/ceph/go-ceph/rgw/admin"
|
||||
opcontroller "github.com/rook/rook/pkg/operator/ceph/controller"
|
||||
"github.com/rook/rook/pkg/operator/ceph/reporting"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"github.com/pkg/errors"
|
||||
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
|
||||
"github.com/rook/rook/pkg/clusterd"
|
||||
cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
|
||||
"github.com/rook/rook/pkg/operator/ceph/object"
|
||||
"github.com/rook/rook/pkg/operator/k8sutil"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
|
||||
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
|
||||
"github.com/rook/rook/pkg/clusterd"
|
||||
cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
|
||||
opcontroller "github.com/rook/rook/pkg/operator/ceph/controller"
|
||||
"github.com/rook/rook/pkg/operator/ceph/object"
|
||||
"github.com/rook/rook/pkg/operator/ceph/reporting"
|
||||
"github.com/rook/rook/pkg/operator/k8sutil"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -124,6 +129,76 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// watch secrets referenced by CephObjectStoreUser.spec.keys
|
||||
const (
|
||||
secretNameField = "spec.keys.secretNames"
|
||||
)
|
||||
|
||||
err = mgr.GetFieldIndexer().IndexField(context.TODO(), &cephv1.CephObjectStoreUser{TypeMeta: controllerTypeMeta}, secretNameField, func(obj client.Object) []string {
|
||||
var secretNames []string
|
||||
for _, k := range obj.(*cephv1.CephObjectStoreUser).Spec.Keys {
|
||||
if k.AccessKeyRef != nil && k.AccessKeyRef.Name != "" {
|
||||
secretNames = append(secretNames, k.AccessKeyRef.Name)
|
||||
}
|
||||
if k.SecretKeyRef != nil && k.SecretKeyRef.Name != "" {
|
||||
secretNames = append(secretNames, k.SecretKeyRef.Name)
|
||||
}
|
||||
}
|
||||
slices.Sort(secretNames)
|
||||
return slices.Compact(secretNames)
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to setup IndexField for CephObjectStoreUser.Spec.Keys")
|
||||
}
|
||||
|
||||
// Always trigger a reconcile when a secret is deleted. This will cause a
|
||||
// reconciliation failure to happen immediately in hopes of alerting the end
|
||||
// user to the configuration problem.
|
||||
changedOrDeleted := predicate.Or(
|
||||
predicate.TypedResourceVersionChangedPredicate[*corev1.Secret]{},
|
||||
predicate.TypedFuncs[*corev1.Secret]{
|
||||
DeleteFunc: func(e event.TypedDeleteEvent[*corev1.Secret]) bool {
|
||||
return true
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
err = c.Watch(
|
||||
source.Kind(
|
||||
mgr.GetCache(),
|
||||
&corev1.Secret{},
|
||||
handler.TypedEnqueueRequestsFromMapFunc(
|
||||
func(ctx context.Context, secret *corev1.Secret) []reconcile.Request {
|
||||
referencingUsers := &cephv1.CephObjectStoreUserList{}
|
||||
err := r.(*ReconcileObjectStoreUser).client.List(ctx, referencingUsers, &client.ListOptions{
|
||||
FieldSelector: fields.OneTermEqualSelector(secretNameField, secret.GetName()),
|
||||
Namespace: secret.GetNamespace(),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Errorf("failed to list CephObjectStoreUser(s) while handling event for secret %q in namespace %q. %v", secret.GetName(), secret.GetNamespace(), err)
|
||||
return []reconcile.Request{}
|
||||
}
|
||||
|
||||
requests := make([]reconcile.Request, len(referencingUsers.Items))
|
||||
for i, item := range referencingUsers.Items {
|
||||
requests[i] = reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: item.GetName(),
|
||||
Namespace: item.GetNamespace(),
|
||||
},
|
||||
}
|
||||
}
|
||||
logger.Tracef("CephObjectStoreUsers referencing Secret %q in namespace %q: %v", secret.GetName(), secret.GetNamespace(), requests)
|
||||
return requests
|
||||
},
|
||||
),
|
||||
changedOrDeleted,
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to configure watch for Secret(s)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -224,6 +299,18 @@ func (r *ReconcileObjectStoreUser) reconcile(request reconcile.Request) (reconci
|
||||
// Generate user config
|
||||
userConfig := generateUserConfig(cephObjectStoreUser)
|
||||
|
||||
referencedSecrets := &map[types.UID]*corev1.Secret{}
|
||||
// Set any provided key pair(s)
|
||||
if len(cephObjectStoreUser.Spec.Keys) > 0 {
|
||||
var keys *[]admin.UserKeySpec
|
||||
keys, referencedSecrets, err = r.generateUserKeySpec(cephObjectStoreUser)
|
||||
if err != nil {
|
||||
return reconcile.Result{}, *cephObjectStoreUser, errors.Wrapf(err, "failed to generate UserKeySpec for %q", cephObjectStoreUser.Name)
|
||||
}
|
||||
|
||||
userConfig.Keys = *keys
|
||||
}
|
||||
|
||||
// DELETE: the CR was deleted
|
||||
if !cephObjectStoreUser.GetDeletionTimestamp().IsZero() {
|
||||
logger.Debugf("deleting object store user %q", request.NamespacedName)
|
||||
@@ -257,6 +344,11 @@ func (r *ReconcileObjectStoreUser) reconcile(request reconcile.Request) (reconci
|
||||
return reconcileResponse, *cephObjectStoreUser, err
|
||||
}
|
||||
|
||||
// Update status of referenced secrets only after the rgw user has
|
||||
// reconciled. Update even when no secrets are referenced as this could be a
|
||||
// transition from explicit keys -> automatic secret generation.
|
||||
r.updateKeyStatus(request.NamespacedName, referencedSecrets)
|
||||
|
||||
// CREATE/UPDATE KUBERNETES SECRET
|
||||
store, err := r.getObjectStore(cephObjectStoreUser.Spec.Store)
|
||||
if err != nil {
|
||||
@@ -293,7 +385,8 @@ func (r *ReconcileObjectStoreUser) createOrUpdateCephUser(u *cephv1.CephObjectSt
|
||||
logCreateOrUpdate := fmt.Sprintf("retrieved existing ceph object user %q", u.Name)
|
||||
var user admin.User
|
||||
var err error
|
||||
user, err = r.objContext.AdminOpsClient.GetUser(r.opManagerContext, *userConfig)
|
||||
// lookup user by name only and not by access key
|
||||
user, err = r.objContext.AdminOpsClient.GetUser(r.opManagerContext, admin.User{ID: u.Name})
|
||||
if err != nil {
|
||||
if errors.Is(err, admin.ErrNoSuchUser) {
|
||||
user, err = r.objContext.AdminOpsClient.CreateUser(r.opManagerContext, *userConfig)
|
||||
@@ -362,12 +455,20 @@ func (r *ReconcileObjectStoreUser) createOrUpdateCephUser(u *cephv1.CephObjectSt
|
||||
return errors.Wrapf(err, "failed to set quotas for user %q", u.Name)
|
||||
}
|
||||
|
||||
// Set access and secret key
|
||||
if len(userConfig.Keys) == 0 {
|
||||
userConfig.Keys = make([]admin.UserKeySpec, 1)
|
||||
// use the keys already set on the user & remove all but one key
|
||||
if len(user.Keys) == 0 {
|
||||
// something is wrong, there should be at least one key
|
||||
return errors.Wrapf(err, "no keys set for user %q", u.Name)
|
||||
}
|
||||
|
||||
userConfig.Keys = []admin.UserKeySpec{user.Keys[0]}
|
||||
logger.Debugf("reducing user %q keypairs to %v", u.Name, userConfig.Keys)
|
||||
}
|
||||
|
||||
if err := r.reconcileUserKeys(u.Name, userConfig.Keys); err != nil {
|
||||
return errors.Wrapf(err, "failed to reconcile keys for user %q", u.Name)
|
||||
}
|
||||
userConfig.Keys[0].AccessKey = user.Keys[0].AccessKey
|
||||
userConfig.Keys[0].SecretKey = user.Keys[0].SecretKey
|
||||
logger.Info(logCreateOrUpdate)
|
||||
|
||||
return nil
|
||||
@@ -549,14 +650,12 @@ func (r *ReconcileObjectStoreUser) reconcileCephUserSecret(cephObjectStoreUser *
|
||||
secret := r.generateCephUserSecret(cephObjectStoreUser, userConfig, tlsSecretName)
|
||||
|
||||
// Set owner ref to the object store user object
|
||||
err := controllerutil.SetControllerReference(cephObjectStoreUser, secret, r.scheme)
|
||||
if err != nil {
|
||||
if err := controllerutil.SetControllerReference(cephObjectStoreUser, secret, r.scheme); err != nil {
|
||||
return reconcile.Result{}, errors.Wrapf(err, "failed to set owner reference of ceph object user secret %q", secret.Name)
|
||||
}
|
||||
|
||||
// Create Kubernetes Secret
|
||||
err = opcontroller.CreateOrUpdateObject(r.opManagerContext, r.client, secret)
|
||||
if err != nil {
|
||||
if err := opcontroller.CreateOrUpdateObject(r.opManagerContext, r.client, secret); err != nil {
|
||||
return reconcile.Result{}, errors.Wrapf(err, "failed to create or update ceph object user %q secret", secret.Name)
|
||||
}
|
||||
|
||||
@@ -703,3 +802,165 @@ func (r *ReconcileObjectStoreUser) updateStatus(observedGeneration int64, name t
|
||||
}
|
||||
logger.Debugf("object store user %q status updated to %q", name, status)
|
||||
}
|
||||
|
||||
// updates `.status.keys`. This functionality is not included as part of
|
||||
// updateStatus() so that the list of referenced secrets, if any, can be
|
||||
// updated at the same time the rgw user key set is reconciled. This avoids the
|
||||
// need to regenerate the list of referenced secrets a second time when the
|
||||
// reconcile has completed and the overall resource status is updated.
|
||||
func (r *ReconcileObjectStoreUser) updateKeyStatus(name types.NamespacedName, referencedSecrets *map[types.UID]*corev1.Secret) {
|
||||
user := &cephv1.CephObjectStoreUser{}
|
||||
if err := r.client.Get(r.opManagerContext, name, user); err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
logger.Debug("CephObjectStoreUser resource not found. Ignoring since object must be deleted.")
|
||||
return
|
||||
}
|
||||
logger.Warningf("failed to retrieve CephObjectStoreUser %q to update .status.keys. %v", name, err)
|
||||
return
|
||||
}
|
||||
if user.Status == nil {
|
||||
user.Status = &cephv1.ObjectStoreUserStatus{}
|
||||
}
|
||||
|
||||
logger.Debugf("updating CephObjectStoreUser %q .status.keys to %+v.", name.Name, referencedSecrets)
|
||||
|
||||
keyStatus := []cephv1.SecretReference{}
|
||||
|
||||
for _, secret := range *referencedSecrets {
|
||||
keyStatus = append(keyStatus, cephv1.SecretReference{
|
||||
SecretReference: corev1.SecretReference{
|
||||
Name: secret.Name,
|
||||
Namespace: secret.Namespace,
|
||||
},
|
||||
UID: secret.UID,
|
||||
ResourceVersion: secret.ResourceVersion,
|
||||
})
|
||||
}
|
||||
|
||||
// assume map key ordering is unstable between reconciles and sort the slice
|
||||
// by secret name
|
||||
sort.Slice(keyStatus, func(i, j int) bool {
|
||||
return keyStatus[i].Name < keyStatus[j].Name
|
||||
})
|
||||
|
||||
user.Status.Keys = keyStatus
|
||||
|
||||
if err := reporting.UpdateStatus(r.client, user); err != nil {
|
||||
logger.Warningf("failed to update CephObjectStoreUser %q .status.keys. %v", name, err)
|
||||
return
|
||||
}
|
||||
logger.Debugf("updated CephObjectStoreUser %q .status.keys.", name)
|
||||
}
|
||||
|
||||
// getSecretValue returns the value of key in a kubernetes secret
|
||||
func (r *ReconcileObjectStoreUser) getSecretValue(selector *corev1.SecretKeySelector, namespace string) (string, *corev1.Secret, error) {
|
||||
secret := &corev1.Secret{}
|
||||
namespacedName := types.NamespacedName{
|
||||
Name: selector.Name,
|
||||
Namespace: namespace,
|
||||
}
|
||||
if err := r.client.Get(r.opManagerContext, namespacedName, secret); err != nil {
|
||||
return "", nil, errors.Wrapf(err, "failed to get secret %q", namespacedName)
|
||||
}
|
||||
value, ok := secret.Data[selector.Key]
|
||||
if !ok {
|
||||
return "", nil, errors.Errorf("failed to find key %q in secret %q", selector.Key, namespacedName)
|
||||
}
|
||||
|
||||
return string(value), secret, nil
|
||||
}
|
||||
|
||||
// reconcileUserKeys ensures the user's RGW keys match exactly the targetKeys slice. Any keys set on the user but not present in targetKeys are purged.
|
||||
func (r *ReconcileObjectStoreUser) reconcileUserKeys(userID string, targetKeys []admin.UserKeySpec) error {
|
||||
ctx := r.opManagerContext
|
||||
client := r.objContext.AdminOpsClient
|
||||
|
||||
// fetch the current user keys
|
||||
userInfo, err := client.GetUser(ctx, admin.User{ID: userID})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get user %q", userID)
|
||||
}
|
||||
|
||||
// create a lookup for the targetkeys (by AccessKey)
|
||||
targetMap := make(map[string]admin.UserKeySpec, len(targetKeys))
|
||||
for _, k := range targetKeys {
|
||||
targetMap[k.AccessKey] = k
|
||||
}
|
||||
|
||||
syncdKeys := make([]admin.UserKeySpec, len(targetKeys))
|
||||
|
||||
// remove any keys configured for the user that aren't present in targetKeys
|
||||
for _, existingKey := range userInfo.Keys {
|
||||
targetKey, found := targetMap[existingKey.AccessKey]
|
||||
if !found {
|
||||
// RemoveKey() requires the UID to be set but GetUser() returns the list of keys with only .User set
|
||||
rmKey := existingKey
|
||||
rmKey.UID = userID
|
||||
|
||||
if err := client.RemoveKey(ctx, rmKey); err != nil {
|
||||
return errors.Wrapf(err, "failed to remove key %q from user %q", existingKey.AccessKey, userID)
|
||||
}
|
||||
logger.Debugf("removed key %q from user %q as it is not in the target list", existingKey.AccessKey, userID)
|
||||
continue
|
||||
}
|
||||
if existingKey.SecretKey != targetKey.SecretKey {
|
||||
// key exists but needs to be updated; delete it so it will be recreated
|
||||
// RemoveKey() requires the UID to be set but GetUser() returns the list of keys with only .User set
|
||||
rmKey := existingKey
|
||||
rmKey.UID = userID
|
||||
|
||||
if err := client.RemoveKey(ctx, rmKey); err != nil {
|
||||
return errors.Wrapf(err, "failed to remove key %q from user %q", existingKey.AccessKey, userID)
|
||||
}
|
||||
logger.Debugf("removed key %q from user %q needs as it needs to be recreated", existingKey.AccessKey, userID)
|
||||
continue
|
||||
}
|
||||
// else key exists and is correct, no action needed
|
||||
// defer removal from targetMap until after the loop to avoid changing the map while iterating
|
||||
syncdKeys = append(syncdKeys, targetKey)
|
||||
}
|
||||
|
||||
// remove any keys from targetKeys that are already in sync
|
||||
for _, k := range syncdKeys {
|
||||
delete(targetMap, k.AccessKey)
|
||||
}
|
||||
|
||||
// create each desired key
|
||||
for _, k := range targetMap {
|
||||
if _, err := client.CreateKey(ctx, k); err != nil {
|
||||
return errors.Wrapf(err, "failed to create key %q for user %q", k.AccessKey, userID)
|
||||
}
|
||||
logger.Debugf("created key %q for user %q", k.AccessKey, userID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Construct a []admin.UserKeySpec from all secrets referenced from CephObjectStoreUser.Spec.Keys
|
||||
func (r *ReconcileObjectStoreUser) generateUserKeySpec(user *cephv1.CephObjectStoreUser) (*[]admin.UserKeySpec, *map[types.UID]*corev1.Secret, error) {
|
||||
referencedSecrets := make(map[types.UID]*corev1.Secret)
|
||||
|
||||
keys := make([]admin.UserKeySpec, len(user.Spec.Keys))
|
||||
for _, key := range user.Spec.Keys {
|
||||
accessKey, secret, err := r.getSecretValue(key.AccessKeyRef, user.Namespace)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "failed to get secret value")
|
||||
}
|
||||
referencedSecrets[secret.UID] = secret
|
||||
|
||||
secretKey, secret, err := r.getSecretValue(key.SecretKeyRef, user.Namespace)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "failed to get secret value")
|
||||
}
|
||||
referencedSecrets[secret.UID] = secret
|
||||
|
||||
keys = append(keys, admin.UserKeySpec{
|
||||
UID: user.Name,
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
KeyType: "s3",
|
||||
})
|
||||
}
|
||||
|
||||
return &keys, &referencedSecrets, nil
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
cephobject "github.com/rook/rook/pkg/operator/ceph/object"
|
||||
exectest "github.com/rook/rook/pkg/util/exec/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -495,7 +496,6 @@ func TestCreateOrUpdateCephUser(t *testing.T) {
|
||||
}
|
||||
adminClient, err := admin.New("rook-ceph-rgw-my-store.mycluster.svc", "53S6B9S809NUP19IJ2K3", "1bXPegzsGClvoGAiJdHQD1uOW2sQBLAZM9j9VtXR", mockClient)
|
||||
assert.NoError(t, err)
|
||||
userConfig := generateUserConfig(objectUser)
|
||||
r := &ReconcileObjectStoreUser{
|
||||
objContext: &cephobject.AdminOpsContext{
|
||||
AdminOpsClient: adminClient,
|
||||
@@ -506,20 +506,21 @@ func TestCreateOrUpdateCephUser(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Run("user with empty name", func(t *testing.T) {
|
||||
userConfig := &admin.User{}
|
||||
err = r.createOrUpdateCephUser(objectUser, userConfig)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("user without any Quotas or Capabilities", func(t *testing.T) {
|
||||
objectUser.Name = name
|
||||
userConfig = generateUserConfig(objectUser)
|
||||
userConfig := generateUserConfig(objectUser)
|
||||
err = r.createOrUpdateCephUser(objectUser, userConfig)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("setting MaxBuckets for the user", func(t *testing.T) {
|
||||
objectUser.Spec.Quotas = &cephv1.ObjectUserQuotaSpec{MaxBuckets: &maxbucket}
|
||||
userConfig = generateUserConfig(objectUser)
|
||||
userConfig := generateUserConfig(objectUser)
|
||||
err = r.createOrUpdateCephUser(objectUser, userConfig)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
@@ -532,7 +533,7 @@ func TestCreateOrUpdateCephUser(t *testing.T) {
|
||||
Roles: "*",
|
||||
Info: "read, write",
|
||||
}
|
||||
userConfig = generateUserConfig(objectUser)
|
||||
userConfig := generateUserConfig(objectUser)
|
||||
err = r.createOrUpdateCephUser(objectUser, userConfig)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
@@ -541,31 +542,36 @@ func TestCreateOrUpdateCephUser(t *testing.T) {
|
||||
t.Run("setting MaxObjects for the user", func(t *testing.T) {
|
||||
objectUser.Spec.Capabilities = nil
|
||||
objectUser.Spec.Quotas = &cephv1.ObjectUserQuotaSpec{MaxObjects: &maxobject}
|
||||
userConfig = generateUserConfig(objectUser)
|
||||
userConfig := generateUserConfig(objectUser)
|
||||
require.NoError(t, err)
|
||||
err = r.createOrUpdateCephUser(objectUser, userConfig)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("setting MaxSize for the user", func(t *testing.T) {
|
||||
objectUser.Spec.Quotas = &cephv1.ObjectUserQuotaSpec{MaxSize: &maxsize}
|
||||
userConfig = generateUserConfig(objectUser)
|
||||
userConfig := generateUserConfig(objectUser)
|
||||
require.NoError(t, err)
|
||||
err = r.createOrUpdateCephUser(objectUser, userConfig)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("resetting MaxSize and MaxObjects for the user", func(t *testing.T) {
|
||||
objectUser.Spec.Quotas = nil
|
||||
userConfig = generateUserConfig(objectUser)
|
||||
userConfig := generateUserConfig(objectUser)
|
||||
require.NoError(t, err)
|
||||
err = r.createOrUpdateCephUser(objectUser, userConfig)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("setting both MaxSize and MaxObjects for the user", func(t *testing.T) {
|
||||
objectUser.Spec.Quotas = &cephv1.ObjectUserQuotaSpec{MaxObjects: &maxobject, MaxSize: &maxsize}
|
||||
userConfig = generateUserConfig(objectUser)
|
||||
userConfig := generateUserConfig(objectUser)
|
||||
require.NoError(t, err)
|
||||
err = r.createOrUpdateCephUser(objectUser, userConfig)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("resetting MaxSize and MaxObjects again for the user", func(t *testing.T) {
|
||||
objectUser.Spec.Quotas = nil
|
||||
userConfig = generateUserConfig(objectUser)
|
||||
userConfig := generateUserConfig(objectUser)
|
||||
require.NoError(t, err)
|
||||
err = r.createOrUpdateCephUser(objectUser, userConfig)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
@@ -578,7 +584,8 @@ func TestCreateOrUpdateCephUser(t *testing.T) {
|
||||
Info: "read, write",
|
||||
}
|
||||
objectUser.Spec.Quotas = &cephv1.ObjectUserQuotaSpec{MaxBuckets: &maxbucket, MaxObjects: &maxobject, MaxSize: &maxsize}
|
||||
userConfig = generateUserConfig(objectUser)
|
||||
userConfig := generateUserConfig(objectUser)
|
||||
require.NoError(t, err)
|
||||
err = r.createOrUpdateCephUser(objectUser, userConfig)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
@@ -27,6 +27,13 @@ import (
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/kube-object-storage/lib-bucket-provisioner/pkg/apis/objectbucket.io/v1alpha1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
|
||||
"github.com/rook/rook/pkg/daemon/ceph/client"
|
||||
rgw "github.com/rook/rook/pkg/operator/ceph/object"
|
||||
@@ -34,12 +41,7 @@ import (
|
||||
"github.com/rook/rook/tests/framework/installer"
|
||||
"github.com/rook/rook/tests/framework/utils"
|
||||
"github.com/rook/rook/tests/integration/object/bucketowner"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"github.com/rook/rook/tests/integration/object/user/userkeys"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -153,6 +155,7 @@ func runObjectE2ETest(helper *clients.TestClient, k8sh *utils.K8sHelper, install
|
||||
testObjectStoreOperations(s, helper, k8sh, namespace, storeName, swiftAndKeystone)
|
||||
|
||||
bucketowner.TestObjectBucketClaimBucketOwner(s.T(), k8sh, installer, logger, tlsEnable)
|
||||
userkeys.TestObjectStoreUserKeys(s.T(), k8sh, installer, logger, tlsEnable)
|
||||
|
||||
bucketNotificationTestStoreName := "bucket-notification-" + storeName
|
||||
createCephObjectStore(s.T(), helper, k8sh, installer, namespace, bucketNotificationTestStoreName, 1, tlsEnable, swiftAndKeystone)
|
||||
|
||||
@@ -0,0 +1,749 @@
|
||||
/*
|
||||
Copyright 2025 The Rook Authors. All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package userkeys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ceph/go-ceph/rgw/admin"
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
|
||||
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
|
||||
"github.com/rook/rook/tests/framework/installer"
|
||||
"github.com/rook/rook/tests/framework/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultName = "test-userkeys"
|
||||
|
||||
ns = &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: defaultName,
|
||||
},
|
||||
}
|
||||
|
||||
objectStore = &cephv1.CephObjectStore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: defaultName,
|
||||
// the CephObjectstore must be in the same ns as the CephCluster
|
||||
Namespace: "object-ns",
|
||||
},
|
||||
Spec: cephv1.ObjectStoreSpec{
|
||||
MetadataPool: cephv1.PoolSpec{
|
||||
Replicated: cephv1.ReplicatedSpec{
|
||||
Size: 1,
|
||||
RequireSafeReplicaSize: false,
|
||||
},
|
||||
},
|
||||
DataPool: cephv1.PoolSpec{
|
||||
Replicated: cephv1.ReplicatedSpec{
|
||||
Size: 1,
|
||||
RequireSafeReplicaSize: false,
|
||||
},
|
||||
},
|
||||
Gateway: cephv1.GatewaySpec{
|
||||
Port: 80,
|
||||
Instances: 1,
|
||||
},
|
||||
AllowUsersInNamespaces: []string{ns.Name},
|
||||
},
|
||||
}
|
||||
|
||||
objectStoreSvc = &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: objectStore.Name,
|
||||
Namespace: objectStore.Namespace,
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app": "rook-ceph-rgw",
|
||||
"rook_cluster": objectStore.Namespace,
|
||||
"rook_object_store": objectStore.Name,
|
||||
},
|
||||
Ports: []corev1.ServicePort{
|
||||
{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
TargetPort: intstr.FromInt(8080),
|
||||
},
|
||||
},
|
||||
SessionAffinity: corev1.ServiceAffinityNone,
|
||||
Type: corev1.ServiceTypeNodePort,
|
||||
},
|
||||
}
|
||||
|
||||
secret1 = &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: defaultName + "-secret1",
|
||||
Namespace: ns.Name,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"AWS_ACCESS_KEY_ID": []byte("P1ANF2BP4K2LPGOR5SBB"),
|
||||
"AWS_SECRET_ACCESS_KEY": []byte("yfCHPhhOed0vJkqZsGEODyJrdKqHD09OMTWCnwjX"),
|
||||
},
|
||||
}
|
||||
|
||||
secret2 = &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: defaultName + "-secret2",
|
||||
Namespace: ns.Name,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"AWS_ACCESS_KEY_ID": []byte("2I6RPUTQFMNCSYEXZ6VM"),
|
||||
"AWS_SECRET_ACCESS_KEY": []byte("uY066SWPfaOVlDeYc7GJyOTfkDejyDdXrqehS6wx"),
|
||||
},
|
||||
}
|
||||
|
||||
secret3 = &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: defaultName + "-secret3",
|
||||
Namespace: ns.Name,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"AWS_ACCESS_KEY_ID": []byte("J4D0P20F3EDR51OSND7Y"),
|
||||
"AWS_SECRET_ACCESS_KEY": []byte("jn89OpkXNoDlIHVVQ23mE2DZgPmuDK3WVH5ExOvQ"),
|
||||
},
|
||||
}
|
||||
|
||||
secret4 = &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: defaultName + "-secret4",
|
||||
Namespace: ns.Name,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"AWS_ACCESS_KEY_ID": []byte("MPZX7DG5WJWQ6VPCSLYT"),
|
||||
"AWS_SECRET_ACCESS_KEY": []byte("phh7DIxnLPeSD2V6FUouhmnWrKlKRD5dBykyXozX"),
|
||||
},
|
||||
}
|
||||
|
||||
secret5 = &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: defaultName + "-secret5",
|
||||
Namespace: ns.Name,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"AWS_ACCESS_KEY_ID": []byte("7TNSSANCO5KXK23IPT91"),
|
||||
"AWS_SECRET_ACCESS_KEY": []byte("HksEDf0hEh3PtTvl7s9x6CyXfkWuY8eAMYAcvH5l"),
|
||||
},
|
||||
}
|
||||
|
||||
osu1 = cephv1.CephObjectStoreUser{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: defaultName + "-user1",
|
||||
Namespace: ns.Name,
|
||||
},
|
||||
Spec: cephv1.ObjectStoreUserSpec{
|
||||
Store: objectStore.Name,
|
||||
ClusterNamespace: objectStore.Namespace,
|
||||
Keys: []cephv1.ObjectUserKey{
|
||||
{
|
||||
AccessKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret1.Name,
|
||||
},
|
||||
Key: "AWS_ACCESS_KEY_ID",
|
||||
},
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret1.Name,
|
||||
},
|
||||
Key: "AWS_SECRET_ACCESS_KEY",
|
||||
},
|
||||
},
|
||||
{
|
||||
AccessKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret2.Name,
|
||||
},
|
||||
Key: "AWS_ACCESS_KEY_ID",
|
||||
},
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret2.Name,
|
||||
},
|
||||
Key: "AWS_SECRET_ACCESS_KEY",
|
||||
},
|
||||
},
|
||||
{
|
||||
AccessKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret3.Name,
|
||||
},
|
||||
Key: "AWS_ACCESS_KEY_ID",
|
||||
},
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret3.Name,
|
||||
},
|
||||
Key: "AWS_SECRET_ACCESS_KEY",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// generate the secret name the operator is expected to generate for the CephObjectStoreUser
|
||||
func generateObjectStoreUserSecretName(osu cephv1.CephObjectStoreUser) string {
|
||||
return "rook-ceph-object-user-" + osu.Namespace + "-" + osu.Name
|
||||
}
|
||||
|
||||
// find a UserKeySpec by AccessKey value
|
||||
func findUserKeySpec(keys []admin.UserKeySpec, accessKey string) (admin.UserKeySpec, error) {
|
||||
for _, k := range keys {
|
||||
if k.AccessKey == accessKey {
|
||||
return k, nil
|
||||
}
|
||||
}
|
||||
return admin.UserKeySpec{}, fmt.Errorf("UserKeySpec with AccessKey %q not found", accessKey)
|
||||
}
|
||||
|
||||
func checkStatusKeys(t *testing.T, k8sh *utils.K8sHelper, osu cephv1.CephObjectStoreUser, expectedSecrets []*corev1.Secret) {
|
||||
t.Run(fmt.Sprintf("cephObjectStoreUser %q has .status.keys set", osu.Name), func(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, liveOsu.Status)
|
||||
assert.Len(t, liveOsu.Status.Keys, len(expectedSecrets))
|
||||
|
||||
for _, secret := range expectedSecrets {
|
||||
secretRef, err := func(secretName string, keys []cephv1.SecretReference) (cephv1.SecretReference, error) {
|
||||
for _, secretRef := range keys {
|
||||
if secretRef.Name == secretName {
|
||||
return secretRef, nil
|
||||
}
|
||||
}
|
||||
return cephv1.SecretReference{}, fmt.Errorf("secretReference for secret %q not found in CephObjectStoreUser.status.keys", secret.Name)
|
||||
}(secret.Name, liveOsu.Status.Keys)
|
||||
require.NoError(t, err)
|
||||
|
||||
// fetch the live secret for UID and ResourceVersion
|
||||
liveSecret, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Get(ctx, secret.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, liveSecret.Name, secretRef.Name)
|
||||
assert.Equal(t, liveSecret.Namespace, secretRef.Namespace)
|
||||
assert.Equal(t, liveSecret.UID, secretRef.UID)
|
||||
assert.Equal(t, liveSecret.ResourceVersion, secretRef.ResourceVersion)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func checkRgwUserKeys(t *testing.T, adminClient *admin.API, osu cephv1.CephObjectStoreUser, expectedSecrets []*corev1.Secret, accessKeyName, secretKeyName string) {
|
||||
t.Run(fmt.Sprintf("rgw user %q has keys set", osu.Name), func(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
for _, secret := range expectedSecrets {
|
||||
var keySpec admin.UserKeySpec
|
||||
|
||||
// assume that the .Phase doesn't change when updating keys
|
||||
inSync := utils.Retry(40, time.Second, fmt.Sprintf("CephObjectStoreUser has key in sync with secret %q ", secret.Name), func() bool {
|
||||
liveUser, err := adminClient.GetUser(ctx, admin.User{ID: osu.Name})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
keySpec, err = findUserKeySpec(liveUser.Keys, string(secret.Data[accessKeyName]))
|
||||
return err == nil
|
||||
})
|
||||
require.True(t, inSync)
|
||||
|
||||
assert.Equal(t, string(secret.Data[accessKeyName]), keySpec.AccessKey)
|
||||
assert.Equal(t, string(secret.Data[secretKeyName]), keySpec.SecretKey)
|
||||
}
|
||||
|
||||
// check that no extra keys are present
|
||||
liveUser, err := adminClient.GetUser(ctx, admin.User{ID: osu.Name})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, len(expectedSecrets), len(liveUser.Keys))
|
||||
})
|
||||
}
|
||||
|
||||
func TestObjectStoreUserKeys(t *testing.T, k8sh *utils.K8sHelper, installer *installer.CephInstaller, logger *capnslog.PackageLogger, tlsEnable bool) {
|
||||
t.Run("ObjectStoreUser keys", func(t *testing.T) {
|
||||
if tlsEnable {
|
||||
// Skip testing with and without TLS to reduce test time
|
||||
t.Skip("skipping test for TLS enabled clusters")
|
||||
}
|
||||
|
||||
var adminClient *admin.API
|
||||
ctx := context.TODO()
|
||||
|
||||
t.Run(fmt.Sprintf("create ns %q", ns.Name), func(t *testing.T) {
|
||||
_, err := k8sh.Clientset.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("create CephObjectStore %q", objectStore.Name), func(t *testing.T) {
|
||||
objectStore, err := k8sh.RookClientset.CephV1().CephObjectStores(objectStore.Namespace).Create(ctx, objectStore, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
osReady := utils.Retry(180, time.Second, "CephObjectStore is Ready", func() bool {
|
||||
liveOs, err := k8sh.RookClientset.CephV1().CephObjectStores(objectStore.Namespace).Get(ctx, objectStore.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if liveOs.Status == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return liveOs.Status.Phase == cephv1.ConditionReady
|
||||
})
|
||||
require.True(t, osReady)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("create svc %q", objectStoreSvc.Name), func(t *testing.T) {
|
||||
_, err := k8sh.Clientset.CoreV1().Services(objectStore.Namespace).Create(ctx, objectStoreSvc, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("create secret %q", secret1.Name), func(t *testing.T) {
|
||||
_, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Create(ctx, secret1, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("create secret %q", secret2.Name), func(t *testing.T) {
|
||||
_, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Create(ctx, secret2, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("create secret %q", secret3.Name), func(t *testing.T) {
|
||||
_, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Create(ctx, secret3, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("create secret %q", secret4.Name), func(t *testing.T) {
|
||||
_, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Create(ctx, secret4, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("create secret %q", secret5.Name), func(t *testing.T) {
|
||||
_, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Create(ctx, secret5, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("create CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
|
||||
_, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Create(ctx, &osu1, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// user creation may be slow right after rgw start up
|
||||
osuReady := utils.Retry(120, time.Second, "CephObjectStoreUser is Ready", func() bool {
|
||||
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if liveOsu.Status == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return liveOsu.Status.Phase == string(cephv1.ConditionReady)
|
||||
})
|
||||
require.True(t, osuReady)
|
||||
})
|
||||
|
||||
t.Run("setup rgw admin api client", func(t *testing.T) {
|
||||
err, output := installer.Execute("radosgw-admin", []string{"user", "info", "--uid=dashboard-admin", fmt.Sprintf("--rgw-realm=%s", objectStore.Name)}, objectStore.Namespace)
|
||||
require.NoError(t, err)
|
||||
|
||||
// extract api creds from json output
|
||||
var userInfo map[string]interface{}
|
||||
err = json.Unmarshal([]byte(output), &userInfo)
|
||||
require.NoError(t, err)
|
||||
|
||||
s3AccessKey, ok := userInfo["keys"].([]interface{})[0].(map[string]interface{})["access_key"].(string)
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, s3AccessKey)
|
||||
|
||||
s3SecretKey, ok := userInfo["keys"].([]interface{})[0].(map[string]interface{})["secret_key"].(string)
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, s3SecretKey)
|
||||
|
||||
// extract rgw endpoint from k8s svc
|
||||
svc, err := k8sh.Clientset.CoreV1().Services(objectStore.Namespace).Get(ctx, objectStore.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
schema := "http://"
|
||||
httpClient := &http.Client{}
|
||||
|
||||
if tlsEnable {
|
||||
schema = "https://"
|
||||
httpClient.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
// nolint:gosec // skip TLS verification as this is a test
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
s3Endpoint := schema + svc.Spec.ClusterIP + ":80"
|
||||
|
||||
logger.Infof("endpoint (%s) Accesskey (%s) secret (%s)", s3Endpoint, s3AccessKey, s3SecretKey)
|
||||
|
||||
adminClient, err = admin.New(s3Endpoint, s3AccessKey, s3SecretKey, httpClient)
|
||||
require.NoError(t, err)
|
||||
|
||||
// verify that admin api is working
|
||||
_, err = adminClient.GetInfo(ctx)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
{
|
||||
secrets := []*corev1.Secret{secret1, secret2, secret3}
|
||||
|
||||
checkRgwUserKeys(t, adminClient, osu1, secrets, "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY")
|
||||
checkStatusKeys(t, k8sh, osu1, secrets)
|
||||
}
|
||||
|
||||
t.Run(fmt.Sprintf("update keys on CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
|
||||
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
liveOsu.Spec.Keys = []cephv1.ObjectUserKey{
|
||||
{
|
||||
AccessKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret4.Name,
|
||||
},
|
||||
Key: "AWS_ACCESS_KEY_ID",
|
||||
},
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret4.Name,
|
||||
},
|
||||
Key: "AWS_SECRET_ACCESS_KEY",
|
||||
},
|
||||
},
|
||||
{
|
||||
AccessKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret5.Name,
|
||||
},
|
||||
Key: "AWS_ACCESS_KEY_ID",
|
||||
},
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret5.Name,
|
||||
},
|
||||
Key: "AWS_SECRET_ACCESS_KEY",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err = k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Update(ctx, liveOsu, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
{
|
||||
secrets := []*corev1.Secret{secret4, secret5}
|
||||
|
||||
checkRgwUserKeys(t, adminClient, osu1, secrets, "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY")
|
||||
checkStatusKeys(t, k8sh, osu1, secrets)
|
||||
}
|
||||
|
||||
// test transition from explicit keys -> automatic secret creation
|
||||
|
||||
// when all explicit keys are removed from CephObjectStoreUser, one should
|
||||
// be left in place and the operator should [still] create a k8s secret for it
|
||||
t.Run(fmt.Sprintf("remove all keys set on CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
|
||||
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
liveOsu.Spec.Keys = nil
|
||||
|
||||
_, err = k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Update(ctx, liveOsu, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// wait for the number of keys to drop to 1
|
||||
inSync := utils.Retry(40, time.Second, "CephObjectStoreUser has 1 key", func() bool {
|
||||
liveUser, err := adminClient.GetUser(ctx, admin.User{ID: osu1.Name})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return len(liveUser.Keys) == 1
|
||||
})
|
||||
require.True(t, inSync)
|
||||
})
|
||||
|
||||
// keys updated on user
|
||||
{
|
||||
// fetch automatic secret as it should be the only key set on the rgw user
|
||||
secretName := generateObjectStoreUserSecretName(osu1)
|
||||
liveSecret, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Get(ctx, secretName, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
secrets := []*corev1.Secret{liveSecret}
|
||||
|
||||
checkRgwUserKeys(t, adminClient, osu1, secrets, "AccessKey", "SecretKey")
|
||||
}
|
||||
|
||||
t.Run(fmt.Sprintf("cephObjectStoreUser %q .status.keys is unset", osu1.Name), func(t *testing.T) {
|
||||
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, liveOsu.Status)
|
||||
assert.Len(t, liveOsu.Status.Keys, 0)
|
||||
})
|
||||
|
||||
// test transition automatic secret creation -> explicit keys
|
||||
t.Run(fmt.Sprintf("add keys to CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
|
||||
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
liveOsu.Spec.Keys = []cephv1.ObjectUserKey{
|
||||
{
|
||||
AccessKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret1.Name,
|
||||
},
|
||||
Key: "AWS_ACCESS_KEY_ID",
|
||||
},
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret1.Name,
|
||||
},
|
||||
Key: "AWS_SECRET_ACCESS_KEY",
|
||||
},
|
||||
},
|
||||
{
|
||||
AccessKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret2.Name,
|
||||
},
|
||||
Key: "AWS_ACCESS_KEY_ID",
|
||||
},
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret2.Name,
|
||||
},
|
||||
Key: "AWS_SECRET_ACCESS_KEY",
|
||||
},
|
||||
},
|
||||
{
|
||||
AccessKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret3.Name,
|
||||
},
|
||||
Key: "AWS_ACCESS_KEY_ID",
|
||||
},
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret3.Name,
|
||||
},
|
||||
Key: "AWS_SECRET_ACCESS_KEY",
|
||||
},
|
||||
},
|
||||
{
|
||||
AccessKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret4.Name,
|
||||
},
|
||||
Key: "AWS_ACCESS_KEY_ID",
|
||||
},
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret4.Name,
|
||||
},
|
||||
Key: "AWS_SECRET_ACCESS_KEY",
|
||||
},
|
||||
},
|
||||
{
|
||||
AccessKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret5.Name,
|
||||
},
|
||||
Key: "AWS_ACCESS_KEY_ID",
|
||||
},
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: secret5.Name,
|
||||
},
|
||||
Key: "AWS_SECRET_ACCESS_KEY",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err = k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Update(ctx, liveOsu, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
{
|
||||
secrets := []*corev1.Secret{secret1, secret2, secret3, secret4, secret5}
|
||||
|
||||
checkRgwUserKeys(t, adminClient, osu1, secrets, "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY")
|
||||
checkStatusKeys(t, k8sh, osu1, secrets)
|
||||
}
|
||||
|
||||
// updating a secret already referenced by a CephObjectStoreUser should trigger a reconcile
|
||||
t.Run(fmt.Sprintf("update secret %q data", secret1.Name), func(t *testing.T) {
|
||||
liveSecret, err := k8sh.Clientset.CoreV1().Secrets(secret1.Namespace).Get(ctx, secret1.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
liveSecret.Data = map[string][]byte{
|
||||
"AWS_ACCESS_KEY_ID": []byte("foo"),
|
||||
"AWS_SECRET_ACCESS_KEY": []byte("bar"),
|
||||
}
|
||||
|
||||
_, err = k8sh.Clientset.CoreV1().Secrets(secret1.Namespace).Update(ctx, liveSecret, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
{
|
||||
// fetch secret that we modified
|
||||
liveSecret, err := k8sh.Clientset.CoreV1().Secrets(secret1.Namespace).Get(ctx, secret1.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
secrets := []*corev1.Secret{liveSecret, secret2, secret3, secret4, secret5}
|
||||
|
||||
checkRgwUserKeys(t, adminClient, osu1, secrets, "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY")
|
||||
checkStatusKeys(t, k8sh, osu1, secrets)
|
||||
}
|
||||
|
||||
// deleting a secret referenced by a CephObjectStoreUser should trigger a reconcile (which fails)
|
||||
t.Run(fmt.Sprintf("delete secret %q", secret1.Name), func(t *testing.T) {
|
||||
err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Delete(ctx, secret1.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("CephObjectStoreUser %q has phase ReconcileFailed", osu1.Name), func(t *testing.T) {
|
||||
// user creation may be slow right after rgw start up
|
||||
osuReady := utils.Retry(40, time.Second, "CephObjectStoreUser is ReconcileFailed", func() bool {
|
||||
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if liveOsu.Status == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return liveOsu.Status.Phase == string(cephv1.ReconcileFailed)
|
||||
})
|
||||
require.True(t, osuReady)
|
||||
})
|
||||
|
||||
// recreate secret to allow CephObjectStoreUser to reconcile so it can be deleted
|
||||
t.Run(fmt.Sprintf("create secret %q", secret1.Name), func(t *testing.T) {
|
||||
_, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Create(ctx, secret1, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("delete CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
|
||||
err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Delete(ctx, osu1.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
absent := utils.Retry(40, time.Second, "CephObjectStoreUser is absent", func() bool {
|
||||
_, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
|
||||
return err != nil
|
||||
})
|
||||
assert.True(t, absent)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("no CephObjectStoreUsers in ns %q", ns.Name), func(t *testing.T) {
|
||||
osus, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).List(ctx, metav1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, osus.Items, 0)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("user %q does not exist", osu1.Name), func(t *testing.T) {
|
||||
_, err := adminClient.GetUser(ctx, admin.User{ID: osu1.Name})
|
||||
require.ErrorIs(t, err, admin.ErrNoSuchUser)
|
||||
})
|
||||
|
||||
// CephObjectStoreUser removal did not delete any secret resources
|
||||
t.Run(fmt.Sprintf("secret %q still exists", secret2.Name), func(t *testing.T) {
|
||||
_, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Get(ctx, secret2.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("secret %q still exists", secret3.Name), func(t *testing.T) {
|
||||
_, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Get(ctx, secret3.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("secret %q still exists", secret4.Name), func(t *testing.T) {
|
||||
_, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Get(ctx, secret4.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("secret %q still exists", secret5.Name), func(t *testing.T) {
|
||||
_, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Get(ctx, secret5.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("delete secret %q", secret5.Name), func(t *testing.T) {
|
||||
err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Delete(ctx, secret5.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("delete secret %q", secret4.Name), func(t *testing.T) {
|
||||
err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Delete(ctx, secret4.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("delete secret %q", secret3.Name), func(t *testing.T) {
|
||||
err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Delete(ctx, secret3.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("delete secret %q", secret2.Name), func(t *testing.T) {
|
||||
err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Delete(ctx, secret2.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("delete secret %q", secret1.Name), func(t *testing.T) {
|
||||
err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Delete(ctx, secret1.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("no secrets in ns %q", ns.Name), func(t *testing.T) {
|
||||
secrets, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).List(ctx, metav1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, secrets.Items, 0)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("delete svc %q", objectStoreSvc.Name), func(t *testing.T) {
|
||||
err := k8sh.Clientset.CoreV1().Services(objectStore.Namespace).Delete(ctx, objectStoreSvc.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("delete CephObjectStore %q", objectStore.Name), func(t *testing.T) {
|
||||
err := k8sh.RookClientset.CephV1().CephObjectStores(objectStore.Namespace).Delete(ctx, objectStore.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("delete ns %q", ns.Name), func(t *testing.T) {
|
||||
err := k8sh.Clientset.CoreV1().Namespaces().Delete(ctx, ns.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user