diff --git a/api/v1alpha1/etcdcluster_types.go b/api/v1alpha1/etcdcluster_types.go index a1e2b658..205861ff 100644 --- a/api/v1alpha1/etcdcluster_types.go +++ b/api/v1alpha1/etcdcluster_types.go @@ -37,6 +37,12 @@ type EtcdClusterSpec struct { Options map[string]string `json:"options,omitempty"` // PodTemplate defines the desired state of PodSpec for etcd members. If not specified, default values will be used. PodTemplate PodTemplate `json:"podTemplate,omitempty"` + // Service defines the desired state of Service for etcd members. If not specified, default values will be used. + // +optional + ServiceTemplate *EmbeddedService `json:"serviceTemplate,omitempty"` + // HeadlessService defines the desired state of HeadlessService for etcd members. If not specified, default values will be used. + // +optional + HeadlessServiceTemplate *EmbeddedMetadataResource `json:"headlessServiceTemplate,omitempty"` // PodDisruptionBudgetTemplate describes PDB resource to create for etcd cluster members. Nil to disable. // +optional PodDisruptionBudgetTemplate *EmbeddedPodDisruptionBudget `json:"podDisruptionBudgetTemplate,omitempty"` @@ -128,6 +134,15 @@ type EmbeddedObjectMetadata struct { Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,12,rep,name=annotations"` } +// ToObjectMeta converts EmbeddedObjectMetadata to metav1.ObjectMeta +func (r *EmbeddedObjectMetadata) ToObjectMeta() metav1.ObjectMeta { + return metav1.ObjectMeta{ + Name: r.Name, + Labels: r.Labels, + Annotations: r.Annotations, + } +} + // PodTemplate allows overrides, such as sidecars, init containers, changes to the security context, etc to the pod template generated by the operator. type PodTemplate struct { // EmbeddedObjectMetadata contains metadata relevant to an EmbeddedResource @@ -226,6 +241,21 @@ type PodDisruptionBudgetSpec struct { MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty"` } +type EmbeddedService struct { + // EmbeddedMetadata contains metadata relevant to an EmbeddedResource. + // +optional + EmbeddedObjectMetadata `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Spec defines the behavior of the service. + // +optional + Spec corev1.ServiceSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` +} + +type EmbeddedMetadataResource struct { + // EmbeddedMetadata contains metadata relevant to an EmbeddedResource. + // +optional + EmbeddedObjectMetadata `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` +} + func init() { SchemeBuilder.Register(&EtcdCluster{}, &EtcdClusterList{}) } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8094a55a..36d0dd5a 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -27,6 +27,22 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EmbeddedMetadataResource) DeepCopyInto(out *EmbeddedMetadataResource) { + *out = *in + in.EmbeddedObjectMetadata.DeepCopyInto(&out.EmbeddedObjectMetadata) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EmbeddedMetadataResource. +func (in *EmbeddedMetadataResource) DeepCopy() *EmbeddedMetadataResource { + if in == nil { + return nil + } + out := new(EmbeddedMetadataResource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EmbeddedObjectMetadata) DeepCopyInto(out *EmbeddedObjectMetadata) { *out = *in @@ -92,6 +108,23 @@ func (in *EmbeddedPodDisruptionBudget) DeepCopy() *EmbeddedPodDisruptionBudget { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EmbeddedService) DeepCopyInto(out *EmbeddedService) { + *out = *in + in.EmbeddedObjectMetadata.DeepCopyInto(&out.EmbeddedObjectMetadata) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EmbeddedService. +func (in *EmbeddedService) DeepCopy() *EmbeddedService { + if in == nil { + return nil + } + out := new(EmbeddedService) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EtcdCluster) DeepCopyInto(out *EtcdCluster) { *out = *in @@ -167,6 +200,16 @@ func (in *EtcdClusterSpec) DeepCopyInto(out *EtcdClusterSpec) { } } in.PodTemplate.DeepCopyInto(&out.PodTemplate) + if in.ServiceTemplate != nil { + in, out := &in.ServiceTemplate, &out.ServiceTemplate + *out = new(EmbeddedService) + (*in).DeepCopyInto(*out) + } + if in.HeadlessServiceTemplate != nil { + in, out := &in.HeadlessServiceTemplate, &out.HeadlessServiceTemplate + *out = new(EmbeddedMetadataResource) + (*in).DeepCopyInto(*out) + } if in.PodDisruptionBudgetTemplate != nil { in, out := &in.PodDisruptionBudgetTemplate, &out.PodDisruptionBudgetTemplate *out = new(EmbeddedPodDisruptionBudget) diff --git a/charts/etcd-operator/crds/etcd-cluster.yaml b/charts/etcd-operator/crds/etcd-cluster.yaml index ded00030..3abbf976 100644 --- a/charts/etcd-operator/crds/etcd-cluster.yaml +++ b/charts/etcd-operator/crds/etcd-cluster.yaml @@ -49,6 +49,41 @@ spec: spec: description: EtcdClusterSpec defines the desired state of EtcdCluster properties: + headlessServiceTemplate: + description: HeadlessService defines the desired state of HeadlessService for etcd members. If not specified, default values will be used. + properties: + metadata: + description: EmbeddedMetadata contains metadata relevant to an EmbeddedResource. + properties: + annotations: + additionalProperties: + type: string + description: |- + 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 + type: object + labels: + additionalProperties: + type: string + description: |- + 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 + type: object + name: + description: |- + 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 + type: string + type: object + type: object options: additionalProperties: type: string @@ -189,6 +224,385 @@ spec: type: string type: object type: object + serviceTemplate: + description: Service defines the desired state of Service for etcd members. If not specified, default values will be used. + properties: + metadata: + description: EmbeddedMetadata contains metadata relevant to an EmbeddedResource. + properties: + annotations: + additionalProperties: + type: string + description: |- + 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 + type: object + labels: + additionalProperties: + type: string + description: |- + 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 + type: object + name: + description: |- + 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 + type: string + type: object + spec: + description: Spec defines the behavior of the service. + properties: + allocateLoadBalancerNodePorts: + description: |- + allocateLoadBalancerNodePorts defines if NodePorts will be automatically + allocated for services with type LoadBalancer. Default is "true". It + may be set to "false" if the cluster load-balancer does not rely on + NodePorts. If the caller requests specific NodePorts (by specifying a + value), those requests will be respected, regardless of this field. + This field may only be set for services with type LoadBalancer and will + be cleared if the type is changed to any other type. + type: boolean + clusterIP: + description: |- + clusterIP is the IP address of the service and is usually assigned + randomly. If an address is specified manually, is in-range (as per + system configuration), and is not in use, it will be allocated to the + service; otherwise creation of the service will fail. This field may not + be changed through updates unless the type field is also being changed + to ExternalName (which requires this field to be blank) or the type + field is being changed from ExternalName (in which case this field may + optionally be specified, as describe above). Valid values are "None", + empty string (""), or a valid IP address. Setting this to "None" makes a + "headless service" (no virtual IP), which is useful when direct endpoint + connections are preferred and proxying is not required. Only applies to + types ClusterIP, NodePort, and LoadBalancer. If this field is specified + when creating a Service of type ExternalName, creation will fail. This + field will be wiped when updating a Service to type ExternalName. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies + type: string + clusterIPs: + description: |- + ClusterIPs is a list of IP addresses assigned to this service, and are + usually assigned randomly. If an address is specified manually, is + in-range (as per system configuration), and is not in use, it will be + allocated to the service; otherwise creation of the service will fail. + This field may not be changed through updates unless the type field is + also being changed to ExternalName (which requires this field to be + empty) or the type field is being changed from ExternalName (in which + case this field may optionally be specified, as describe above). Valid + values are "None", empty string (""), or a valid IP address. Setting + this to "None" makes a "headless service" (no virtual IP), which is + useful when direct endpoint connections are preferred and proxying is + not required. Only applies to types ClusterIP, NodePort, and + LoadBalancer. If this field is specified when creating a Service of type + ExternalName, creation will fail. This field will be wiped when updating + a Service to type ExternalName. If this field is not specified, it will + be initialized from the clusterIP field. If this field is specified, + clients must ensure that clusterIPs[0] and clusterIP have the same + value. + + + This field may hold a maximum of two entries (dual-stack IPs, in either order). + These IPs must correspond to the values of the ipFamilies field. Both + clusterIPs and ipFamilies are governed by the ipFamilyPolicy field. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies + items: + type: string + type: array + x-kubernetes-list-type: atomic + externalIPs: + description: |- + externalIPs is a list of IP addresses for which nodes in the cluster + will also accept traffic for this service. These IPs are not managed by + Kubernetes. The user is responsible for ensuring that traffic arrives + at a node with this IP. A common example is external load-balancers + that are not part of the Kubernetes system. + items: + type: string + type: array + x-kubernetes-list-type: atomic + externalName: + description: |- + externalName is the external reference that discovery mechanisms will + return as an alias for this service (e.g. a DNS CNAME record). No + proxying will be involved. Must be a lowercase RFC-1123 hostname + (https://tools.ietf.org/html/rfc1123) and requires `type` to be "ExternalName". + type: string + externalTrafficPolicy: + description: |- + externalTrafficPolicy describes how nodes distribute service traffic they + receive on one of the Service's "externally-facing" addresses (NodePorts, + ExternalIPs, and LoadBalancer IPs). If set to "Local", the proxy will configure + the service in a way that assumes that external load balancers will take care + of balancing the service traffic between nodes, and so each node will deliver + traffic only to the node-local endpoints of the service, without masquerading + the client source IP. (Traffic mistakenly sent to a node with no endpoints will + be dropped.) The default value, "Cluster", uses the standard behavior of + routing to all endpoints evenly (possibly modified by topology and other + features). Note that traffic sent to an External IP or LoadBalancer IP from + within the cluster will always get "Cluster" semantics, but clients sending to + a NodePort from within the cluster may need to take traffic policy into account + when picking a node. + type: string + healthCheckNodePort: + description: |- + healthCheckNodePort specifies the healthcheck nodePort for the service. + This only applies when type is set to LoadBalancer and + externalTrafficPolicy is set to Local. If a value is specified, is + in-range, and is not in use, it will be used. If not specified, a value + will be automatically allocated. External systems (e.g. load-balancers) + can use this port to determine if a given node holds endpoints for this + service or not. If this field is specified when creating a Service + which does not need it, creation will fail. This field will be wiped + when updating a Service to no longer need it (e.g. changing type). + This field cannot be updated once set. + format: int32 + type: integer + internalTrafficPolicy: + description: |- + InternalTrafficPolicy describes how nodes distribute service traffic they + receive on the ClusterIP. If set to "Local", the proxy will assume that pods + only want to talk to endpoints of the service on the same node as the pod, + dropping the traffic if there are no local endpoints. The default value, + "Cluster", uses the standard behavior of routing to all endpoints evenly + (possibly modified by topology and other features). + type: string + ipFamilies: + description: |- + IPFamilies is a list of IP families (e.g. IPv4, IPv6) assigned to this + service. This field is usually assigned automatically based on cluster + configuration and the ipFamilyPolicy field. If this field is specified + manually, the requested family is available in the cluster, + and ipFamilyPolicy allows it, it will be used; otherwise creation of + the service will fail. This field is conditionally mutable: it allows + for adding or removing a secondary IP family, but it does not allow + changing the primary IP family of the Service. Valid values are "IPv4" + and "IPv6". This field only applies to Services of types ClusterIP, + NodePort, and LoadBalancer, and does apply to "headless" services. + This field will be wiped when updating a Service to type ExternalName. + + + This field may hold a maximum of two entries (dual-stack families, in + either order). These families must correspond to the values of the + clusterIPs field, if specified. Both clusterIPs and ipFamilies are + governed by the ipFamilyPolicy field. + items: + description: |- + IPFamily represents the IP Family (IPv4 or IPv6). This type is used + to express the family of an IP expressed by a type (e.g. service.spec.ipFamilies). + type: string + type: array + x-kubernetes-list-type: atomic + ipFamilyPolicy: + description: |- + IPFamilyPolicy represents the dual-stack-ness requested or required by + this Service. If there is no value provided, then this field will be set + to SingleStack. Services can be "SingleStack" (a single IP family), + "PreferDualStack" (two IP families on dual-stack configured clusters or + a single IP family on single-stack clusters), or "RequireDualStack" + (two IP families on dual-stack configured clusters, otherwise fail). The + ipFamilies and clusterIPs fields depend on the value of this field. This + field will be wiped when updating a service to type ExternalName. + type: string + loadBalancerClass: + description: |- + loadBalancerClass is the class of the load balancer implementation this Service belongs to. + If specified, the value of this field must be a label-style identifier, with an optional prefix, + e.g. "internal-vip" or "example.com/internal-vip". Unprefixed names are reserved for end-users. + This field can only be set when the Service type is 'LoadBalancer'. If not set, the default load + balancer implementation is used, today this is typically done through the cloud provider integration, + but should apply for any default implementation. If set, it is assumed that a load balancer + implementation is watching for Services with a matching class. Any default load balancer + implementation (e.g. cloud providers) should ignore Services that set this field. + This field can only be set when creating or updating a Service to type 'LoadBalancer'. + Once set, it can not be changed. This field will be wiped when a service is updated to a non 'LoadBalancer' type. + type: string + loadBalancerIP: + description: |- + Only applies to Service Type: LoadBalancer. + This feature depends on whether the underlying cloud-provider supports specifying + the loadBalancerIP when a load balancer is created. + This field will be ignored if the cloud-provider does not support the feature. + Deprecated: This field was under-specified and its meaning varies across implementations. + Using it is non-portable and it may not support dual-stack. + Users are encouraged to use implementation-specific annotations when available. + type: string + loadBalancerSourceRanges: + description: |- + If specified and supported by the platform, this will restrict traffic through the cloud-provider + load-balancer will be restricted to the specified client IPs. This field will be ignored if the + cloud-provider does not support the feature." + More info: https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/ + items: + type: string + type: array + x-kubernetes-list-type: atomic + ports: + description: |- + The list of ports that are exposed by this service. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies + items: + description: ServicePort contains information on service's port. + properties: + appProtocol: + description: |- + The application protocol for this port. + This is used as a hint for implementations to offer richer behavior for protocols that they understand. + This field follows standard Kubernetes label syntax. + Valid values are either: + + + * Un-prefixed protocol names - reserved for IANA standard service names (as per + RFC-6335 and https://www.iana.org/assignments/service-names). + + + * Kubernetes-defined prefixed names: + * 'kubernetes.io/h2c' - HTTP/2 prior knowledge over cleartext as described in https://www.rfc-editor.org/rfc/rfc9113.html#name-starting-http-2-with-prior- + * 'kubernetes.io/ws' - WebSocket over cleartext as described in https://www.rfc-editor.org/rfc/rfc6455 + * 'kubernetes.io/wss' - WebSocket over TLS as described in https://www.rfc-editor.org/rfc/rfc6455 + + + * Other protocols should use implementation-defined prefixed names such as + mycompany.com/my-custom-protocol. + type: string + name: + description: |- + The name of this port within the service. This must be a DNS_LABEL. + All ports within a ServiceSpec must have unique names. When considering + the endpoints for a Service, this must match the 'name' field in the + EndpointPort. + Optional if only one ServicePort is defined on this service. + type: string + nodePort: + description: |- + The port on each node on which this service is exposed when type is + NodePort or LoadBalancer. Usually assigned by the system. If a value is + specified, in-range, and not in use it will be used, otherwise the + operation will fail. If not specified, a port will be allocated if this + Service requires one. If this field is specified when creating a + Service which does not need it, creation will fail. This field will be + wiped when updating a Service to no longer need it (e.g. changing type + from NodePort to ClusterIP). + More info: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport + format: int32 + type: integer + port: + description: The port that will be exposed by this service. + format: int32 + type: integer + protocol: + default: TCP + description: |- + The IP protocol for this port. Supports "TCP", "UDP", and "SCTP". + Default is TCP. + type: string + targetPort: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the pods targeted by the service. + Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME. + If this is a string, it will be looked up as a named port in the + target Pod's container ports. If this is not specified, the value + of the 'port' field is used (an identity map). + This field is ignored for services with clusterIP=None, and should be + omitted or set equal to the 'port' field. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service + x-kubernetes-int-or-string: true + required: + - port + type: object + type: array + x-kubernetes-list-map-keys: + - port + - protocol + x-kubernetes-list-type: map + publishNotReadyAddresses: + description: |- + publishNotReadyAddresses indicates that any agent which deals with endpoints for this + Service should disregard any indications of ready/not-ready. + The primary use case for setting this field is for a StatefulSet's Headless Service to + propagate SRV DNS records for its Pods for the purpose of peer discovery. + The Kubernetes controllers that generate Endpoints and EndpointSlice resources for + Services interpret this to mean that all endpoints are considered "ready" even if the + Pods themselves are not. Agents which consume only Kubernetes generated endpoints + through the Endpoints or EndpointSlice resources can safely assume this behavior. + type: boolean + selector: + additionalProperties: + type: string + description: |- + Route service traffic to pods with label keys and values matching this + selector. If empty or not present, the service is assumed to have an + external process managing its endpoints, which Kubernetes will not + modify. Only applies to types ClusterIP, NodePort, and LoadBalancer. + Ignored if type is ExternalName. + More info: https://kubernetes.io/docs/concepts/services-networking/service/ + type: object + x-kubernetes-map-type: atomic + sessionAffinity: + description: |- + Supports "ClientIP" and "None". Used to maintain session affinity. + Enable client IP based session affinity. + Must be ClientIP or None. + Defaults to None. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies + type: string + sessionAffinityConfig: + description: sessionAffinityConfig contains the configurations of session affinity. + properties: + clientIP: + description: clientIP contains the configurations of Client IP based session affinity. + properties: + timeoutSeconds: + description: |- + timeoutSeconds specifies the seconds of ClientIP type session sticky time. + The value must be >0 && <=86400(for 1 day) if ServiceAffinity == "ClientIP". + Default value is 10800(for 3 hours). + format: int32 + type: integer + type: object + type: object + trafficDistribution: + description: |- + TrafficDistribution offers a way to express preferences for how traffic is + distributed to Service endpoints. Implementations can use this field as a + hint, but are not required to guarantee strict adherence. If the field is + not set, the implementation will apply its default routing strategy. If set + to "PreferClose", implementations should prioritize endpoints that are + topologically close (e.g., same zone). + type: string + type: + description: |- + type determines how the Service is exposed. Defaults to ClusterIP. Valid + options are ExternalName, ClusterIP, NodePort, and LoadBalancer. + "ClusterIP" allocates a cluster-internal IP address for load-balancing + to endpoints. Endpoints are determined by the selector or if that is not + specified, by manual construction of an Endpoints object or + EndpointSlice objects. If clusterIP is "None", no virtual IP is + allocated and the endpoints are published as a set of endpoints rather + than a virtual IP. + "NodePort" builds on ClusterIP and allocates a port on every node which + routes to the same endpoints as the clusterIP. + "LoadBalancer" builds on NodePort and creates an external load-balancer + (if supported in the current cloud) which routes to the same endpoints + as the clusterIP. + "ExternalName" aliases this service to the specified externalName. + Several other fields do not apply to ExternalName services. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: string + type: object + type: object storage: description: |- StorageSpec defines the configured storage for a etcd members. @@ -281,6 +695,7 @@ spec: items: type: string type: array + x-kubernetes-list-type: atomic dataSource: description: |- dataSource field can be used to specify either: @@ -417,11 +832,13 @@ spec: items: type: string type: array + x-kubernetes-list-type: atomic required: - key - operator type: object type: array + x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string @@ -449,7 +866,7 @@ spec: If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource exists. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass + More info: https://kubernetes.io/docs/concepts/storage/volume-attributes-classes/ (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. type: string volumeMode: @@ -474,6 +891,7 @@ spec: items: type: string type: array + x-kubernetes-list-type: atomic allocatedResourceStatuses: additionalProperties: description: |- @@ -505,7 +923,7 @@ spec: conditions: description: |- conditions is the current Condition of persistent volume claim. If underlying persistent volume is being - resized then the Condition will be set to 'ResizeStarted'. + resized then the Condition will be set to 'Resizing'. items: description: PersistentVolumeClaimCondition contains details about state of pvc properties: @@ -523,7 +941,7 @@ spec: reason: description: |- reason is a unique, this should be a short, machine understandable string that gives the reason - for condition's last transition. If it reports "ResizeStarted" that means the underlying + for condition's last transition. If it reports "Resizing" that means the underlying persistent volume is being resized. type: string status: @@ -536,6 +954,9 @@ spec: - type type: object type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map currentVolumeAttributesClassName: description: |- currentVolumeAttributesClassName is the current name of the VolumeAttributesClass the PVC is using. diff --git a/config/crd/bases/etcd.aenix.io_etcdclusters.yaml b/config/crd/bases/etcd.aenix.io_etcdclusters.yaml index de16f112..a824d508 100644 --- a/config/crd/bases/etcd.aenix.io_etcdclusters.yaml +++ b/config/crd/bases/etcd.aenix.io_etcdclusters.yaml @@ -39,6 +39,41 @@ spec: spec: description: EtcdClusterSpec defines the desired state of EtcdCluster properties: + headlessServiceTemplate: + description: HeadlessService defines the desired state of HeadlessService for etcd members. If not specified, default values will be used. + properties: + metadata: + description: EmbeddedMetadata contains metadata relevant to an EmbeddedResource. + properties: + annotations: + additionalProperties: + type: string + description: |- + 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 + type: object + labels: + additionalProperties: + type: string + description: |- + 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 + type: object + name: + description: |- + 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 + type: string + type: object + type: object options: additionalProperties: type: string @@ -179,6 +214,385 @@ spec: type: string type: object type: object + serviceTemplate: + description: Service defines the desired state of Service for etcd members. If not specified, default values will be used. + properties: + metadata: + description: EmbeddedMetadata contains metadata relevant to an EmbeddedResource. + properties: + annotations: + additionalProperties: + type: string + description: |- + 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 + type: object + labels: + additionalProperties: + type: string + description: |- + 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 + type: object + name: + description: |- + 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 + type: string + type: object + spec: + description: Spec defines the behavior of the service. + properties: + allocateLoadBalancerNodePorts: + description: |- + allocateLoadBalancerNodePorts defines if NodePorts will be automatically + allocated for services with type LoadBalancer. Default is "true". It + may be set to "false" if the cluster load-balancer does not rely on + NodePorts. If the caller requests specific NodePorts (by specifying a + value), those requests will be respected, regardless of this field. + This field may only be set for services with type LoadBalancer and will + be cleared if the type is changed to any other type. + type: boolean + clusterIP: + description: |- + clusterIP is the IP address of the service and is usually assigned + randomly. If an address is specified manually, is in-range (as per + system configuration), and is not in use, it will be allocated to the + service; otherwise creation of the service will fail. This field may not + be changed through updates unless the type field is also being changed + to ExternalName (which requires this field to be blank) or the type + field is being changed from ExternalName (in which case this field may + optionally be specified, as describe above). Valid values are "None", + empty string (""), or a valid IP address. Setting this to "None" makes a + "headless service" (no virtual IP), which is useful when direct endpoint + connections are preferred and proxying is not required. Only applies to + types ClusterIP, NodePort, and LoadBalancer. If this field is specified + when creating a Service of type ExternalName, creation will fail. This + field will be wiped when updating a Service to type ExternalName. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies + type: string + clusterIPs: + description: |- + ClusterIPs is a list of IP addresses assigned to this service, and are + usually assigned randomly. If an address is specified manually, is + in-range (as per system configuration), and is not in use, it will be + allocated to the service; otherwise creation of the service will fail. + This field may not be changed through updates unless the type field is + also being changed to ExternalName (which requires this field to be + empty) or the type field is being changed from ExternalName (in which + case this field may optionally be specified, as describe above). Valid + values are "None", empty string (""), or a valid IP address. Setting + this to "None" makes a "headless service" (no virtual IP), which is + useful when direct endpoint connections are preferred and proxying is + not required. Only applies to types ClusterIP, NodePort, and + LoadBalancer. If this field is specified when creating a Service of type + ExternalName, creation will fail. This field will be wiped when updating + a Service to type ExternalName. If this field is not specified, it will + be initialized from the clusterIP field. If this field is specified, + clients must ensure that clusterIPs[0] and clusterIP have the same + value. + + + This field may hold a maximum of two entries (dual-stack IPs, in either order). + These IPs must correspond to the values of the ipFamilies field. Both + clusterIPs and ipFamilies are governed by the ipFamilyPolicy field. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies + items: + type: string + type: array + x-kubernetes-list-type: atomic + externalIPs: + description: |- + externalIPs is a list of IP addresses for which nodes in the cluster + will also accept traffic for this service. These IPs are not managed by + Kubernetes. The user is responsible for ensuring that traffic arrives + at a node with this IP. A common example is external load-balancers + that are not part of the Kubernetes system. + items: + type: string + type: array + x-kubernetes-list-type: atomic + externalName: + description: |- + externalName is the external reference that discovery mechanisms will + return as an alias for this service (e.g. a DNS CNAME record). No + proxying will be involved. Must be a lowercase RFC-1123 hostname + (https://tools.ietf.org/html/rfc1123) and requires `type` to be "ExternalName". + type: string + externalTrafficPolicy: + description: |- + externalTrafficPolicy describes how nodes distribute service traffic they + receive on one of the Service's "externally-facing" addresses (NodePorts, + ExternalIPs, and LoadBalancer IPs). If set to "Local", the proxy will configure + the service in a way that assumes that external load balancers will take care + of balancing the service traffic between nodes, and so each node will deliver + traffic only to the node-local endpoints of the service, without masquerading + the client source IP. (Traffic mistakenly sent to a node with no endpoints will + be dropped.) The default value, "Cluster", uses the standard behavior of + routing to all endpoints evenly (possibly modified by topology and other + features). Note that traffic sent to an External IP or LoadBalancer IP from + within the cluster will always get "Cluster" semantics, but clients sending to + a NodePort from within the cluster may need to take traffic policy into account + when picking a node. + type: string + healthCheckNodePort: + description: |- + healthCheckNodePort specifies the healthcheck nodePort for the service. + This only applies when type is set to LoadBalancer and + externalTrafficPolicy is set to Local. If a value is specified, is + in-range, and is not in use, it will be used. If not specified, a value + will be automatically allocated. External systems (e.g. load-balancers) + can use this port to determine if a given node holds endpoints for this + service or not. If this field is specified when creating a Service + which does not need it, creation will fail. This field will be wiped + when updating a Service to no longer need it (e.g. changing type). + This field cannot be updated once set. + format: int32 + type: integer + internalTrafficPolicy: + description: |- + InternalTrafficPolicy describes how nodes distribute service traffic they + receive on the ClusterIP. If set to "Local", the proxy will assume that pods + only want to talk to endpoints of the service on the same node as the pod, + dropping the traffic if there are no local endpoints. The default value, + "Cluster", uses the standard behavior of routing to all endpoints evenly + (possibly modified by topology and other features). + type: string + ipFamilies: + description: |- + IPFamilies is a list of IP families (e.g. IPv4, IPv6) assigned to this + service. This field is usually assigned automatically based on cluster + configuration and the ipFamilyPolicy field. If this field is specified + manually, the requested family is available in the cluster, + and ipFamilyPolicy allows it, it will be used; otherwise creation of + the service will fail. This field is conditionally mutable: it allows + for adding or removing a secondary IP family, but it does not allow + changing the primary IP family of the Service. Valid values are "IPv4" + and "IPv6". This field only applies to Services of types ClusterIP, + NodePort, and LoadBalancer, and does apply to "headless" services. + This field will be wiped when updating a Service to type ExternalName. + + + This field may hold a maximum of two entries (dual-stack families, in + either order). These families must correspond to the values of the + clusterIPs field, if specified. Both clusterIPs and ipFamilies are + governed by the ipFamilyPolicy field. + items: + description: |- + IPFamily represents the IP Family (IPv4 or IPv6). This type is used + to express the family of an IP expressed by a type (e.g. service.spec.ipFamilies). + type: string + type: array + x-kubernetes-list-type: atomic + ipFamilyPolicy: + description: |- + IPFamilyPolicy represents the dual-stack-ness requested or required by + this Service. If there is no value provided, then this field will be set + to SingleStack. Services can be "SingleStack" (a single IP family), + "PreferDualStack" (two IP families on dual-stack configured clusters or + a single IP family on single-stack clusters), or "RequireDualStack" + (two IP families on dual-stack configured clusters, otherwise fail). The + ipFamilies and clusterIPs fields depend on the value of this field. This + field will be wiped when updating a service to type ExternalName. + type: string + loadBalancerClass: + description: |- + loadBalancerClass is the class of the load balancer implementation this Service belongs to. + If specified, the value of this field must be a label-style identifier, with an optional prefix, + e.g. "internal-vip" or "example.com/internal-vip". Unprefixed names are reserved for end-users. + This field can only be set when the Service type is 'LoadBalancer'. If not set, the default load + balancer implementation is used, today this is typically done through the cloud provider integration, + but should apply for any default implementation. If set, it is assumed that a load balancer + implementation is watching for Services with a matching class. Any default load balancer + implementation (e.g. cloud providers) should ignore Services that set this field. + This field can only be set when creating or updating a Service to type 'LoadBalancer'. + Once set, it can not be changed. This field will be wiped when a service is updated to a non 'LoadBalancer' type. + type: string + loadBalancerIP: + description: |- + Only applies to Service Type: LoadBalancer. + This feature depends on whether the underlying cloud-provider supports specifying + the loadBalancerIP when a load balancer is created. + This field will be ignored if the cloud-provider does not support the feature. + Deprecated: This field was under-specified and its meaning varies across implementations. + Using it is non-portable and it may not support dual-stack. + Users are encouraged to use implementation-specific annotations when available. + type: string + loadBalancerSourceRanges: + description: |- + If specified and supported by the platform, this will restrict traffic through the cloud-provider + load-balancer will be restricted to the specified client IPs. This field will be ignored if the + cloud-provider does not support the feature." + More info: https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/ + items: + type: string + type: array + x-kubernetes-list-type: atomic + ports: + description: |- + The list of ports that are exposed by this service. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies + items: + description: ServicePort contains information on service's port. + properties: + appProtocol: + description: |- + The application protocol for this port. + This is used as a hint for implementations to offer richer behavior for protocols that they understand. + This field follows standard Kubernetes label syntax. + Valid values are either: + + + * Un-prefixed protocol names - reserved for IANA standard service names (as per + RFC-6335 and https://www.iana.org/assignments/service-names). + + + * Kubernetes-defined prefixed names: + * 'kubernetes.io/h2c' - HTTP/2 prior knowledge over cleartext as described in https://www.rfc-editor.org/rfc/rfc9113.html#name-starting-http-2-with-prior- + * 'kubernetes.io/ws' - WebSocket over cleartext as described in https://www.rfc-editor.org/rfc/rfc6455 + * 'kubernetes.io/wss' - WebSocket over TLS as described in https://www.rfc-editor.org/rfc/rfc6455 + + + * Other protocols should use implementation-defined prefixed names such as + mycompany.com/my-custom-protocol. + type: string + name: + description: |- + The name of this port within the service. This must be a DNS_LABEL. + All ports within a ServiceSpec must have unique names. When considering + the endpoints for a Service, this must match the 'name' field in the + EndpointPort. + Optional if only one ServicePort is defined on this service. + type: string + nodePort: + description: |- + The port on each node on which this service is exposed when type is + NodePort or LoadBalancer. Usually assigned by the system. If a value is + specified, in-range, and not in use it will be used, otherwise the + operation will fail. If not specified, a port will be allocated if this + Service requires one. If this field is specified when creating a + Service which does not need it, creation will fail. This field will be + wiped when updating a Service to no longer need it (e.g. changing type + from NodePort to ClusterIP). + More info: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport + format: int32 + type: integer + port: + description: The port that will be exposed by this service. + format: int32 + type: integer + protocol: + default: TCP + description: |- + The IP protocol for this port. Supports "TCP", "UDP", and "SCTP". + Default is TCP. + type: string + targetPort: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the pods targeted by the service. + Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME. + If this is a string, it will be looked up as a named port in the + target Pod's container ports. If this is not specified, the value + of the 'port' field is used (an identity map). + This field is ignored for services with clusterIP=None, and should be + omitted or set equal to the 'port' field. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service + x-kubernetes-int-or-string: true + required: + - port + type: object + type: array + x-kubernetes-list-map-keys: + - port + - protocol + x-kubernetes-list-type: map + publishNotReadyAddresses: + description: |- + publishNotReadyAddresses indicates that any agent which deals with endpoints for this + Service should disregard any indications of ready/not-ready. + The primary use case for setting this field is for a StatefulSet's Headless Service to + propagate SRV DNS records for its Pods for the purpose of peer discovery. + The Kubernetes controllers that generate Endpoints and EndpointSlice resources for + Services interpret this to mean that all endpoints are considered "ready" even if the + Pods themselves are not. Agents which consume only Kubernetes generated endpoints + through the Endpoints or EndpointSlice resources can safely assume this behavior. + type: boolean + selector: + additionalProperties: + type: string + description: |- + Route service traffic to pods with label keys and values matching this + selector. If empty or not present, the service is assumed to have an + external process managing its endpoints, which Kubernetes will not + modify. Only applies to types ClusterIP, NodePort, and LoadBalancer. + Ignored if type is ExternalName. + More info: https://kubernetes.io/docs/concepts/services-networking/service/ + type: object + x-kubernetes-map-type: atomic + sessionAffinity: + description: |- + Supports "ClientIP" and "None". Used to maintain session affinity. + Enable client IP based session affinity. + Must be ClientIP or None. + Defaults to None. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies + type: string + sessionAffinityConfig: + description: sessionAffinityConfig contains the configurations of session affinity. + properties: + clientIP: + description: clientIP contains the configurations of Client IP based session affinity. + properties: + timeoutSeconds: + description: |- + timeoutSeconds specifies the seconds of ClientIP type session sticky time. + The value must be >0 && <=86400(for 1 day) if ServiceAffinity == "ClientIP". + Default value is 10800(for 3 hours). + format: int32 + type: integer + type: object + type: object + trafficDistribution: + description: |- + TrafficDistribution offers a way to express preferences for how traffic is + distributed to Service endpoints. Implementations can use this field as a + hint, but are not required to guarantee strict adherence. If the field is + not set, the implementation will apply its default routing strategy. If set + to "PreferClose", implementations should prioritize endpoints that are + topologically close (e.g., same zone). + type: string + type: + description: |- + type determines how the Service is exposed. Defaults to ClusterIP. Valid + options are ExternalName, ClusterIP, NodePort, and LoadBalancer. + "ClusterIP" allocates a cluster-internal IP address for load-balancing + to endpoints. Endpoints are determined by the selector or if that is not + specified, by manual construction of an Endpoints object or + EndpointSlice objects. If clusterIP is "None", no virtual IP is + allocated and the endpoints are published as a set of endpoints rather + than a virtual IP. + "NodePort" builds on ClusterIP and allocates a port on every node which + routes to the same endpoints as the clusterIP. + "LoadBalancer" builds on NodePort and creates an external load-balancer + (if supported in the current cloud) which routes to the same endpoints + as the clusterIP. + "ExternalName" aliases this service to the specified externalName. + Several other fields do not apply to ExternalName services. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: string + type: object + type: object storage: description: |- StorageSpec defines the configured storage for a etcd members. @@ -271,6 +685,7 @@ spec: items: type: string type: array + x-kubernetes-list-type: atomic dataSource: description: |- dataSource field can be used to specify either: @@ -407,11 +822,13 @@ spec: items: type: string type: array + x-kubernetes-list-type: atomic required: - key - operator type: object type: array + x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string @@ -439,7 +856,7 @@ spec: If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource exists. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass + More info: https://kubernetes.io/docs/concepts/storage/volume-attributes-classes/ (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. type: string volumeMode: @@ -464,6 +881,7 @@ spec: items: type: string type: array + x-kubernetes-list-type: atomic allocatedResourceStatuses: additionalProperties: description: |- @@ -495,7 +913,7 @@ spec: conditions: description: |- conditions is the current Condition of persistent volume claim. If underlying persistent volume is being - resized then the Condition will be set to 'ResizeStarted'. + resized then the Condition will be set to 'Resizing'. items: description: PersistentVolumeClaimCondition contains details about state of pvc properties: @@ -513,7 +931,7 @@ spec: reason: description: |- reason is a unique, this should be a short, machine understandable string that gives the reason - for condition's last transition. If it reports "ResizeStarted" that means the underlying + for condition's last transition. If it reports "Resizing" that means the underlying persistent volume is being resized. type: string status: @@ -526,6 +944,9 @@ spec: - type type: object type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map currentVolumeAttributesClassName: description: |- currentVolumeAttributesClassName is the current name of the VolumeAttributesClass the PVC is using. diff --git a/examples/manifests/etcdcluster-kamaji.yaml b/examples/manifests/etcdcluster-kamaji.yaml index 5a198afb..6145e439 100644 --- a/examples/manifests/etcdcluster-kamaji.yaml +++ b/examples/manifests/etcdcluster-kamaji.yaml @@ -21,9 +21,9 @@ metadata: spec: driver: etcd endpoints: - - etcd-0.etcd.kamaji-system.svc:2379 - - etcd-1.etcd.kamaji-system.svc:2379 - - etcd-2.etcd.kamaji-system.svc:2379 + - etcd-0.etcd-headless.kamaji-system.svc:2379 + - etcd-1.etcd-headless.kamaji-system.svc:2379 + - etcd-2.etcd-headless.kamaji-system.svc:2379 tlsConfig: certificateAuthority: certificate: @@ -140,17 +140,17 @@ spec: - "key encipherment" dnsNames: - etcd-0 - - etcd-0.etcd - - etcd-0.etcd.kamaji-system.svc - - etcd-0.etcd.kamaji-system.svc.cluster.local + - etcd-0.etcd-headless + - etcd-0.etcd-headless.kamaji-system.svc + - etcd-0.etcd-headless.kamaji-system.svc.cluster.local - etcd-1 - - etcd-1.etcd - - etcd-1.etcd.kamaji-system.svc - - etcd-1.etcd.kamaji-system.svc.cluster.local + - etcd-1.etcd-headless + - etcd-1.etcd-headless.kamaji-system.svc + - etcd-1.etcd-headless.kamaji-system.svc.cluster.local - etcd-2 - - etcd-2.etcd - - etcd-2.etcd.kamaji-system.svc - - etcd-2.etcd.kamaji-system.svc.cluster.local + - etcd-2.etcd-headless + - etcd-2.etcd-headless.kamaji-system.svc + - etcd-2.etcd-headless.kamaji-system.svc.cluster.local - localhost - "127.0.0.1" privateKey: @@ -175,17 +175,17 @@ spec: - "key encipherment" dnsNames: - etcd-0 - - etcd-0.etcd - - etcd-0.etcd.kamaji-system.svc - - etcd-0.etcd.kamaji-system.svc.cluster.local + - etcd-0.etcd-headless + - etcd-0.etcd-headless.kamaji-system.svc + - etcd-0.etcd-headless.kamaji-system.svc.cluster.local - etcd-1 - - etcd-1.etcd - - etcd-1.etcd.kamaji-system.svc - - etcd-1.etcd.kamaji-system.svc.cluster.local + - etcd-1.etcd-headless + - etcd-1.etcd-headless.kamaji-system.svc + - etcd-1.etcd-headless.kamaji-system.svc.cluster.local - etcd-2 - - etcd-2.etcd - - etcd-2.etcd.kamaji-system.svc - - etcd-2.etcd.kamaji-system.svc.cluster.local + - etcd-2.etcd-headless + - etcd-2.etcd-headless.kamaji-system.svc + - etcd-2.etcd-headless.kamaji-system.svc.cluster.local - localhost - "127.0.0.1" privateKey: diff --git a/examples/manifests/etcdcluster-with-external-certificates.yaml b/examples/manifests/etcdcluster-with-external-certificates.yaml index 59c3b43e..5b40dddc 100644 --- a/examples/manifests/etcdcluster-with-external-certificates.yaml +++ b/examples/manifests/etcdcluster-with-external-certificates.yaml @@ -141,20 +141,20 @@ spec: - key encipherment dnsNames: - test-0 - - test-0.test - - test-0.test.default.svc - - test-0.test.default.svc.cluster.local + - test-0.test-headless + - test-0.test-headless.default.svc + - test-0.test-headless.default.svc.cluster.local - test-1 - - test-1.test - - test-1.test.default.svc - - test-1.test.default.svc.cluster.local + - test-1.test-headless + - test-1.test-headless.default.svc + - test-1.test-headless.default.svc.cluster.local - test-2 - - test-2.test - - test-2.test.default.svc - - test-2.test.default.svc.cluster.local - - test-client - - test-client.default.svc - - test-client.default.svc.cluster.local + - test-2.test-headless + - test-2.test-headless.default.svc + - test-2.test-headless.default.svc.cluster.local + - test + - test.default.svc + - test.default.svc.cluster.local - localhost - "127.0.0.1" privateKey: @@ -179,17 +179,17 @@ spec: - key encipherment dnsNames: - test-0 - - test-0.test - - test-0.test.default.svc - - test-0.test.default.svc.cluster.local + - test-0.test-headless + - test-0.test-headless.default.svc + - test-0.test-headless.default.svc.cluster.local - test-1 - - test-1.test - - test-1.test.default.svc - - test-1.test.default.svc.cluster.local + - test-1.test-headless + - test-1.test-headless.default.svc + - test-1.test-headless.default.svc.cluster.local - test-2 - - test-2.test - - test-2.test.default.svc - - test-2.test.default.svc.cluster.local + - test-2.test-headless + - test-2.test-headless.default.svc + - test-2.test-headless.default.svc.cluster.local - localhost - "127.0.0.1" privateKey: diff --git a/internal/controller/etcdcluster_controller.go b/internal/controller/etcdcluster_controller.go index 0e7be751..f9860315 100644 --- a/internal/controller/etcdcluster_controller.go +++ b/internal/controller/etcdcluster_controller.go @@ -130,7 +130,7 @@ func (r *EtcdClusterReconciler) ensureClusterObjects( if err := factory.CreateOrUpdateClusterStateConfigMap(ctx, cluster, r.Client, r.Scheme); err != nil { return err } - if err := factory.CreateOrUpdateClusterService(ctx, cluster, r.Client, r.Scheme); err != nil { + if err := factory.CreateOrUpdateHeadlessService(ctx, cluster, r.Client, r.Scheme); err != nil { return err } if err := factory.CreateOrUpdateStatefulSet(ctx, cluster, r.Client, r.Scheme); err != nil { diff --git a/internal/controller/etcdcluster_controller_test.go b/internal/controller/etcdcluster_controller_test.go index c646ac2d..8f67311b 100644 --- a/internal/controller/etcdcluster_controller_test.go +++ b/internal/controller/etcdcluster_controller_test.go @@ -90,14 +90,14 @@ var _ = Describe("EtcdCluster Controller", func() { headlessService = corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Namespace: ns.GetName(), - Name: etcdcluster.GetName(), + Name: factory.GetHeadlessServiceName(&etcdcluster), }, } DeferCleanup(k8sClient.Delete, &headlessService) service = corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Namespace: ns.GetName(), - Name: factory.GetClientServiceName(&etcdcluster), + Name: factory.GetServiceName(&etcdcluster), }, } DeferCleanup(k8sClient.Delete, &service) diff --git a/internal/controller/factory/builders.go b/internal/controller/factory/builders.go index 0a09b1f0..af18e07d 100644 --- a/internal/controller/factory/builders.go +++ b/internal/controller/factory/builders.go @@ -75,6 +75,10 @@ func reconcileService(ctx context.Context, rclient client.Client, crdName string logger := log.FromContext(ctx) logger.V(2).Info("service reconciliation started") + if svc == nil { + return fmt.Errorf("service is nil for crd_object: %s", crdName) + } + currentSvc := &corev1.Service{} logger.V(2).Info("service found", "svc_name", currentSvc.Name) diff --git a/internal/controller/factory/configMap.go b/internal/controller/factory/configMap.go index 6580dc6a..2271f79d 100644 --- a/internal/controller/factory/configMap.go +++ b/internal/controller/factory/configMap.go @@ -26,8 +26,9 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - etcdaenixiov1alpha1 "github.com/aenix-io/etcd-operator/api/v1alpha1" "sigs.k8s.io/controller-runtime/pkg/log" + + etcdaenixiov1alpha1 "github.com/aenix-io/etcd-operator/api/v1alpha1" ) func GetClusterStateConfigMapName(cluster *etcdaenixiov1alpha1.EtcdCluster) string { @@ -41,13 +42,14 @@ func CreateOrUpdateClusterStateConfigMap( rscheme *runtime.Scheme, ) error { initialCluster := "" + clusterService := fmt.Sprintf("%s.%s.svc:2380", GetHeadlessServiceName(cluster), cluster.Namespace) for i := int32(0); i < *cluster.Spec.Replicas; i++ { if i > 0 { initialCluster += "," } - initialCluster += fmt.Sprintf("%s-%d=https://%s-%d.%s.%s.svc:2380", - cluster.Name, i, - cluster.Name, i, cluster.Name, cluster.Namespace, + podName := fmt.Sprintf("%s-%d", cluster.Name, i) + initialCluster += fmt.Sprintf("%s=https://%s.%s", + podName, podName, clusterService, ) } diff --git a/internal/controller/factory/statefulset.go b/internal/controller/factory/statefulset.go index e99c6603..2660f5a4 100644 --- a/internal/controller/factory/statefulset.go +++ b/internal/controller/factory/statefulset.go @@ -28,9 +28,10 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + etcdaenixiov1alpha1 "github.com/aenix-io/etcd-operator/api/v1alpha1" "github.com/aenix-io/etcd-operator/internal/k8sutils" - "sigs.k8s.io/controller-runtime/pkg/log" ) const ( @@ -91,7 +92,7 @@ func CreateOrUpdateStatefulSet( Spec: appsv1.StatefulSetSpec{ // initialize static fields that cannot be changed across updates. Replicas: cluster.Spec.Replicas, - ServiceName: cluster.Name, + ServiceName: GetHeadlessServiceName(cluster), PodManagementPolicy: appsv1.ParallelPodManagement, Selector: &metav1.LabelSelector{ MatchLabels: NewLabelsBuilder().WithName().WithInstance(cluster.Name).WithManagedBy(), @@ -296,9 +297,9 @@ func generateEtcdArgs(cluster *etcdaenixiov1alpha1.EtcdCluster) []string { "--listen-metrics-urls=http://0.0.0.0:2381", "--listen-peer-urls=https://0.0.0.0:2380", fmt.Sprintf("--listen-client-urls=%s://0.0.0.0:2379", serverProtocol), - fmt.Sprintf("--initial-advertise-peer-urls=https://$(POD_NAME).%s.$(POD_NAMESPACE).svc:2380", cluster.Name), + fmt.Sprintf("--initial-advertise-peer-urls=https://$(POD_NAME).%s.$(POD_NAMESPACE).svc:2380", GetHeadlessServiceName(cluster)), "--data-dir=/var/run/etcd/default.etcd", - fmt.Sprintf("--advertise-client-urls=%s://$(POD_NAME).%s.$(POD_NAMESPACE).svc:2379", serverProtocol, cluster.Name), + fmt.Sprintf("--advertise-client-urls=%s://$(POD_NAME).%s.$(POD_NAMESPACE).svc:2379", serverProtocol, GetHeadlessServiceName(cluster)), }...) args = append(args, peerTlsSettings...) diff --git a/internal/controller/factory/svc.go b/internal/controller/factory/svc.go index a223d23f..499c0780 100644 --- a/internal/controller/factory/svc.go +++ b/internal/controller/factory/svc.go @@ -27,27 +27,52 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - etcdaenixiov1alpha1 "github.com/aenix-io/etcd-operator/api/v1alpha1" "sigs.k8s.io/controller-runtime/pkg/log" + + etcdaenixiov1alpha1 "github.com/aenix-io/etcd-operator/api/v1alpha1" + "github.com/aenix-io/etcd-operator/internal/k8sutils" ) -func GetClientServiceName(cluster *etcdaenixiov1alpha1.EtcdCluster) string { - return fmt.Sprintf("%s-client", cluster.Name) +func GetServiceName(cluster *etcdaenixiov1alpha1.EtcdCluster) string { + if cluster.Spec.ServiceTemplate != nil && cluster.Spec.ServiceTemplate.Name != "" { + return cluster.Spec.ServiceTemplate.Name + } + + return cluster.Name } -func CreateOrUpdateClusterService( +func GetHeadlessServiceName(cluster *etcdaenixiov1alpha1.EtcdCluster) string { + if cluster.Spec.HeadlessServiceTemplate != nil && cluster.Spec.HeadlessServiceTemplate.Name != "" { + return cluster.Spec.HeadlessServiceTemplate.Name + } + + return fmt.Sprintf("%s-headless", cluster.Name) +} + +func CreateOrUpdateHeadlessService( ctx context.Context, cluster *etcdaenixiov1alpha1.EtcdCluster, rclient client.Client, rscheme *runtime.Scheme, ) error { logger := log.FromContext(ctx) + var err error + + metadata := metav1.ObjectMeta{ + Name: GetHeadlessServiceName(cluster), + Namespace: cluster.Namespace, + Labels: NewLabelsBuilder().WithName().WithInstance(cluster.Name).WithManagedBy(), + } + + if cluster.Spec.HeadlessServiceTemplate != nil { + metadata, err = k8sutils.StrategicMerge(metadata, cluster.Spec.HeadlessServiceTemplate.ToObjectMeta()) + if err != nil { + return fmt.Errorf("cannot strategic-merge base svc metadata with headlessServiceTemplate.metadata: %w", err) + } + } + svc := &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: cluster.Name, - Namespace: cluster.Namespace, - Labels: NewLabelsBuilder().WithName().WithInstance(cluster.Name).WithManagedBy(), - }, + ObjectMeta: metadata, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ {Name: "peer", TargetPort: intstr.FromInt32(2380), Port: 2380, Protocol: corev1.ProtocolTCP}, @@ -59,6 +84,7 @@ func CreateOrUpdateClusterService( PublishNotReadyAddresses: true, }, } + logger.V(2).Info("cluster service spec generated", "svc_name", svc.Name, "svc_spec", svc.Spec) if err := ctrl.SetControllerReference(cluster, svc, rscheme); err != nil { @@ -75,9 +101,11 @@ func CreateOrUpdateClientService( rscheme *runtime.Scheme, ) error { logger := log.FromContext(ctx) - svc := &corev1.Service{ + var err error + + svc := corev1.Service{ ObjectMeta: metav1.ObjectMeta{ - Name: GetClientServiceName(cluster), + Name: GetServiceName(cluster), Namespace: cluster.Namespace, Labels: NewLabelsBuilder().WithName().WithInstance(cluster.Name).WithManagedBy(), }, @@ -89,11 +117,22 @@ func CreateOrUpdateClientService( Selector: NewLabelsBuilder().WithName().WithInstance(cluster.Name).WithManagedBy(), }, } + + if cluster.Spec.ServiceTemplate != nil { + svc, err = k8sutils.StrategicMerge(svc, corev1.Service{ + ObjectMeta: cluster.Spec.ServiceTemplate.EmbeddedObjectMetadata.ToObjectMeta(), + Spec: cluster.Spec.ServiceTemplate.Spec, + }) + if err != nil { + return fmt.Errorf("cannot strategic-merge base svc with serviceTemplate: %w", err) + } + } + logger.V(2).Info("client service spec generated", "svc_name", svc.Name, "svc_spec", svc.Spec) - if err := ctrl.SetControllerReference(cluster, svc, rscheme); err != nil { + if err := ctrl.SetControllerReference(cluster, &svc, rscheme); err != nil { return fmt.Errorf("cannot set controller reference: %w", err) } - return reconcileService(ctx, rclient, cluster.Name, svc) + return reconcileService(ctx, rclient, cluster.Name, &svc) } diff --git a/internal/controller/factory/svc_test.go b/internal/controller/factory/svc_test.go index 41d3464a..3f8bd498 100644 --- a/internal/controller/factory/svc_test.go +++ b/internal/controller/factory/svc_test.go @@ -45,7 +45,7 @@ var _ = Describe("CreateOrUpdateService handlers", func() { DeferCleanup(k8sClient.Delete, ns) }) - Context("when ensuring cluster service", func() { + Context("when ensuring cluster services", func() { var ( etcdcluster etcdaenixiov1alpha1.EtcdCluster headlessService corev1.Service @@ -72,13 +72,13 @@ var _ = Describe("CreateOrUpdateService handlers", func() { headlessService = corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Namespace: ns.GetName(), - Name: etcdcluster.GetName(), + Name: GetHeadlessServiceName(&etcdcluster), }, } clientService = corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Namespace: ns.GetName(), - Name: GetClientServiceName(&etcdcluster), + Name: GetServiceName(&etcdcluster), }, } }) @@ -98,15 +98,40 @@ var _ = Describe("CreateOrUpdateService handlers", func() { } }) - It("should successfully ensure cluster headless service", func(ctx SpecContext) { - Expect(CreateOrUpdateClusterService(ctx, &etcdcluster, k8sClient, k8sClient.Scheme())).To(Succeed()) + It("should successfully ensure headless service", func(ctx SpecContext) { + Expect(CreateOrUpdateHeadlessService(ctx, &etcdcluster, k8sClient, k8sClient.Scheme())).To(Succeed()) Eventually(Object(&headlessService)).Should(SatisfyAll( HaveField("Spec.Type", Equal(corev1.ServiceTypeClusterIP)), HaveField("Spec.ClusterIP", Equal(corev1.ClusterIPNone)), )) }) - It("should successfully ensure cluster client service", func(ctx SpecContext) { + It("should successfully ensure headless service with custom metadata", func(ctx SpecContext) { + cluster := etcdcluster.DeepCopy() + cluster.Spec.HeadlessServiceTemplate = &etcdaenixiov1alpha1.EmbeddedMetadataResource{ + EmbeddedObjectMetadata: etcdaenixiov1alpha1.EmbeddedObjectMetadata{ + Name: "headless-name", + Labels: map[string]string{"label": "value"}, + Annotations: map[string]string{"annotation": "value"}, + }, + } + svc := headlessService.DeepCopy() + svc.Name = cluster.Spec.HeadlessServiceTemplate.Name + + Expect(CreateOrUpdateHeadlessService(ctx, cluster, k8sClient, k8sClient.Scheme())).To(Succeed()) + Eventually(Object(svc)).Should(SatisfyAll( + HaveField("ObjectMeta.Name", Equal(cluster.Spec.HeadlessServiceTemplate.Name)), + HaveField("ObjectMeta.Labels", SatisfyAll( + HaveKeyWithValue("label", "value"), + HaveKeyWithValue("app.kubernetes.io/name", "etcd"), + )), + HaveField("ObjectMeta.Annotations", Equal(cluster.Spec.HeadlessServiceTemplate.Annotations)), + )) + // We need to manually cleanup here because we changed the name of the service + Expect(k8sClient.Delete(ctx, svc)).Should(Succeed()) + }) + + It("should successfully ensure client service", func(ctx SpecContext) { Expect(CreateOrUpdateClientService(ctx, &etcdcluster, k8sClient, k8sClient.Scheme())).To(Succeed()) Eventually(Object(&clientService)).Should(SatisfyAll( HaveField("Spec.Type", Equal(corev1.ServiceTypeClusterIP)), @@ -114,9 +139,65 @@ var _ = Describe("CreateOrUpdateService handlers", func() { )) }) - It("should fail to create service with invalid owner reference", func(ctx SpecContext) { + It("should successfully ensure client service with custom metadata", func(ctx SpecContext) { + cluster := etcdcluster.DeepCopy() + cluster.Spec.ServiceTemplate = &etcdaenixiov1alpha1.EmbeddedService{ + EmbeddedObjectMetadata: etcdaenixiov1alpha1.EmbeddedObjectMetadata{ + Name: "client-name", + Labels: map[string]string{"label": "value"}, + Annotations: map[string]string{"annotation": "value"}, + }, + } + svc := clientService.DeepCopy() + svc.Name = cluster.Spec.ServiceTemplate.Name + + Expect(CreateOrUpdateClientService(ctx, cluster, k8sClient, k8sClient.Scheme())).To(Succeed()) + Eventually(Object(svc)).Should(SatisfyAll( + HaveField("ObjectMeta.Name", Equal(cluster.Spec.ServiceTemplate.Name)), + HaveField("ObjectMeta.Labels", SatisfyAll( + HaveKeyWithValue("label", "value"), + HaveKeyWithValue("app.kubernetes.io/name", "etcd"), + )), + HaveField("ObjectMeta.Annotations", Equal(cluster.Spec.ServiceTemplate.Annotations)), + )) + // We need to manually cleanup here because we changed the name of the service + Expect(k8sClient.Delete(ctx, svc)).Should(Succeed()) + }) + + It("should successfully ensure client service with custom spec", func(ctx SpecContext) { + cluster := etcdcluster.DeepCopy() + cluster.Spec.ServiceTemplate = &etcdaenixiov1alpha1.EmbeddedService{ + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + Ports: []corev1.ServicePort{ + { + Name: "client", + Port: 2379, + Protocol: corev1.ProtocolUDP, + }, + }, + LoadBalancerClass: ptr.To("someClass"), + }, + } + + Expect(CreateOrUpdateClientService(ctx, cluster, k8sClient, k8sClient.Scheme())).To(Succeed()) + Eventually(Object(&clientService)).Should(SatisfyAll( + HaveField("Spec.Type", Equal(corev1.ServiceTypeLoadBalancer)), + HaveField("Spec.LoadBalancerClass", Equal(ptr.To("someClass"))), + HaveField("Spec.Ports", SatisfyAll( + HaveLen(1), + HaveEach(SatisfyAll( + HaveField("Name", Equal("client")), + HaveField("Port", Equal(int32(2379))), + HaveField("Protocol", Equal(corev1.ProtocolUDP)), + )), + )), + )) + }) + + It("should fail on creating the client service with invalid owner reference", func(ctx SpecContext) { emptyScheme := runtime.NewScheme() - Expect(CreateOrUpdateClusterService(ctx, &etcdcluster, k8sClient, emptyScheme)).NotTo(Succeed()) + Expect(CreateOrUpdateHeadlessService(ctx, &etcdcluster, k8sClient, emptyScheme)).NotTo(Succeed()) Expect(CreateOrUpdateClientService(ctx, &etcdcluster, k8sClient, emptyScheme)).NotTo(Succeed()) }) }) diff --git a/internal/k8sutils/strategicmerge.go b/internal/k8sutils/strategicmerge.go index a0f6f801..9147c617 100644 --- a/internal/k8sutils/strategicmerge.go +++ b/internal/k8sutils/strategicmerge.go @@ -7,6 +7,9 @@ import ( "k8s.io/apimachinery/pkg/util/strategicpatch" ) +// StrategicMerge merges two objects using strategic merge patch. +// It accepts two objects, base and patch, and returns a merged object. +// Both base and patch objects must be of the same type and must not be a pointer. func StrategicMerge[K any](base, patch K) (merged K, err error) { baseBytes, err := json.Marshal(base) if err != nil { @@ -20,7 +23,7 @@ func StrategicMerge[K any](base, patch K) (merged K, err error) { mergedBytes, err := strategicpatch.StrategicMergePatch(baseBytes, patchBytes, &merged) if err != nil { - return merged, fmt.Errorf("cannot patch base pod spec with podTemplate.spec: %w", err) + return merged, fmt.Errorf("cannot patch base object with given spec: %w", err) } err = json.Unmarshal(mergedBytes, &merged) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 4866bae1..e245fb11 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -21,9 +21,10 @@ import ( "strconv" "sync" - "github.com/aenix-io/etcd-operator/test/utils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + "github.com/aenix-io/etcd-operator/test/utils" ) var _ = Describe("etcd-operator", Ordered, func() { @@ -87,7 +88,7 @@ var _ = Describe("etcd-operator", Ordered, func() { defer GinkgoRecover() defer wg.Done() cmd = exec.Command("kubectl", "port-forward", - "service/test-client", strconv.Itoa(port)+":2379", + "service/test", strconv.Itoa(port)+":2379", "--namespace", namespace, ) _, err = utils.Run(cmd)