From 3c61ead736c6723452e45d1c96c4aa98bbd41c57 Mon Sep 17 00:00:00 2001 From: "Jose A. Rivera" Date: Wed, 17 Aug 2022 11:37:07 -0500 Subject: [PATCH] controllers: add StorageConsumerClient api and controller Signed-off-by: Jose A. Rivera --- PROJECT | 10 + api/v1alpha1/ocsclient_types.go | 88 ++++ api/v1alpha1/zz_generated.deepcopy.go | 121 ++++++ ...client-operator.clusterserviceversion.yaml | 46 +- .../odf.openshift.io_ocsclients.yaml | 85 ++++ .../bases/odf.openshift.io_ocsclients.yaml | 87 ++++ config/crd/kustomization.yaml | 21 + config/crd/kustomizeconfig.yaml | 19 + .../patches/cainjection_in_ocsclients.yaml | 7 + config/crd/patches/webhook_in_ocsclients.yaml | 16 + config/default/kustomization.yaml | 1 + ...client-operator.clusterserviceversion.yaml | 8 +- config/manifests/kustomization.yaml | 1 + config/rbac/ocsclient_editor_role.yaml | 24 ++ config/rbac/ocsclient_viewer_role.yaml | 20 + config/rbac/role.yaml | 26 ++ config/samples/kustomization.yaml | 4 + config/samples/odf_v1alpha1_ocsclient.yaml | 6 + controllers/generate.go | 12 + controllers/ocsclient_controller.go | 395 ++++++++++++++++++ go.mod | 7 +- main.go | 8 + 22 files changed, 1008 insertions(+), 4 deletions(-) create mode 100644 api/v1alpha1/ocsclient_types.go create mode 100644 api/v1alpha1/zz_generated.deepcopy.go create mode 100644 bundle/manifests/odf.openshift.io_ocsclients.yaml create mode 100644 config/crd/bases/odf.openshift.io_ocsclients.yaml create mode 100644 config/crd/kustomization.yaml create mode 100644 config/crd/kustomizeconfig.yaml create mode 100644 config/crd/patches/cainjection_in_ocsclients.yaml create mode 100644 config/crd/patches/webhook_in_ocsclients.yaml create mode 100644 config/rbac/ocsclient_editor_role.yaml create mode 100644 config/rbac/ocsclient_viewer_role.yaml create mode 100644 config/samples/kustomization.yaml create mode 100644 config/samples/odf_v1alpha1_ocsclient.yaml create mode 100644 controllers/ocsclient_controller.go diff --git a/PROJECT b/PROJECT index 8e71f07a0..82b84293c 100644 --- a/PROJECT +++ b/PROJECT @@ -6,4 +6,14 @@ plugins: scorecard.sdk.operatorframework.io/v2: {} projectName: ocs-client-operator repo: github.com/red-hat-storage/ocs-client-operator +resources: +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: openshift.io + group: odf + kind: OcsClient + path: github.com/red-hat-storage/ocs-client-operator/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/v1alpha1/ocsclient_types.go b/api/v1alpha1/ocsclient_types.go new file mode 100644 index 000000000..277de927f --- /dev/null +++ b/api/v1alpha1/ocsclient_types.go @@ -0,0 +1,88 @@ +/* +Copyright 2022 Red Hat, Inc. + +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 v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ocsClientPhase string + +const ( + // OcsClientInitializing represents Initializing state of OcsClient + OcsClientInitializing ocsClientPhase = "Initializing" + // OcsClientOnboarding represents Onboarding state of OcsClient + OcsClientOnboarding ocsClientPhase = "Onboarding" + // OcsClientConnected represents Onboarding state of OcsClient + OcsClientConnected ocsClientPhase = "Connected" + // OcsClientUpdating represents Onboarding state of OcsClient + OcsClientUpdating ocsClientPhase = "Updating" + // OcsClientOffboarding represents Onboarding state of OcsClient + OcsClientOffboarding ocsClientPhase = "Offboarding" +) + +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// OcsClientSpec defines the desired state of OcsClient +type OcsClientSpec struct { + // StorageProviderEndpoint holds info to establish connection with the storage providing cluster. + StorageProviderEndpoint string `json:"storageProviderEndpoint"` + + // OnboardingTicket holds an identity information required for consumer to onboard. + OnboardingTicket string `json:"onboardingTicket"` + + // RequestedCapacity Will define the desired capacity requested by a consumer cluster. + RequestedCapacity *resource.Quantity `json:"requestedCapacity,omitempty"` +} + +// OcsClientStatus defines the observed state of OcsClient +type OcsClientStatus struct { + Phase ocsClientPhase `json:"phase,omitempty"` + + // GrantedCapacity Will report the actual capacity + // granted to the consumer cluster by the provider cluster. + GrantedCapacity resource.Quantity `json:"grantedCapacity,omitempty"` + + // ConsumerID will hold the identity of this cluster inside the attached provider cluster + ConsumerID string `json:"id,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// OcsClient is the Schema for the ocsclients API +type OcsClient struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec OcsClientSpec `json:"spec,omitempty"` + Status OcsClientStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// OcsClientList contains a list of OcsClient +type OcsClientList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []OcsClient `json:"items"` +} + +func init() { + SchemeBuilder.Register(&OcsClient{}, &OcsClientList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 000000000..aefc651d9 --- /dev/null +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,121 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2022 Red Hat, Inc. + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OcsClient) DeepCopyInto(out *OcsClient) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OcsClient. +func (in *OcsClient) DeepCopy() *OcsClient { + if in == nil { + return nil + } + out := new(OcsClient) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OcsClient) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OcsClientList) DeepCopyInto(out *OcsClientList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]OcsClient, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OcsClientList. +func (in *OcsClientList) DeepCopy() *OcsClientList { + if in == nil { + return nil + } + out := new(OcsClientList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OcsClientList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OcsClientSpec) DeepCopyInto(out *OcsClientSpec) { + *out = *in + if in.RequestedCapacity != nil { + in, out := &in.RequestedCapacity, &out.RequestedCapacity + x := (*in).DeepCopy() + *out = &x + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OcsClientSpec. +func (in *OcsClientSpec) DeepCopy() *OcsClientSpec { + if in == nil { + return nil + } + out := new(OcsClientSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OcsClientStatus) DeepCopyInto(out *OcsClientStatus) { + *out = *in + out.GrantedCapacity = in.GrantedCapacity.DeepCopy() +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OcsClientStatus. +func (in *OcsClientStatus) DeepCopy() *OcsClientStatus { + if in == nil { + return nil + } + out := new(OcsClientStatus) + in.DeepCopyInto(out) + return out +} diff --git a/bundle/manifests/ocs-client-operator.clusterserviceversion.yaml b/bundle/manifests/ocs-client-operator.clusterserviceversion.yaml index 838a9a49c..8f5933103 100644 --- a/bundle/manifests/ocs-client-operator.clusterserviceversion.yaml +++ b/bundle/manifests/ocs-client-operator.clusterserviceversion.yaml @@ -2,7 +2,17 @@ apiVersion: operators.coreos.com/v1alpha1 kind: ClusterServiceVersion metadata: annotations: - alm-examples: '[]' + alm-examples: |- + [ + { + "apiVersion": "odf.openshift.io/v1alpha1", + "kind": "OcsClient", + "metadata": { + "name": "ocsclient-sample" + }, + "spec": null + } + ] capabilities: Basic Install olm.skipRange: "" operators.operatorframework.io/builder: operator-sdk-v1.19.0+git @@ -12,7 +22,13 @@ metadata: namespace: placeholder spec: apiservicedefinitions: {} - customresourcedefinitions: {} + customresourcedefinitions: + owned: + - description: OcsClient is the Schema for the ocsclients API + displayName: Ocs Client + kind: OcsClient + name: ocsclients.odf.openshift.io + version: v1alpha1 description: foo displayName: OpenShift Data Foundation Client Operator icon: @@ -22,6 +38,32 @@ spec: spec: clusterPermissions: - rules: + - apiGroups: + - odf.openshift.io + resources: + - ocsclients + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - odf.openshift.io + resources: + - ocsclients/finalizers + verbs: + - update + - apiGroups: + - odf.openshift.io + resources: + - ocsclients/status + verbs: + - get + - patch + - update - apiGroups: - authentication.k8s.io resources: diff --git a/bundle/manifests/odf.openshift.io_ocsclients.yaml b/bundle/manifests/odf.openshift.io_ocsclients.yaml new file mode 100644 index 000000000..1cca2b02f --- /dev/null +++ b/bundle/manifests/odf.openshift.io_ocsclients.yaml @@ -0,0 +1,85 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: ocsclients.odf.openshift.io +spec: + group: odf.openshift.io + names: + kind: OcsClient + listKind: OcsClientList + plural: ocsclients + singular: ocsclient + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: OcsClient is the Schema for the ocsclients API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: OcsClientSpec defines the desired state of OcsClient + properties: + onboardingTicket: + description: OnboardingTicket holds an identity information required + for consumer to onboard. + type: string + requestedCapacity: + anyOf: + - type: integer + - type: string + description: RequestedCapacity Will define the desired capacity requested + by a consumer cluster. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + storageProviderEndpoint: + description: StorageProviderEndpoint holds info to establish connection + with the storage providing cluster. + type: string + required: + - onboardingTicket + - storageProviderEndpoint + type: object + status: + description: OcsClientStatus defines the observed state of OcsClient + properties: + grantedCapacity: + anyOf: + - type: integer + - type: string + description: GrantedCapacity Will report the actual capacity granted + to the consumer cluster by the provider cluster. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + id: + description: ConsumerID will hold the identity of this cluster inside + the attached provider cluster + type: string + phase: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/bases/odf.openshift.io_ocsclients.yaml b/config/crd/bases/odf.openshift.io_ocsclients.yaml new file mode 100644 index 000000000..b9cf03d21 --- /dev/null +++ b/config/crd/bases/odf.openshift.io_ocsclients.yaml @@ -0,0 +1,87 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: ocsclients.odf.openshift.io +spec: + group: odf.openshift.io + names: + kind: OcsClient + listKind: OcsClientList + plural: ocsclients + singular: ocsclient + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: OcsClient is the Schema for the ocsclients API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: OcsClientSpec defines the desired state of OcsClient + properties: + onboardingTicket: + description: OnboardingTicket holds an identity information required + for consumer to onboard. + type: string + requestedCapacity: + anyOf: + - type: integer + - type: string + description: RequestedCapacity Will define the desired capacity requested + by a consumer cluster. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + storageProviderEndpoint: + description: StorageProviderEndpoint holds info to establish connection + with the storage providing cluster. + type: string + required: + - onboardingTicket + - storageProviderEndpoint + type: object + status: + description: OcsClientStatus defines the observed state of OcsClient + properties: + grantedCapacity: + anyOf: + - type: integer + - type: string + description: GrantedCapacity Will report the actual capacity granted + to the consumer cluster by the provider cluster. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + id: + description: ConsumerID will hold the identity of this cluster inside + the attached provider cluster + type: string + phase: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml new file mode 100644 index 000000000..2997fd438 --- /dev/null +++ b/config/crd/kustomization.yaml @@ -0,0 +1,21 @@ +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/odf.openshift.io_ocsclients.yaml +#+kubebuilder:scaffold:crdkustomizeresource + +patchesStrategicMerge: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +#- patches/webhook_in_ocsclients.yaml +#+kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- patches/cainjection_in_ocsclients.yaml +#+kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +- kustomizeconfig.yaml diff --git a/config/crd/kustomizeconfig.yaml b/config/crd/kustomizeconfig.yaml new file mode 100644 index 000000000..ec5c150a9 --- /dev/null +++ b/config/crd/kustomizeconfig.yaml @@ -0,0 +1,19 @@ +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/config/crd/patches/cainjection_in_ocsclients.yaml b/config/crd/patches/cainjection_in_ocsclients.yaml new file mode 100644 index 000000000..1e47f6bb0 --- /dev/null +++ b/config/crd/patches/cainjection_in_ocsclients.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: ocsclients.odf.openshift.io diff --git a/config/crd/patches/webhook_in_ocsclients.yaml b/config/crd/patches/webhook_in_ocsclients.yaml new file mode 100644 index 000000000..b977db673 --- /dev/null +++ b/config/crd/patches/webhook_in_ocsclients.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: ocsclients.odf.openshift.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index f1740bf03..b824a8088 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -43,6 +43,7 @@ patchesStrategicMerge: apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: +- ../crd - ../rbac - ../manager images: diff --git a/config/manifests/bases/ocs-client-operator.clusterserviceversion.yaml b/config/manifests/bases/ocs-client-operator.clusterserviceversion.yaml index 0bcf265f8..97251cf1b 100644 --- a/config/manifests/bases/ocs-client-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/ocs-client-operator.clusterserviceversion.yaml @@ -8,7 +8,13 @@ metadata: namespace: placeholder spec: apiservicedefinitions: {} - customresourcedefinitions: {} + customresourcedefinitions: + owned: + - description: OcsClient is the Schema for the ocsclients API + displayName: Ocs Client + kind: OcsClient + name: ocsclients.odf.openshift.io + version: v1alpha1 description: foo displayName: OpenShift Data Foundation Client icon: diff --git a/config/manifests/kustomization.yaml b/config/manifests/kustomization.yaml index d130fabae..cfa5fbdfa 100644 --- a/config/manifests/kustomization.yaml +++ b/config/manifests/kustomization.yaml @@ -3,6 +3,7 @@ resources: - bases - ../default +- ../samples - ../scorecard # [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix. diff --git a/config/rbac/ocsclient_editor_role.yaml b/config/rbac/ocsclient_editor_role.yaml new file mode 100644 index 000000000..a2a39ffa1 --- /dev/null +++ b/config/rbac/ocsclient_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit ocsclients. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: ocsclient-editor-role +rules: +- apiGroups: + - odf.openshift.io + resources: + - ocsclients + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - odf.openshift.io + resources: + - ocsclients/status + verbs: + - get diff --git a/config/rbac/ocsclient_viewer_role.yaml b/config/rbac/ocsclient_viewer_role.yaml new file mode 100644 index 000000000..f6faf346a --- /dev/null +++ b/config/rbac/ocsclient_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view ocsclients. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: ocsclient-viewer-role +rules: +- apiGroups: + - odf.openshift.io + resources: + - ocsclients + verbs: + - get + - list + - watch +- apiGroups: + - odf.openshift.io + resources: + - ocsclients/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index e74f87a94..0f19f88ff 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -6,3 +6,29 @@ metadata: creationTimestamp: null name: manager-role rules: +- apiGroups: + - odf.openshift.io + resources: + - ocsclients + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - odf.openshift.io + resources: + - ocsclients/finalizers + verbs: + - update +- apiGroups: + - odf.openshift.io + resources: + - ocsclients/status + verbs: + - get + - patch + - update diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml new file mode 100644 index 000000000..147cd5fb4 --- /dev/null +++ b/config/samples/kustomization.yaml @@ -0,0 +1,4 @@ +## Append samples you want in your CSV to this file as resources ## +resources: +- odf_v1alpha1_ocsclient.yaml +#+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/odf_v1alpha1_ocsclient.yaml b/config/samples/odf_v1alpha1_ocsclient.yaml new file mode 100644 index 000000000..2cca830e8 --- /dev/null +++ b/config/samples/odf_v1alpha1_ocsclient.yaml @@ -0,0 +1,6 @@ +apiVersion: odf.openshift.io/v1alpha1 +kind: OcsClient +metadata: + name: ocsclient-sample +spec: + # TODO(user): Add fields here diff --git a/controllers/generate.go b/controllers/generate.go index 3284e6b45..64fa254a8 100644 --- a/controllers/generate.go +++ b/controllers/generate.go @@ -15,3 +15,15 @@ limitations under the License. */ package controllers + +import ( + "fmt" +) + +func generateNameForCephFilesystemSC(name string) string { + return fmt.Sprintf("%s-cephfs", name) +} + +func generateNameForCephBlockPoolSC(name string) string { + return fmt.Sprintf("%s-ceph-rbd", name) +} diff --git a/controllers/ocsclient_controller.go b/controllers/ocsclient_controller.go new file mode 100644 index 000000000..e1112d5ef --- /dev/null +++ b/controllers/ocsclient_controller.go @@ -0,0 +1,395 @@ +/* +Copyright 2022 Red Hat, Inc. + +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 controllers + +import ( + "context" + "fmt" + "time" + + v1alpha1 "github.com/red-hat-storage/ocs-client-operator/api/v1alpha1" + + providerClient "github.com/red-hat-storage/ocs-operator/services/provider/client" + + configv1 "github.com/openshift/api/config/v1" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const ( + // grpcCallNames + OnboardConsumer = "OnboardConsumer" + OffboardConsumer = "OffboardConsumer" + UpdateCapacity = "UpdateCapacity" + GetStorageConfig = "GetStorageConfig" + AcknowledgeOnboarding = "AcknowledgeOnboarding" + + ocsClientAnnotation = "odf.openshift.io/ocsclient" + ocsClientFinalizer = "ocsclient.odf.openshift.io" +) + +// OcsClientReconciler reconciles a OcsClient object +type OcsClientReconciler struct { + client.Client + Log klog.Logger + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=odf.openshift.io,resources=ocsclients,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=odf.openshift.io,resources=ocsclients/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=odf.openshift.io,resources=ocsclients/finalizers,verbs=update + +// SetupWithManager sets up the controller with the Manager. +func (r *OcsClientReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.OcsClient{}). + Complete(r) +} + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile +func (r *OcsClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var err error + + r.Log = log.FromContext(ctx, "OcsClient", req) + r.Log.Info("Reconciling OcsClient") + + // Fetch the OcsClient instance + instance := &v1alpha1.OcsClient{} + instance.Name = req.Name + instance.Namespace = req.Namespace + + if err = r.Client.Get(ctx, types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}, instance); err != nil { + if errors.IsNotFound(err) { + r.Log.Info("OcsClient resource not found. Ignoring since object must be deleted.") + return reconcile.Result{}, nil + } + r.Log.Error(err, "Failed to get OcsClient.") + return reconcile.Result{}, err + } + + result, reconcileErr := r.reconcilePhases(instance) + + // Apply status changes to the OcsClient + statusErr := r.Client.Status().Update(ctx, instance) + if statusErr != nil { + r.Log.Info("Failed to update OcsClient status.") + } + + if reconcileErr != nil { + err = reconcileErr + } else if statusErr != nil { + err = statusErr + } + return result, err +} + +func (r *OcsClientReconciler) reconcilePhases(instance *v1alpha1.OcsClient) (ctrl.Result, error) { + externalClusterClient, err := r.newExternalClusterClient(instance) + if err != nil { + return reconcile.Result{}, err + } + defer externalClusterClient.Close() + + // deletion phase + if !instance.GetDeletionTimestamp().IsZero() { + return r.deletionPhase(instance, externalClusterClient) + } + + instance.Status.Phase = v1alpha1.OcsClientInitializing + + // ensure finalizer + if !contains(instance.GetFinalizers(), ocsClientFinalizer) { + r.Log.Info("Finalizer not found for OcsClient. Adding finalizer.", "OcsClient", klog.KRef(instance.Namespace, instance.Name)) + instance.ObjectMeta.Finalizers = append(instance.ObjectMeta.Finalizers, ocsClientFinalizer) + if err := r.Client.Update(context.TODO(), instance); err != nil { + r.Log.Info("Failed to update OcsClient with finalizer.", "OcsClient", klog.KRef(instance.Namespace, instance.Name)) + return reconcile.Result{}, err + } + } + + if instance.Status.ConsumerID == "" { + return r.onboardConsumer(instance, externalClusterClient) + } else if instance.Status.Phase == v1alpha1.OcsClientOnboarding { + return r.acknowledgeOnboarding(instance, externalClusterClient) + } else if !instance.Spec.RequestedCapacity.Equal(instance.Status.GrantedCapacity) { + res, err := r.updateConsumerCapacity(instance, externalClusterClient) + if err != nil || !res.IsZero() { + return res, err + } + } + + return reconcile.Result{}, nil +} + +func (r *OcsClientReconciler) deletionPhase(instance *v1alpha1.OcsClient, externalClusterClient *providerClient.OCSProviderClient) (ctrl.Result, error) { + if contains(instance.GetFinalizers(), ocsClientFinalizer) { + instance.Status.Phase = v1alpha1.OcsClientOffboarding + + if res, err := r.offboardConsumer(instance, externalClusterClient); err != nil { + r.Log.Info("Offboarding in progress.", "Status", err) + //r.recorder.ReportIfNotPresent(instance, corev1.EventTypeWarning, statusutil.EventReasonUninstallPending, err.Error()) + return reconcile.Result{RequeueAfter: time.Second * time.Duration(1)}, nil + } else if !res.IsZero() { + // result is not empty + return res, nil + } + r.Log.Info("removing finalizer from OcsClient.", "OcsClient", klog.KRef(instance.Namespace, instance.Name)) + // Once all finalizers have been removed, the object will be deleted + instance.ObjectMeta.Finalizers = remove(instance.ObjectMeta.Finalizers, ocsClientFinalizer) + if err := r.Client.Update(context.TODO(), instance); err != nil { + r.Log.Info("Failed to remove finalizer from OcsClient", "OcsClient", klog.KRef(instance.Namespace, instance.Name)) + return reconcile.Result{}, err + } + } + r.Log.Info("OcsClient is offboarded", "OcsClient", klog.KRef(instance.Namespace, instance.Name)) + //returnErr := r.SetOperatorConditions("Skipping OcsClient reconciliation", "Terminated", metav1.ConditionTrue, nil) + return reconcile.Result{}, nil +} + +// newExternalClusterClient returns the *providerClient.OCSProviderClient +func (r *OcsClientReconciler) newExternalClusterClient(instance *v1alpha1.OcsClient) (*providerClient.OCSProviderClient, error) { + + ocsProviderClient, err := providerClient.NewProviderClient( + context.Background(), instance.Spec.StorageProviderEndpoint, time.Second*10) + if err != nil { + return nil, err + } + + return ocsProviderClient, nil +} + +// onboardConsumer makes an API call to the external storage provider cluster for onboarding +func (r *OcsClientReconciler) onboardConsumer(instance *v1alpha1.OcsClient, externalClusterClient *providerClient.OCSProviderClient) (reconcile.Result, error) { + + clusterVersion := &configv1.ClusterVersion{} + err := r.Client.Get(context.Background(), types.NamespacedName{Name: "version"}, clusterVersion) + if err != nil { + r.Log.Error(err, "failed to get the clusterVersion version of the OCP cluster") + return reconcile.Result{}, err + } + + name := fmt.Sprintf("ocsclient-%s-%s", instance.Name, clusterVersion.Spec.ClusterID) + response, err := externalClusterClient.OnboardConsumer( + context.Background(), instance.Spec.OnboardingTicket, name, + instance.Spec.RequestedCapacity.String()) + if err != nil { + if s, ok := status.FromError(err); ok { + r.logGrpcErrorAndReportEvent(instance, OnboardConsumer, err, s.Code()) + } + return reconcile.Result{}, err + } + + if response.StorageConsumerUUID == "" || response.GrantedCapacity == "" { + err = fmt.Errorf("storage provider response is empty") + r.Log.Error(err, "empty response") + return reconcile.Result{}, err + } + + instance.Status.ConsumerID = response.StorageConsumerUUID + instance.Status.GrantedCapacity = resource.MustParse(response.GrantedCapacity) + instance.Status.Phase = v1alpha1.OcsClientOnboarding + + r.Log.Info("onboarding complete") + return reconcile.Result{Requeue: true}, nil +} + +func (r *OcsClientReconciler) acknowledgeOnboarding(instance *v1alpha1.OcsClient, externalClusterClient *providerClient.OCSProviderClient) (reconcile.Result, error) { + + _, err := externalClusterClient.AcknowledgeOnboarding(context.Background(), instance.Status.ConsumerID) + if err != nil { + if s, ok := status.FromError(err); ok { + r.logGrpcErrorAndReportEvent(instance, AcknowledgeOnboarding, err, s.Code()) + } + r.Log.Error(err, "External-OCS:Failed to acknowledge onboarding.") + return reconcile.Result{}, err + } + + // claims should be created only once and should not be created/updated again if user deletes/update it. + err = r.createDefaultStorageClassClaims(instance) + if err != nil { + return reconcile.Result{}, err + } + + // instance.Status.Phase = statusutil.PhaseProgressing + + r.Log.Info("External-OCS:Onboarding is acknowledged successfully.") + return reconcile.Result{Requeue: true}, nil +} + +// offboardConsumer makes an API call to the external storage provider cluster for offboarding +func (r *OcsClientReconciler) offboardConsumer(instance *v1alpha1.OcsClient, externalClusterClient *providerClient.OCSProviderClient) (reconcile.Result, error) { + + _, err := externalClusterClient.OffboardConsumer(context.Background(), instance.Status.ConsumerID) + if err != nil { + if s, ok := status.FromError(err); ok { + r.logGrpcErrorAndReportEvent(instance, OffboardConsumer, err, s.Code()) + } + return reconcile.Result{}, err + } + + return reconcile.Result{}, nil +} + +// updateConsumerCapacity makes an API call to the external storage provider cluster to update the capacity +func (r *OcsClientReconciler) updateConsumerCapacity(instance *v1alpha1.OcsClient, externalClusterClient *providerClient.OCSProviderClient) (reconcile.Result, error) { + instance.Status.Phase = v1alpha1.OcsClientUpdating + + response, err := externalClusterClient.UpdateCapacity( + context.Background(), + instance.Status.ConsumerID, + instance.Spec.RequestedCapacity.String()) + if err != nil { + if s, ok := status.FromError(err); ok { + r.logGrpcErrorAndReportEvent(instance, UpdateCapacity, err, s.Code()) + } + return reconcile.Result{}, err + } + + responseQuantity, err := resource.ParseQuantity(response.GrantedCapacity) + if err != nil { + r.Log.Error(err, "Failed to parse GrantedCapacity from UpdateCapacity response.", "GrantedCapacity", response.GrantedCapacity) + return reconcile.Result{}, err + } + + if !instance.Spec.RequestedCapacity.Equal(responseQuantity) { + klog.Warningf("GrantedCapacity is not equal to the RequestedCapacity in the UpdateCapacity response.", + "GrantedCapacity", response.GrantedCapacity, "RequestedCapacity", instance.Spec.RequestedCapacity) + } + + instance.Status.GrantedCapacity = responseQuantity + instance.Status.Phase = v1alpha1.OcsClientConnected + + return reconcile.Result{}, nil +} + +/* +// getExternalConfigFromProvider makes an API call to the external storage provider cluster for json blob +func (r *OcsClientReconciler) getExternalConfigFromProvider( + instance *v1alpha1.OcsClient, externalClusterClient *providerClient.OCSProviderClient) ([]ExternalResource, reconcile.Result, error) { + + response, err := externalClusterClient.GetStorageConfig(context.Background(), instance.Status.ConsumerID) + if err != nil { + if s, ok := status.FromError(err); ok { + r.logGrpcErrorAndReportEvent(instance, GetStorageConfig, err, s.Code()) + + // storage consumer is not ready yet, requeue after some time + if s.Code() == codes.Unavailable { + return nil, reconcile.Result{RequeueAfter: time.Second * 5}, nil + } + } + + return nil, reconcile.Result{}, err + } + + var externalResources []ExternalResource + + for _, eResource := range response.ExternalResource { + + data := map[string]string{} + err = json.Unmarshal(eResource.Data, &data) + if err != nil { + r.Log.Error(err, "Failed to Unmarshal response of GetStorageConfig", "Kind", eResource.Kind, "Name", eResource.Name, "Data", eResource.Data) + return nil, reconcile.Result{}, err + } + + externalResources = append(externalResources, ExternalResource{ + Kind: eResource.Kind, + Data: data, + Name: eResource.Name, + }) + } + + return externalResources, reconcile.Result{}, nil +} +*/ +func (r *OcsClientReconciler) logGrpcErrorAndReportEvent(instance *v1alpha1.OcsClient, grpcCallName string, err error, errCode codes.Code) { + + // var msg, eventReason, eventType string + var msg string + + if grpcCallName == OnboardConsumer { + if errCode == codes.InvalidArgument { + msg = "Token is invalid. Verify the token again or contact the provider admin" + //eventReason = "TokenInvalid" + //eventType = corev1.EventTypeWarning + } else if errCode == codes.AlreadyExists { + msg = "Token is already used. Contact provider admin for a new token" + //eventReason = "TokenAlreadyUsed" + //eventType = corev1.EventTypeWarning + } + } else if grpcCallName == AcknowledgeOnboarding { + if errCode == codes.NotFound { + msg = "StorageConsumer not found. Contact the provider admin" + //eventReason = "NotFound" + //eventType = corev1.EventTypeWarning + } + } else if grpcCallName == OffboardConsumer { + if errCode == codes.InvalidArgument { + msg = "StorageConsumer UID is not valid. Contact the provider admin" + //eventReason = "UIDInvalid" + //eventType = corev1.EventTypeWarning + } + } else if grpcCallName == UpdateCapacity { + if errCode == codes.InvalidArgument { + msg = "StorageConsumer UID or requested capacity is not valid. Contact the provider admin" + //eventReason = "UIDorCapacityInvalid" + //eventType = corev1.EventTypeWarning + } else if errCode == codes.NotFound { + msg = "StorageConsumer UID not found. Contact the provider admin" + //eventReason = "UIDNotFound" + //eventType = corev1.EventTypeWarning + } + } else if grpcCallName == GetStorageConfig { + if errCode == codes.InvalidArgument { + msg = "StorageConsumer UID is not valid. Contact the provider admin" + //eventReason = "UIDInvalid" + //eventType = corev1.EventTypeWarning + } else if errCode == codes.NotFound { + msg = "StorageConsumer UID not found. Contact the provider admin" + //eventReason = "UIDNotFound" + //eventType = corev1.EventTypeWarning + } else if errCode == codes.Unavailable { + msg = "StorageConsumer is not ready yet. Will requeue after 5 second" + //eventReason = "NotReady" + //eventType = corev1.EventTypeNormal + } + } + + if msg != "" { + r.Log.Error(err, "StorageProvider:"+grpcCallName+":"+msg) + // r.recorder.ReportIfNotPresent(instance, eventType, eventReason, msg) + } +} + +// TODO: claims should be created only once and should not be created/updated again if user deletes/update it. +func (r *OcsClientReconciler) createDefaultStorageClassClaims(instance *v1alpha1.OcsClient) error { + + return nil +} diff --git a/go.mod b/go.mod index bcf8417e1..b914d6b54 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,8 @@ replace ( github.com/red-hat-storage/ocs-operator/services/provider => github.com/jarrpa/ocs-operator/services/provider v1.4.12-0.rc2 ) +require github.com/red-hat-storage/ocs-operator/services/provider v1.4.12-0.rc2 + // === Rook hacks === // This tag doesn't exist, but is imported by github.com/portworx/sched-ops. @@ -23,9 +25,12 @@ replace ( require ( github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.18.1 + github.com/openshift/api v0.0.0-20220421141645-441fe135b2fc github.com/stretchr/testify v1.7.0 + google.golang.org/grpc v1.45.0 k8s.io/apimachinery v0.23.6 k8s.io/client-go v12.0.0+incompatible + k8s.io/klog/v2 v2.60.1 sigs.k8s.io/controller-runtime v0.11.2 ) @@ -77,6 +82,7 @@ require ( golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20220207185906-7721543eae58 // indirect google.golang.org/protobuf v1.27.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -86,7 +92,6 @@ require ( k8s.io/api v0.23.6 // indirect k8s.io/apiextensions-apiserver v0.23.5 // indirect k8s.io/component-base v0.23.5 // indirect - k8s.io/klog/v2 v2.60.1 // indirect k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf // indirect k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect diff --git a/main.go b/main.go index 8a8390e95..e2f4186cc 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" apiv1alpha1 "github.com/red-hat-storage/ocs-client-operator/api/v1alpha1" + "github.com/red-hat-storage/ocs-client-operator/controllers" //+kubebuilder:scaffold:imports ) @@ -77,6 +78,13 @@ func main() { os.Exit(1) } + if err = (&controllers.OcsClientReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "OcsClient") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {