From 4ed66293fd3db94e50055373a3a608e69d2c42ec Mon Sep 17 00:00:00 2001 From: Tim Ebert Date: Tue, 10 Sep 2024 16:47:11 +0200 Subject: [PATCH] Allow configuring NTP pools and servers for LBMs (#411) * Allow configuring NTP pools and servers for LBMs * Support new config in chart * Format and improve linter settings * go mod tidy * Remove chronyd depencency in favor of restart It seems desirable to start chrony as soon as possible even if it doesn't have its final config yet. If the clock is off by too much, chrony should perform a step instead of slewing the clock. It's better to do this quickly after boot before time-sensitive processes might notice. * Replace deprecated `initstepslew` directive * Comment on not using cloud-init's NTP module * earthly +generate --- .gitignore | 5 +- .golangci.yml | 116 +++++---- charts/yawol-controller/Chart.yaml | 4 +- ...ol.stackit.cloud_loadbalancermachines.yaml | 79 ++++--- .../yawol.stackit.cloud_loadbalancers.yaml | 220 ++++++++++-------- .../yawol.stackit.cloud_loadbalancersets.yaml | 173 +++++++------- .../templates/yawol-controller.yaml | 6 + charts/yawol-controller/values.yaml | 8 + cmd/yawol-controller/main.go | 12 + .../loadbalancermachine_controller.go | 32 +-- go.mod | 7 +- internal/helper/loadbalancermachine.go | 53 ++++- internal/helper/loadbalancermachine_test.go | 114 +++++++++ internal/helper/templates/chrony.conf.tpl | 19 ++ 14 files changed, 542 insertions(+), 306 deletions(-) create mode 100644 internal/helper/templates/chrony.conf.tpl diff --git a/.gitignore b/.gitignore index be148e58..eb3b04c3 100644 --- a/.gitignore +++ b/.gitignore @@ -14,9 +14,8 @@ out # Output of the go coverage tool, specifically when used with LiteIDE *.out -# Kubernetes Generated files - skip generated files, except for vendored files - -!vendor/**/zz_generated.* +# Earthly +.tmp-earthly-out # editor and IDE paraphernalia .idea diff --git a/.golangci.yml b/.golangci.yml index 218ad899..4ecd3a03 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -6,30 +6,21 @@ linters-settings: statements: 50 gocritic: enabled-tags: - - diagnostic - - experimental - - opinionated - - performance - - style + - diagnostic + - experimental + - opinionated + - performance + - style disabled-checks: - - dupImport # https://github.com/go-critic/go-critic/issues/845 - - ifElseChain - - octalLiteral - - whyNoLint - - wrapperFunc - - hugeParam + - dupImport # https://github.com/go-critic/go-critic/issues/845 + - ifElseChain + - octalLiteral + - whyNoLint + - wrapperFunc + - hugeParam gocyclo: - min-complexity: - 30 # decrease this - # gci: - # local-prefixes: insert your package name here - # goimports: - # local-prefixes: insert your package name here - #revive: - #rules: - #- name: var-naming - #severity: warning + min-complexity: 30 gomnd: settings: mnd: @@ -54,31 +45,31 @@ linters: # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint disable-all: true enable: - - bodyclose - - dogsled - - gocritic - - gofmt - - goimports - - goprintffuncname - - gosimple - - govet - - ineffassign - - lll - - misspell - - nakedret - - rowserrcheck - - staticcheck - - stylecheck - - typecheck - - unconvert - - unparam - - unused - - whitespace - - revive - - dupl - - gocyclo - - gosec - - nolintlint + - bodyclose + - dogsled + - gocritic + - gofmt + - goimports + - goprintffuncname + - gosimple + - govet + - ineffassign + - lll + - misspell + - nakedret + - rowserrcheck + - staticcheck + - stylecheck + - typecheck + - unconvert + - unparam + - unused + - whitespace + - revive + - dupl + - gocyclo + - gosec + - nolintlint # don't enable: # - noctx @@ -107,25 +98,28 @@ linters: issues: # Excluding configuration per-path, per-linter, per-text and per-source exclude-rules: - - path: _test\.go - linters: - - gomnd - - gochecknoglobals - - gosec - - noctx - - goerr113 - - goconst - - dupl - - unparam + - path: _test\.go + linters: + - gomnd + - gochecknoglobals + - gosec + - noctx + - goerr113 + - goconst + - dupl + - unparam + + # https://github.com/go-critic/go-critic/issues/926 + - linters: + - gocritic + text: "unnecessaryDefer:" - # https://github.com/go-critic/go-critic/issues/926 - - linters: - - gocritic - text: "unnecessaryDefer:" + - text: "should not use dot imports" + path: _test\.go # silence stupid linter errors exclude: - - directive `// nolint.*` should be written without leading space + - directive `// nolint.*` should be written without leading space run: timeout: 15m diff --git a/charts/yawol-controller/Chart.yaml b/charts/yawol-controller/Chart.yaml index a4c2650a..6de988e3 100644 --- a/charts/yawol-controller/Chart.yaml +++ b/charts/yawol-controller/Chart.yaml @@ -3,5 +3,5 @@ description: Helm chart for yawol-controller name: yawol-controller sources: - https://github.com/stackitcloud/yawol -version: "0.24.0" -appVersion: v0.24.0 +version: "0.25.0" +appVersion: v0.25.0 diff --git a/charts/yawol-controller/crds/yawol.stackit.cloud_loadbalancermachines.yaml b/charts/yawol-controller/crds/yawol.stackit.cloud_loadbalancermachines.yaml index 144dda98..7316cdfa 100644 --- a/charts/yawol-controller/crds/yawol.stackit.cloud_loadbalancermachines.yaml +++ b/charts/yawol-controller/crds/yawol.stackit.cloud_loadbalancermachines.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.13.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: loadbalancermachines.yawol.stackit.cloud spec: group: yawol.stackit.cloud @@ -39,14 +39,19 @@ spec: API. properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object @@ -89,9 +94,9 @@ spec: zone for the LoadBalancer. type: string defaultNetwork: - description: DefaultNetwork defines the default/listener network - for the Loadbalancer. TODO Remove optional when Deprecations - are removed + description: |- + DefaultNetwork defines the default/listener network for the Loadbalancer. + TODO Remove optional when Deprecations are removed properties: floatingNetID: description: FloatingNetID defines an openstack ID for the @@ -101,8 +106,7 @@ spec: description: NetworkID defines an openstack ID for the network. type: string subnetID: - description: SubnetID defines an openstack ID for the - subnet. + description: SubnetID defines an openstack ID for the subnet. type: string required: - networkID @@ -124,21 +128,23 @@ spec: virtual machines. type: string flavorName: - description: NOT IMPLEMENTED ONLY FlavorID is supported. FlavorName - is the name of the flavor used for requesting virtual machines. + description: |- + NOT IMPLEMENTED ONLY FlavorID is supported. + FlavorName is the name of the flavor used for requesting virtual machines. FlavorName is only used if FlavorID is not defined. type: string flavorSearch: - description: NOT IMPLEMENTED ONLY FlavorID is supported. FlavorSearch - is a search string to find the flavor used for requesting - virtual machines. Search will be performed in metadata of - the flavors. FlavorSearch is only used if FlavorName and - FlavorID are not defined. + description: |- + NOT IMPLEMENTED ONLY FlavorID is supported. + FlavorSearch is a search string to find the flavor used for requesting virtual machines. + Search will be performed in metadata of the flavors. + FlavorSearch is only used if FlavorName and FlavorID are not defined. type: string type: object floatingNetID: - description: 'Deprecated: use defaultNetwork instead FloatingNetID - defines a openstack ID for the floatingNet.' + description: |- + Deprecated: use defaultNetwork instead + FloatingNetID defines a openstack ID for the floatingNet. type: string image: description: Image defines openstack image for the LoadBalancer. @@ -157,26 +163,28 @@ spec: machines. type: string imageName: - description: NOT IMPLEMENTED ONLY ImageID is supported. ImageName - is the name of the image used for requesting virtual machines. + description: |- + NOT IMPLEMENTED ONLY ImageID is supported. + ImageName is the name of the image used for requesting virtual machines. ImageName is only used if ImageID is not defined. type: string imageSearch: - description: NOT IMPLEMENTED ONLY ImageID is supported. ImageSearch - is a search string to find the image used for requesting - virtual machines. Search will be performed in metadata of - the images. ImageSearch is only used if ImageName and ImageID - are not defined. + description: |- + NOT IMPLEMENTED ONLY ImageID is supported. + ImageSearch is a search string to find the image used for requesting virtual machines. + Search will be performed in metadata of the images. + ImageSearch is only used if ImageName and ImageID are not defined. type: string type: object networkID: - description: 'Deprecated: use defaultNetwork instead NetworkID - defines a openstack ID for the network.' + description: |- + Deprecated: use defaultNetwork instead + NetworkID defines a openstack ID for the network. type: string projectID: - description: ProjectID defines an openstack project ID which will - be used instead of the project from the secret ref. If not set - the project from the secret ref will be used. + description: |- + ProjectID defines an openstack project ID which will be used instead of the project from the secret ref. + If not set the project from the secret ref will be used. type: string required: - authSecretRef @@ -292,8 +300,9 @@ spec: type: object type: array portID: - description: 'Deprecated: use defaultPortID instead PortID contains - the openstack port ID for a LoadBalancerMachine.' + description: |- + Deprecated: use defaultPortID instead + PortID contains the openstack port ID for a LoadBalancerMachine. type: string roleBindingName: description: RoleBindingName contains the namespacedName from the diff --git a/charts/yawol-controller/crds/yawol.stackit.cloud_loadbalancers.yaml b/charts/yawol-controller/crds/yawol.stackit.cloud_loadbalancers.yaml index 4f858936..790ffd28 100644 --- a/charts/yawol-controller/crds/yawol.stackit.cloud_loadbalancers.yaml +++ b/charts/yawol-controller/crds/yawol.stackit.cloud_loadbalancers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.13.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: loadbalancers.yawol.stackit.cloud spec: group: yawol.stackit.cloud @@ -38,14 +38,19 @@ spec: description: LoadBalancer is the Schema for the YAWOL LoadBalancer API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object @@ -120,9 +125,9 @@ spec: zone for the LoadBalancer. type: string defaultNetwork: - description: DefaultNetwork defines the default/listener network - for the Loadbalancer. TODO Remove optional when Deprecations - are removed + description: |- + DefaultNetwork defines the default/listener network for the Loadbalancer. + TODO Remove optional when Deprecations are removed properties: floatingNetID: description: FloatingNetID defines an openstack ID for the @@ -132,8 +137,7 @@ spec: description: NetworkID defines an openstack ID for the network. type: string subnetID: - description: SubnetID defines an openstack ID for the - subnet. + description: SubnetID defines an openstack ID for the subnet. type: string required: - networkID @@ -155,21 +159,23 @@ spec: virtual machines. type: string flavorName: - description: NOT IMPLEMENTED ONLY FlavorID is supported. FlavorName - is the name of the flavor used for requesting virtual machines. + description: |- + NOT IMPLEMENTED ONLY FlavorID is supported. + FlavorName is the name of the flavor used for requesting virtual machines. FlavorName is only used if FlavorID is not defined. type: string flavorSearch: - description: NOT IMPLEMENTED ONLY FlavorID is supported. FlavorSearch - is a search string to find the flavor used for requesting - virtual machines. Search will be performed in metadata of - the flavors. FlavorSearch is only used if FlavorName and - FlavorID are not defined. + description: |- + NOT IMPLEMENTED ONLY FlavorID is supported. + FlavorSearch is a search string to find the flavor used for requesting virtual machines. + Search will be performed in metadata of the flavors. + FlavorSearch is only used if FlavorName and FlavorID are not defined. type: string type: object floatingNetID: - description: 'Deprecated: use defaultNetwork instead FloatingNetID - defines a openstack ID for the floatingNet.' + description: |- + Deprecated: use defaultNetwork instead + FloatingNetID defines a openstack ID for the floatingNet. type: string image: description: Image defines openstack image for the LoadBalancer. @@ -188,26 +194,28 @@ spec: machines. type: string imageName: - description: NOT IMPLEMENTED ONLY ImageID is supported. ImageName - is the name of the image used for requesting virtual machines. + description: |- + NOT IMPLEMENTED ONLY ImageID is supported. + ImageName is the name of the image used for requesting virtual machines. ImageName is only used if ImageID is not defined. type: string imageSearch: - description: NOT IMPLEMENTED ONLY ImageID is supported. ImageSearch - is a search string to find the image used for requesting - virtual machines. Search will be performed in metadata of - the images. ImageSearch is only used if ImageName and ImageID - are not defined. + description: |- + NOT IMPLEMENTED ONLY ImageID is supported. + ImageSearch is a search string to find the image used for requesting virtual machines. + Search will be performed in metadata of the images. + ImageSearch is only used if ImageName and ImageID are not defined. type: string type: object networkID: - description: 'Deprecated: use defaultNetwork instead NetworkID - defines a openstack ID for the network.' + description: |- + Deprecated: use defaultNetwork instead + NetworkID defines a openstack ID for the network. type: string projectID: - description: ProjectID defines an openstack project ID which will - be used instead of the project from the secret ref. If not set - the project from the secret ref will be used. + description: |- + ProjectID defines an openstack project ID which will be used instead of the project from the secret ref. + If not set the project from the secret ref will be used. type: string required: - authSecretRef @@ -246,34 +254,33 @@ spec: type: string type: object serverGroupPolicy: - description: ServerGroupPolicy creates a server group with that - policy. Can be 'affinity', 'anti-affinity' 'soft-affinity', - 'soft-anti-affinity' depending on the OpenStack Infrastructure. - If empty Openstack server group will not be used. Default is - disabled + description: |- + ServerGroupPolicy creates a server group with that policy. + Can be 'affinity', 'anti-affinity' 'soft-affinity', 'soft-anti-affinity' depending on the OpenStack Infrastructure. + If empty Openstack server group will not be used. Default is disabled type: string tcpIdleTimeout: - description: TCPIdleTimeout sets TCP idle Timeout for all TCP - connections from this LoadBalancer. Value is in Seconds. With - 0 you disable the idle timeout, be careful this can lead to - side effects. Default is 1h. + description: |- + TCPIdleTimeout sets TCP idle Timeout for all TCP connections from this LoadBalancer. + Value is in Seconds. With 0 you disable the idle timeout, be careful this can lead to side effects. + Default is 1h. type: string tcpProxyProtocol: description: TCPProxyProtocol enables HAProxy TCP Proxy Protocol type: boolean tcpProxyProtocolPortFilter: - description: TCPProxyProtocolPortList enables HAProxy TCP Proxy - Protocol for specified ports. If empty it is enabled for all - ports. Only has an affect if TCPProxyProtocol is enabled. + description: |- + TCPProxyProtocolPortList enables HAProxy TCP Proxy Protocol for specified ports. + If empty it is enabled for all ports. Only has an affect if TCPProxyProtocol is enabled. items: format: int32 type: integer type: array udpIdleTimeout: - description: UDPIdleTimeout sets UDP idle Timeout for all UDP - connections from this LoadBalancer. Value is in Seconds. With - 0 you disable the idle timeout, be careful this can lead to - side effects. Default is 1m. + description: |- + UDPIdleTimeout sets UDP idle Timeout for all UDP connections from this LoadBalancer. + Value is in Seconds. With 0 you disable the idle timeout, be careful this can lead to side effects. + Default is 1m. type: string type: object ports: @@ -283,37 +290,45 @@ spec: 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: \n * Un-prefixed - protocol names - reserved for IANA standard service names - (as per RFC-6335 and https://www.iana.org/assignments/service-names). - \n * Kubernetes-defined prefixed names: * 'kubernetes.io/h2c' - - HTTP/2 over cleartext as described in https://www.rfc-editor.org/rfc/rfc7540 - * '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 - \n * Other protocols should use implementation-defined prefixed - names such as mycompany.com/my-custom-protocol." + 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. + 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' + 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: @@ -322,21 +337,23 @@ spec: type: integer protocol: default: TCP - description: The IP protocol for this port. Supports "TCP", - "UDP", and "SCTP". Default is 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' + 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 @@ -356,41 +373,42 @@ spec: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement is a selector that - contains values, a key, and an operator that relates the key - and values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a key's relationship to - a set of values. Valid operators are In, NotIn, Exists - and DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of string values. If the - operator is In or NotIn, the values array must be non-empty. - If the operator is Exists or DoesNotExist, the values - array must be empty. This array is replaced during a strategic + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic merge patch. 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 - description: matchLabels is a map of {key,value} pairs. A single - {key,value} in the matchLabels map is equivalent to an element - of matchExpressions, whose key field is "key", the operator - is "In", and the values array contains only "value". The requirements - are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic diff --git a/charts/yawol-controller/crds/yawol.stackit.cloud_loadbalancersets.yaml b/charts/yawol-controller/crds/yawol.stackit.cloud_loadbalancersets.yaml index 67879fbc..ef3c5106 100644 --- a/charts/yawol-controller/crds/yawol.stackit.cloud_loadbalancersets.yaml +++ b/charts/yawol-controller/crds/yawol.stackit.cloud_loadbalancersets.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.13.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: loadbalancersets.yawol.stackit.cloud spec: group: yawol.stackit.cloud @@ -38,14 +38,19 @@ spec: description: LoadBalancerSet is the Schema for the LoadBalancerSet's API. properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object @@ -66,41 +71,42 @@ spec: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: - description: A label selector requirement is a selector that - contains values, a key, and an operator that relates the key - and values. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: - description: operator represents a key's relationship to - a set of values. Valid operators are In, NotIn, Exists - and DoesNotExist. + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: - description: values is an array of string values. If the - operator is In or NotIn, the values array must be non-empty. - If the operator is Exists or DoesNotExist, the values - array must be empty. This array is replaced during a strategic + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic merge patch. 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 - description: matchLabels is a map of {key,value} pairs. A single - {key,value} in the matchLabels map is equivalent to an element - of matchExpressions, whose key field is "key", the operator - is "In", and the values array contains only "value". The requirements - are ANDed. + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object x-kubernetes-map-type: atomic @@ -153,9 +159,9 @@ spec: zone for the LoadBalancer. type: string defaultNetwork: - description: DefaultNetwork defines the default/listener - network for the Loadbalancer. TODO Remove optional when - Deprecations are removed + description: |- + DefaultNetwork defines the default/listener network for the Loadbalancer. + TODO Remove optional when Deprecations are removed properties: floatingNetID: description: FloatingNetID defines an openstack ID @@ -166,8 +172,8 @@ spec: the network. type: string subnetID: - description: SubnetID defines an openstack ID - for the subnet. + description: SubnetID defines an openstack ID for + the subnet. type: string required: - networkID @@ -189,23 +195,23 @@ spec: virtual machines. type: string flavorName: - description: NOT IMPLEMENTED ONLY FlavorID is supported. - FlavorName is the name of the flavor used for requesting - virtual machines. FlavorName is only used if FlavorID - is not defined. + description: |- + NOT IMPLEMENTED ONLY FlavorID is supported. + FlavorName is the name of the flavor used for requesting virtual machines. + FlavorName is only used if FlavorID is not defined. type: string flavorSearch: - description: NOT IMPLEMENTED ONLY FlavorID is supported. - FlavorSearch is a search string to find the flavor - used for requesting virtual machines. Search will - be performed in metadata of the flavors. FlavorSearch - is only used if FlavorName and FlavorID are not - defined. + description: |- + NOT IMPLEMENTED ONLY FlavorID is supported. + FlavorSearch is a search string to find the flavor used for requesting virtual machines. + Search will be performed in metadata of the flavors. + FlavorSearch is only used if FlavorName and FlavorID are not defined. type: string type: object floatingNetID: - description: 'Deprecated: use defaultNetwork instead FloatingNetID - defines a openstack ID for the floatingNet.' + description: |- + Deprecated: use defaultNetwork instead + FloatingNetID defines a openstack ID for the floatingNet. type: string image: description: Image defines openstack image for the LoadBalancer. @@ -224,28 +230,28 @@ spec: virtual machines. type: string imageName: - description: NOT IMPLEMENTED ONLY ImageID is supported. - ImageName is the name of the image used for requesting - virtual machines. ImageName is only used if ImageID - is not defined. + description: |- + NOT IMPLEMENTED ONLY ImageID is supported. + ImageName is the name of the image used for requesting virtual machines. + ImageName is only used if ImageID is not defined. type: string imageSearch: - description: NOT IMPLEMENTED ONLY ImageID is supported. - ImageSearch is a search string to find the image - used for requesting virtual machines. Search will - be performed in metadata of the images. ImageSearch - is only used if ImageName and ImageID are not defined. + description: |- + NOT IMPLEMENTED ONLY ImageID is supported. + ImageSearch is a search string to find the image used for requesting virtual machines. + Search will be performed in metadata of the images. + ImageSearch is only used if ImageName and ImageID are not defined. type: string type: object networkID: - description: 'Deprecated: use defaultNetwork instead NetworkID - defines a openstack ID for the network.' + description: |- + Deprecated: use defaultNetwork instead + NetworkID defines a openstack ID for the network. type: string projectID: - description: ProjectID defines an openstack project ID - which will be used instead of the project from the secret - ref. If not set the project from the secret ref will - be used. + description: |- + ProjectID defines an openstack project ID which will be used instead of the project from the secret ref. + If not set the project from the secret ref will be used. type: string required: - authSecretRef @@ -299,42 +305,42 @@ spec: description: Conditions contains condition information for a LoadBalancerSet. items: description: "Condition contains details for one aspect of the current - state of this API Resource. --- This struct is intended for direct - use as an array at the field path .status.conditions. For example, - \n type FooStatus struct{ // Represents the observations of a - foo's current state. // Known .status.conditions.type are: \"Available\", - \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge - // +listType=map // +listMapKey=type Conditions []metav1.Condition - `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" - protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" properties: lastTransitionTime: - description: lastTransitionTime is the last time the condition - transitioned from one status to another. This should be when - the underlying condition changed. If that is not known, then - using the time when the API field changed is acceptable. + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: - description: message is a human readable message indicating - details about the transition. This may be an empty string. + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. maxLength: 32768 type: string observedGeneration: - description: observedGeneration represents the .metadata.generation - that the condition was set based upon. For instance, if .metadata.generation - is currently 12, but the .status.conditions[x].observedGeneration - is 9, the condition is out of date with respect to the current - state of the instance. + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. format: int64 minimum: 0 type: integer reason: - description: reason contains a programmatic identifier indicating - the reason for the condition's last transition. Producers - of specific condition types may define expected values and - meanings for this field, and whether the values are considered - a guaranteed API. The value should be a CamelCase string. + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. This field may not be empty. maxLength: 1024 minLength: 1 @@ -348,11 +354,12 @@ spec: - Unknown type: string type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - --- Many .condition.type values are consistent across resources - like Available, but because arbitrary conditions can be useful - (see .node.status.conditions), the ability to deconflict is - important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string diff --git a/charts/yawol-controller/templates/yawol-controller.yaml b/charts/yawol-controller/templates/yawol-controller.yaml index 2a8a7c3d..4dafd93f 100644 --- a/charts/yawol-controller/templates/yawol-controller.yaml +++ b/charts/yawol-controller/templates/yawol-controller.yaml @@ -122,6 +122,12 @@ spec: {{- if .Values.yawolController.errorBackoffMaxDelay }} - --error-backoff-max-delay={{ .Values.yawolController.errorBackoffMaxDelay }} {{- end }} + {{- range .Values.ntp.pools }} + - --ntp-pool={{ . }} + {{- end }} + {{- range .Values.ntp.servers }} + - --ntp-server={{ . }} + {{- end }} {{- include "logFlags" . | indent 10 }} env: {{- if .Values.namespace }} diff --git a/charts/yawol-controller/values.yaml b/charts/yawol-controller/values.yaml index 6b027d0d..b9b536a2 100644 --- a/charts/yawol-controller/values.yaml +++ b/charts/yawol-controller/values.yaml @@ -88,6 +88,14 @@ resources: #openstackTimeout: 20s #yawolletRequeueTime: 60 +# NTP pools/servers to configure on LoadBalancerMachines. +# If neither pools nor servers are set, it defaults to using pool.ntp.org. +ntp: + # list of NTP pools + pools: [] + # list of individual NTP servers + servers: [] + # the name of the Kubernetes secret that contains the .openrc file contents # with the correct permissions to connect to the OpenStack API # diff --git a/cmd/yawol-controller/main.go b/cmd/yawol-controller/main.go index d0e2a162..4c270843 100644 --- a/cmd/yawol-controller/main.go +++ b/cmd/yawol-controller/main.go @@ -67,6 +67,9 @@ func main() { var yawolletRequeueTime int var lbmDeletionGracePeriod time.Duration + var ntpPools []string + var ntpServers []string + var openstackTimeout time.Duration // settings for leases @@ -114,6 +117,13 @@ func main() { "Grace period before deleting a load balancer machine AFTER the machine has first been identified as unready.", ) + fs.StringSliceVar(&ntpPools, "ntp-pool", ntpPools, + "List of NTP pools to configure on LoadBalancerMachines. Can be specified multiple times. "+ + "If neither ntp-pool nor ntp-server is set, it defaults to using pool.ntp.org.") + fs.StringSliceVar(&ntpServers, "ntp-server", ntpServers, + "List of individual NTP servers to configure on LoadBalancerMachines. Can be specified multiple times. "+ + "If neither ntp-pool nor ntp-server is set, it defaults to using pool.ntp.org.") + fs.DurationVar(&openstackTimeout, "openstack-timeout", 20*time.Second, "Timeout for all requests against Openstack.") fs.IntVar(&leasesDurationInt, "leases-duration", 60, @@ -300,6 +310,8 @@ func main() { Metrics: &helpermetrics.LoadBalancerMachineMetrics, OpenstackTimeout: openstackTimeout, YawolletRequeueTime: yawolletRequeueTime, + NTPPools: ntpPools, + NTPServers: ntpServers, DiscoveryClient: discoveryClient, RateLimiter: rateLimiter, }).SetupWithManager(loadBalancerMachineMgr); err != nil { diff --git a/controllers/yawol-controller/loadbalancermachine/loadbalancermachine_controller.go b/controllers/yawol-controller/loadbalancermachine/loadbalancermachine_controller.go index 6fd35989..1c1cc8ca 100644 --- a/controllers/yawol-controller/loadbalancermachine/loadbalancermachine_controller.go +++ b/controllers/yawol-controller/loadbalancermachine/loadbalancermachine_controller.go @@ -63,6 +63,8 @@ type LoadBalancerMachineReconciler struct { //nolint:revive // naming from kubeb WorkerCount int OpenstackTimeout time.Duration YawolletRequeueTime int + NTPPools []string + NTPServers []string DiscoveryClient *discovery.DiscoveryClient RateLimiter ratelimiter.RateLimiter } @@ -429,7 +431,7 @@ func (r *LoadBalancerMachineReconciler) reconcileRoleBinding( return nil } -func (r *LoadBalancerMachineReconciler) reconcilePort( //nolint: gocyclo // TODO reduce complexity in future +func (r *LoadBalancerMachineReconciler) reconcilePort( // nolint: gocyclo // TODO reduce complexity in future ctx context.Context, osClient os.Client, req ctrl.Request, @@ -439,20 +441,20 @@ func (r *LoadBalancerMachineReconciler) reconcilePort( //nolint: gocyclo // TODO var err error // TODO cleanup after removing deprecated fields - if lbm.Status.PortID != nil && lbm.Status.DefaultPortID == nil { //nolint: staticcheck // needed to be backwards compatible + if lbm.Status.PortID != nil && lbm.Status.DefaultPortID == nil { // nolint: staticcheck // needed to be backwards compatible if err := helper.PatchLBMStatus( ctx, r.Client.Status(), lbm, - yawolv1beta1.LoadBalancerMachineStatus{DefaultPortID: lbm.Status.PortID}, //nolint: staticcheck // needed to be backwards compatible + yawolv1beta1.LoadBalancerMachineStatus{DefaultPortID: lbm.Status.PortID}, // nolint: staticcheck // needed to be backwards compatible ); err != nil { return err } } // TODO cleanup after removing deprecated fields - if lbm.Status.PortID != nil && lbm.Status.DefaultPortID != nil && //nolint: staticcheck // needed to be backwards compatible - *lbm.Status.PortID == *lbm.Status.DefaultPortID { //nolint: staticcheck // needed to be backwards compatible + if lbm.Status.PortID != nil && lbm.Status.DefaultPortID != nil && // nolint: staticcheck // needed to be backwards compatible + *lbm.Status.PortID == *lbm.Status.DefaultPortID { // nolint: staticcheck // needed to be backwards compatible if err := helper.RemoveFromLBMStatus(ctx, r.Client.Status(), lbm, "portID"); err != nil { return err } @@ -460,8 +462,8 @@ func (r *LoadBalancerMachineReconciler) reconcilePort( //nolint: gocyclo // TODO // TODO cleanup after removing deprecated fields var networkID string - if lbm.Spec.Infrastructure.NetworkID != "" { //nolint: staticcheck // needed to be backwards compatible - networkID = lbm.Spec.Infrastructure.NetworkID //nolint: staticcheck // needed to be backwards compatible + if lbm.Spec.Infrastructure.NetworkID != "" { // nolint: staticcheck // needed to be backwards compatible + networkID = lbm.Spec.Infrastructure.NetworkID // nolint: staticcheck // needed to be backwards compatible } if lbm.Spec.Infrastructure.DefaultNetwork.NetworkID != "" { networkID = lbm.Spec.Infrastructure.DefaultNetwork.NetworkID @@ -685,6 +687,8 @@ func (r *LoadBalancerMachineReconciler) reconcileServer( loadBalancerMachine, vip, r.YawolletRequeueTime, + r.NTPPools, + r.NTPServers, ) if err != nil { return err @@ -766,8 +770,8 @@ func (r *LoadBalancerMachineReconciler) createServer( // TODO cleanup after removing deprecated fields var networkID string - if loadBalancerMachine.Spec.Infrastructure.NetworkID != "" { //nolint: staticcheck // needed to be backwards compatible - networkID = loadBalancerMachine.Spec.Infrastructure.NetworkID //nolint: staticcheck // needed to be backwards compatible + if loadBalancerMachine.Spec.Infrastructure.NetworkID != "" { // nolint: staticcheck // needed to be backwards compatible + networkID = loadBalancerMachine.Spec.Infrastructure.NetworkID // nolint: staticcheck // needed to be backwards compatible } if loadBalancerMachine.Spec.Infrastructure.DefaultNetwork.NetworkID != "" { networkID = loadBalancerMachine.Spec.Infrastructure.DefaultNetwork.NetworkID @@ -922,8 +926,8 @@ func (r *LoadBalancerMachineReconciler) deletePort( } // TODO cleanup after removing deprecated fields - if lbm.Status.PortID != nil { //nolint: staticcheck // needed to be backwards compatible - //nolint: staticcheck // needed to be backwards compatible + if lbm.Status.PortID != nil { // nolint: staticcheck // needed to be backwards compatible + // nolint: staticcheck // needed to be backwards compatible if err = openstackhelper.DeletePort(ctx, portClient, *lbm.Status.PortID); err != nil { switch err.(type) { case gophercloud.ErrDefault404: @@ -977,7 +981,7 @@ func (r *LoadBalancerMachineReconciler) deleteSA( Namespace: lbm.Namespace, }, } - if err := r.Client.Delete(ctx, &sa); client.IgnoreNotFound(err) != nil { //nolint: gocritic // ignore of not found is intended + if err := r.Client.Delete(ctx, &sa); client.IgnoreNotFound(err) != nil { // nolint: gocritic // ignore of not found is intended return err } @@ -994,7 +998,7 @@ func (r *LoadBalancerMachineReconciler) deleteRoleBinding( Namespace: lbm.Namespace, }, } - if err := r.Client.Delete(ctx, &rb); client.IgnoreNotFound(err) != nil { //nolint: gocritic // ignore of not found is intended + if err := r.Client.Delete(ctx, &rb); client.IgnoreNotFound(err) != nil { // nolint: gocritic // ignore of not found is intended return err } @@ -1011,7 +1015,7 @@ func (r *LoadBalancerMachineReconciler) deleteRole( Namespace: lbm.Namespace, }, } - if err := r.Client.Delete(ctx, &role); client.IgnoreNotFound(err) != nil { //nolint: gocritic // ignore of not found is intended + if err := r.Client.Delete(ctx, &role); client.IgnoreNotFound(err) != nil { // nolint: gocritic // ignore of not found is intended return err } diff --git a/go.mod b/go.mod index d9052729..41ae8879 100644 --- a/go.mod +++ b/go.mod @@ -8,12 +8,15 @@ require ( github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 github.com/envoyproxy/go-control-plane v0.13.0 github.com/go-logr/logr v1.4.2 + github.com/go-task/slim-sprig/v3 v3.0.0 github.com/golang/protobuf v1.5.4 github.com/gophercloud/gophercloud v1.13.0 github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 github.com/onsi/ginkgo/v2 v2.20.2 github.com/onsi/gomega v1.34.2 github.com/prometheus/client_golang v1.20.1 + github.com/shirou/gopsutil/v4 v4.24.7 + github.com/spf13/pflag v1.0.5 go.uber.org/zap v1.27.0 golang.org/x/time v0.6.0 google.golang.org/grpc v1.65.0 @@ -29,8 +32,6 @@ require ( sigs.k8s.io/controller-tools v0.15.0 ) -require github.com/shirou/gopsutil/v4 v4.24.7 - require ( cel.dev/expr v0.15.0 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -48,7 +49,6 @@ require ( github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect - github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gobuffalo/flect v1.0.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -80,7 +80,6 @@ require ( github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cobra v1.8.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect diff --git a/internal/helper/loadbalancermachine.go b/internal/helper/loadbalancermachine.go index fd9047f5..35b9bbde 100644 --- a/internal/helper/loadbalancermachine.go +++ b/internal/helper/loadbalancermachine.go @@ -3,22 +3,38 @@ package helper import ( "bytes" "context" + _ "embed" "encoding/base64" "encoding/json" "fmt" "strconv" + "text/template" "time" - yawolv1beta1 "github.com/stackitcloud/yawol/api/v1beta1" - helpermetrics "github.com/stackitcloud/yawol/internal/metrics" - + sprig "github.com/go-task/slim-sprig/v3" "gopkg.in/yaml.v3" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" "sigs.k8s.io/controller-runtime/pkg/client" + + yawolv1beta1 "github.com/stackitcloud/yawol/api/v1beta1" + helpermetrics "github.com/stackitcloud/yawol/internal/metrics" ) +var ( + //go:embed templates/chrony.conf.tpl + chronyConfigTemplateRaw string + chronyConfigTemplate *template.Template +) + +func init() { + var err error + chronyConfigTemplate, err = template.New("chrony.conf").Funcs(sprig.HermeticTxtFuncMap()).Parse(chronyConfigTemplateRaw) + utilruntime.Must(err) +} + // LoadBalancerMachineOpenstackReconcileIsNeeded returns true if an openstack reconcile is needed. func LoadBalancerMachineOpenstackReconcileIsNeeded(lbm *yawolv1beta1.LoadBalancerMachine) bool { // LastOpenstackReconcile is nil, first run @@ -251,6 +267,7 @@ func GenerateUserData( loadbalancerMachine *yawolv1beta1.LoadBalancerMachine, vip string, yawolletRequeueTime int, + ntpPools, ntpServers []string, ) (string, error) { var err error const ( @@ -300,9 +317,24 @@ func GenerateUserData( yawolletArgs = yawolletArgs + "--requeue-time=" + strconv.Itoa(yawolletRequeueTime) + " " } + // cloud-init also features an NTP module that can configure chrony, see + // https://cloudinit.readthedocs.io/en/latest/reference/modules.html#ntp. + // We don't use cloud-init's NTP module as we want to set the `cmdport` directive. To do so via the cloud-config, + // we would need to write a jinja template for the chrony config file. I.e., we would have gain nothing over + // generating the chrony config ourselves and explicitly restarting the service. + chronyConfig, err := generateChronyConfig(ntpPools, ntpServers) + if err != nil { + return "", fmt.Errorf("failed generating chrony config: %w", err) + } + return ` #cloud-config write_files: +- encoding: b64 + content: ` + base64.StdEncoding.EncodeToString(chronyConfig) + ` + owner: root:root + path: /etc/chrony/chrony.conf + permissions: '0644' - encoding: b64 content: ` + kubeconfigBase64 + ` owner: yawol:yawol @@ -327,6 +359,7 @@ write_files: YAWOLLET_ARGS="` + yawolletArgs + `" path: /etc/yawol/env.conf runcmd: + - [ /sbin/rc-service, chronyd, restart ] - [ /sbin/rc-service, promtail, ` + promtailOpenRCState + ` ] - [ /sbin/rc-update, ` + promtailOpenRC + `, promtail, default ] - [ /sbin/rc-service, sshd, ` + sshOpenRCState + ` ] @@ -337,6 +370,20 @@ runcmd: `, nil } +func generateChronyConfig(pools, servers []string) ([]byte, error) { + // If neither --ntp-pool nor --ntp-server is set, default to using pool.ntp.org (resembles the default chrony config). + if len(pools) == 0 && len(servers) == 0 { + pools = []string{"pool.ntp.org"} + } + + conf := &bytes.Buffer{} + err := chronyConfigTemplate.Execute(conf, map[string]any{ + "pools": pools, + "servers": servers, + }) + return conf.Bytes(), err +} + func generateKeepalivedConfig(vip string) string { return `! Configuration File for keepalived diff --git a/internal/helper/loadbalancermachine_test.go b/internal/helper/loadbalancermachine_test.go index caa2a688..56760bd5 100644 --- a/internal/helper/loadbalancermachine_test.go +++ b/internal/helper/loadbalancermachine_test.go @@ -160,3 +160,117 @@ var _ = DescribeTable("areRelevantConditionsMet", expect: false, }), ) + +var _ = Describe("generateChronyConfig", func() { + var ( + pools, servers []string + ) + + BeforeEach(func() { + pools = []string{"pool.a.org", "pool.b.org"} + servers = []string{"ntp.a.org", "10.0.10.1"} + }) + + It("should use pool.ntp.org if neither pools nor servers are specified", func() { + Expect(generateChronyConfig(nil, nil)).To(BeEquivalentTo(`pool pool.ntp.org iburst +# Settings from alpine default chrony config +driftfile /var/lib/chrony/chrony.drift +rtcsync +# prevent chrony from opening ports on the LoadBalancer machine +cmdport 0 + +# Settings from cloud-init generated chrony config +# Stop bad estimates upsetting machine clock. +maxupdateskew 100.0 +# Step the system clock instead of slewing it if the adjustment is larger than +# one second, but only in the first three clock updates. +makestep 1 3 +`)) + }) + + It("should use the configured pools", func() { + Expect(generateChronyConfig(pools[:1], nil)).To(BeEquivalentTo(`pool pool.a.org iburst +# Settings from alpine default chrony config +driftfile /var/lib/chrony/chrony.drift +rtcsync +# prevent chrony from opening ports on the LoadBalancer machine +cmdport 0 + +# Settings from cloud-init generated chrony config +# Stop bad estimates upsetting machine clock. +maxupdateskew 100.0 +# Step the system clock instead of slewing it if the adjustment is larger than +# one second, but only in the first three clock updates. +makestep 1 3 +`)) + + Expect(generateChronyConfig(pools, nil)).To(BeEquivalentTo(`pool pool.a.org iburst +pool pool.b.org iburst +# Settings from alpine default chrony config +driftfile /var/lib/chrony/chrony.drift +rtcsync +# prevent chrony from opening ports on the LoadBalancer machine +cmdport 0 + +# Settings from cloud-init generated chrony config +# Stop bad estimates upsetting machine clock. +maxupdateskew 100.0 +# Step the system clock instead of slewing it if the adjustment is larger than +# one second, but only in the first three clock updates. +makestep 1 3 +`)) + }) + + It("should use the configured servers", func() { + Expect(generateChronyConfig(nil, servers[:1])).To(BeEquivalentTo(`server ntp.a.org iburst +# Settings from alpine default chrony config +driftfile /var/lib/chrony/chrony.drift +rtcsync +# prevent chrony from opening ports on the LoadBalancer machine +cmdport 0 + +# Settings from cloud-init generated chrony config +# Stop bad estimates upsetting machine clock. +maxupdateskew 100.0 +# Step the system clock instead of slewing it if the adjustment is larger than +# one second, but only in the first three clock updates. +makestep 1 3 +`)) + + Expect(generateChronyConfig(nil, servers)).To(BeEquivalentTo(`server ntp.a.org iburst +server 10.0.10.1 iburst +# Settings from alpine default chrony config +driftfile /var/lib/chrony/chrony.drift +rtcsync +# prevent chrony from opening ports on the LoadBalancer machine +cmdport 0 + +# Settings from cloud-init generated chrony config +# Stop bad estimates upsetting machine clock. +maxupdateskew 100.0 +# Step the system clock instead of slewing it if the adjustment is larger than +# one second, but only in the first three clock updates. +makestep 1 3 +`)) + }) + + It("should use both the configured pools and servers", func() { + Expect(generateChronyConfig(pools, servers)).To(BeEquivalentTo(`pool pool.a.org iburst +pool pool.b.org iburst +server ntp.a.org iburst +server 10.0.10.1 iburst +# Settings from alpine default chrony config +driftfile /var/lib/chrony/chrony.drift +rtcsync +# prevent chrony from opening ports on the LoadBalancer machine +cmdport 0 + +# Settings from cloud-init generated chrony config +# Stop bad estimates upsetting machine clock. +maxupdateskew 100.0 +# Step the system clock instead of slewing it if the adjustment is larger than +# one second, but only in the first three clock updates. +makestep 1 3 +`)) + }) +}) diff --git a/internal/helper/templates/chrony.conf.tpl b/internal/helper/templates/chrony.conf.tpl new file mode 100644 index 00000000..ab26fad8 --- /dev/null +++ b/internal/helper/templates/chrony.conf.tpl @@ -0,0 +1,19 @@ +{{- range .pools -}} +pool {{ . }} iburst +{{ end -}} +{{- range .servers -}} +server {{ . }} iburst +{{ end -}} + +# Settings from alpine default chrony config +driftfile /var/lib/chrony/chrony.drift +rtcsync +# prevent chrony from opening ports on the LoadBalancer machine +cmdport 0 + +# Settings from cloud-init generated chrony config +# Stop bad estimates upsetting machine clock. +maxupdateskew 100.0 +# Step the system clock instead of slewing it if the adjustment is larger than +# one second, but only in the first three clock updates. +makestep 1 3