Skip to content

Commit

Permalink
Implement PVC or EmptyDir for Storage (#67)
Browse files Browse the repository at this point in the history
fixes #49
  • Loading branch information
sircthulhu authored Mar 23, 2024
1 parent b3b91c6 commit 5715962
Show file tree
Hide file tree
Showing 7 changed files with 700 additions and 53 deletions.
72 changes: 64 additions & 8 deletions api/v1alpha1/etcdcluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,18 @@ limitations under the License.
package v1alpha1

import (
"k8s.io/apimachinery/pkg/api/resource"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type Storage struct {
StorageClass string `json:"storageClass"`
Size resource.Quantity `json:"size"`
}

// EtcdClusterSpec defines the desired state of EtcdCluster
type EtcdClusterSpec struct {
// Replicas is the count of etcd instances in cluster.
// +optional
// +kubebuilder:default:=3
// +kubebuilder:validation:Minimum:=0
Replicas *int32 `json:"replicas,omitempty"`
Storage Storage `json:"storage,omitempty"`
Replicas *int32 `json:"replicas,omitempty"`
Storage StorageSpec `json:"storage"`
}

const (
Expand Down Expand Up @@ -76,6 +71,67 @@ type EtcdClusterList struct {
Items []EtcdCluster `json:"items"`
}

// EmbeddedObjectMetadata contains a subset of the fields included in k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta
// Only fields which are relevant to embedded resources are included.
type EmbeddedObjectMetadata struct {
// Name must be unique within a namespace. Is required when creating resources, although
// some resources may allow a client to request the generation of an appropriate name
// automatically. Name is primarily intended for creation idempotence and configuration
// definition.
// Cannot be updated.
// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names
// +optional
Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"`

// Labels Map of string keys and values that can be used to organize and categorize
// (scope and select) objects. May match selectors of replication controllers
// and services.
// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels
// +optional
Labels map[string]string `json:"labels,omitempty" protobuf:"bytes,11,rep,name=labels"`

// Annotations is an unstructured key value map stored with a resource that may be
// set by external tools to store and retrieve arbitrary metadata. They are not
// queryable and should be preserved when modifying objects.
// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations
// +optional
Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,12,rep,name=annotations"`
}

// StorageSpec defines the configured storage for a etcd members.
// If neither `emptyDir` nor `volumeClaimTemplate` is specified, then by default an [EmptyDir](https://kubernetes.io/docs/concepts/storage/volumes/#emptydir) will be used.
// +k8s:openapi-gen=true
type StorageSpec struct {
// EmptyDirVolumeSource to be used by the StatefulSets. If specified, used in place of any volumeClaimTemplate. More
// info: https://kubernetes.io/docs/concepts/storage/volumes/#emptydir
// +optional
EmptyDir *corev1.EmptyDirVolumeSource `json:"emptyDir,omitempty"`
// A PVC spec to be used by the StatefulSets.
// +optional
VolumeClaimTemplate EmbeddedPersistentVolumeClaim `json:"volumeClaimTemplate,omitempty"`
}

// EmbeddedPersistentVolumeClaim is an embedded version of k8s.io/api/core/v1.PersistentVolumeClaim.
// It contains TypeMeta and a reduced ObjectMeta.
type EmbeddedPersistentVolumeClaim struct {
metav1.TypeMeta `json:",inline"`

// EmbeddedMetadata contains metadata relevant to an EmbeddedResource.
// +optional
EmbeddedObjectMetadata `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

// Spec defines the desired characteristics of a volume requested by a pod author.
// More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims
// +optional
Spec corev1.PersistentVolumeClaimSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"`

// Status represents the current information/status of a persistent volume claim.
// Read-only.
// More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims
// +optional
Status corev1.PersistentVolumeClaimStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
}

func init() {
SchemeBuilder.Register(&EtcdCluster{}, &EtcdClusterList{})
}
36 changes: 33 additions & 3 deletions api/v1alpha1/etcdcluster_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ limitations under the License.
package v1alpha1

import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
Expand All @@ -42,8 +46,16 @@ var _ webhook.Defaulter = &EtcdCluster{}
// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *EtcdCluster) Default() {
etcdclusterlog.Info("default", "name", r.Name)
if r.Spec.Storage.Size.IsZero() {
r.Spec.Storage.Size = resource.MustParse("4Gi")
if r.Spec.Storage.EmptyDir == nil {
if len(r.Spec.Storage.VolumeClaimTemplate.Spec.AccessModes) == 0 {
r.Spec.Storage.VolumeClaimTemplate.Spec.AccessModes = []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}
}
storage := r.Spec.Storage.VolumeClaimTemplate.Spec.Resources.Requests.Storage()
if storage == nil || storage.IsZero() {
r.Spec.Storage.VolumeClaimTemplate.Spec.Resources.Requests = corev1.ResourceList{
corev1.ResourceStorage: resource.MustParse("4Gi"),
}
}
}
}

Expand All @@ -61,9 +73,27 @@ func (r *EtcdCluster) ValidateCreate() (admission.Warnings, error) {
func (r *EtcdCluster) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
etcdclusterlog.Info("validate update", "name", r.Name)
var warnings admission.Warnings
if old.(*EtcdCluster).Spec.Replicas != r.Spec.Replicas {
oldCluster := old.(*EtcdCluster)
if *oldCluster.Spec.Replicas != *r.Spec.Replicas {
warnings = append(warnings, "cluster resize is not currently supported")
}

var allErrors field.ErrorList
if oldCluster.Spec.Storage.EmptyDir != r.Spec.Storage.EmptyDir {
allErrors = append(allErrors, field.Invalid(
field.NewPath("spec", "storage", "emptyDir"),
r.Spec.Storage.EmptyDir,
"field is immutable"),
)
}

if len(allErrors) > 0 {
err := errors.NewInvalid(
schema.GroupKind{Group: GroupVersion.Group, Kind: "EtcdCluster"},
r.Name, allErrors)
return warnings, err
}

return warnings, nil
}

Expand Down
51 changes: 46 additions & 5 deletions api/v1alpha1/etcdcluster_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package v1alpha1
import (
. "github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/utils/ptr"
)
Expand All @@ -30,22 +32,39 @@ var _ = Describe("EtcdCluster Webhook", func() {
etcdCluster := &EtcdCluster{}
etcdCluster.Default()
gomega.Expect(etcdCluster.Spec.Replicas).To(gomega.BeNil(), "User should have an opportunity to create cluster with 0 replicas")
gomega.Expect(etcdCluster.Spec.Storage.Size).To(gomega.Equal(resource.MustParse("4Gi")))
gomega.Expect(etcdCluster.Spec.Storage.EmptyDir).To(gomega.BeNil())
storage := etcdCluster.Spec.Storage.VolumeClaimTemplate.Spec.Resources.Requests.Storage()
if gomega.Expect(storage).NotTo(gomega.BeNil()) {
gomega.Expect(*storage).To(gomega.Equal(resource.MustParse("4Gi")))
}
})

It("Should not override fields with default values if not empty", func() {
etcdCluster := &EtcdCluster{
Spec: EtcdClusterSpec{
Replicas: ptr.To(int32(5)),
Storage: Storage{
StorageClass: "local-path",
Size: resource.MustParse("10Gi"),
Storage: StorageSpec{
VolumeClaimTemplate: EmbeddedPersistentVolumeClaim{
Spec: corev1.PersistentVolumeClaimSpec{
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
StorageClassName: ptr.To("local-path"),
Resources: corev1.VolumeResourceRequirements{
Requests: map[corev1.ResourceName]resource.Quantity{
corev1.ResourceStorage: resource.MustParse("10Gi"),
},
},
},
},
},
},
}
etcdCluster.Default()
gomega.Expect(*etcdCluster.Spec.Replicas).To(gomega.Equal(int32(5)))
gomega.Expect(etcdCluster.Spec.Storage.Size).To(gomega.Equal(resource.MustParse("10Gi")))
gomega.Expect(etcdCluster.Spec.Storage.EmptyDir).To(gomega.BeNil())
storage := etcdCluster.Spec.Storage.VolumeClaimTemplate.Spec.Resources.Requests.Storage()
if gomega.Expect(storage).NotTo(gomega.BeNil()) {
gomega.Expect(*storage).To(gomega.Equal(resource.MustParse("10Gi")))
}
})
})

Expand All @@ -62,4 +81,26 @@ var _ = Describe("EtcdCluster Webhook", func() {
})
})

Context("When updating EtcdCluster under Validating Webhook", func() {
It("Should reject changing storage type", func() {
etcdCluster := &EtcdCluster{
Spec: EtcdClusterSpec{
Replicas: ptr.To(int32(1)),
Storage: StorageSpec{EmptyDir: &corev1.EmptyDirVolumeSource{}},
},
}
oldCluster := &EtcdCluster{
Spec: EtcdClusterSpec{
Replicas: ptr.To(int32(1)),
Storage: StorageSpec{EmptyDir: nil},
},
}
w, err := etcdCluster.ValidateUpdate(oldCluster)
gomega.Expect(w).To(gomega.BeEmpty())
if gomega.Expect(err).To(gomega.HaveOccurred()) {
statusErr := err.(*errors.StatusError)
gomega.Expect(statusErr.ErrStatus.Message).To(gomega.ContainSubstring("field is immutable"))
}
})
})
})
64 changes: 59 additions & 5 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 5715962

Please sign in to comment.