Skip to content

Commit 5715962

Browse files
authored
Implement PVC or EmptyDir for Storage (#67)
fixes #49
1 parent b3b91c6 commit 5715962

File tree

7 files changed

+700
-53
lines changed

7 files changed

+700
-53
lines changed

api/v1alpha1/etcdcluster_types.go

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,18 @@ limitations under the License.
1717
package v1alpha1
1818

1919
import (
20-
"k8s.io/apimachinery/pkg/api/resource"
20+
corev1 "k8s.io/api/core/v1"
2121
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2222
)
2323

24-
type Storage struct {
25-
StorageClass string `json:"storageClass"`
26-
Size resource.Quantity `json:"size"`
27-
}
28-
2924
// EtcdClusterSpec defines the desired state of EtcdCluster
3025
type EtcdClusterSpec struct {
3126
// Replicas is the count of etcd instances in cluster.
3227
// +optional
3328
// +kubebuilder:default:=3
3429
// +kubebuilder:validation:Minimum:=0
35-
Replicas *int32 `json:"replicas,omitempty"`
36-
Storage Storage `json:"storage,omitempty"`
30+
Replicas *int32 `json:"replicas,omitempty"`
31+
Storage StorageSpec `json:"storage"`
3732
}
3833

3934
const (
@@ -76,6 +71,67 @@ type EtcdClusterList struct {
7671
Items []EtcdCluster `json:"items"`
7772
}
7873

74+
// EmbeddedObjectMetadata contains a subset of the fields included in k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta
75+
// Only fields which are relevant to embedded resources are included.
76+
type EmbeddedObjectMetadata struct {
77+
// Name must be unique within a namespace. Is required when creating resources, although
78+
// some resources may allow a client to request the generation of an appropriate name
79+
// automatically. Name is primarily intended for creation idempotence and configuration
80+
// definition.
81+
// Cannot be updated.
82+
// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names
83+
// +optional
84+
Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"`
85+
86+
// Labels Map of string keys and values that can be used to organize and categorize
87+
// (scope and select) objects. May match selectors of replication controllers
88+
// and services.
89+
// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels
90+
// +optional
91+
Labels map[string]string `json:"labels,omitempty" protobuf:"bytes,11,rep,name=labels"`
92+
93+
// Annotations is an unstructured key value map stored with a resource that may be
94+
// set by external tools to store and retrieve arbitrary metadata. They are not
95+
// queryable and should be preserved when modifying objects.
96+
// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations
97+
// +optional
98+
Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,12,rep,name=annotations"`
99+
}
100+
101+
// StorageSpec defines the configured storage for a etcd members.
102+
// If neither `emptyDir` nor `volumeClaimTemplate` is specified, then by default an [EmptyDir](https://kubernetes.io/docs/concepts/storage/volumes/#emptydir) will be used.
103+
// +k8s:openapi-gen=true
104+
type StorageSpec struct {
105+
// EmptyDirVolumeSource to be used by the StatefulSets. If specified, used in place of any volumeClaimTemplate. More
106+
// info: https://kubernetes.io/docs/concepts/storage/volumes/#emptydir
107+
// +optional
108+
EmptyDir *corev1.EmptyDirVolumeSource `json:"emptyDir,omitempty"`
109+
// A PVC spec to be used by the StatefulSets.
110+
// +optional
111+
VolumeClaimTemplate EmbeddedPersistentVolumeClaim `json:"volumeClaimTemplate,omitempty"`
112+
}
113+
114+
// EmbeddedPersistentVolumeClaim is an embedded version of k8s.io/api/core/v1.PersistentVolumeClaim.
115+
// It contains TypeMeta and a reduced ObjectMeta.
116+
type EmbeddedPersistentVolumeClaim struct {
117+
metav1.TypeMeta `json:",inline"`
118+
119+
// EmbeddedMetadata contains metadata relevant to an EmbeddedResource.
120+
// +optional
121+
EmbeddedObjectMetadata `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
122+
123+
// Spec defines the desired characteristics of a volume requested by a pod author.
124+
// More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims
125+
// +optional
126+
Spec corev1.PersistentVolumeClaimSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"`
127+
128+
// Status represents the current information/status of a persistent volume claim.
129+
// Read-only.
130+
// More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims
131+
// +optional
132+
Status corev1.PersistentVolumeClaimStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
133+
}
134+
79135
func init() {
80136
SchemeBuilder.Register(&EtcdCluster{}, &EtcdClusterList{})
81137
}

api/v1alpha1/etcdcluster_webhook.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ limitations under the License.
1717
package v1alpha1
1818

1919
import (
20+
corev1 "k8s.io/api/core/v1"
21+
"k8s.io/apimachinery/pkg/api/errors"
2022
"k8s.io/apimachinery/pkg/api/resource"
2123
"k8s.io/apimachinery/pkg/runtime"
24+
"k8s.io/apimachinery/pkg/runtime/schema"
25+
"k8s.io/apimachinery/pkg/util/validation/field"
2226
ctrl "sigs.k8s.io/controller-runtime"
2327
logf "sigs.k8s.io/controller-runtime/pkg/log"
2428
"sigs.k8s.io/controller-runtime/pkg/webhook"
@@ -42,8 +46,16 @@ var _ webhook.Defaulter = &EtcdCluster{}
4246
// Default implements webhook.Defaulter so a webhook will be registered for the type
4347
func (r *EtcdCluster) Default() {
4448
etcdclusterlog.Info("default", "name", r.Name)
45-
if r.Spec.Storage.Size.IsZero() {
46-
r.Spec.Storage.Size = resource.MustParse("4Gi")
49+
if r.Spec.Storage.EmptyDir == nil {
50+
if len(r.Spec.Storage.VolumeClaimTemplate.Spec.AccessModes) == 0 {
51+
r.Spec.Storage.VolumeClaimTemplate.Spec.AccessModes = []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}
52+
}
53+
storage := r.Spec.Storage.VolumeClaimTemplate.Spec.Resources.Requests.Storage()
54+
if storage == nil || storage.IsZero() {
55+
r.Spec.Storage.VolumeClaimTemplate.Spec.Resources.Requests = corev1.ResourceList{
56+
corev1.ResourceStorage: resource.MustParse("4Gi"),
57+
}
58+
}
4759
}
4860
}
4961

@@ -61,9 +73,27 @@ func (r *EtcdCluster) ValidateCreate() (admission.Warnings, error) {
6173
func (r *EtcdCluster) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
6274
etcdclusterlog.Info("validate update", "name", r.Name)
6375
var warnings admission.Warnings
64-
if old.(*EtcdCluster).Spec.Replicas != r.Spec.Replicas {
76+
oldCluster := old.(*EtcdCluster)
77+
if *oldCluster.Spec.Replicas != *r.Spec.Replicas {
6578
warnings = append(warnings, "cluster resize is not currently supported")
6679
}
80+
81+
var allErrors field.ErrorList
82+
if oldCluster.Spec.Storage.EmptyDir != r.Spec.Storage.EmptyDir {
83+
allErrors = append(allErrors, field.Invalid(
84+
field.NewPath("spec", "storage", "emptyDir"),
85+
r.Spec.Storage.EmptyDir,
86+
"field is immutable"),
87+
)
88+
}
89+
90+
if len(allErrors) > 0 {
91+
err := errors.NewInvalid(
92+
schema.GroupKind{Group: GroupVersion.Group, Kind: "EtcdCluster"},
93+
r.Name, allErrors)
94+
return warnings, err
95+
}
96+
6797
return warnings, nil
6898
}
6999

api/v1alpha1/etcdcluster_webhook_test.go

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package v1alpha1
1919
import (
2020
. "github.com/onsi/ginkgo/v2"
2121
"github.com/onsi/gomega"
22+
corev1 "k8s.io/api/core/v1"
23+
"k8s.io/apimachinery/pkg/api/errors"
2224
"k8s.io/apimachinery/pkg/api/resource"
2325
"k8s.io/utils/ptr"
2426
)
@@ -30,22 +32,39 @@ var _ = Describe("EtcdCluster Webhook", func() {
3032
etcdCluster := &EtcdCluster{}
3133
etcdCluster.Default()
3234
gomega.Expect(etcdCluster.Spec.Replicas).To(gomega.BeNil(), "User should have an opportunity to create cluster with 0 replicas")
33-
gomega.Expect(etcdCluster.Spec.Storage.Size).To(gomega.Equal(resource.MustParse("4Gi")))
35+
gomega.Expect(etcdCluster.Spec.Storage.EmptyDir).To(gomega.BeNil())
36+
storage := etcdCluster.Spec.Storage.VolumeClaimTemplate.Spec.Resources.Requests.Storage()
37+
if gomega.Expect(storage).NotTo(gomega.BeNil()) {
38+
gomega.Expect(*storage).To(gomega.Equal(resource.MustParse("4Gi")))
39+
}
3440
})
3541

3642
It("Should not override fields with default values if not empty", func() {
3743
etcdCluster := &EtcdCluster{
3844
Spec: EtcdClusterSpec{
3945
Replicas: ptr.To(int32(5)),
40-
Storage: Storage{
41-
StorageClass: "local-path",
42-
Size: resource.MustParse("10Gi"),
46+
Storage: StorageSpec{
47+
VolumeClaimTemplate: EmbeddedPersistentVolumeClaim{
48+
Spec: corev1.PersistentVolumeClaimSpec{
49+
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
50+
StorageClassName: ptr.To("local-path"),
51+
Resources: corev1.VolumeResourceRequirements{
52+
Requests: map[corev1.ResourceName]resource.Quantity{
53+
corev1.ResourceStorage: resource.MustParse("10Gi"),
54+
},
55+
},
56+
},
57+
},
4358
},
4459
},
4560
}
4661
etcdCluster.Default()
4762
gomega.Expect(*etcdCluster.Spec.Replicas).To(gomega.Equal(int32(5)))
48-
gomega.Expect(etcdCluster.Spec.Storage.Size).To(gomega.Equal(resource.MustParse("10Gi")))
63+
gomega.Expect(etcdCluster.Spec.Storage.EmptyDir).To(gomega.BeNil())
64+
storage := etcdCluster.Spec.Storage.VolumeClaimTemplate.Spec.Resources.Requests.Storage()
65+
if gomega.Expect(storage).NotTo(gomega.BeNil()) {
66+
gomega.Expect(*storage).To(gomega.Equal(resource.MustParse("10Gi")))
67+
}
4968
})
5069
})
5170

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

84+
Context("When updating EtcdCluster under Validating Webhook", func() {
85+
It("Should reject changing storage type", func() {
86+
etcdCluster := &EtcdCluster{
87+
Spec: EtcdClusterSpec{
88+
Replicas: ptr.To(int32(1)),
89+
Storage: StorageSpec{EmptyDir: &corev1.EmptyDirVolumeSource{}},
90+
},
91+
}
92+
oldCluster := &EtcdCluster{
93+
Spec: EtcdClusterSpec{
94+
Replicas: ptr.To(int32(1)),
95+
Storage: StorageSpec{EmptyDir: nil},
96+
},
97+
}
98+
w, err := etcdCluster.ValidateUpdate(oldCluster)
99+
gomega.Expect(w).To(gomega.BeEmpty())
100+
if gomega.Expect(err).To(gomega.HaveOccurred()) {
101+
statusErr := err.(*errors.StatusError)
102+
gomega.Expect(statusErr.ErrStatus.Message).To(gomega.ContainSubstring("field is immutable"))
103+
}
104+
})
105+
})
65106
})

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 59 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)