diff --git a/api/csiaddons/v1alpha1/csiaddonsnode_types.go b/api/csiaddons/v1alpha1/csiaddonsnode_types.go index 9db238ca9..36e7a5b30 100644 --- a/api/csiaddons/v1alpha1/csiaddonsnode_types.go +++ b/api/csiaddons/v1alpha1/csiaddonsnode_types.go @@ -74,6 +74,9 @@ type CSIAddonsNodeStatus struct { // for machine parsing and tidy display in the CLI. // +optional Reason string `json:"reason,omitempty"` + + // A list of capabilities advertised by the sidecar + Capabilities []string `json:"capabilities,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/csiaddons/v1alpha1/zz_generated.deepcopy.go b/api/csiaddons/v1alpha1/zz_generated.deepcopy.go index 917230e32..afd8974f9 100644 --- a/api/csiaddons/v1alpha1/zz_generated.deepcopy.go +++ b/api/csiaddons/v1alpha1/zz_generated.deepcopy.go @@ -32,7 +32,7 @@ func (in *CSIAddonsNode) DeepCopyInto(out *CSIAddonsNode) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) out.Spec = in.Spec - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CSIAddonsNode. @@ -119,6 +119,11 @@ func (in *CSIAddonsNodeSpec) DeepCopy() *CSIAddonsNodeSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CSIAddonsNodeStatus) DeepCopyInto(out *CSIAddonsNodeStatus) { *out = *in + if in.Capabilities != nil { + in, out := &in.Capabilities, &out.Capabilities + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CSIAddonsNodeStatus. diff --git a/config/crd/bases/csiaddons.openshift.io_csiaddonsnodes.yaml b/config/crd/bases/csiaddons.openshift.io_csiaddonsnodes.yaml index 1d258dc17..6367a25ad 100644 --- a/config/crd/bases/csiaddons.openshift.io_csiaddonsnodes.yaml +++ b/config/crd/bases/csiaddons.openshift.io_csiaddonsnodes.yaml @@ -94,6 +94,11 @@ spec: status: description: CSIAddonsNodeStatus defines the observed state of CSIAddonsNode properties: + capabilities: + description: A list of capabilities advertised by the sidecar + items: + type: string + type: array message: description: |- Message is a human-readable message indicating details about why the CSIAddonsNode diff --git a/deploy/controller/crds.yaml b/deploy/controller/crds.yaml index f5cb3cf37..b7b56bab3 100644 --- a/deploy/controller/crds.yaml +++ b/deploy/controller/crds.yaml @@ -93,6 +93,11 @@ spec: status: description: CSIAddonsNodeStatus defines the observed state of CSIAddonsNode properties: + capabilities: + description: A list of capabilities advertised by the sidecar + items: + type: string + type: array message: description: |- Message is a human-readable message indicating details about why the CSIAddonsNode diff --git a/internal/controller/csiaddons/csiaddonsnode_controller.go b/internal/controller/csiaddons/csiaddonsnode_controller.go index f5a4cd862..33747ef5b 100644 --- a/internal/controller/csiaddons/csiaddonsnode_controller.go +++ b/internal/controller/csiaddons/csiaddonsnode_controller.go @@ -27,6 +27,7 @@ import ( csiaddonsv1alpha1 "github.com/csi-addons/kubernetes-csi-addons/api/csiaddons/v1alpha1" "github.com/csi-addons/kubernetes-csi-addons/internal/connection" "github.com/csi-addons/kubernetes-csi-addons/internal/util" + "github.com/csi-addons/spec/lib/go/identity" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" @@ -142,6 +143,7 @@ func (r *CSIAddonsNodeReconciler) Reconcile(ctx context.Context, req ctrl.Reques csiAddonsNode.Status.State = csiaddonsv1alpha1.CSIAddonsNodeStateConnected csiAddonsNode.Status.Message = "Successfully established connection with sidecar" + csiAddonsNode.Status.Capabilities = parseCapabilities(newConn.Capabilities) err = r.Client.Status().Update(ctx, csiAddonsNode) if err != nil { logger.Error(err, "Failed to update status") @@ -271,3 +273,23 @@ func validateCSIAddonsNodeSpec(csiaddonsnode *csiaddonsv1alpha1.CSIAddonsNode) e return nil } + +// parseCapabilities returns a list of capabilities in the format +// capability.Type +// e.g. A cap.String with value "service:{type:NODE_SERVICE}" +// Will be parsed and returned as "service.NODE_SERVICE" +func parseCapabilities(caps []*identity.Capability) []string { + if len(caps) == 0 { + return []string{} + } + + capabilities := make([]string, len(caps)) + + for i, cap := range caps { + capStr := strings.ReplaceAll(cap.String(), ":{type:", ".") + capStr = strings.ReplaceAll(capStr, "}", "") + capabilities[i] = capStr + } + + return capabilities +} diff --git a/internal/controller/csiaddons/csiaddonsnode_controller_test.go b/internal/controller/csiaddons/csiaddonsnode_controller_test.go index d1c4110a1..a177376e6 100644 --- a/internal/controller/csiaddons/csiaddonsnode_controller_test.go +++ b/internal/controller/csiaddons/csiaddonsnode_controller_test.go @@ -20,6 +20,8 @@ import ( "errors" "testing" + "github.com/csi-addons/spec/lib/go/identity" + "github.com/stretchr/testify/assert" ) @@ -42,3 +44,77 @@ func TestParseEndpoint(t *testing.T) { _, _, _, err = parseEndpoint("pod://pod.ns.cluster.local:5678") assert.Error(t, err) } + +func TestParseCapabilities(t *testing.T) { + tests := []struct { + name string + caps []*identity.Capability + expected []string + }{ + { + name: "Empty capabilities", + caps: []*identity.Capability{}, + expected: []string{}, + }, + { + name: "Single capability", + caps: []*identity.Capability{ + { + Type: &identity.Capability_Service_{ + Service: &identity.Capability_Service{ + Type: identity.Capability_Service_NODE_SERVICE, + }, + }, + }, + }, + expected: []string{"service.NODE_SERVICE"}, + }, + { + name: "Multiple capabilities", + caps: []*identity.Capability{ + { + Type: &identity.Capability_Service_{ + Service: &identity.Capability_Service{ + Type: identity.Capability_Service_NODE_SERVICE, + }, + }, + }, + { + Type: &identity.Capability_ReclaimSpace_{ + ReclaimSpace: &identity.Capability_ReclaimSpace{ + Type: identity.Capability_ReclaimSpace_ONLINE, + }, + }, + }, + }, + expected: []string{"service.NODE_SERVICE", "reclaim_space.ONLINE"}, + }, + { + name: "Same capability with different types", + caps: []*identity.Capability{ + { + Type: &identity.Capability_ReclaimSpace_{ + ReclaimSpace: &identity.Capability_ReclaimSpace{ + Type: identity.Capability_ReclaimSpace_ONLINE, + }, + }, + }, + { + Type: &identity.Capability_ReclaimSpace_{ + ReclaimSpace: &identity.Capability_ReclaimSpace{ + Type: identity.Capability_ReclaimSpace_OFFLINE, + }, + }, + }, + }, + expected: []string{"reclaim_space.ONLINE", "reclaim_space.OFFLINE"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := parseCapabilities(tt.caps) + assert.Equal(t, tt.expected, result) + }) + } +}