diff --git a/api/deploy/kubernetes/nfs/csi-provisioner-rbac-cr.yaml b/api/deploy/kubernetes/nfs/csi-provisioner-rbac-cr.yaml new file mode 100644 index 00000000000..3a67486f4c1 --- /dev/null +++ b/api/deploy/kubernetes/nfs/csi-provisioner-rbac-cr.yaml @@ -0,0 +1,48 @@ +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: nfs-external-provisioner-runner +rules: + - apiGroups: [""] + resources: ["nodes"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list"] + - apiGroups: [""] + resources: ["events"] + verbs: ["list", "watch", "create", "update", "patch"] + - apiGroups: [""] + resources: ["persistentvolumes"] + verbs: ["get", "list", "watch", "create", "update", "delete", "patch"] + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: ["get", "list", "watch", "update"] + - apiGroups: ["storage.k8s.io"] + resources: ["storageclasses"] + verbs: ["get", "list", "watch"] + - apiGroups: ["storage.k8s.io"] + resources: ["volumeattachments"] + verbs: ["get", "list", "watch", "update", "patch"] + - apiGroups: ["storage.k8s.io"] + resources: ["volumeattachments/status"] + verbs: ["patch"] + - apiGroups: [""] + resources: ["persistentvolumeclaims/status"] + verbs: ["update", "patch"] + - apiGroups: ["storage.k8s.io"] + resources: ["csinodes"] + verbs: ["get", "list", "watch"] + - apiGroups: ["snapshot.storage.k8s.io"] + resources: ["volumesnapshotclasses"] + verbs: ["get", "list", "watch"] + - apiGroups: ["snapshot.storage.k8s.io"] + resources: ["volumesnapshotcontents"] + verbs: ["get", "list", "watch", "update", "patch"] + - apiGroups: ["snapshot.storage.k8s.io"] + resources: ["volumesnapshotcontents/status"] + verbs: ["update", "patch"] + - apiGroups: ["snapshot.storage.k8s.io"] + resources: ["volumesnapshots"] + verbs: ["get", "list"] diff --git a/api/deploy/kubernetes/nfs/csi-provisioner-rbac-crb.yaml b/api/deploy/kubernetes/nfs/csi-provisioner-rbac-crb.yaml new file mode 100644 index 00000000000..edb1cb33a1d --- /dev/null +++ b/api/deploy/kubernetes/nfs/csi-provisioner-rbac-crb.yaml @@ -0,0 +1,13 @@ +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: "{{ .ServiceAccount }}-role" +subjects: + - kind: ServiceAccount + name: "{{ .ServiceAccount }}" + namespace: "{{ .Namespace }}" +roleRef: + kind: ClusterRole + name: nfs-external-provisioner-runner + apiGroup: rbac.authorization.k8s.io diff --git a/api/deploy/kubernetes/nfs/csi-provisioner-rbac-r.yaml b/api/deploy/kubernetes/nfs/csi-provisioner-rbac-r.yaml new file mode 100644 index 00000000000..01002ca8ee5 --- /dev/null +++ b/api/deploy/kubernetes/nfs/csi-provisioner-rbac-r.yaml @@ -0,0 +1,14 @@ +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + namespace: "{{ .Namespace }}" + name: nfs-external-provisioner-cfg +rules: + # remove this once we stop supporting v1.0.0 + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "list", "create", "delete"] + - apiGroups: ["coordination.k8s.io"] + resources: ["leases"] + verbs: ["get", "watch", "list", "delete", "update", "create"] diff --git a/api/deploy/kubernetes/nfs/csi-provisioner-rbac-rb.yaml b/api/deploy/kubernetes/nfs/csi-provisioner-rbac-rb.yaml new file mode 100644 index 00000000000..e71b5561a2f --- /dev/null +++ b/api/deploy/kubernetes/nfs/csi-provisioner-rbac-rb.yaml @@ -0,0 +1,14 @@ +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: "{{ .ServiceAccount }}-role-cfg" + namespace: "{{ .Namespace }}" +subjects: + - kind: ServiceAccount + name: "{{ .ServiceAccount }}" + namespace: "{{ .Namespace }}" +roleRef: + kind: Role + name: nfs-external-provisioner-cfg + apiGroup: rbac.authorization.k8s.io diff --git a/api/deploy/kubernetes/nfs/csi-provisioner-rbac.go b/api/deploy/kubernetes/nfs/csi-provisioner-rbac.go new file mode 100644 index 00000000000..ee7ee84730a --- /dev/null +++ b/api/deploy/kubernetes/nfs/csi-provisioner-rbac.go @@ -0,0 +1,220 @@ +/* +Copyright 2024 The Ceph-CSI Authors. + +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 nfs + +import ( + "bytes" + _ "embed" + "fmt" + "text/template" + "strings" + + "github.com/ghodss/yaml" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + + "github.com/ceph/ceph-csi/api/deploy/kubernetes" +) + +//go:embed csi-provisioner-rbac-cr.yaml +var csiProvisionerClusterRole string + +//go:embed csi-provisioner-rbac-crb.yaml +var csiProvisionerClusterRoleBinding string + +//go:embed csi-provisioner-rbac-r.yaml +var csiProvisionerRole string + +//go:embed csi-provisioner-rbac-rb.yaml +var csiProvisionerRoleBinding string + +var CSIProvisionerRBACDefaults = kubernetes.CSIProvisionerRBACValues{ + Namespace: "default", + ServiceAccount: "nfs-csi-provisioner", +} + +// NewCSIProvisionerRBAC takes a driver name from the CSIProvisionerRBACValues +// struct and replaces the value in the template. A CSIProvisionerRBAC object +// is returned which can be used to create permissions for the provisioner in +// the Kubernetes cluster. +func NewCSIProvisionerRBAC(values kubernetes.CSIProvisionerRBACValues) (kubernetes.CSIProvisionerRBAC, error) { + sa := &corev1.ServiceAccount{} + sa.Namespace = values.Namespace + sa.Name = values.ServiceAccount + + cr, err := newClusterRole(values) + if err != nil { + return nil, err + } + + crb, err := newClusterRoleBinding(values) + if err != nil { + return nil, err + } + + r, err := newRole(values) + if err != nil { + return nil, err + } + + rb, err := newRoleBinding(values) + if err != nil { + return nil, err + } + + return &csiProvisionerRBAC{ + serviceAccount: sa, + clusterRole: cr, + clusterRoleBinding: crb, + role: r, + roleBinding: rb, + }, nil +} + +func NewCSIProvisionerRBACYAML(values kubernetes.CSIProvisionerRBACValues) (string, error) { + docs := []string{} + + data, err := newYAML("csiProvisionerClusterRole", csiProvisionerClusterRole, values) + if err != nil { + return "", err + } + docs = append(docs, data) + + data, err = newYAML("csiProvisionerClusterRoleBinding", csiProvisionerClusterRoleBinding, values) + if err != nil { + return "", err + } + docs = append(docs, data) + + data, err = newYAML("csiProvisionerRole", csiProvisionerRole, values) + if err != nil { + return "", err + } + docs = append(docs, data) + + data, err = newYAML("csiProvisionerRoleBinding", csiProvisionerRoleBinding, values) + if err != nil { + return "", err + } + docs = append(docs, data) + + return strings.Join(docs, "\n"), nil +} + + +func newYAML(name, data string, values kubernetes.CSIProvisionerRBACValues) (string, error) { + var buf bytes.Buffer + + tmpl, err := template.New(name).Parse(data) + if err != nil { + return "", fmt.Errorf("failed to parse template: %w", err) + } + err = tmpl.Execute(&buf, values) + if err != nil { + return "", fmt.Errorf("failed to replace values in template: %w", err) + } + + return buf.String(), nil +} + +func newClusterRole(values kubernetes.CSIProvisionerRBACValues) (*rbacv1.ClusterRole, error) { + data, err := newYAML("csiProvisionerClusterRole", csiProvisionerClusterRole, values) + if err != nil { + return nil, err + } + + cr := &rbacv1.ClusterRole{} + err = yaml.Unmarshal([]byte(data), cr) + if err != nil { + return nil, fmt.Errorf("failed convert YAML to %T: %w", cr, err) + } + + return cr, nil +} + +func newClusterRoleBinding(values kubernetes.CSIProvisionerRBACValues) (*rbacv1.ClusterRoleBinding, error) { + data, err := newYAML("csiProvisionerClusterRoleBinding", csiProvisionerClusterRoleBinding, values) + if err != nil { + return nil, err + } + + crb := &rbacv1.ClusterRoleBinding{} + err = yaml.Unmarshal([]byte(data), crb) + if err != nil { + return nil, fmt.Errorf("failed convert YAML to %T: %w", crb, err) + } + + return crb, nil +} + +func newRole(values kubernetes.CSIProvisionerRBACValues) (*rbacv1.Role, error) { + data, err := newYAML("csiProvisionerRole", csiProvisionerRole, values) + if err != nil { + return nil, err + } + + r := &rbacv1.Role{} + err = yaml.Unmarshal([]byte(data), r) + if err != nil { + return nil, fmt.Errorf("failed convert YAML to %T: %w", r, err) + } + + return r, nil +} + +func newRoleBinding(values kubernetes.CSIProvisionerRBACValues) (*rbacv1.RoleBinding, error) { + data, err := newYAML("csiProvisionerRoleBinding", csiProvisionerRoleBinding, values) + if err != nil { + return nil, err + } + + rb := &rbacv1.RoleBinding{} + err = yaml.Unmarshal([]byte(data), rb) + if err != nil { + return nil, fmt.Errorf("failed convert YAML to %T: %w", rb, err) + } + + return rb, nil +} + +type csiProvisionerRBAC struct { + serviceAccount *corev1.ServiceAccount + clusterRole *rbacv1.ClusterRole + clusterRoleBinding *rbacv1.ClusterRoleBinding + role *rbacv1.Role + roleBinding *rbacv1.RoleBinding +} + +func (rbac *csiProvisionerRBAC) GetServiceAccount() *corev1.ServiceAccount { + return rbac.serviceAccount +} + +func (rbac *csiProvisionerRBAC) GetClusterRole() *rbacv1.ClusterRole { + return rbac.clusterRole +} + +func (rbac *csiProvisionerRBAC) GetClusterRoleBinding() *rbacv1.ClusterRoleBinding { + return rbac.clusterRoleBinding +} + +func (rbac *csiProvisionerRBAC) GetRole() *rbacv1.Role { + return rbac.role +} + +func (rbac *csiProvisionerRBAC) GetRoleBinding() *rbacv1.RoleBinding { + return rbac.roleBinding +} diff --git a/api/deploy/kubernetes/nfs/csi-provisioner-rbac_test.go b/api/deploy/kubernetes/nfs/csi-provisioner-rbac_test.go new file mode 100644 index 00000000000..ace94a03fd4 --- /dev/null +++ b/api/deploy/kubernetes/nfs/csi-provisioner-rbac_test.go @@ -0,0 +1,67 @@ +/* +Copyright 2024 The Ceph-CSI Authors. + +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 nfs + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewCSIProvisionerRBAC(t *testing.T) { + rbac, err := NewCSIProvisionerRBAC(CSIProvisionerRBACDefaults) + + require.NoError(t, err) + require.NotNil(t, rbac) + require.Equal(t, rbac.GetServiceAccount().Namespace, CSIProvisionerRBACDefaults.Namespace) + require.Equal(t, rbac.GetServiceAccount().Name, CSIProvisionerRBACDefaults.ServiceAccount) +} + +func TestNewCSIProvisionerRBACYAML(t *testing.T) { + yaml, err := NewCSIProvisionerRBACYAML(CSIProvisionerRBACDefaults) + + require.NoError(t, err) + require.NotEqual(t, "", yaml) +} + +func TestNewClusterRole(t *testing.T) { + cr, err := newClusterRole(CSIProvisionerRBACDefaults) + + require.NoError(t, err) + require.NotNil(t, cr) +} + +func TestNewClusterRoleBinding(t *testing.T) { + crb, err := newClusterRoleBinding(CSIProvisionerRBACDefaults) + + require.NoError(t, err) + require.NotNil(t, crb) +} + +func TestNewRole(t *testing.T) { + r, err := newRole(CSIProvisionerRBACDefaults) + + require.NoError(t, err) + require.NotNil(t, r) +} + +func TestNewRoleBinding(t *testing.T) { + rb, err := newRoleBinding(CSIProvisionerRBACDefaults) + + require.NoError(t, err) + require.NotNil(t, rb) +} diff --git a/api/deploy/kubernetes/provisioner.go b/api/deploy/kubernetes/provisioner.go new file mode 100644 index 00000000000..cad2116fd84 --- /dev/null +++ b/api/deploy/kubernetes/provisioner.go @@ -0,0 +1,39 @@ +/* +Copyright 2024 The Ceph-CSI Authors. + +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 kubernetes + +import ( + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" +) + +// CSIProvisionerRBAC describes the interface that is provided by different +// provisioner backends to get details about the required RBAC. +type CSIProvisionerRBAC interface { + GetServiceAccount() *corev1.ServiceAccount + GetClusterRole() *rbacv1.ClusterRole + GetClusterRoleBinding() *rbacv1.ClusterRoleBinding + GetRole() *rbacv1.Role + GetRoleBinding() *rbacv1.RoleBinding +} + +// CSIProvisionerRBACValues contains values that can be passed to +// NewCSIProvisionerRBAC() functions for different provisioner backends. +type CSIProvisionerRBACValues struct { + Namespace string + ServiceAccount string +}