diff --git a/examples/arango-local-storage.yaml b/examples/arango-local-storage.yaml index aa018c5ee..6c986ecd0 100644 --- a/examples/arango-local-storage.yaml +++ b/examples/arango-local-storage.yaml @@ -5,5 +5,6 @@ metadata: spec: storageClass: name: my-local-ssd + isDefault: true localPath: - /var/lib/arango-storage diff --git a/pkg/storage/storage_class.go b/pkg/storage/storage_class.go index d490cc021..a5e40b7f7 100644 --- a/pkg/storage/storage_class.go +++ b/pkg/storage/storage_class.go @@ -38,6 +38,7 @@ var ( // ensureStorageClass creates a storage class for the given local storage. // If such a class already exists, the create is ignored. func (l *LocalStorage) ensureStorageClass(apiObject *api.ArangoLocalStorage) error { + log := l.deps.Log spec := apiObject.Spec.StorageClass bindingMode := v1.VolumeBindingWaitForFirstConsumer reclaimPolicy := corev1.PersistentVolumeReclaimRetain @@ -49,10 +50,60 @@ func (l *LocalStorage) ensureStorageClass(apiObject *api.ArangoLocalStorage) err VolumeBindingMode: &bindingMode, Provisioner: storageClassProvisioner, } - if _, err := l.deps.KubeCli.StorageV1().StorageClasses().Create(sc); !k8sutil.IsAlreadyExists(err) && err != nil { + // Note: We do not attach the StorageClass to the apiObject (OwnerRef) because many + // ArangoLocalStorage resource may use the same StorageClass. + cli := l.deps.KubeCli.StorageV1() + if _, err := cli.StorageClasses().Create(sc); k8sutil.IsAlreadyExists(err) { + log.Debug(). + Str("storageclass", sc.GetName()). + Msg("StorageClass already exists") + } else if err != nil { + log.Debug().Err(err). + Str("storageclass", sc.GetName()). + Msg("Failed to create StorageClass") return maskAny(err) + } else { + log.Debug(). + Str("storageclass", sc.GetName()). + Msg("StorageClass created") + } + + if apiObject.Spec.StorageClass.IsDefault { + // UnMark current default (if any) + list, err := cli.StorageClasses().List(metav1.ListOptions{}) + if err != nil { + log.Debug().Err(err).Msg("Listing StorageClasses failed") + return maskAny(err) + } + for _, scX := range list.Items { + if !k8sutil.StorageClassIsDefault(&scX) || scX.GetName() == sc.GetName() { + continue + } + // Mark storage class as non-default + if err := k8sutil.PatchStorageClassIsDefault(cli, scX.GetName(), false); err != nil { + log.Debug(). + Err(err). + Str("storageclass", scX.GetName()). + Msg("Failed to mark StorageClass as not-default") + return maskAny(err) + } + log.Debug(). + Str("storageclass", scX.GetName()). + Msg("Marked StorageClass as not-default") + } + + // Mark StorageClass default + if err := k8sutil.PatchStorageClassIsDefault(cli, sc.GetName(), true); err != nil { + log.Debug(). + Err(err). + Str("storageclass", sc.GetName()). + Msg("Failed to mark StorageClass as default") + return maskAny(err) + } + log.Debug(). + Str("storageclass", sc.GetName()). + Msg("Marked StorageClass as default") } - // TODO make default (if needed) return nil } diff --git a/pkg/util/k8sutil/storageclass.go b/pkg/util/k8sutil/storageclass.go new file mode 100644 index 000000000..45619a54c --- /dev/null +++ b/pkg/util/k8sutil/storageclass.go @@ -0,0 +1,59 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package k8sutil + +import ( + "fmt" + "strconv" + + "k8s.io/api/storage/v1" + "k8s.io/apimachinery/pkg/types" + storagev1 "k8s.io/client-go/kubernetes/typed/storage/v1" +) + +const ( + annStorageClassIsDefault = "storageclass.kubernetes.io/is-default-class" +) + +// StorageClassIsDefault returns true if the given storage class is marked default, +// false otherwise. +func StorageClassIsDefault(sc *v1.StorageClass) bool { + value, found := sc.GetObjectMeta().GetAnnotations()[annStorageClassIsDefault] + if !found { + return false + } + boolValue, err := strconv.ParseBool(value) + if err != nil { + return false + } + return boolValue +} + +// PatchStorageClassIsDefault changes the default flag of the given storage class. +func PatchStorageClassIsDefault(cli storagev1.StorageV1Interface, name string, isDefault bool) error { + jsonPatch := fmt.Sprintf(`{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"%v"}}}`, isDefault) + if _, err := cli.StorageClasses().Patch(name, types.StrategicMergePatchType, []byte(jsonPatch)); err != nil { + return maskAny(err) + } + return nil +} diff --git a/pkg/util/k8sutil/storageclass_test.go b/pkg/util/k8sutil/storageclass_test.go new file mode 100644 index 000000000..91df6f187 --- /dev/null +++ b/pkg/util/k8sutil/storageclass_test.go @@ -0,0 +1,72 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package k8sutil + +import ( + "testing" + + "k8s.io/api/storage/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// StorageClassIsDefault returns true if the given storage class is marked default, +// false otherwise. +func TestStorageClassIsDefault(t *testing.T) { + tests := []struct { + StorageClass v1.StorageClass + IsDefault bool + }{ + {v1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, + }, false}, + {v1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annStorageClassIsDefault: "false", + }, + }, + }, false}, + {v1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annStorageClassIsDefault: "foo", + }, + }, + }, false}, + {v1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annStorageClassIsDefault: "true", + }, + }, + }, true}, + } + for _, test := range tests { + result := StorageClassIsDefault(&test.StorageClass) + if result != test.IsDefault { + t.Errorf("StorageClassIsDefault failed. Expected %v, got %v for %#v", test.IsDefault, result, test.StorageClass) + } + } +}