Skip to content

Commit 13b51c9

Browse files
committed
add bucketaccess sidecar reconciliation
Implement the final portion of BucketAccess provisioning after handoff to the Sidecar. Signed-off-by: Blaine Gardner <[email protected]>
1 parent f3bf0f5 commit 13b51c9

File tree

27 files changed

+2944
-439
lines changed

27 files changed

+2944
-439
lines changed

client/apis/objectstorage/v1alpha2/definitions.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ const (
2525

2626
// Annotations
2727
const (
28+
// BucketClaimBeingDeletedAnnotation : This annotation is applied by the COSI Controller to a
29+
// Bucket when its BucketClaim is being deleted.
30+
BucketClaimBeingDeletedAnnotation = `objectstorage.k8.io/bucketclaim-being-deleted`
31+
2832
// HasBucketAccessReferencesAnnotation : This annotation is applied by the COSI Controller to a
2933
// BucketClaim when a BucketAccess that references the BucketClaim is created. The annotation
3034
// remains for as long as any BucketAccess references the BucketClaim. Once all BucketAccesses

controller/internal/reconciler/bucketaccess.go

Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ import (
3636

3737
cosiapi "sigs.k8s.io/container-object-storage-interface/client/apis/objectstorage/v1alpha2"
3838
objectstoragev1alpha2 "sigs.k8s.io/container-object-storage-interface/client/apis/objectstorage/v1alpha2"
39+
"sigs.k8s.io/container-object-storage-interface/internal/bucketaccess"
3940
cosierr "sigs.k8s.io/container-object-storage-interface/internal/errors"
40-
"sigs.k8s.io/container-object-storage-interface/internal/handoff"
4141
cosipredicate "sigs.k8s.io/container-object-storage-interface/internal/predicate"
4242
)
4343

@@ -67,7 +67,7 @@ func (r *BucketAccessReconciler) Reconcile(ctx context.Context, req ctrl.Request
6767
return ctrl.Result{}, err
6868
}
6969

70-
if handoff.BucketAccessManagedBySidecar(access) {
70+
if bucketaccess.ManagedBySidecar(access) {
7171
logger.V(1).Info("not reconciling BucketAccess that should be managed by sidecar")
7272
return ctrl.Result{}, nil
7373
}
@@ -142,12 +142,12 @@ func (r *BucketAccessReconciler) reconcile(
142142
return cosierr.NonRetryableError(fmt.Errorf("deletion is not yet implemented")) // TODO
143143
}
144144

145-
needInit, err := needsControllerInitialization(&access.Status)
145+
initialized, err := bucketaccess.SidecarRequirementsPresent(&access.Status)
146146
if err != nil {
147147
logger.Error(err, "processed a degraded BucketAccess")
148148
return cosierr.NonRetryableError(fmt.Errorf("processed a degraded BucketAccess: %w", err))
149149
}
150-
if !needInit {
150+
if initialized {
151151
// BucketAccessClass info should only be copied to the BucketAccess status once, upon
152152
// initial provisioning. After the info is copied, make no attempt to fill in any missing or
153153
// lost info because we don't know whether the current Class is compatible with the info
@@ -244,38 +244,6 @@ func (r *BucketAccessReconciler) reconcile(
244244
return nil
245245
}
246246

247-
// Return true if the Controller needs to initialize the BucketAccess with BucketClaim and
248-
// BucketAccessClass info. Return false if required info is set.
249-
// Return an error if any required info is only partially set. This indicates some sort of
250-
// degradation or bug.
251-
func needsControllerInitialization(s *cosiapi.BucketAccessStatus) (bool, error) {
252-
requiredFields := map[string]bool{}
253-
requiredFieldIsSet := func(fieldName string, isSet bool) {
254-
requiredFields[fieldName] = isSet
255-
}
256-
257-
requiredFieldIsSet("status.accessedBuckets", len(s.AccessedBuckets) > 0)
258-
requiredFieldIsSet("status.driverName", s.DriverName != "")
259-
requiredFieldIsSet("status.authenticationType", string(s.AuthenticationType) != "")
260-
261-
set := []string{}
262-
for field, isSet := range requiredFields {
263-
if isSet {
264-
set = append(set, field)
265-
}
266-
}
267-
268-
if len(set) == 0 {
269-
return true, nil
270-
}
271-
272-
if len(set) == len(requiredFields) {
273-
return false, nil
274-
}
275-
276-
return false, fmt.Errorf("required Controller-managed fields are only partially set: %v", requiredFields)
277-
}
278-
279247
// Get all BucketClaims that this BucketAccess references.
280248
// If any claims don't exist, assume they don't exist YET; mark them nil in the resulting map
281249
// without treating nonexistence as an error.

controller/internal/reconciler/bucketaccess_test.go

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import (
2929
"k8s.io/apimachinery/pkg/types"
3030
"k8s.io/utils/ptr"
3131
cosiapi "sigs.k8s.io/container-object-storage-interface/client/apis/objectstorage/v1alpha2"
32-
"sigs.k8s.io/container-object-storage-interface/internal/handoff"
32+
"sigs.k8s.io/container-object-storage-interface/internal/bucketaccess"
3333
ctrl "sigs.k8s.io/controller-runtime"
3434
"sigs.k8s.io/controller-runtime/pkg/client"
3535
"sigs.k8s.io/controller-runtime/pkg/client/fake"
@@ -195,10 +195,10 @@ func TestBucketAccessReconcile(t *testing.T) {
195195
status.Parameters,
196196
)
197197

198-
assert.True(t, handoff.BucketAccessManagedBySidecar(access)) // MUST hand off to sidecar
199-
needInit, err := needsControllerInitialization(&access.Status) // MUST be fully initialized
198+
assert.True(t, bucketaccess.ManagedBySidecar(access)) // MUST hand off to sidecar
199+
initialized, err := bucketaccess.SidecarRequirementsPresent(&access.Status) // MUST be fully initialized
200200
assert.NoError(t, err)
201-
assert.False(t, needInit)
201+
assert.True(t, initialized)
202202

203203
crw := &cosiapi.BucketClaim{}
204204
err = c.Get(ctx, readWriteClaimNsName, crw)
@@ -267,10 +267,10 @@ func TestBucketAccessReconcile(t *testing.T) {
267267
assert.Empty(t, status.AuthenticationType)
268268
assert.Empty(t, status.Parameters)
269269

270-
assert.False(t, handoff.BucketAccessManagedBySidecar(access)) // MUST NOT hand off to sidecar
271-
needInit, err := needsControllerInitialization(&access.Status) // MUST NOT be initialized
270+
assert.False(t, bucketaccess.ManagedBySidecar(access)) // MUST NOT hand off to sidecar
271+
initialized, err := bucketaccess.SidecarRequirementsPresent(&access.Status) // MUST NOT be initialized
272272
assert.NoError(t, err)
273-
assert.True(t, needInit)
273+
assert.False(t, initialized)
274274

275275
crw := &cosiapi.BucketClaim{}
276276
err = c.Get(ctx, readWriteClaimNsName, crw)
@@ -315,10 +315,10 @@ func TestBucketAccessReconcile(t *testing.T) {
315315
assert.Empty(t, status.AuthenticationType)
316316
assert.Empty(t, status.Parameters)
317317

318-
assert.False(t, handoff.BucketAccessManagedBySidecar(access)) // MUST NOT hand off to sidecar
319-
needInit, err := needsControllerInitialization(&access.Status) // MUST NOT be initialized
318+
assert.False(t, bucketaccess.ManagedBySidecar(access)) // MUST NOT hand off to sidecar
319+
initialized, err := bucketaccess.SidecarRequirementsPresent(&access.Status) // MUST NOT be initialized
320320
assert.NoError(t, err)
321-
assert.True(t, needInit)
321+
assert.False(t, initialized)
322322

323323
crw := &cosiapi.BucketClaim{}
324324
err = c.Get(ctx, readWriteClaimNsName, crw)
@@ -371,10 +371,10 @@ func TestBucketAccessReconcile(t *testing.T) {
371371
assert.Empty(t, status.AuthenticationType)
372372
assert.Empty(t, status.Parameters)
373373

374-
assert.False(t, handoff.BucketAccessManagedBySidecar(access)) // MUST NOT hand off to sidecar
375-
needInit, err := needsControllerInitialization(&access.Status) // MUST NOT be initialized
374+
assert.False(t, bucketaccess.ManagedBySidecar(access)) // MUST NOT hand off to sidecar
375+
initialized, err := bucketaccess.SidecarRequirementsPresent(&access.Status) // MUST NOT be initialized
376376
assert.NoError(t, err)
377-
assert.True(t, needInit)
377+
assert.False(t, initialized)
378378

379379
crw := &cosiapi.BucketClaim{}
380380
err = c.Get(ctx, readWriteClaimNsName, crw)
@@ -424,10 +424,10 @@ func TestBucketAccessReconcile(t *testing.T) {
424424
assert.Empty(t, status.AuthenticationType)
425425
assert.Empty(t, status.Parameters)
426426

427-
assert.False(t, handoff.BucketAccessManagedBySidecar(access)) // MUST NOT hand off to sidecar
428-
needInit, err := needsControllerInitialization(&access.Status) // MUST NOT be initialized
427+
assert.False(t, bucketaccess.ManagedBySidecar(access)) // MUST NOT hand off to sidecar
428+
initialized, err := bucketaccess.SidecarRequirementsPresent(&access.Status) // MUST NOT be initialized
429429
assert.NoError(t, err)
430-
assert.True(t, needInit)
430+
assert.False(t, initialized)
431431

432432
crw := &cosiapi.BucketClaim{}
433433
err = c.Get(ctx, readWriteClaimNsName, crw)
@@ -474,10 +474,10 @@ func TestBucketAccessReconcile(t *testing.T) {
474474
assert.Empty(t, status.AuthenticationType)
475475
assert.Empty(t, status.Parameters)
476476

477-
assert.False(t, handoff.BucketAccessManagedBySidecar(access)) // MUST NOT hand off to sidecar
478-
needInit, err := needsControllerInitialization(&access.Status) // MUST NOT be initialized
477+
assert.False(t, bucketaccess.ManagedBySidecar(access)) // MUST NOT hand off to sidecar
478+
initialized, err := bucketaccess.SidecarRequirementsPresent(&access.Status) // MUST NOT be initialized
479479
assert.NoError(t, err)
480-
assert.True(t, needInit)
480+
assert.False(t, initialized)
481481

482482
crw := &cosiapi.BucketClaim{}
483483
err = c.Get(ctx, readWriteClaimNsName, crw)
@@ -526,10 +526,10 @@ func TestBucketAccessReconcile(t *testing.T) {
526526
assert.Empty(t, status.AuthenticationType)
527527
assert.Empty(t, status.Parameters)
528528

529-
assert.False(t, handoff.BucketAccessManagedBySidecar(access)) // MUST NOT hand off to sidecar
530-
needInit, err := needsControllerInitialization(&access.Status) // MUST NOT be initialized
529+
assert.False(t, bucketaccess.ManagedBySidecar(access)) // MUST NOT hand off to sidecar
530+
initialized, err := bucketaccess.SidecarRequirementsPresent(&access.Status) // MUST NOT be initialized
531531
assert.NoError(t, err)
532-
assert.True(t, needInit)
532+
assert.False(t, initialized)
533533

534534
crw := &cosiapi.BucketClaim{}
535535
err = c.Get(ctx, readWriteClaimNsName, crw)
@@ -594,10 +594,10 @@ func TestBucketAccessReconcile(t *testing.T) {
594594
status.Parameters,
595595
)
596596

597-
assert.True(t, handoff.BucketAccessManagedBySidecar(access)) // MUST hand off to sidecar
598-
needInit, err := needsControllerInitialization(&access.Status) // MUST be fully initialized
597+
assert.True(t, bucketaccess.ManagedBySidecar(access)) // MUST hand off to sidecar
598+
initialized, err := bucketaccess.SidecarRequirementsPresent(&access.Status) // MUST be fully initialized
599599
assert.NoError(t, err)
600-
assert.False(t, needInit)
600+
assert.True(t, initialized)
601601

602602
crw := &cosiapi.BucketClaim{}
603603
err = c.Get(ctx, readWriteClaimNsName, crw)
@@ -650,10 +650,10 @@ func TestBucketAccessReconcile(t *testing.T) {
650650
assert.Empty(t, status.AuthenticationType)
651651
assert.Empty(t, status.Parameters)
652652

653-
assert.False(t, handoff.BucketAccessManagedBySidecar(access)) // MUST NOT hand off to sidecar
654-
needInit, err := needsControllerInitialization(&access.Status) // MUST NOT be initialized
653+
assert.False(t, bucketaccess.ManagedBySidecar(access)) // MUST NOT hand off to sidecar
654+
initialized, err := bucketaccess.SidecarRequirementsPresent(&access.Status) // MUST NOT be initialized
655655
assert.NoError(t, err)
656-
assert.True(t, needInit)
656+
assert.False(t, initialized)
657657

658658
crw := &cosiapi.BucketClaim{}
659659
err = c.Get(ctx, readWriteClaimNsName, crw)
@@ -706,10 +706,10 @@ func TestBucketAccessReconcile(t *testing.T) {
706706
assert.Empty(t, status.AuthenticationType)
707707
assert.Empty(t, status.Parameters)
708708

709-
assert.False(t, handoff.BucketAccessManagedBySidecar(access)) // MUST NOT hand off to sidecar
710-
needInit, err := needsControllerInitialization(&access.Status) // MUST NOT be initialized
709+
assert.False(t, bucketaccess.ManagedBySidecar(access)) // MUST NOT hand off to sidecar
710+
initialized, err := bucketaccess.SidecarRequirementsPresent(&access.Status) // MUST NOT be initialized
711711
assert.NoError(t, err)
712-
assert.True(t, needInit)
712+
assert.False(t, initialized)
713713

714714
crw := &cosiapi.BucketClaim{}
715715
err = c.Get(ctx, readWriteClaimNsName, crw)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
github.com/go-logr/logr v1.4.3
1414
github.com/stretchr/testify v1.11.1
1515
google.golang.org/grpc v1.75.1
16+
k8s.io/api v0.34.2
1617
k8s.io/apimachinery v0.34.2
1718
k8s.io/client-go v0.34.2
1819
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d
@@ -99,7 +100,6 @@ require (
99100
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
100101
gopkg.in/inf.v0 v0.9.1 // indirect
101102
gopkg.in/yaml.v3 v3.0.1 // indirect
102-
k8s.io/api v0.34.2 // indirect
103103
k8s.io/apiextensions-apiserver v0.34.1 // indirect
104104
k8s.io/apiserver v0.34.1 // indirect
105105
k8s.io/component-base v0.34.1 // indirect
Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,18 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
// Package handoff defines logic needed for handing off control of resources between Controller and
18-
// Sidecar.
19-
package handoff
17+
// Package bucketaccess defines logic for BucketAccess resources needed by both Controller and
18+
// Sidecar. BucketAccesses need to be handed off between the two, and some logic is shared.
19+
package bucketaccess
2020

2121
import (
22+
"fmt"
23+
2224
cosiapi "sigs.k8s.io/container-object-storage-interface/client/apis/objectstorage/v1alpha2"
2325
)
2426

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+
// ManagedBySidecar returns true if a BucketAccess should be managed by the Sidecar.
28+
// It returns false if it should be managed by the Controller instead.
2729
//
2830
// In order for COSI Controller and any given Sidecar to work well together, they should avoid
2931
// managing the same BucketAccess resource at the same time. This will help prevent the Controller
@@ -39,19 +41,19 @@ import (
3941
// 2. Sidecar version low, Controller version high
4042
// 3. Sidecar version high, Controller version low
4143
// 4. Sidecar version high, Controller version high
42-
func BucketAccessManagedBySidecar(ba *cosiapi.BucketAccess) bool {
44+
func ManagedBySidecar(ba *cosiapi.BucketAccess) bool {
4345
// Allow a future-compatible mechanism by which the Controller can override the normal
4446
// BucketAccess management handoff logic in order to resolve a bug.
4547
// Instances where this is utilized should be infrequent -- ideally, never used.
4648
if _, ok := ba.Annotations[cosiapi.ControllerManagementOverrideAnnotation]; ok {
4749
return false
4850
}
4951

50-
// During provisioning, there are several status fields that the Controller needs to set before
51-
// the Sidecar can provision an access. However, tying this function's logic to ALL of the
52-
// status items could make long-term Controller-Sidecar handoff logic fragile. More logic means
53-
// more risk of unmanaged resources and more difficulty reasoning about how changes will impact
54-
// ownership during version skew. Minimize risk by relying on a single determining status field.
52+
// During provisioning, there are several status fields that the Controller needs to initialize
53+
// before the Sidecar can provision an access. However, tying this function's logic to ALL of
54+
// the status items could make long-term Controller-Sidecar handoff logic fragile. More logic
55+
// means more risk of unmanaged resources and more difficulty reasoning about how changes will
56+
// impact ownership during version skew. Minimize risk by relying on a single status field.
5557
if ba.Status.DriverName == "" {
5658
return false
5759
}
@@ -66,3 +68,40 @@ func BucketAccessManagedBySidecar(ba *cosiapi.BucketAccess) bool {
6668

6769
return true
6870
}
71+
72+
// SidecarRequirementsPresent verifies that BucketAccess status information required by the Sidecar
73+
// to provision the BucketAccess is fully set.
74+
//
75+
// Return true if the fields needed by the Sidecar are all set (Controller initialization finished).
76+
// Return false if the fields needed by the Sidecar are all unset (needs Controller initialization).
77+
// Return an error if required info is only partially set, indicating some sort of degradation/bug.
78+
//
79+
// Do not use this function to determine whether a BucketAccess should be managed by the Sidecar or
80+
// Controller, or whether handoff has occurred. Use ManagedBySidecar() for that purpose instead.
81+
// This function is appropriate for use within a controller to check requirements before/after
82+
// initialization/provisioning.
83+
func SidecarRequirementsPresent(s *cosiapi.BucketAccessStatus) (bool, error) {
84+
requiredFields := map[string]bool{}
85+
set := []string{}
86+
87+
requiredFieldIsSet := func(fieldName string, isSet bool) {
88+
requiredFields[fieldName] = isSet
89+
if isSet {
90+
set = append(set, fieldName)
91+
}
92+
}
93+
94+
requiredFieldIsSet("status.accessedBuckets", len(s.AccessedBuckets) > 0)
95+
requiredFieldIsSet("status.driverName", s.DriverName != "")
96+
requiredFieldIsSet("status.authenticationType", string(s.AuthenticationType) != "")
97+
98+
if len(set) == 0 {
99+
return false, nil
100+
}
101+
102+
if len(set) == len(requiredFields) {
103+
return true, nil
104+
}
105+
106+
return false, fmt.Errorf("fields required for sidecar provisioning are only partially set: %v", requiredFields)
107+
}

internal/handoff/handoff_test.go renamed to internal/bucketaccess/bucketaccess_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
package handoff
17+
package bucketaccess
1818

1919
import (
2020
"testing"
@@ -27,7 +27,7 @@ import (
2727
cosiapi "sigs.k8s.io/container-object-storage-interface/client/apis/objectstorage/v1alpha2"
2828
)
2929

30-
func TestBucketAccessManagedBySidecar(t *testing.T) {
30+
func TestManagedBySidecar(t *testing.T) {
3131
tests := []struct {
3232
name string // description of this test case
3333
// input parameters for target function.
@@ -125,8 +125,8 @@ func TestBucketAccessManagedBySidecar(t *testing.T) {
125125
if tt.isHandedOffToSidecar {
126126
copy.Status.AccessedBuckets = []cosiapi.AccessedBucket{
127127
{
128-
BucketName: "bc-asdfgh",
129-
AccessMode: cosiapi.BucketAccessModeReadWrite,
128+
BucketName: "bc-asdfgh",
129+
BucketClaimName: "bc-1",
130130
},
131131
}
132132
copy.Status.DriverName = "some.driver.io"
@@ -142,12 +142,12 @@ func TestBucketAccessManagedBySidecar(t *testing.T) {
142142
copy.Annotations[cosiapi.SidecarCleanupFinishedAnnotation] = ""
143143
}
144144

145-
got := BucketAccessManagedBySidecar(copy)
145+
got := ManagedBySidecar(copy)
146146
assert.Equal(t, tt.want, got)
147147

148148
// for all cases,applying the controller override annotation makes it controller-managed
149149
copy.Annotations[cosiapi.ControllerManagementOverrideAnnotation] = ""
150-
withOverride := BucketAccessManagedBySidecar(copy)
150+
withOverride := ManagedBySidecar(copy)
151151
assert.False(t, withOverride)
152152
})
153153
}

0 commit comments

Comments
 (0)