Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement mounting user certificates and keys #125

Merged
merged 13 commits into from
Apr 11, 2024
38 changes: 38 additions & 0 deletions api/v1alpha1/etcdcluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ type EtcdClusterSpec struct {
// +optional
PodDisruptionBudgetTemplate *EmbeddedPodDisruptionBudget `json:"podDisruptionBudgetTemplate,omitempty"`
Storage StorageSpec `json:"storage"`
// Security describes security settings of etcd (authentication, certificates, rbac)
// +optional
Security *SecuritySpec `json:"security,omitempty"`
Kirill-Garbar marked this conversation as resolved.
Show resolved Hide resolved
}

const (
Expand Down Expand Up @@ -200,6 +203,41 @@ type StorageSpec struct {
VolumeClaimTemplate EmbeddedPersistentVolumeClaim `json:"volumeClaimTemplate,omitempty"`
}

// SecuritySpec defines security settings for etcd.
// +k8s:openapi-gen=true
type SecuritySpec struct {
// +optional
Peer *PeerSpec `json:"peer,omitempty"`
// +optional
ClientServer *ClientServerSpec `json:"clientServer,omitempty"`
}

type PeerSpec struct {
// +optional
Ca SecretSpec `json:"ca,omitempty"`
// +optional
Cert SecretSpec `json:"cert,omitempty"`
}

type ClientServerSpec struct {
// +optional
Ca SecretSpec `json:"ca,omitempty"`
Kirill-Garbar marked this conversation as resolved.
Show resolved Hide resolved
// +optional
ServerCert SecretSpec `json:"serverCert,omitempty"`
// +optional
RootClientCert SecretSpec `json:"rootClientCert,omitempty"`
lllamnyp marked this conversation as resolved.
Show resolved Hide resolved
}

type SecretSpec struct {
// +optional
SecretName string `json:"secretName,omitempty"`
}

type RbacSpec struct {
// +optional
Enabled bool `json:"enabled,omitempty"`
}

// EmbeddedPersistentVolumeClaim is an embedded version of k8s.io/api/core/v1.PersistentVolumeClaim.
// It contains TypeMeta and a reduced ObjectMeta.
type EmbeddedPersistentVolumeClaim struct {
Expand Down
51 changes: 51 additions & 0 deletions api/v1alpha1/etcdcluster_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ func (r *EtcdCluster) ValidateCreate() (admission.Warnings, error) {
allErrors = append(allErrors, pdbErr...)
}

securityErr := r.validateSecurity()
if securityErr != nil {
allErrors = append(allErrors, securityErr...)
}

if errOptions := validateOptions(r); errOptions != nil {
allErrors = append(allErrors, field.Invalid(
field.NewPath("spec", "options"),
Expand Down Expand Up @@ -139,6 +144,11 @@ func (r *EtcdCluster) ValidateUpdate(old runtime.Object) (admission.Warnings, er
warnings = append(warnings, pdbWarnings...)
}

securityErr := r.validateSecurity()
if securityErr != nil {
allErrors = append(allErrors, securityErr...)
}

if errOptions := validateOptions(r); errOptions != nil {
allErrors = append(allErrors, field.Invalid(
field.NewPath("spec", "options"),
Expand Down Expand Up @@ -256,6 +266,47 @@ func (r *EtcdCluster) validatePdb() (admission.Warnings, field.ErrorList) {
return warnings, nil
}

func (r *EtcdCluster) validateSecurity() field.ErrorList {

var allErrors field.ErrorList

if r.Spec.Security == nil {
return nil
}

security := r.Spec.Security

if security.Peer != nil {
if (security.Peer.Ca.SecretName != "" && security.Peer.Cert.SecretName == "") ||
(security.Peer.Ca.SecretName == "" && security.Peer.Cert.SecretName != "") {

allErrors = append(allErrors, field.Invalid(
field.NewPath("spec", "security", "peer"),
security.Peer,
"both peer.ca.secretName and peer.cert.secretName must be filled or empty"),
)
}
}

if security.ClientServer != nil {
if (security.ClientServer.Ca.SecretName != "" && security.ClientServer.ServerCert.SecretName == "") ||
(security.ClientServer.Ca.SecretName == "" && security.ClientServer.ServerCert.SecretName != "") {

allErrors = append(allErrors, field.Invalid(
field.NewPath("spec", "security", "clientServer"),
security.ClientServer,
"both clientServer.ca.secretName and ClientServer.ServerCert.secretName must be filled or empty"),
)
}
}

if len(allErrors) > 0 {
return allErrors
}

return nil
}

func validateOptions(cluster *EtcdCluster) error {
if len(cluster.Spec.Options) == 0 {
return nil
Expand Down
95 changes: 95 additions & 0 deletions api/v1alpha1/etcdcluster_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,101 @@ var _ = Describe("EtcdCluster Webhook", func() {
})
})

Context("Validate Security", func() {
etcdCluster := &EtcdCluster{
Spec: EtcdClusterSpec{
Replicas: ptr.To(int32(3)),
Security: &SecuritySpec{},
},
}
It("Should admit enabled empty security", func() {
localCluster := etcdCluster.DeepCopy()
err := localCluster.validateSecurity()
Expect(err).To(BeNil())
})

It("Should reject if only one secret in peer section is defined", func() {
localCluster := etcdCluster.DeepCopy()
localCluster.Spec.Security.Peer = &PeerSpec{
Ca: SecretSpec{
SecretName: "test-peer-ca-cert",
},
}
err := localCluster.validateSecurity()
if Expect(err).NotTo(BeNil()) {
expectedFieldErr := field.Invalid(
field.NewPath("spec", "security", "peer"),
localCluster.Spec.Security.Peer,
"both peer.ca.secretName and peer.cert.secretName must be filled or empty",
)
if Expect(err).To(HaveLen(1)) {
Expect(*(err[0])).To(Equal(*expectedFieldErr))
}
}
})

It("Should reject if only one secret in peer section is defined", func() {
localCluster := etcdCluster.DeepCopy()
localCluster.Spec.Security.Peer = &PeerSpec{
Cert: SecretSpec{
SecretName: "test-peer-cert",
},
}
err := localCluster.validateSecurity()
if Expect(err).NotTo(BeNil()) {
expectedFieldErr := field.Invalid(
field.NewPath("spec", "security", "peer"),
localCluster.Spec.Security.Peer,
"both peer.ca.secretName and peer.cert.secretName must be filled or empty",
)
if Expect(err).To(HaveLen(1)) {
Expect(*(err[0])).To(Equal(*expectedFieldErr))
}
}
})

It("Should reject if only one secret in clientServer section is defined", func() {
localCluster := etcdCluster.DeepCopy()
localCluster.Spec.Security.ClientServer = &ClientServerSpec{
Ca: SecretSpec{
SecretName: "test-ca-server-cert",
},
}
err := localCluster.validateSecurity()
if Expect(err).NotTo(BeNil()) {
expectedFieldErr := field.Invalid(
field.NewPath("spec", "security", "clientServer"),
localCluster.Spec.Security.ClientServer,
"both clientServer.ca.secretName and ClientServer.ServerCert.secretName must be filled or empty",
)
if Expect(err).To(HaveLen(1)) {
Expect(*(err[0])).To(Equal(*expectedFieldErr))
}
}
})

It("Should reject if only one secret in clientServer section is defined", func() {
localCluster := etcdCluster.DeepCopy()
localCluster.Spec.Security.ClientServer = &ClientServerSpec{
ServerCert: SecretSpec{
SecretName: "test-server-cert",
},
}
err := localCluster.validateSecurity()
if Expect(err).NotTo(BeNil()) {
expectedFieldErr := field.Invalid(
field.NewPath("spec", "security", "clientServer"),
localCluster.Spec.Security.ClientServer,
"both clientServer.ca.secretName and ClientServer.ServerCert.secretName must be filled or empty",
)
if Expect(err).To(HaveLen(1)) {
Expect(*(err[0])).To(Equal(*expectedFieldErr))
}
}
})

})

Context("Validate PDB", func() {
etcdCluster := &EtcdCluster{
Spec: EtcdClusterSpec{
Expand Down
95 changes: 95 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

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

35 changes: 35 additions & 0 deletions charts/etcd-operator/crds/etcd-cluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4318,6 +4318,41 @@ spec:
format: int32
minimum: 0
type: integer
security:
description: Security describes security settings of etcd (authentication, certificates, rbac)
properties:
clientServer:
properties:
ca:
properties:
secretName:
type: string
type: object
rootClientCert:
properties:
secretName:
type: string
type: object
serverCert:
properties:
secretName:
type: string
type: object
type: object
peer:
properties:
ca:
properties:
secretName:
type: string
type: object
cert:
properties:
secretName:
type: string
type: object
type: object
type: object
storage:
description: |-
StorageSpec defines the configured storage for a etcd members.
Expand Down
Loading