diff --git a/buildscripts/zfs-driver/Dockerfile b/buildscripts/zfs-driver/Dockerfile index 1353f5b..19630b9 100644 --- a/buildscripts/zfs-driver/Dockerfile +++ b/buildscripts/zfs-driver/Dockerfile @@ -4,9 +4,9 @@ # FROM ubuntu:18.04 +RUN apt-get clean && rm -rf /var/lib/apt/lists/* RUN apt-get update; exit 0 -RUN apt-get -y install rsyslog libssl-dev xfsprogs -#RUN apt-get clean && rm -rf /var/lib/apt/lists/* +RUN apt-get -y install rsyslog libssl-dev xfsprogs ca-certificates COPY zfs-driver /usr/local/bin/ COPY entrypoint.sh /usr/local/bin/ diff --git a/deploy/zfs-operator.yaml b/deploy/zfs-operator.yaml index 498e71b..ffd8013 100644 --- a/deploy/zfs-operator.yaml +++ b/deploy/zfs-operator.yaml @@ -512,6 +512,9 @@ rules: - apiGroups: [""] resources: ["secrets"] verbs: ["get", "list"] + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["*"] - apiGroups: [""] resources: ["persistentvolumes", "services"] verbs: ["get", "list", "watch", "create", "delete"] @@ -647,6 +650,10 @@ spec: value: unix:///var/lib/csi/sockets/pluginproxy/csi.sock - name: OPENEBS_NAMESPACE value: openebs + - name: OPENEBS_IO_INSTALLER_TYPE + value: "zfs-operator" + - name: OPENEBS_IO_ENABLE_ANALYTICS + value: "true" args : - "--endpoint=$(OPENEBS_CSI_ENDPOINT)" - "--plugin=$(OPENEBS_CONTROLLER_DRIVER)" diff --git a/pkg/client/k8s/v1alpha1/clientset.go b/pkg/client/k8s/v1alpha1/clientset.go new file mode 100644 index 0000000..823a8d0 --- /dev/null +++ b/pkg/client/k8s/v1alpha1/clientset.go @@ -0,0 +1,43 @@ +/* +Copyright 2020 The OpenEBS 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 v1alpha1 + +import ( + "github.com/pkg/errors" + "k8s.io/client-go/kubernetes" +) + +// ClientsetGetter abstracts fetching of kubernetes clientset +type ClientsetGetter interface { + Get() (*kubernetes.Clientset, error) +} + +type clientset struct{} + +// Clientset returns a pointer to clientset struct +func Clientset() *clientset { + return &clientset{} +} + +// Get returns a new instance of kubernetes clientset +func (c *clientset) Get() (*kubernetes.Clientset, error) { + config, err := Config().Get() + if err != nil { + return nil, errors.Wrap(err, "failed to get kubernetes clientset") + } + return kubernetes.NewForConfig(config) +} diff --git a/pkg/client/k8s/v1alpha1/clientset_test.go b/pkg/client/k8s/v1alpha1/clientset_test.go new file mode 100644 index 0000000..75182b5 --- /dev/null +++ b/pkg/client/k8s/v1alpha1/clientset_test.go @@ -0,0 +1,38 @@ +/* +Copyright 2020 The OpenEBS 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 v1alpha1 + +import ( + "testing" +) + +func TestClientsetGet(t *testing.T) { + tests := map[string]struct { + iserr bool + }{ + "101": {true}, + } + + for name, mock := range tests { + t.Run(name, func(t *testing.T) { + _, err := Clientset().Get() + if !mock.iserr && err != nil { + t.Fatalf("Test '%s' failed: expected no error: actual '%s'", name, err) + } + }) + } +} diff --git a/pkg/client/k8s/v1alpha1/config.go b/pkg/client/k8s/v1alpha1/config.go new file mode 100644 index 0000000..74419e0 --- /dev/null +++ b/pkg/client/k8s/v1alpha1/config.go @@ -0,0 +1,99 @@ +/* +Copyright 2020 The OpenEBS 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 v1alpha1 + +import ( + "github.com/openebs/zfs-localpv/pkg/common/env" + "github.com/pkg/errors" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "strings" +) + +// ConfigGetter abstracts fetching of kubernetes client config +type ConfigGetter interface { + Get() (*rest.Config, error) + Name() string +} + +// configFromENV is an implementation of ConfigGetter +type configFromENV struct{} + +// Name returns the name of this config getter instance +func (c *configFromENV) Name() string { + return "k8s-config-from-env" +} + +// Get returns kubernetes rest config based on kubernetes environment values +func (c *configFromENV) Get() (*rest.Config, error) { + k8sMaster := env.Get(env.KubeMaster) + kubeConfig := env.Get(env.KubeConfig) + + if len(strings.TrimSpace(k8sMaster)) == 0 && len(strings.TrimSpace(kubeConfig)) == 0 { + return nil, errors.New("missing kubernetes master as well as kubeconfig: failed to get kubernetes client config") + } + + return clientcmd.BuildConfigFromFlags(k8sMaster, kubeConfig) +} + +// configFromREST is an implementation of ConfigGetter +type configFromREST struct{} + +// Name returns the name of this config getter instance +func (c *configFromREST) Name() string { + return "k8s-config-from-rest" +} + +// Get returns kubernetes rest config based on in-cluster config implementation +func (c *configFromREST) Get() (*rest.Config, error) { + return rest.InClusterConfig() +} + +// ConfigGetters holds a list of ConfigGetter instances +// +// NOTE: +// This is an implementation of ConfigGetter +type ConfigGetters []ConfigGetter + +// Name returns the name of this config getter instance +func (c ConfigGetters) Name() string { + return "list-of-k8s-config-getter" +} + +// Get fetches the kubernetes client config that is used to make kubernetes API +// calls. It makes use of its list of getter instances to fetch kubernetes +// config. +func (c ConfigGetters) Get() (config *rest.Config, err error) { + var errs []error + for _, g := range c { + config, err = g.Get() + if err == nil { + return + } + errs = append(errs, errors.Wrapf(err, "failed to get kubernetes client config via %s", g.Name())) + } + // at this point; all getters have failed + err = errors.Errorf("%+v", errs) + err = errors.Wrap(err, "failed to get kubernetes client config") + return +} + +// Config provides appropriate config getter instances that help in fetching +// kubernetes client config to invoke kubernetes API calls +func Config() ConfigGetter { + return ConfigGetters{&configFromENV{}, &configFromREST{}} +} diff --git a/pkg/client/k8s/v1alpha1/config_test.go b/pkg/client/k8s/v1alpha1/config_test.go new file mode 100644 index 0000000..8314642 --- /dev/null +++ b/pkg/client/k8s/v1alpha1/config_test.go @@ -0,0 +1,74 @@ +/* +Copyright 2020 The OpenEBS 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 v1alpha1 + +import ( + "github.com/openebs/zfs-localpv/pkg/common/env" + "os" + "testing" +) + +// test if configFromENV implements ConfigGetter interface +var _ ConfigGetter = &configFromENV{} + +// test if configFromREST implements ConfigGetter interface +var _ ConfigGetter = &configFromREST{} + +// test if ConfigGetters implements ConfigGetter interface +var _ ConfigGetter = ConfigGetters{} + +func TestConfigFromENV(t *testing.T) { + tests := map[string]struct { + masterip string + kubeconfig string + iserr bool + }{ + "101": {"", "", true}, + "102": {"", "/etc/config/kubeconfig", true}, + "103": {"0.0.0.0", "", false}, + "104": {"0.0.0.0", "/etc/config/config", true}, + } + + // Sub tests is not used here as env key is set & unset to test. Since env + // is a global setting, the tests should run serially + for name, mock := range tests { + masterip := os.Getenv(string(env.KubeMaster)) + defer os.Setenv(string(env.KubeMaster), masterip) + + kubeconfig := os.Getenv(string(env.KubeConfig)) + defer os.Setenv(string(env.KubeConfig), kubeconfig) + + err := os.Setenv(string(env.KubeMaster), mock.masterip) + if err != nil { + t.Fatalf("Test '%s' failed: %s", name, err) + } + err = os.Setenv(string(env.KubeConfig), mock.kubeconfig) + if err != nil { + t.Fatalf("Test '%s' failed: %s", name, err) + } + + c := &configFromENV{} + config, err := c.Get() + + if !mock.iserr && config == nil { + t.Fatalf("Test '%s' failed: expected config: actual nil config", name) + } + if !mock.iserr && err != nil { + t.Fatalf("Test '%s' failed: expected no error: actual %s", name, err) + } + } +} diff --git a/pkg/client/k8s/v1alpha1/configmap.go b/pkg/client/k8s/v1alpha1/configmap.go new file mode 100644 index 0000000..5b1fcf7 --- /dev/null +++ b/pkg/client/k8s/v1alpha1/configmap.go @@ -0,0 +1,52 @@ +/* +Copyright 2020 The OpenEBS 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 v1alpha1 + +import ( + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "strings" +) + +// ConfigMapGetter abstracts fetching of ConfigMap instance from kubernetes +// cluster +type ConfigMapGetter interface { + Get(options metav1.GetOptions) (*corev1.ConfigMap, error) +} + +type configmap struct { + namespace string // namespace where this configmap exists + name string // name of this configmap +} + +// ConfigMap returns a new instance of configmap +func ConfigMap(namespace, name string) *configmap { + return &configmap{namespace: namespace, name: name} +} + +// Get returns configmap instance from kubernetes cluster +func (c *configmap) Get(options metav1.GetOptions) (cm *corev1.ConfigMap, err error) { + if len(strings.TrimSpace(c.name)) == 0 { + return nil, errors.Errorf("missing config map name: failed to get config map from namespace %s", c.namespace) + } + cs, err := Clientset().Get() + if err != nil { + return nil, errors.Wrapf(err, "failed to get config map %s %s", c.namespace, c.name) + } + return cs.CoreV1().ConfigMaps(c.namespace).Get(c.name, options) +} diff --git a/pkg/client/k8s/v1alpha1/configmap_test.go b/pkg/client/k8s/v1alpha1/configmap_test.go new file mode 100644 index 0000000..a1ab72d --- /dev/null +++ b/pkg/client/k8s/v1alpha1/configmap_test.go @@ -0,0 +1,47 @@ +/* +Copyright 2020 The OpenEBS 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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "testing" +) + +// test if configmap implements ConfigMapGetter interface +var _ ConfigMapGetter = &configmap{} + +func TestConfigMapGet(t *testing.T) { + tests := map[string]struct { + namespace string + name string + options metav1.GetOptions + iserr bool + }{ + "101": {"", "", metav1.GetOptions{}, true}, + "102": {"default", "", metav1.GetOptions{}, true}, + "103": {"default", "myconf", metav1.GetOptions{}, true}, + } + + for name, mock := range tests { + t.Run(name, func(t *testing.T) { + _, err := ConfigMap(mock.namespace, mock.name).Get(mock.options) + if !mock.iserr && err != nil { + t.Fatalf("Test '%s' failed: expected no error: actual '%s'", name, err) + } + }) + } +} diff --git a/pkg/client/k8s/v1alpha1/dynamic.go b/pkg/client/k8s/v1alpha1/dynamic.go new file mode 100644 index 0000000..7cc5b2a --- /dev/null +++ b/pkg/client/k8s/v1alpha1/dynamic.go @@ -0,0 +1,44 @@ +/* +Copyright 2020 The OpenEBS 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 v1alpha1 + +import ( + "github.com/pkg/errors" + k8sdynamic "k8s.io/client-go/dynamic" +) + +// DynamicProvider abstracts providing kubernetes dynamic client interface +type DynamicProvider interface { + Provide() (k8sdynamic.Interface, error) +} + +type dynamic struct{} + +// Dynamic returns a new instance of dynamic +func Dynamic() *dynamic { + return &dynamic{} +} + +// Provide provides a kubernetes dynamic client capable of invoking operations +// against kubernetes resources +func (d *dynamic) Provide() (k8sdynamic.Interface, error) { + config, err := Config().Get() + if err != nil { + return nil, errors.Wrap(err, "failed to provide dynamic client") + } + return k8sdynamic.NewForConfig(config) +} diff --git a/pkg/client/k8s/v1alpha1/dynamic_test.go b/pkg/client/k8s/v1alpha1/dynamic_test.go new file mode 100644 index 0000000..439e397 --- /dev/null +++ b/pkg/client/k8s/v1alpha1/dynamic_test.go @@ -0,0 +1,41 @@ +/* +Copyright 2020 The OpenEBS 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 v1alpha1 + +import ( + "testing" +) + +// test if dynamic implements DynamicProvider interface +var _ DynamicProvider = &dynamic{} + +func TestDynamicProvider(t *testing.T) { + tests := map[string]struct { + iserr bool + }{ + "101": {true}, + } + + for name, mock := range tests { + t.Run(name, func(t *testing.T) { + _, err := Dynamic().Provide() + if !mock.iserr && err != nil { + t.Fatalf("Test '%s' failed: expected no error: actual '%s'", name, err) + } + }) + } +} diff --git a/pkg/client/k8s/v1alpha1/k8s.go b/pkg/client/k8s/v1alpha1/k8s.go new file mode 100644 index 0000000..78f8cbc --- /dev/null +++ b/pkg/client/k8s/v1alpha1/k8s.go @@ -0,0 +1,32 @@ +/* +Copyright 2020 The OpenEBS 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 v1alpha1 + +import ( + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/version" +) + +// GetServerVersion uses the client-go Discovery client to get the +// kubernetes version struct +func GetServerVersion() (*version.Info, error) { + cs, err := Clientset().Get() + if err != nil { + return nil, errors.Wrapf(err, "failed to get apiserver version") + } + return cs.Discovery().ServerVersion() +} diff --git a/pkg/client/k8s/v1alpha1/namespace.go b/pkg/client/k8s/v1alpha1/namespace.go new file mode 100644 index 0000000..fc4af40 --- /dev/null +++ b/pkg/client/k8s/v1alpha1/namespace.go @@ -0,0 +1,59 @@ +/* +Copyright 2020 The OpenEBS 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 v1alpha1 + +import ( + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Namespacegetter abstracts fetching of Namespace from kubernetes cluster +type NamespaceGetter interface { + Get(name string, options metav1.GetOptions) (*corev1.Namespace, error) +} + +// NamespaceLister abstracts fetching of a list of namespaces from kubernetes cluster +type NamespaceLister interface { + List(options metav1.ListOptions) (*corev1.NamespaceList, error) +} +type namespace struct{} + +// Namespace returns a pointer to the namespace struct +func Namespace() *namespace { + return &namespace{} +} + +// Get returns a namespace instance from kubernetes cluster +func (ns *namespace) Get(name string, options metav1.GetOptions) (*corev1.Namespace, error) { + cs, err := Clientset().Get() + if err != nil { + return nil, errors.Wrapf(err, "failed to get namespace: %s", name) + } else { + return cs.CoreV1().Namespaces().Get(name, options) + } +} + +// List returns a slice of namespaces defined in a Kubernetes cluster +func (ns *namespace) List(options metav1.ListOptions) (*corev1.NamespaceList, error) { + cs, err := Clientset().Get() + if err != nil { + return nil, errors.Wrapf(err, "failed to get namespaces") + } else { + return cs.CoreV1().Namespaces().List(options) + } +} diff --git a/pkg/client/k8s/v1alpha1/node.go b/pkg/client/k8s/v1alpha1/node.go new file mode 100644 index 0000000..42137cc --- /dev/null +++ b/pkg/client/k8s/v1alpha1/node.go @@ -0,0 +1,81 @@ +/* +Copyright 2020 The OpenEBS 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 v1alpha1 + +import ( + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// NodeGetter abstracts fetching of Node details from kubernetes cluster +type NodeGetter interface { + Get(name string, options metav1.GetOptions) (*corev1.Node, error) +} + +// NodeLister abstracts fetching of Nodes from kubernetes cluster +type NodeLister interface { + List(options metav1.ListOptions) (*corev1.NodeList, error) +} +type node struct{} + +func Node() *node { + return &node{} +} + +// Get returns a node instance from kubernetes cluster +func (n *node) Get(name string, options metav1.GetOptions) (*corev1.Node, error) { + cs, err := Clientset().Get() + if err != nil { + return nil, errors.Wrapf(err, "failed to get node: %s", name) + } else { + return cs.CoreV1().Nodes().Get(name, options) + } +} + +// List returns a slice of Nodes registered in a Kubernetes cluster +func (n *node) List(options metav1.ListOptions) (*corev1.NodeList, error) { + cs, err := Clientset().Get() + if err != nil { + return nil, errors.Wrapf(err, "failed to get nodes") + } else { + return cs.CoreV1().Nodes().List(options) + } +} + +// NumberOfNodes returns the number of nodes registered in a Kubernetes cluster +func NumberOfNodes() (int, error) { + n := Node() + nodes, err := n.List(metav1.ListOptions{}) + if err != nil { + return 0, errors.Wrapf(err, "failed to get the number of nodes") + } else { + return len(nodes.Items), nil + } +} + +// GetOSAndKernelVersion gets us the OS,Kernel version +func GetOSAndKernelVersion() (string, error) { + nodes := Node() + // get a single node + firstNode, err := nodes.List(metav1.ListOptions{Limit: 1}) + if err != nil { + return "unknown, unknown", errors.Wrapf(err, "failed to get the os kernel/arch") + } + nodedetails := firstNode.Items[0].Status.NodeInfo + return nodedetails.OSImage + ", " + nodedetails.KernelVersion, nil +} diff --git a/pkg/client/k8s/v1alpha1/resource.go b/pkg/client/k8s/v1alpha1/resource.go new file mode 100644 index 0000000..a2d9769 --- /dev/null +++ b/pkg/client/k8s/v1alpha1/resource.go @@ -0,0 +1,361 @@ +/* +Copyright 2020 The OpenEBS 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. +*/ + +// TODO +// Move this file to pkg/k8sresource/v1alpha1 + +package v1alpha1 + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/klog" +) + +// ResourceCreator abstracts creating an unstructured instance in kubernetes +// cluster +type ResourceCreator interface { + Create(obj *unstructured.Unstructured, subresources ...string) (*unstructured.Unstructured, error) +} + +// ResourceGetter abstracts fetching an unstructured instance from kubernetes +// cluster +type ResourceGetter interface { + Get(name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) +} + +// ResourceLister abstracts fetching an unstructured list of instance from kubernetes +// cluster +type ResourceLister interface { + List(options metav1.ListOptions) (*unstructured.UnstructuredList, error) +} + +// ResourceUpdater abstracts updating an unstructured instance found in +// kubernetes cluster +type ResourceUpdater interface { + Update(oldobj, newobj *unstructured.Unstructured, subresources ...string) (u *unstructured.Unstructured, err error) +} + +// ResourceApplier abstracts applying an unstructured instance that may or may +// not be available in kubernetes cluster +type ResourceApplier interface { + Apply(obj *unstructured.Unstructured, subresources ...string) (*unstructured.Unstructured, error) +} + +// ResourceDeleter abstracts deletes an unstructured instance that is available in kubernetes cluster +type ResourceDeleter interface { + Delete(obj *unstructured.Unstructured, subresources ...string) error +} + +type resource struct { + gvr schema.GroupVersionResource // identify a resource + namespace string // namespace where this resource is to be operated at +} + +// String implements Stringer interface +func (r *resource) String() string { + return r.gvr.String() +} + +// Resource returns a new resource instance +func Resource(gvr schema.GroupVersionResource, namespace string) *resource { + return &resource{gvr: gvr, namespace: namespace} +} + +// Create creates a new resource in kubernetes cluster +func (r *resource) Create(obj *unstructured.Unstructured, subresources ...string) (u *unstructured.Unstructured, err error) { + if obj == nil { + err = errors.Errorf("nil resource instance: failed to create resource '%s' at '%s'", r.gvr, r.namespace) + return + } + dynamic, err := Dynamic().Provide() + if err != nil { + err = errors.Wrapf(err, "failed to create resource '%s' '%s' at '%s'", r.gvr, obj.GetName(), r.namespace) + return + } + u, err = dynamic.Resource(r.gvr).Namespace(r.namespace).Create(obj, metav1.CreateOptions{}, subresources...) + if err != nil { + err = errors.Wrapf(err, "failed to create resource '%s' '%s' at '%s'", r.gvr, obj.GetName(), r.namespace) + return + } + return +} + +// Delete deletes a existing resource in kubernetes cluster +func (r *resource) Delete(obj *unstructured.Unstructured, subresources ...string) error { + if obj == nil { + return errors.Errorf("nil resource instance: failed to delete resource '%s' at '%s'", r.gvr, r.namespace) + } + dynamic, err := Dynamic().Provide() + if err != nil { + return errors.Wrapf(err, "failed to delete resource '%s' '%s' at '%s'", r.gvr, obj.GetName(), r.namespace) + } + err = dynamic.Resource(r.gvr).Namespace(r.namespace).Delete(obj.GetName(), &metav1.DeleteOptions{}) + if err != nil { + return errors.Wrapf(err, "failed to delete resource '%s' '%s' at '%s'", r.gvr, obj.GetName(), r.namespace) + } + return nil +} + +// Get returns a specific resource from kubernetes cluster +func (r *resource) Get(name string, opts metav1.GetOptions, subresources ...string) (u *unstructured.Unstructured, err error) { + if len(strings.TrimSpace(name)) == 0 { + err = errors.Errorf("missing resource name: failed to get resource '%s' at '%s'", r.gvr, r.namespace) + return + } + dynamic, err := Dynamic().Provide() + if err != nil { + err = errors.Wrapf(err, "failed to get resource '%s' '%s' at '%s'", r.gvr, name, r.namespace) + return + } + u, err = dynamic.Resource(r.gvr).Namespace(r.namespace).Get(name, opts, subresources...) + if err != nil { + err = errors.Wrapf(err, "failed to get resource '%s' '%s' at '%s'", r.gvr, name, r.namespace) + return + } + return +} + +// Update updates the resource at kubernetes cluster +func (r *resource) Update(oldobj, newobj *unstructured.Unstructured, subresources ...string) (u *unstructured.Unstructured, err error) { + if oldobj == nil { + err = errors.Errorf("nil old resource instance: failed to update resource '%s' at '%s'", r.gvr, r.namespace) + return + } + if newobj == nil { + err = errors.Errorf("nil new resource instance: failed to update resource '%s' at '%s'", r.gvr, r.namespace) + return + } + dynamic, err := Dynamic().Provide() + if err != nil { + err = errors.Wrapf(err, "failed to update resource '%s' '%s' at '%s'", r.gvr, oldobj.GetName(), r.namespace) + return + } + + resourceVersion := oldobj.GetResourceVersion() + newobj.SetResourceVersion(resourceVersion) + + u, err = dynamic.Resource(r.gvr).Namespace(r.namespace).Update(newobj, metav1.UpdateOptions{}, subresources...) + if err != nil { + err = errors.Wrapf(err, "failed to update resource '%s' '%s' at '%s'", r.gvr, oldobj.GetName(), r.namespace) + return + } + return +} + +// List returns a list of specific resource at kubernetes cluster +func (r *resource) List(opts metav1.ListOptions) (u *unstructured.UnstructuredList, err error) { + dynamic, err := Dynamic().Provide() + if err != nil { + err = errors.Wrapf(err, "failed to list resource '%s' at '%s'", r.gvr, r.namespace) + return + } + u, err = dynamic.Resource(r.gvr).Namespace(r.namespace).List(opts) + if err != nil { + err = errors.Wrapf(err, "failed to list resource '%s' at '%s'", r.gvr, r.namespace) + return + } + return +} + +// ResourceCreateOrUpdater as the name suggests manages to either +// create or update a given resource. It does so by implementing +// ResourceApplier interface +type ResourceCreateOrUpdater struct { + *resource + + // Various executors required to perform Apply + // This is how this instance decouples its dependencies + Getter ResourceGetter + Creator ResourceCreator + Updater ResourceUpdater + + // IsSkipUpdate will not update this resource if set to true. + // In other words, enabling this flag can only create the + // resource in the cluster if not created previously + IsSkipUpdate bool +} + +// ResourceCreateOrUpdaterOption is a typed function used to +// build an instance of ResourceCreateOrUpdater +// +// NOTE: +// This follows the pattern known as "functional options". It +// is a function that operates on a given structure as a value +// to build (initialise, configure, sensible defaults, etc) this +// same structure. +type ResourceCreateOrUpdaterOption func(*ResourceCreateOrUpdater) + +// ResourceCreateOrUpdaterSkipUpdate sets IsSkipUpdate based +// on the provided flag +func ResourceCreateOrUpdaterSkipUpdate(skip bool) ResourceCreateOrUpdaterOption { + return func(r *ResourceCreateOrUpdater) { + r.IsSkipUpdate = skip + } +} + +// NewResourceCreateOrUpdater returns a new instance of +// ResourceCreateOrUpdater +func NewResourceCreateOrUpdater( + gvr schema.GroupVersionResource, + namespace string, + options ...ResourceCreateOrUpdaterOption, +) *ResourceCreateOrUpdater { + resource := Resource(gvr, namespace) + t := &ResourceCreateOrUpdater{ + resource: resource, + Getter: resource, + Creator: resource, + Updater: resource, + } + for _, o := range options { + o(t) + } + return t +} + +// String implements Stringer interface +func (r *ResourceCreateOrUpdater) String() string { + if r.resource == nil { + return fmt.Sprint("ResourceCreateOrUpdater") + } + return fmt.Sprintf("ResourceCreateOrUpdater %s", r.resource) +} + +// Apply applies a resource to the kubernetes cluster. In other words, it +// creates a new resource if it does not exist or updates the existing +// resource. +func (r *ResourceCreateOrUpdater) Apply( + obj *unstructured.Unstructured, + subresources ...string, +) (resource *unstructured.Unstructured, err error) { + if r.Getter == nil { + err = errors.Errorf("%s: Apply failed: Nil getter", r) + return + } + if r.Creator == nil { + err = errors.Errorf("%s: Apply failed: Nil creator", r) + return + } + if r.Updater == nil { + err = errors.Errorf("%s: Apply failed: Nil updater", r) + return + } + if obj == nil { + err = errors.Errorf("%s: Apply failed: Nil resource", r) + return + } + resource, err = r.Getter.Get(obj.GetName(), metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(errors.Cause(err)) { + return r.Creator.Create(obj, subresources...) + } + return nil, err + } + if r.IsSkipUpdate { + klog.V(2).Infof("%s: Skipping update", r) + return resource, nil + } + return r.Updater.Update(resource, obj, subresources...) +} + +// ResourceDeleteOptions is a utility instance used during the resource's delete operations +type ResourceDeleteOptions struct { + Deleter ResourceDeleter +} + +// Delete is a resource that is suitable to be executed as a Delete operation +type Delete struct { + *resource + options ResourceDeleteOptions +} + +// DeleteResource returns a new instance of delete resource +func DeleteResource(gvr schema.GroupVersionResource, namespace string) *Delete { + resource := Resource(gvr, namespace) + options := ResourceDeleteOptions{Deleter: resource} + return &Delete{resource: resource, options: options} +} + +// Delete deletes a resource from a kubernetes cluster +func (d *Delete) Delete(obj *unstructured.Unstructured, subresources ...string) error { + if d.options.Deleter == nil { + return errors.New("nil resource deleter instance: failed to delete resource") + } else if obj == nil { + return errors.New("nil resource instance: failed to delete resource") + } + return d.options.Deleter.Delete(obj, subresources...) +} + +// ResourceListOptions is a utility instance used during the resource's list operations +type ResourceListOptions struct { + Lister ResourceLister +} + +// List is a resource resource that is suitable to be executed as a List operation +type List struct { + *resource + options ResourceListOptions +} + +// ListResource returns a new instance of list resource +func ListResource(gvr schema.GroupVersionResource, namespace string) *List { + resource := Resource(gvr, namespace) + options := ResourceListOptions{Lister: resource} + return &List{resource: resource, options: options} +} + +// List lists a resource from a kubernetes cluster +func (l *List) List(options metav1.ListOptions) (u *unstructured.UnstructuredList, err error) { + if l.options.Lister == nil { + err = errors.New("nil resource lister instance: failed to list resource") + return + } + return l.options.Lister.List(options) +} + +// ResourceGetOptions is a utility instance used during the resource's get operations +type ResourceGetOptions struct { + Getter ResourceGetter +} + +// Get is resource that is suitable to be executed as Get operation +type Get struct { + *resource + options ResourceGetOptions +} + +// GetResource returns a new instance of get resource +func GetResource(gvr schema.GroupVersionResource, namespace string) *Get { + resource := Resource(gvr, namespace) + options := ResourceGetOptions{Getter: resource} + return &Get{resource: resource, options: options} +} + +// Get gets a resource from a kubernetes cluster +func (g *Get) Get(name string, opts metav1.GetOptions, subresources ...string) (u *unstructured.Unstructured, err error) { + if g.options.Getter == nil { + err = errors.New("nil resource getter instance: failed to get resource") + return + } + return g.options.Getter.Get(name, opts, subresources...) +} diff --git a/pkg/client/k8s/v1alpha1/resource_test.go b/pkg/client/k8s/v1alpha1/resource_test.go new file mode 100644 index 0000000..195aa34 --- /dev/null +++ b/pkg/client/k8s/v1alpha1/resource_test.go @@ -0,0 +1,31 @@ +/* +Copyright 2020 The OpenEBS 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. +*/ + +// TODO +// Move this file to pkg/k8sresource/v1alpha1 +package v1alpha1 + +// verify if resource struct is an implementation of ResourceGetter +var _ ResourceGetter = &resource{} + +// verify if resource struct is an implementation of ResourceCreator +var _ ResourceCreator = &resource{} + +// verify if resource struct is an implementation of ResourceUpdater +var _ ResourceUpdater = &resource{} + +// verify if createOrUpdate struct is an implementation of ResourceApplier +var _ ResourceApplier = &ResourceCreateOrUpdater{} diff --git a/pkg/common/env/env.go b/pkg/common/env/env.go index 1fb83e7..3f6905d 100644 --- a/pkg/common/env/env.go +++ b/pkg/common/env/env.go @@ -22,6 +22,14 @@ import ( "strings" ) +const ( + // KubeConfig is the ENV variable to fetch kubernetes kubeconfig + KubeConfig = "OPENEBS_IO_KUBE_CONFIG" + + // KubeMaster is the ENV variable to fetch kubernetes master's address + KubeMaster = "OPENEBS_IO_K8S_MASTER" +) + // EnvironmentSetter abstracts setting of environment variable type EnvironmentSetter func(envKey string, value string) (err error) diff --git a/pkg/common/kubernetes/client/client.go b/pkg/common/kubernetes/client/client.go index c401564..93fa027 100644 --- a/pkg/common/kubernetes/client/client.go +++ b/pkg/common/kubernetes/client/client.go @@ -28,16 +28,6 @@ import ( "k8s.io/client-go/tools/clientcmd" ) -const ( - // K8sMasterIPEnvironmentKey is the environment variable key used to - // determine the kubernetes master IP address - K8sMasterIPEnvironmentKey string = "OPENEBS_IO_K8S_MASTER" - - // KubeConfigEnvironmentKey is the environment variable key used to - // determine the kubernetes config - KubeConfigEnvironmentKey string = "OPENEBS_IO_KUBE_CONFIG" -) - // getInClusterConfigFunc abstracts the logic to get // kubernetes incluster config // @@ -213,8 +203,8 @@ func (c *Client) Config() (config *rest.Config, err error) { } // ENV holds second priority - if strings.TrimSpace(c.getKubeMasterIP(K8sMasterIPEnvironmentKey)) != "" || - strings.TrimSpace(c.getKubeConfigPath(KubeConfigEnvironmentKey)) != "" { + if strings.TrimSpace(c.getKubeMasterIP(env.KubeMaster)) != "" || + strings.TrimSpace(c.getKubeConfigPath(env.KubeConfig)) != "" { return c.getConfigFromENV() } @@ -235,14 +225,14 @@ func (c *Client) GetConfigForPathOrDirect() (config *rest.Config, err error) { } func (c *Client) getConfigFromENV() (config *rest.Config, err error) { - k8sMaster := c.getKubeMasterIP(K8sMasterIPEnvironmentKey) - kubeConfig := c.getKubeConfigPath(KubeConfigEnvironmentKey) + k8sMaster := c.getKubeMasterIP(env.KubeMaster) + kubeConfig := c.getKubeConfigPath(env.KubeConfig) if strings.TrimSpace(k8sMaster) == "" && strings.TrimSpace(kubeConfig) == "" { return nil, errors.Errorf( "failed to get kubernetes config: missing ENV: atleast one should be set: {%s} or {%s}", - K8sMasterIPEnvironmentKey, - KubeConfigEnvironmentKey, + env.KubeMaster, + env.KubeConfig, ) } return c.buildConfigFromFlags(k8sMaster, kubeConfig) diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index 088510e..6e491a0 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -28,6 +28,7 @@ import ( "github.com/openebs/zfs-localpv/pkg/builder/volbuilder" errors "github.com/openebs/zfs-localpv/pkg/common/errors" csipayload "github.com/openebs/zfs-localpv/pkg/response" + analytics "github.com/openebs/zfs-localpv/pkg/usage" zfs "github.com/openebs/zfs-localpv/pkg/zfs" "golang.org/x/net/context" "google.golang.org/grpc/codes" @@ -58,6 +59,19 @@ var SupportedVolumeCapabilityAccessModes = []*csi.VolumeCapability_AccessMode{ }, } +// sendEventOrIgnore sends anonymous local-pv provision/delete events +func sendEventOrIgnore(pvName, capacity, stgType, method string) { + if zfs.GoogleAnalyticsEnabled == "true" { + analytics.New().Build().ApplicationBuilder(). + SetVolumeType(stgType, method). + SetDocumentTitle(pvName). + SetLabel(analytics.EventLabelCapacity). + SetReplicaCount(analytics.LocalPVReplicaCount, method). + SetCategory(method). + SetVolumeCapacity(capacity).Send() + } +} + func CreateZFSVolume(req *csi.CreateVolumeRequest) (string, error) { volName := req.GetName() size := req.GetCapacityRange().RequiredBytes @@ -190,6 +204,8 @@ func (cs *controller) CreateVolume( return nil, status.Error(codes.Internal, err.Error()) } + sendEventOrIgnore(volName, strconv.FormatInt(int64(size), 10), "zfs-localpv", analytics.VolumeProvision) + topology := map[string]string{zfs.ZFSTopologyKey: selected} return csipayload.NewCreateVolumeResponseBuilder(). @@ -232,6 +248,9 @@ func (cs *controller) DeleteVolume( volumeID, ) } + + sendEventOrIgnore(volumeID, vol.Spec.Capacity, "zfs-localpv", analytics.VolumeDeprovision) + deleteResponse: return csipayload.NewDeleteVolumeResponseBuilder().Build(), nil } diff --git a/pkg/usage/const.go b/pkg/usage/const.go new file mode 100644 index 0000000..d92b510 --- /dev/null +++ b/pkg/usage/const.go @@ -0,0 +1,49 @@ +/* +Copyright 2020 The OpenEBS 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 usage + +const ( + // GAclientID is the unique code of OpenEBS project in Google Analytics + GAclientID string = "UA-127388617-1" + + // supported events categories + + // Install event is sent on pod starts + InstallEvent string = "install" + // Ping event is sent periodically + Ping string = "zfs-ping" + // VolumeProvision event is sent when a volume is created + VolumeProvision string = "volume-provision" + //VolumeDeprovision event is sent when a volume is deleted + VolumeDeprovision string = "volume-deprovision" + AppName string = "OpenEBS" + + // Event labels + RunningStatus string = "running" + EventLabelNode string = "nodes" + EventLabelCapacity string = "capacity" + + // Event action + Replica string = "replica:" + DefaultReplicaCount string = "replica:1" + + // Event application name constant for volume event + DefaultCASType string = "zfs-localpv" + + // LocalPVReplicaCount is the constant used by usage to represent + // replication factor in LocalPV + LocalPVReplicaCount string = "1" +) diff --git a/pkg/usage/googleanalytics.go b/pkg/usage/googleanalytics.go new file mode 100644 index 0000000..7fa5d90 --- /dev/null +++ b/pkg/usage/googleanalytics.go @@ -0,0 +1,52 @@ +/* +Copyright 2020 The OpenEBS 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 usage + +import ( + analytics "github.com/jpillora/go-ogle-analytics" + "k8s.io/klog" +) + +// Send sends a single usage metric to Google Analytics with some +// compulsory fields defined in Google Analytics API +// bindings(jpillora/go-ogle-analytics) +func (u *Usage) Send() { + // Instantiate a Gclient with the tracking ID + go func() { + // Un-wrap the gaClient struct back here + gaClient, err := analytics.NewClient(u.Gclient.trackID) + if err != nil { + return + } + gaClient.ClientID(u.clientID). + CampaignSource(u.campaignSource). + CampaignContent(u.clientID). + ApplicationID(u.appID). + ApplicationVersion(u.appVersion). + DataSource(u.dataSource). + ApplicationName(u.appName). + ApplicationInstallerID(u.appInstallerID). + DocumentTitle(u.documentTitle) + // Un-wrap the Event struct back here + event := analytics.NewEvent(u.category, u.action) + event.Label(u.label) + event.Value(u.value) + if err := gaClient.Send(event); err != nil { + klog.Errorf(err.Error()) + return + } + }() +} diff --git a/pkg/usage/ping.go b/pkg/usage/ping.go new file mode 100644 index 0000000..dfb8633 --- /dev/null +++ b/pkg/usage/ping.go @@ -0,0 +1,63 @@ +/* +Copyright 2020 The OpenEBS 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 usage + +import ( + "time" + + "github.com/openebs/zfs-localpv/pkg/common/env" +) + +var OpenEBSPingPeriod = "OPENEBS_IO_ANALYTICS_PING_INTERVAL" + +const ( + // defaultPingPeriod sets the default ping heartbeat interval + defaultPingPeriod time.Duration = 24 * time.Hour + // minimumPingPeriod sets the minimum possible configurable + // heartbeat period, if a value lower than this will be set, the + // defaultPingPeriod will be used + minimumPingPeriod time.Duration = 1 * time.Hour +) + +// PingCheck sends ping events to Google Analytics +func PingCheck() { + // Create a new usage field + u := New() + duration := getPingPeriod() + ticker := time.NewTicker(duration) + for _ = range ticker.C { + u.Build(). + InstallBuilder(true). + SetCategory(Ping). + Send() + } +} + +// getPingPeriod sets the duration of health events, defaults to 24 +func getPingPeriod() time.Duration { + value := env.GetOrDefault(OpenEBSPingPeriod, string(defaultPingPeriod)) + duration, _ := time.ParseDuration(value) + // Sanitychecks for setting time duration of health events + // This way, we are checking for negative and zero time duration and we + // also have a minimum possible configurable time duration between health events + if duration < minimumPingPeriod { + // Avoid corner case when the ENV value is undesirable + return time.Duration(defaultPingPeriod) + } else { + return time.Duration(duration) + } +} diff --git a/pkg/usage/ping_test.go b/pkg/usage/ping_test.go new file mode 100644 index 0000000..1d1f3bc --- /dev/null +++ b/pkg/usage/ping_test.go @@ -0,0 +1,56 @@ +// Copyright 2020 The OpenEBS 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 usage + +import ( + "os" + "testing" + "time" +) + +func TestGetPingPeriod(t *testing.T) { + beforeFunc := func(value string) { + if err := os.Setenv(string(OpenEBSPingPeriod), value); err != nil { + t.Logf("Unable to set environment variable") + } + } + afterFunc := func() { + if err := os.Unsetenv(string(OpenEBSPingPeriod)); err != nil { + t.Logf("Unable to unset environment variable") + } + } + testSuite := map[string]struct { + OpenEBSPingPeriodValue string + ExpectedPeriodValue time.Duration + }{ + "24 seconds": {"24s", 86400000000000}, + "24 minutes": {"24m", 86400000000000}, + "24 hours": {"24h", 86400000000000}, + "Negative 24 hours": {"-24h", 86400000000000}, + "Random string input": {"Apache", 86400000000000}, + "Two hours": {"2h", 7200000000000}, + "Three hundred hours": {"300h", 1080000000000000}, + "Fifty two seconds": {"52000000000ns", 86400000000000}, + "Empty env value": {"", 86400000000000}, + } + for testKey, testData := range testSuite { + beforeFunc(testData.OpenEBSPingPeriodValue) + evaluatedValue := getPingPeriod() + if evaluatedValue != testData.ExpectedPeriodValue { + t.Fatalf("Tests failed for %s, expected=%d, got=%d", testKey, testData.ExpectedPeriodValue, evaluatedValue) + } + afterFunc() + } +} diff --git a/pkg/usage/size.go b/pkg/usage/size.go new file mode 100644 index 0000000..c435d0d --- /dev/null +++ b/pkg/usage/size.go @@ -0,0 +1,27 @@ +/* +Copyright 2020 The OpenEBS 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 usage + +import units "github.com/docker/go-units" + +// toGigaUnits converts a size from xB to bytes where x={k,m,g,t,p...} +// and return the number of Gigabytes as an integer +// 1 gigabyte=1000 megabyte +func toGigaUnits(size string) (int64, error) { + sizeInBytes, err := units.FromHumanSize(size) + return sizeInBytes / units.GB, err +} diff --git a/pkg/usage/size_test.go b/pkg/usage/size_test.go new file mode 100644 index 0000000..23e8563 --- /dev/null +++ b/pkg/usage/size_test.go @@ -0,0 +1,61 @@ +/* +Copyright 2020 The OpenEBS 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 usage + +import "testing" + +func TestToGigaUnits(t *testing.T) { + tests := map[string]struct { + stringSize string + expectedGsize int64 + positiveTest bool + }{ + "One Hundred Twenty Three thousand Four Hundred Fifty Six Teribytes": { + "123456 TiB", + 123456000, + true, + }, + "One Gibibyte": { + "1 GiB", + 1, + true, + }, + "One Megabyte": { + "1 MB", + 0, // One cannot express <1GB in integer + true, + }, + "One Megabyte negative-case": { + "1 MB", + 1, + false, + // 1 MB isn't 1 GB + }, + "One hundred four point five gigabyte": { + "104.5 GB", + 104, + true, + }, + } + + for testKey, testSuite := range tests { + gotValue, err := toGigaUnits(testSuite.stringSize) + if (gotValue != testSuite.expectedGsize || err != nil) && testSuite.positiveTest { + t.Fatalf("Tests failed for %s, expected=%d, got=%d", testKey, testSuite.expectedGsize, gotValue) + } + } +} diff --git a/pkg/usage/usage.go b/pkg/usage/usage.go new file mode 100644 index 0000000..3e407f4 --- /dev/null +++ b/pkg/usage/usage.go @@ -0,0 +1,257 @@ +/* +Copyright 2020 The OpenEBS 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 usage + +import ( + k8sapi "github.com/openebs/zfs-localpv/pkg/client/k8s/v1alpha1" +) + +// Usage struct represents all information about a usage metric sent to +// Google Analytics with respect to the application +type Usage struct { + // Embedded Event struct as we are currently only sending hits of type + // 'event' + Event + + // https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#an + // use-case: cstor or jiva volume, or m-apiserver application + // Embedded field for application + Application + + // Embedded Gclient struct + Gclient +} + +// Event is a represents usage of OpenEBS +// Event contains all the query param fields when hits is of type='event' +// Ref: https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ec +type Event struct { + // (Required) Event Category, ec + category string + // (Required) Event Action, ea + action string + // (Optional) Event Label, el + label string + // (Optional) Event vallue, ev + // Non negative + value int64 +} + +// NewEvent returns an Event struct with eventCategory, eventAction, +// eventLabel, eventValue fields +func (u *Usage) NewEvent(c, a, l string, v int64) *Usage { + u.category = c + u.action = a + u.label = l + u.value = v + return u +} + +// Application struct holds details about the Application +type Application struct { + // eg. project version + appVersion string + + // eg. kubernetes version + appInstallerID string + + // Name of the application, usage(OpenEBS/NDM) + appID string + + // eg. usage(os-type/architecture) of system or volume's CASType + appName string +} + +// Gclient struct represents a Google Analytics hit +type Gclient struct { + // constant tracking-id used to send a hit + trackID string + + // anonymous client-id + clientID string + + // anonymous campaign source + campaignSource string + + // https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ds + // (usecase) node-detail + dataSource string + + // Document-title property in Google Analytics + // https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#dt + // use-case: uuid of the volume objects or a uuid to anonymously tell objects apart + documentTitle string +} + +// New returns an instance of Usage +func New() *Usage { + return &Usage{} +} + +// SetDataSource : usage(os-type, kernel) +func (u *Usage) SetDataSource(dataSource string) *Usage { + u.dataSource = dataSource + return u +} + +// SetTrackingID Sets the GA-code for the project +func (u *Usage) SetTrackingID(track string) *Usage { + u.trackID = track + return u +} + +// SetCampaignSource : source of openebs installater like: +// helm or operator etc. This will have to be configured +// via ENV variable OPENEBS_IO_INSTALLER_TYPE +func (u *Usage) SetCampaignSource(campaignSrc string) *Usage { + u.campaignSource = campaignSrc + return u +} + +// SetDocumentTitle : usecase(anonymous-id) +func (u *Usage) SetDocumentTitle(documentTitle string) *Usage { + u.documentTitle = documentTitle + return u +} + +// SetApplicationName : usecase(os-type/arch, volume CASType) +func (u *Usage) SetApplicationName(appName string) *Usage { + u.appName = appName + return u +} + +// SetApplicationID : usecase(OpenEBS/NDM) +func (u *Usage) SetApplicationID(appID string) *Usage { + u.appID = appID + return u +} + +// SetApplicationVersion : usecase(project-version) +func (u *Usage) SetApplicationVersion(appVersion string) *Usage { + u.appVersion = appVersion + return u +} + +// SetApplicationInstallerID : usecase(k8s-version) +func (u *Usage) SetApplicationInstallerID(appInstallerID string) *Usage { + u.appInstallerID = appInstallerID + return u +} + +// SetClientID sets the anonymous user id +func (u *Usage) SetClientID(userID string) *Usage { + u.clientID = userID + return u +} + +// SetCategory sets the category of an event +func (u *Usage) SetCategory(c string) *Usage { + u.category = c + return u +} + +// SetAction sets the action of an event +func (u *Usage) SetAction(a string) *Usage { + u.action = a + return u +} + +// SetLabel sets the label for an event +func (u *Usage) SetLabel(l string) *Usage { + u.label = l + return u +} + +// SetValue sets the value for an event's label +func (u *Usage) SetValue(v int64) *Usage { + u.value = v + return u +} + +// Build is a builder method for Usage struct +func (u *Usage) Build() *Usage { + // Default ApplicationID for openebs project is OpenEBS + v := NewVersion() + v.getVersion(false) + u.SetApplicationID(AppName). + SetTrackingID(GAclientID). + SetClientID(v.id). + SetCampaignSource(v.installerType) + // TODO: Add condition for version over-ride + // Case: CAS/Jiva version, etc + return u +} + +// Application builder is used for adding k8s&openebs environment detail +// for non install events +func (u *Usage) ApplicationBuilder() *Usage { + v := NewVersion() + v.getVersion(false) + u.SetApplicationVersion(v.openebsVersion). + SetApplicationName(v.k8sArch). + SetApplicationInstallerID(v.k8sVersion). + SetDataSource(v.nodeType) + return u +} + +// SetVolumeCapacity sets the storage capacity of the volume for a volume event +func (u *Usage) SetVolumeCapacity(volCapG string) *Usage { + s, _ := toGigaUnits(volCapG) + u.SetValue(s) + return u +} + +// Wrapper for setting the default storage-engine for volume-provision event +func (u *Usage) SetVolumeType(volType, method string) *Usage { + if method == VolumeProvision && volType == "" { + // Set the default storage engine, if not specified in the request + u.SetApplicationName(DefaultCASType) + } else { + u.SetApplicationName(volType) + } + return u +} + +// Wrapper for setting replica count for volume events +// NOTE: This doesn't get the replica count in a volume de-provision event. +// TODO: Pick the current value of replica-count from the CAS-engine +func (u *Usage) SetReplicaCount(count, method string) *Usage { + if method == VolumeProvision && count == "" { + // Case: When volume-provision the replica count isn't specified + // it is set to three by default by the m-apiserver + u.SetAction(DefaultReplicaCount) + } else { + // Catch all case for volume-deprovision event and + // volume-provision event with an overriden replica-count + u.SetAction(Replica + count) + } + return u +} + +// InstallBuilder is a concrete builder for install events +func (u *Usage) InstallBuilder(override bool) *Usage { + v := NewVersion() + clusterSize, _ := k8sapi.NumberOfNodes() + v.getVersion(override) + u.SetApplicationVersion(v.openebsVersion). + SetApplicationName(v.k8sArch). + SetApplicationInstallerID(v.k8sVersion). + SetDataSource(v.nodeType). + SetDocumentTitle(v.id). + SetApplicationID(AppName). + NewEvent(InstallEvent, RunningStatus, EventLabelNode, int64(clusterSize)) + return u +} diff --git a/pkg/usage/versionset.go b/pkg/usage/versionset.go new file mode 100644 index 0000000..f4cfe53 --- /dev/null +++ b/pkg/usage/versionset.go @@ -0,0 +1,111 @@ +/* +Copyright 2020 The OpenEBS 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 usage + +import ( + k8sapi "github.com/openebs/zfs-localpv/pkg/client/k8s/v1alpha1" + env "github.com/openebs/zfs-localpv/pkg/common/env" + openebsversion "github.com/openebs/zfs-localpv/pkg/version" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog" +) + +var ( + clusterUUID = "OPENEBS_IO_USAGE_UUID" + clusterVersion = "OPENEBS_IO_K8S_VERSION" + clusterArch = "OPENEBS_IO_K8S_ARCH" + openEBSversion = "OPENEBS_IO_VERSION_TAG" + nodeType = "OPENEBS_IO_NODE_TYPE" + installerType = "OPENEBS_IO_INSTALLER_TYPE" +) + +// versionSet is a struct which stores (sort of) fixed information about a +// k8s environment +type versionSet struct { + id string // OPENEBS_IO_USAGE_UUID + k8sVersion string // OPENEBS_IO_K8S_VERSION + k8sArch string // OPENEBS_IO_K8S_ARCH + openebsVersion string // OPENEBS_IO_VERSION_TAG + nodeType string // OPENEBS_IO_NODE_TYPE + installerType string // OPENEBS_IO_INSTALLER_TYPE +} + +// NewVersion returns a new versionSet struct +func NewVersion() *versionSet { + return &versionSet{} +} + +// fetchAndSetVersion consumes the Kubernetes API to get environment constants +// and returns a versionSet struct +func (v *versionSet) fetchAndSetVersion() error { + var err error + v.id, err = getUUIDbyNS("default") + if err != nil { + return err + } + env.Set(clusterUUID, v.id) + + k8s, err := k8sapi.GetServerVersion() + if err != nil { + return err + } + // eg. linux/amd64 + v.k8sArch = k8s.Platform + v.k8sVersion = k8s.GitVersion + env.Set(clusterArch, v.k8sArch) + env.Set(clusterVersion, v.k8sVersion) + v.nodeType, err = k8sapi.GetOSAndKernelVersion() + env.Set(nodeType, v.nodeType) + if err != nil { + return err + } + v.openebsVersion = openebsversion.GetVersionDetails() + env.Set(openEBSversion, v.openebsVersion) + return nil +} + +// getVersion is a wrapper over fetchAndSetVersion +func (v *versionSet) getVersion(override bool) error { + // If ENVs aren't set or the override is true, fetch the required + // values from the K8s APIserver + if _, present := env.Lookup(openEBSversion); !present || override { + if err := v.fetchAndSetVersion(); err != nil { + klog.Error(err.Error()) + return err + } + } + // Fetch data from ENV + v.id = env.Get(clusterUUID) + v.k8sArch = env.Get(clusterArch) + v.k8sVersion = env.Get(clusterVersion) + v.nodeType = env.Get(nodeType) + v.openebsVersion = env.Get(openEBSversion) + v.installerType = env.Get(installerType) + return nil +} + +// getUUIDbyNS returns the metadata.object.uid of a namespace in Kubernetes +func getUUIDbyNS(namespace string) (string, error) { + ns := k8sapi.Namespace() + NSstruct, err := ns.Get(namespace, metav1.GetOptions{}) + if err != nil { + return "", err + } + if NSstruct != nil { + return string(NSstruct.GetObjectMeta().GetUID()), nil + } + return "", nil +} diff --git a/pkg/version/version.go b/pkg/version/version.go index 620067c..55ec848 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -108,6 +108,10 @@ func GetGitCommit() string { return strings.TrimSpace(string(output)) } +func GetVersionDetails() string { + return "zfs-" + strings.Join([]string{Get(), GetGitCommit()[0:7]}, "-") +} + // Verbose returns version details with git // commit info func Verbose() string { diff --git a/pkg/zfs/volume.go b/pkg/zfs/volume.go index 28787b7..70aa6d5 100644 --- a/pkg/zfs/volume.go +++ b/pkg/zfs/volume.go @@ -30,6 +30,8 @@ const ( // // This environment variable is set via kubernetes downward API OpenEBSNamespaceKey string = "OPENEBS_NAMESPACE" + // This environment variable is set via env + GoogleAnalyticsKey string = "OPENEBS_IO_ENABLE_ANALYTICS" // ZFSFinalizer for the ZfsVolume CR ZFSFinalizer string = "zfs.openebs.io/finalizer" // ZFSVolKey for the ZfsSnapshot CR to store Persistence Volume name @@ -50,6 +52,9 @@ var ( // NodeID is the NodeID of the node on which the pod is present NodeID string + + // should send google analytics or not + GoogleAnalyticsEnabled string ) func init() { @@ -62,6 +67,8 @@ func init() { if NodeID == "" && os.Getenv("OPENEBS_NODE_DRIVER") != "" { logrus.Fatalf("NodeID environment variable not set") } + + GoogleAnalyticsEnabled = os.Getenv(GoogleAnalyticsKey) } // ProvisionVolume creates a ZFSVolume(zv) CR,