Skip to content

Commit e4e0aaf

Browse files
committed
add initial BucketAccess controller reconciliation
Add initial implementation for BucketAccess reconciliation based on v1alpha2 KEP. This initial implementation covers only the first section of Controller reconciliation for a new BucketAccess. Coverage ends at the point where reconciliation is handed off to the Sidecar. Signed-off-by: Blaine Gardner <[email protected]>
1 parent dbe43d4 commit e4e0aaf

File tree

5 files changed

+308
-13
lines changed

5 files changed

+308
-13
lines changed

client/apis/objectstorage/v1alpha2/definitions.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,30 @@ limitations under the License.
1616

1717
package v1alpha2
1818

19+
// Finalizers
1920
const (
2021
// ProtectionFinalizer is applied to a COSI resource object to protect it from deletion while
2122
// COSI processes deletion of the object's intermediate and backend resources.
2223
ProtectionFinalizer = `objectstorage.k8s.io/protection`
2324
)
2425

26+
// Annotations
27+
const (
28+
// SidecarCleanupFinishedAnnotation : This annotation is applied by a COSI Sidecar to a managed
29+
// BucketAccess when the resources is being deleted. The Sidecar calls the Driver to perform
30+
// backend deletion actions and then hands off final deletion cleanup to the COSI Controller
31+
// by setting this annotation on the resource.
32+
SidecarCleanupFinishedAnnotation = `objectstorage.k8s.io/sidecar-cleanup-finished`
33+
34+
// ControllerManagementOverrideAnnotation : This annotation can be applied to a resource by the
35+
// COSI Controller in order to reclaim management of the resource temporarily when it would
36+
// otherwise be managed by a COSI Sidecar. This is intended for scenarios where a bug in
37+
// provisioning needs to be rectified by a newer version of the COSI Controller. Once the bug is
38+
// resolved, the annotation should be removed to allow normal Sidecar handoff to occur.
39+
ControllerManagementOverrideAnnotation = `objectstorage.k8s.io/controller-management-override`
40+
)
41+
42+
// Sidecar RPC definitions
2543
const (
2644
// RpcEndpointDefault is the default RPC endpoint unix socket location.
2745
RpcEndpointDefault = "unix:///var/lib/cosi/cosi.sock"

controller/internal/reconciler/bucketaccess.go

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@ package reconciler
1818

1919
import (
2020
"context"
21+
"time"
2122

23+
"github.com/go-logr/logr"
24+
kerrors "k8s.io/apimachinery/pkg/api/errors"
2225
"k8s.io/apimachinery/pkg/runtime"
2326
ctrl "sigs.k8s.io/controller-runtime"
2427
"sigs.k8s.io/controller-runtime/pkg/client"
25-
logf "sigs.k8s.io/controller-runtime/pkg/log"
28+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
2629

30+
cosiapi "sigs.k8s.io/container-object-storage-interface/client/apis/objectstorage/v1alpha2"
2731
objectstoragev1alpha2 "sigs.k8s.io/container-object-storage-interface/client/apis/objectstorage/v1alpha2"
2832
)
2933

@@ -33,25 +37,54 @@ type BucketAccessReconciler struct {
3337
Scheme *runtime.Scheme
3438
}
3539

36-
// +kubebuilder:rbac:groups=objectstorage.k8s.io,resources=bucketaccesses,verbs=get;list;watch;create;update;patch;delete
37-
// +kubebuilder:rbac:groups=objectstorage.k8s.io,resources=bucketaccesses/status,verbs=get;update;patch
40+
// +kubebuilder:rbac:groups=objectstorage.k8s.io,resources=bucketaccesses,verbs=get;list;watch;create;update
41+
// +kubebuilder:rbac:groups=objectstorage.k8s.io,resources=bucketaccesses/status,verbs=get;update
3842
// +kubebuilder:rbac:groups=objectstorage.k8s.io,resources=bucketaccesses/finalizers,verbs=update
3943

4044
// Reconcile is part of the main kubernetes reconciliation loop which aims to
4145
// move the current state of the cluster closer to the desired state.
42-
// TODO(user): Modify the Reconcile function to compare the state specified by
43-
// the BucketAccess object against the actual cluster state, and then
44-
// perform operations to make the cluster state reflect the state specified by
45-
// the user.
46-
//
47-
// For more details, check Reconcile and its Result here:
48-
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
4946
func (r *BucketAccessReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
50-
_ = logf.FromContext(ctx)
47+
logger := ctrl.LoggerFrom(ctx)
5148

52-
// TODO(user): your logic here
49+
access := &cosiapi.BucketAccess{}
50+
if err := r.Get(ctx, req.NamespacedName, access); err != nil {
51+
if kerrors.IsNotFound(err) {
52+
logger.V(1).Info("not reconciling nonexistent BucketClaim")
53+
return ctrl.Result{}, nil
54+
}
55+
// no resource to add status to or report an event for
56+
logger.Error(err, "failed to get BucketClaim")
57+
return ctrl.Result{}, err
58+
}
5359

54-
return ctrl.Result{}, nil
60+
retryError, err := r.reconcile(ctx, logger, access)
61+
if err != nil {
62+
// Record any error as a timestamped error in the status.
63+
access.Status.Error = cosiapi.NewTimestampedError(time.Now(), err.Error())
64+
if updErr := r.Status().Update(ctx, access); updErr != nil {
65+
logger.Error(err, "failed to update BucketAccess status after reconcile error", "updateError", updErr)
66+
// If status update fails, we must retry the error regardless of the reconcile return.
67+
// The reconcile needs to run again to make sure the status is eventually be updated.
68+
return reconcile.Result{}, err
69+
}
70+
71+
if !retryError {
72+
return reconcile.Result{}, reconcile.TerminalError(err)
73+
}
74+
return reconcile.Result{}, err
75+
}
76+
77+
// On success, clear any errors in the status.
78+
if access.Status.Error != nil {
79+
access.Status.Error = nil
80+
if err := r.Status().Update(ctx, access); err != nil {
81+
logger.Error(err, "failed to update BucketClaim status after reconcile success")
82+
// Retry the reconcile so status can be updated eventually.
83+
return reconcile.Result{}, err
84+
}
85+
}
86+
87+
return reconcile.Result{}, err
5588
}
5689

5790
// SetupWithManager sets up the controller with the Manager.
@@ -61,3 +94,9 @@ func (r *BucketAccessReconciler) SetupWithManager(mgr ctrl.Manager) error {
6194
Named("bucketaccess").
6295
Complete(r)
6396
}
97+
98+
func (r *BucketAccessReconciler) reconcile(
99+
ctx context.Context, logger logr.Logger, claim *cosiapi.BucketAccess,
100+
) (retryErrorType, error) {
101+
return NoError, nil
102+
}

internal/handoff/handoff.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package handoff defines logic needed for handing off control of resources between Controller and
18+
// Sidecar.
19+
package handoff
20+
21+
import (
22+
cosiapi "sigs.k8s.io/container-object-storage-interface/client/apis/objectstorage/v1alpha2"
23+
)
24+
25+
// BucketAccessManagedBySidecar returns true if a BucketAccess should be managed by the Sidecar.
26+
// A false return value indicates that it should be managed by the Controller instead.
27+
//
28+
// In order for COSI Controller and any given Sidecar to work well together, they should avoid
29+
// managing the same BucketAccess resource at the same time. Instances where a resource has no
30+
// manager MUST be avoided without exception.
31+
//
32+
// Version skew between Controller and Sidecar should be assumed. In order for version skew issues
33+
// to be minimized, avoid updating this logic unless it is absolutely critical. If updates are made,
34+
// be sure to carefully consider all version skew cases below. Minimize dual-ownership scenarios,
35+
// and avoid no-owner scenarios.
36+
//
37+
// 1. Sidecar version low, Controller version low
38+
// 2. Sidecar version low, Controller version high
39+
// 3. Sidecar version high, Controller version low
40+
// 4. Sidecar version high, Controller version high
41+
func BucketAccessManagedBySidecar(ba *cosiapi.BucketAccess) bool {
42+
// Allow a future-compatible mechanism by which the Controller can override the normal
43+
// BucketAccess management handoff logic in order to resolve a bug.
44+
// Instances where this is utilized should be infrequent -- ideally, never used.
45+
if _, ok := ba.Annotations[cosiapi.ControllerManagementOverrideAnnotation]; ok {
46+
return false
47+
}
48+
49+
// During provisioning, there are several status fields that the Controller needs to set before
50+
// the Sidecar can provision an access. However, tying this function's logic to ALL of the
51+
// status items could make long-term Controller-Sidecar handoff logic fragile. More logic means
52+
// more risk of unmanaged resources and more difficulty reasoning about how changes will impact
53+
// ownership during version skew. Minimize risk by relying on a single determining status field.
54+
if ba.Status.DriverName == "" {
55+
return false
56+
}
57+
58+
// During deletion, as long as the access was handed off to the Sidecar at some point, the
59+
// Sidecar must first clean up the backend bucket, then hand back final deletion to the
60+
// Controller by setting an annotation.
61+
if !ba.DeletionTimestamp.IsZero() {
62+
_, ok := ba.Annotations[cosiapi.SidecarCleanupFinishedAnnotation]
63+
return !ok // ok means sidecar is done cleaning up
64+
}
65+
66+
return true
67+
}

internal/handoff/handoff_test.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package handoff
18+
19+
import (
20+
"testing"
21+
"time"
22+
23+
"github.com/stretchr/testify/assert"
24+
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
"k8s.io/apimachinery/pkg/types"
26+
27+
cosiapi "sigs.k8s.io/container-object-storage-interface/client/apis/objectstorage/v1alpha2"
28+
)
29+
30+
func TestBucketAccessManagedBySidecar(t *testing.T) {
31+
tests := []struct {
32+
name string // description of this test case
33+
// input parameters for target function.
34+
isHandedOffToSidecar bool
35+
hasDeletionTimestamp bool
36+
hasSidecarCleanupFinishedAnnotation bool
37+
// desired result
38+
want bool
39+
}{
40+
// expected real-world scenarios
41+
{name: "new BA",
42+
isHandedOffToSidecar: false,
43+
hasDeletionTimestamp: false,
44+
hasSidecarCleanupFinishedAnnotation: false,
45+
want: false,
46+
},
47+
{name: "BA handoff to sidecar",
48+
isHandedOffToSidecar: true,
49+
hasDeletionTimestamp: false,
50+
hasSidecarCleanupFinishedAnnotation: false,
51+
want: true,
52+
},
53+
{name: "sidecar-managed BA begins deleting",
54+
isHandedOffToSidecar: true,
55+
hasDeletionTimestamp: true,
56+
hasSidecarCleanupFinishedAnnotation: false,
57+
want: true,
58+
},
59+
{name: "controller hand-back after sidecar deletion cleanup",
60+
isHandedOffToSidecar: true,
61+
hasDeletionTimestamp: true,
62+
hasSidecarCleanupFinishedAnnotation: true,
63+
want: false,
64+
},
65+
{name: "BA deleted before sidecar handoff",
66+
isHandedOffToSidecar: false,
67+
hasDeletionTimestamp: true,
68+
hasSidecarCleanupFinishedAnnotation: false,
69+
want: false,
70+
},
71+
// degraded scenarios
72+
{name: "new BA, erroneous sidecar cleanup annotation",
73+
isHandedOffToSidecar: false,
74+
hasDeletionTimestamp: false,
75+
hasSidecarCleanupFinishedAnnotation: true, // erroneous
76+
want: false,
77+
},
78+
{name: "sidecar-managed BA, erroneous sidecar cleanup annotation",
79+
isHandedOffToSidecar: true,
80+
hasDeletionTimestamp: false,
81+
hasSidecarCleanupFinishedAnnotation: true, // erroneous
82+
want: true,
83+
},
84+
{name: "BA deleted before sidecar handoff, erroneous sidecar cleanup annotation",
85+
isHandedOffToSidecar: false,
86+
hasDeletionTimestamp: true,
87+
hasSidecarCleanupFinishedAnnotation: true, // erroneous
88+
want: false,
89+
},
90+
}
91+
for _, tt := range tests {
92+
t.Run(tt.name, func(t *testing.T) {
93+
base := &cosiapi.BucketAccess{
94+
ObjectMeta: meta.ObjectMeta{
95+
Name: "my-access",
96+
Namespace: "tenant",
97+
Finalizers: []string{
98+
cosiapi.ProtectionFinalizer,
99+
"something-else",
100+
},
101+
Annotations: map[string]string{
102+
"user-annotation": "value",
103+
"key-only": "",
104+
},
105+
CreationTimestamp: meta.NewTime(time.Now()),
106+
Generation: 2,
107+
UID: types.UID("qwerty"),
108+
},
109+
Spec: cosiapi.BucketAccessSpec{
110+
BucketClaims: []cosiapi.BucketClaimAccess{
111+
{
112+
BucketClaimName: "bc-1",
113+
AccessMode: cosiapi.BucketAccessModeReadWrite,
114+
AccessSecretName: "bc-1-creds",
115+
},
116+
},
117+
BucketAccessClassName: "bac-standard",
118+
Protocol: cosiapi.ObjectProtocolS3,
119+
ServiceAccountName: "my-app",
120+
},
121+
}
122+
123+
copy := base.DeepCopy()
124+
125+
if tt.isHandedOffToSidecar {
126+
copy.Status.AccessedBuckets = []cosiapi.AccessedBucket{
127+
{
128+
BucketName: "bc-asdfgh",
129+
AccessMode: cosiapi.BucketAccessModeReadWrite,
130+
},
131+
}
132+
copy.Status.DriverName = "some.driver.io"
133+
copy.Status.AuthenticationType = cosiapi.BucketAccessAuthenticationTypeKey
134+
copy.Status.Parameters = map[string]string{}
135+
}
136+
137+
if tt.hasDeletionTimestamp {
138+
copy.DeletionTimestamp = &meta.Time{Time: time.Now()}
139+
}
140+
141+
if tt.hasSidecarCleanupFinishedAnnotation {
142+
copy.Annotations[cosiapi.SidecarCleanupFinishedAnnotation] = ""
143+
}
144+
145+
got := BucketAccessManagedBySidecar(copy)
146+
assert.Equal(t, tt.want, got)
147+
148+
// for all cases,applying the controller override annotation makes it controller-managed
149+
copy.Annotations[cosiapi.ControllerManagementOverrideAnnotation] = ""
150+
withOverride := BucketAccessManagedBySidecar(copy)
151+
assert.False(t, withOverride)
152+
})
153+
}
154+
}

vendor/sigs.k8s.io/container-object-storage-interface/client/apis/objectstorage/v1alpha2/definitions.go

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)