From 05f51b8ca384b8f44caecbaee27781c524f686d2 Mon Sep 17 00:00:00 2001 From: Pavel Okhlopkov Date: Thu, 26 Feb 2026 16:04:17 +0300 Subject: [PATCH 1/2] try Signed-off-by: Pavel Okhlopkov --- test/config-values.yaml | 1089 +++++++++++++++++++++++++++ test/module-config.yaml | 51 ++ test/validate_module_config_test.go | 400 ++++++++++ 3 files changed, 1540 insertions(+) create mode 100644 test/config-values.yaml create mode 100644 test/module-config.yaml create mode 100644 test/validate_module_config_test.go diff --git a/test/config-values.yaml b/test/config-values.yaml new file mode 100644 index 00000000..cf50194a --- /dev/null +++ b/test/config-values.yaml @@ -0,0 +1,1089 @@ +x-config-version: 3 +type: object +properties: + globalVersion: + type: string + pattern: '^[0-9]+\.[0-9]+$' + description: Specific version of Istio control-plane which handles unspecific versions of data plane (namespaces with `istio-injection=enabled` label, not `istio.io/rev=`). + examples: ["1.21"] + default: "1.21" + additionalVersions: + type: array + description: | + Additional versions of Istio control plane to install. You can use specific namespace labels (`istio.io/rev=`) to switch between installed revisions. + examples: + - ["1.21"] + default: [] + items: + type: string + pattern: '^[0-9]+\.[0-9]+$' + outboundTrafficPolicyMode: + type: string + enum: [AllowAny, RegistryOnly] + x-examples: ["AllowAny"] + description: | + How to handle requests directed to external services which aren't registered in service mesh. + - `AllowAny` — allow. + - `RegistryOnly` — deny. In this case to work with external services you need to register them with ServiceEntry custom resource or to organize egressgateway. + default: AllowAny + ingressClass: + type: string + pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + description: | + The class of the Ingress controller used for Kiali, metadata-exporter and proxy-api. + + Optional. By default, the `modules.ingressClass` global value is used. + federation: + type: object + x-doc-d8Editions: + - ee + - cse-pro + description: Parameters for federating with other clusters. + default: {} + properties: + enabled: + type: boolean + description: Designate this cluster as a federation member (see [Enabling federation](./#enabling-the-federation)). + default: false + x-examples: [true] + multicluster: + type: object + x-doc-d8Editions: + - ee + - cse-pro + description: Multicluster parameters. + default: {} + properties: + enabled: + type: boolean + description: Designate this cluster as a multicluster member (see [Enabling multicluster](./#enabling-the-multicluster)). + default: false + x-examples: [true] + alliance: + type: object + x-doc-d8Editions: + - ee + - cse-pro + description: Common options both for federation and multicluster. + default: {} + properties: + ingressGateway: + type: object + description: ingressgateway settings. + x-doc-d8Editions: + - ee + - cse-pro + default: {} + properties: + advertise: + type: array + description: The actual addresses that will be announced to remote clusters for organizing intercluster application requests. If not specified, the addresses will be discovered automatically. + x-doc-d8Editions: + - ee + - cse-pro + default: [] + x-examples: [[{"address": "172.16.0.5", "port": 15443}],[{"address": "somehost.example.com", "port": 15443}]] + items: + type: object + required: ["address", "port"] + properties: + address: + anyOf: + - type: string + example: "somehost.example.com" + pattern: '^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$' + - type: string + example: "172.16.0.5" + pattern: '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' + port: + type: integer + minimum: 1024 + maximum: 65535 + inlet: + type: string + enum: [LoadBalancer, NodePort] + x-examples: [LoadBalancer] + x-doc-d8Editions: + - ee + - cse-pro + description: | + The method for exposing ingressgateway. + - `LoadBalancer` — is a recommended method if you have a cloud-based cluster and it supports Load Balancing. + - `NodePort` — for installations that do not have the LB. + default: LoadBalancer + nodePort: + type: object + description: Special settings for NodePort inlet. + x-doc-d8Editions: + - ee + - cse-pro + default: {} + x-examples: [{}, {"port": 30001}] + properties: + port: + type: integer + description: Static port number for NodePort-type Service. Must be in range, set by kube-apiserver --service-node-port-range argument (default is 30000-32767). + minimum: 1024 + maximum: 65535 + serviceAnnotations: + type: object + additionalProperties: + type: string + description: Additional service annotations. They can be used, e.g., for configuring a local LB in the Yandex Cloud (using the `yandex.cpi.flant.com/listener-subnet-id` annotation). + x-doc-d8Editions: + - ee + - cse-pro + example: + yandex.cpi.flant.com/listener-subnet-id: xyz-123 + nodeSelector: + type: object + additionalProperties: + type: string + x-examples: [{"type":"ingress"}] + description: | + ingressgateway DaemonSet nodeSelector. + + The same as the `spec.nodeSelector` pod parameter in Kubernetes. + x-doc-d8Editions: + - ee + - cse-pro + tolerations: + type: array + description: | + ingressgateway DaemonSet tolerations. + + The same as `spec.tolerations` for the Kubernetes pod. + x-doc-d8Editions: + - ee + - cse-pro + items: + type: object + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + format: int64 + value: + type: string + x-examples: + - [{"operator": "Exists"}] + tracing: + type: object + description: Tracing parameters. + default: {} + properties: + enabled: + type: boolean + description: Turn on or off tracing collection and displaying in Kiali. + default: false + x-examples: [true] + sampling: + type: number + minimum: 0.01 + maximum: 100.0 + multipleOf: 0.01 + description: | + The sampling rate option can be used to control what percentage of requests get reported to your tracing system. + + This should be configured depending upon your traffic in the mesh and the amount of tracing data you want to collect. + + It is possible to override this option with the following Pod annotation: + + ```yaml + proxy.istio.io/config: | + tracing: + sampling: 100.0 + ``` + default: 1.0 + x-examples: [50.05] + collector: + type: object + description: Tracing collection settings. + default: {} + properties: + zipkin: + type: object + description: | + Zipkin protocol parameters used by Istio for sending traces. Jaeger supports this protocol. + + If tracing is enabled, this settings section is mandatory. + default: {} + properties: + address: + type: string + description: Network address of zipkin collector in `:` format. + pattern: '[0-9a-zA-Z\.-]+' + example: "zipkin.myjaeger.svc:9411" + kiali: + type: object + description: | + Span displaying settings for Kiali. + + When not configured, Kiali won't show any tracing dashboards. + default: {} + properties: + jaegerURLForUsers: + type: string + description: Jaeger UI address for users. Mandatory parameter if Kiali is enabled. + example: "https://tracing-service:4443/jaeger" + jaegerGRPCEndpoint: + type: string + description: | + Accessible from cluster address of jaeger GRPC interface for system queries by Kiali. + + When not configured, Kiali will only show external links using the `jaegerURLForUsers` config without interpretationing. + example: "http://tracing.myjaeger.svc:16685/" + x-examples: + - {} + - jaegerURLForUsers: https://tracing-service:4443/jaeger + jaegerGRPCEndpoint: http://tracing.myjaeger.svc:16685/ + sidecar: + type: object + description: Network settings for traffic capture by istio sidecar. + default: {} + properties: + includeOutboundIPRanges: + description: | + Traffic to these IP ranges is forcibly routed through Istio. + + You can redefine this parameter for single Pod using the `traffic.sidecar.istio.io/includeOutboundIPRanges` annotation. + type: array + items: + type: string + pattern: '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$' + default: ["0.0.0.0/0"] + example: ["10.1.1.0/24"] + x-examples: + - [] + - ["1.1.1.1/32", "1.2.3.0/24"] + excludeOutboundIPRanges: + description: | + Traffic to these IP ranges is guaranteed not to flow through Istio. + + You can redefine this parameter for single Pod using the `traffic.sidecar.istio.io/excludeOutboundIPRanges` annotation. + type: array + items: + type: string + pattern: '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$' + default: [] + example: ["10.1.1.0/24"] + x-examples: + - ["1.1.1.1/32", "1.2.3.0/24"] + excludeInboundPorts: + description: | + The range of inbound ports whose traffic is guaranteed not to flow through Istio. + + You can redefine this parameter for single Pod using the `traffic.sidecar.istio.io/excludeInboundPorts` annotation. + type: array + items: + type: string + pattern: '^[0-9]{1,5}$' + default: [] + example: ["8080", "8443"] + x-examples: + - [] + - ["8080", "8443"] + excludeOutboundPorts: + description: | + The range of outbound ports whose traffic is guaranteed not to flow through Istio. + + You can redefine this parameter for single Pod using the `traffic.sidecar.istio.io/excludeOutboundPorts` annotation. + type: array + items: + type: string + pattern: '^[0-9]{1,5}$' + default: [] + example: ["8080", "8443"] + x-examples: + - ["8080", "8443"] + resourcesManagement: + description: | + Manages Istio sidecar container resources. + + **Caution!** The setting only applies to new Pods with `istio-proxy`. + type: object + default: {} + x-examples: + - static: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "2000m" + memory: "1Gi" + properties: + mode: + type: string + description: | + Resource management mode: + - `Static` — allows you to specify requests/limits. The parameters of this mode are defined in the [static](#parameters-sidecar-resourcesmanagement-static) parameter section; + enum: [ 'Static'] + default: 'Static' + static: + type: object + description: | + Resource management options for the `Static` mode. + properties: + requests: + type: object + description: | + Resource requests settings for pods. + properties: + cpu: + type: string + pattern: '^[0-9]+m?$' + default: '100m' + description: | + Configuring CPU requests. + memory: + oneOf: + - type: string + pattern: '^[0-9]+(\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki)?$' + - type: number + default: '128Mi' + description: | + Configuring memory requests. + limits: + type: object + description: | + Configuring CPU and memory limits. + properties: + cpu: + type: string + pattern: '^[0-9]+m?$' + default: '2000m' + description: | + Configuring CPU limits. + memory: + oneOf: + - type: string + pattern: '^[0-9]+(\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki)?$' + - type: number + default: '1Gi' + description: | + Configuring memory limits. + ca: + type: object + description: | + Explicitly specified root certificate. It signs individual service certificates to use in mutual TLS connections. + To create a certificate, you can use the [example](/products/kubernetes-platform/documentation/v1/user/security/tls.html#certificate-generation-example), in which `basicConstraints = CA:FALSE` needs to be replaced with `basicConstraints = CA:TRUE`. + default: {} + properties: + cert: + type: string + description: The root or intermediate certificate in PEM format. + key: + type: string + description: The key to the root certificate in PEM format. + chain: + type: string + description: A certificate chain in PEM format if `cert` is an intermediate certificate. + root: + type: string + description: The root certificate in PEM format if `cert` is an intermediate certificate. + controlPlane: + type: object + description: istiod specific settings. + default: {} + oneOf: + - properties: + replicasManagement: + properties: + mode: + enum: ['Standard', 'Static'] + resourcesManagement: + properties: + mode: + enum: ['Static', 'VPA'] + - properties: + replicasManagement: + properties: + mode: + enum: ['HPA'] + resourcesManagement: + properties: + mode: + enum: ['Static'] + properties: + nodeSelector: + type: object + additionalProperties: + type: string + description: | + Optional `nodeSelector` for istiod. The same as the `spec.nodeSelector` pod parameter in Kubernetes. + + If the parameter is omitted or `false`, it will be determined [automatically](https://deckhouse.io/products/kubernetes-platform/documentation/v1/#advanced-scheduling). + tolerations: + type: array + description: | + Optional `tolerations` for istiod. The same as `spec.tolerations` for the Kubernetes pod. + + If the parameter is omitted or `false`, it will be determined [automatically](https://deckhouse.io/products/kubernetes-platform/documentation/v1/#advanced-scheduling). + items: + type: object + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + format: int64 + value: + type: string + replicasManagement: + description: | + Replication management settings and scaling of istiod. + type: object + default: {} + x-examples: + - mode: Standard + - mode: Static + static: + replicas: 3 + x-doc-examples: + - mode: Standard + - mode: Static + static: + replicas: 3 + - mode: HPA + hpa: + minReplicas: 2 + maxReplicas: 5 + metrics: + - type: CPU + targetAverageUtilization: 80 + properties: + mode: + type: string + description: | + Replicas management mode: + - `Standard` — replicas management and scaling mode according to the global fault tolerance mode (the [highAvailability](/products/kubernetes-platform/documentation/v1/reference/api/global.html#parameters-highavailability) parameter); + - `Static` — the mode, where the number of replicas is specified explicitly (the [static.replicas](#parameters-controlplane-replicasmanagement-static-replicas) parameter); + - `HPA` — the mode, where the number of replicas is calculated automatically using [HPA](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/) based on CPU usage. You can configure this mode by modifying parameters in the [hpa](#parameters-controlplane-replicasmanagement-hpa) parameter section. + enum: ['Standard', 'Static', 'HPA'] + default: 'Standard' + static: + type: object + description: | + Options for replicas management for the `Static` mode. + required: ["replicas"] + properties: + replicas: + type: number + minimum: 1 + description: | + Desired number of replicas. + hpa: + type: object + description: | + Options for replicas management for the `HPA` mode. + required: ["minReplicas", "maxReplicas", "metrics"] + properties: + minReplicas: + type: number + minimum: 1 + description: | + The lower limit for the number of replicas to which the HPA can scale down. + maxReplicas: + type: number + minimum: 1 + description: | + The upper limit for the number of replicas to which the HPA can scale up. It cannot be less that `minReplicas`. + metrics: + type: array + description: | + The HPA will use these metrics to decide whether to increase or decrease the number of replicates. + minItems: 1 + items: + type: object + required: ["type","targetAverageUtilization"] + properties: + type: + type: string + description: | + Metric type. + enum: ['CPU'] + targetAverageUtilization: + type: number + description: | + The target value of the average of the resource metric across all relevant pods, represented as a percentage of the requested value of the resource for the pods. + minimum: 1 + maximum: 100 + resourcesManagement: + description: | + Settings for CPU and memory requests and limits by istiod pods. + type: object + default: {} + x-examples: + - mode: VPA + vpa: + mode: "InPlaceOrRecreate" + cpu: + min: "50m" + max: 2 + limitRatio: 1.5 + memory: + min: "256Mi" + max: "2Gi" + limitRatio: 1.5 + - mode: Static + static: + requests: + cpu: "55m" + memory: "256Mi" + limits: + cpu: "2" + memory: "2Gi" + - mode: VPA + vpa: + mode: Auto + cpu: + min: "50m" + max: 2 + limitRatio: 1.5 + memory: + min: "256Mi" + max: "2Gi" + limitRatio: 1.5 + properties: + mode: + type: string + description: | + Resource management mode: + - `Static` — allows you to specify requests/limits. The parameters of this mode are defined in the [static](#parameters-controlplane-resourcesmanagement-static) parameter section; + - `VPA` — uses [VPA](https://github.com/kubernetes/design-proposals-archive/blob/main/autoscaling/vertical-pod-autoscaler.md). You can configure this mode by modifying parameters in the [vpa](#parameters-controlplane-resourcesmanagement-vpa) parameter section. + enum: ['VPA', 'Static'] + default: 'VPA' + vpa: + type: object + default: {} + description: | + Resource management options for the `VPA` mode. + properties: + mode: + type: string + description: | + VPA operating mode. + + - `Initial` — VPA sets initial values for pod resource requests and limits at pod creation time. + Resource values are not changed automatically afterwards. + + - `InPlaceOrRecreate` — VPA attempts to update pod resources in place when supported by the cluster. + If in-place updates are not possible, the pod will be recreated. + + - `Auto` — VPA automatically recreates pods to apply updated resource values. + + > Starting from Deckhouse version 1.75, this mode is considered legacy. + > It is recommended to use `InPlaceOrRecreate` instead. + enum: ['Initial', 'InPlaceOrRecreate', 'Auto'] + default: 'InPlaceOrRecreate' + cpu: + type: object + default: {} + description: | + CPU-related VPA settings. + properties: + max: + description: | + The maximum value that the VPA can set for the CPU requests. + default: 2 + oneOf: + - type: string + pattern: "^[0-9]+m?$" + - type: number + min: + description: | + The minimum value that the VPA can set for the CPU requests. + default: '50m' + oneOf: + - type: string + pattern: "^[0-9]+m?$" + - type: number + limitRatio: + type: number + examples: [1.5] + description: | + The CPU limits/requests ratio. + + This ratio is used for calculating the initial CPU limits for a pod. + + If this parameter is set, the VPA will recalculate the CPU limits while maintaining the specified limits/requests ratio. + memory: + type: object + default: {} + description: | + Memory-related VPA settings. + properties: + max: + description: | + The maximum memory requests the VPA can set. + default: '2Gi' + oneOf: + - type: string + pattern: '^[0-9]+(\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki)?$' + - type: number + min: + description: | + The minimum memory requests the VPA can set. + default: '256Mi' + oneOf: + - type: string + pattern: '^[0-9]+(\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki)?$' + - type: number + limitRatio: + type: number + examples: [1.5] + description: | + The memory limits/requests ratio. + + This ratio is used for calculating the initial memory limits for a pod. + + If this parameter is set, the VPA will recalculate the memory limits while maintaining the specified limits/requests ratio. + static: + type: object + description: | + Resource management options for the `Static` mode. + properties: + requests: + type: object + description: | + Resource requests settings for pods. + properties: + cpu: + type: string + pattern: "^[0-9]+m?$" + description: | + Configuring CPU requests. + memory: + oneOf: + - type: string + pattern: '^[0-9]+(\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki)?$' + - type: number + description: | + Configuring memory requests. + limits: + type: object + description: | + Configuring CPU and memory limits. + properties: + cpu: + type: string + pattern: "^[0-9]+m?$" + description: | + Configuring CPU limits. + memory: + oneOf: + - type: string + pattern: '^[0-9]+(\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki)?$' + - type: number + description: | + Configuring memory limits. + dataPlane: + type: object + default: {} + properties: + accessLog: + type: object + default: {} + properties: + type: + type: string + enum: [Text, JSON] + default: Text + textFormat: + type: string + description: Sidecar's access log format template. Template operators are described in [envoy documentation](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#command-operators). + default: '[%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%" %RESPONSE_CODE% %RESPONSE_FLAGS% %RESPONSE_CODE_DETAILS% %CONNECTION_TERMINATION_DETAILS% "%UPSTREAM_TRANSPORT_FAILURE_REASON%" %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-FORWARDED-FOR)%" "%REQ(USER-AGENT)%" "%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%" %UPSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_REMOTE_ADDRESS% %REQUESTED_SERVER_NAME% %ROUTE_NAME%' + jsonLabels: + type: object + additionalProperties: + type: string + description: Structured access log format using key-value pairs. Template operators are described in [envoy documentation](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#command-operators). + default: + start_time: "%START_TIME%" + method: "%REQ(:METHOD)%" + path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%" + protocol: "%PROTOCOL%" + response_code: "%RESPONSE_CODE%" + response_flags: "%RESPONSE_FLAGS%" + bytes_received: "%BYTES_RECEIVED%" + bytes_sent: "%BYTES_SENT%" + duration: "%DURATION%" + user_agent: "%REQ(USER-AGENT)%" + authority: "%REQ(:AUTHORITY)%" + upstream_host: "%UPSTREAM_HOST%" + trafficRedirectionSetupMode: + type: string + description: | + Managing the redirection mode of application traffic to be forwarded under Istio control in the Pod's network namespace. + - `CNIPlugin` — in this mode, the configuration is performed by a CNI plugin when creating a Pod on a node. This mode does not require additional permissions for Pods and is recommended. This mode has [limitations](./examples.html#cniplugin-application-traffic-redirection-mode-restrictions) when using application init-containers that perform network communication with other services. + - `InitContainer` — classic mode, each application Pod is automatically injected with a special init-container that configures the network environment of the Pod. In order to perform this configuration, the init-container is given additional permissions, which may not meet the security requirements of individual installations. + default: "InitContainer" + enum: ["CNIPlugin", "InitContainer"] + x-examples: ["CNIPlugin", "InitContainer"] + enableHTTP10: + type: boolean + default: false + x-examples: [ true ] + description: | + Whether to handle HTTP/1.0 requests in istio-sidecars or deny them with `426 Upgrade Required` response. + proxyConfig: + type: object + description: | + Mesh-wide defaults for [ProxyConfig configurations](https://istio.io/latest/docs/reference/config/istio.mesh.v1alpha1/#ProxyConfig). + default: { } + properties: + holdApplicationUntilProxyStarts: + type: boolean + x-examples: [ true ] + default: false + description: | + With this feature, the sidecar-injector injects the sidecar at the first place of Pod's container list and adds a postStart hook to be sure if the Envoy proxy is initialized before the application. So the Envoy is able to handle requests without application network errors. + + This global flag can be overriden per Pod by an annotation — `proxy.istio.io/config: '{ "holdApplicationUntilProxyStarts": true }'`. + idleTimeout: + type: string + pattern: '^[0-9]+(s|m|h)$' + x-examples: [ 24h ] + default: 1h + description: | + Timeout for connections without application activity established between the client's istio-sidecar and the service. When the timeout expires, the connection between the sidecar and the service is closed, but the connection between the application and the sidecar is not closed. If set to `0s`, the timeout is disabled. + + This global flag can be overriden per Pod by an annotation: + ```yaml + proxy.istio.io/config: |- + proxyMetadata: + ISTIO_META_IDLE_TIMEOUT: "12h" + ``` + > **Warning!** Disabling this timeout (setting the value to `0s`) is very likely to result in leaky connections due to TCP FIN packet loss, etc. + > **Warning!** After changing this setting, a restart of the client pods is required. + ztunnel: + type: object + description: | + ztunnel specific settings. + default: {} + x-experimental: true + properties: + resourcesManagement: + description: | + Settings for CPU and memory requests and limits by istiod pods. + type: object + default: {} + x-examples: + - mode: VPA + vpa: + mode: InPlaceOrRecreate + cpu: + min: "50m" + max: 2 + limitRatio: 1.5 + memory: + min: "256Mi" + max: "2Gi" + limitRatio: 1.5 + - mode: Static + static: + requests: + cpu: "55m" + memory: "256Mi" + limits: + cpu: "2" + memory: "2Gi" + - mode: VPA + vpa: + mode: Auto + cpu: + min: "50m" + max: 2 + limitRatio: 1.5 + memory: + min: "256Mi" + max: "2Gi" + limitRatio: 1.5 + properties: + mode: + type: string + description: | + Resource management mode: + - `Static` — allows you to specify requests/limits. The parameters of this mode are defined in the [static](#parameters-dataplane-ztunnel-resourcesmanagement-static) parameter section; + - `VPA` — uses [VPA](https://github.com/kubernetes/design-proposals-archive/blob/main/autoscaling/vertical-pod-autoscaler.md). You can configure this mode by modifying parameters in the [vpa](#parameters-dataplane-ztunnel-resourcesmanagement-vpa) parameter section. + enum: ['VPA', 'Static'] + default: 'VPA' + vpa: + type: object + default: {} + description: | + Resource management options for the `VPA` mode. + properties: + mode: + type: string + description: | + VPA operating mode. + + - `Initial` — VPA sets initial values for pod resource requests and limits at pod creation time. + Resource values are not changed automatically afterwards. + + - `InPlaceOrRecreate` — VPA attempts to update pod resources in place when supported by the cluster. + If in-place updates are not possible, the pod will be recreated. + + - `Auto` — VPA automatically recreates pods to apply updated resource values. + + > Starting from Deckhouse version 1.75, this mode is considered legacy. + > It is recommended to use `InPlaceOrRecreate` instead. + enum: ['Initial', 'InPlaceOrRecreate', 'Auto'] + default: 'InPlaceOrRecreate' + cpu: + type: object + default: {} + description: | + CPU-related VPA settings. + properties: + max: + description: | + The maximum value that the VPA can set for the CPU requests. + default: 2 + oneOf: + - type: string + pattern: "^[0-9]+m?$" + - type: number + min: + description: | + The minimum value that the VPA can set for the CPU requests. + default: '50m' + oneOf: + - type: string + pattern: "^[0-9]+m?$" + - type: number + limitRatio: + type: number + examples: [1.5] + description: | + The CPU limits/requests ratio. + + This ratio is used for calculating the initial CPU limits for a pod. + + If this parameter is set, the VPA will recalculate the CPU limits while maintaining the specified limits/requests ratio. + memory: + type: object + default: {} + description: | + Memory-related VPA settings. + properties: + max: + description: | + The maximum memory requests the VPA can set. + default: '2Gi' + oneOf: + - type: string + pattern: '^[0-9]+(\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki)?$' + - type: number + min: + description: | + The minimum memory requests the VPA can set. + default: '256Mi' + oneOf: + - type: string + pattern: '^[0-9]+(\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki)?$' + - type: number + limitRatio: + type: number + examples: [1.5] + description: | + The memory limits/requests ratio. + + This ratio is used for calculating the initial memory limits for a pod. + + If this parameter is set, the VPA will recalculate the memory limits while maintaining the specified limits/requests ratio. + static: + type: object + description: | + Resource management options for the `Static` mode. + properties: + requests: + type: object + description: | + Resource requests settings for pods. + properties: + cpu: + type: string + pattern: "^[0-9]+m?$" + description: | + Configuring CPU requests. + memory: + oneOf: + - type: string + pattern: '^[0-9]+(\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki)?$' + - type: number + description: | + Configuring memory requests. + limits: + type: object + description: | + Configuring CPU and memory limits. + properties: + cpu: + type: string + pattern: "^[0-9]+m?$" + description: | + Configuring CPU limits. + memory: + oneOf: + - type: string + pattern: '^[0-9]+(\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki)?$' + - type: number + description: | + Configuring memory limits. + nodeSelector: + type: object + additionalProperties: + type: string + description: | + Optional `nodeSelector` for istio-operator, metadata-exporter and Kiali. The same as the `spec.nodeSelector` pod parameter in Kubernetes. + + If the parameter is omitted or `false`, it will be determined [automatically](https://deckhouse.io/products/kubernetes-platform/documentation/v1/#advanced-scheduling). + tolerations: + type: array + description: | + Optional `tolerations` for istio-operator, metadata-exporter and Kiali. The same as `spec.tolerations` for the Kubernetes pod. + + If the parameter is omitted or `false`, it will be determined [automatically](https://deckhouse.io/products/kubernetes-platform/documentation/v1/#advanced-scheduling). + items: + type: object + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + format: int64 + value: + type: string + https: + type: object + x-examples: + - mode: CustomCertificate + customCertificate: + secretName: "foobar" + - mode: CertManager + certManager: + clusterIssuerName: letsencrypt + description: | + What certificate type to use with module's public web interfaces. + + This parameter completely overrides the `global.modules.https` settings. + properties: + mode: + type: string + default: "CertManager" + description: | + The HTTPS usage mode: + - `CertManager` — Kiali/metadata-exporter (including SPIFFE endpoint)/api-proxy will use HTTPS and get a certificate from the clusterissuer defined in the `certManager.clusterIssuerName` parameter. + - `CustomCertificate` — Kiali/metadata-exporter (including SPIFFE endpoint)/api-proxy will use HTTPS using the certificate from the `d8-system` namespace. + - `OnlyInURI` — Kiali/metadata-exporter (including SPIFFE endpoint)/api-proxy will work over HTTP (thinking that there is an external HTTPS load balancer in front that terminates HTTPS traffic). All the links in the `user-authn` will be generated using the HTTPS scheme. Load balancer should provide a redirect from HTTP to HTTPS. + + **Caution!** Unlike other modules, Istio doesn't support non-secured HTTP (`mode: Disabled`). + enum: + - "CertManager" + - "CustomCertificate" + - "OnlyInURI" + certManager: + type: object + properties: + clusterIssuerName: + type: string + default: "letsencrypt" + description: | + What ClusterIssuer to use for Kiali/metadata-exporter (including SPIFFE endpoint)/api-proxy. + + Currently, `letsencrypt`, `letsencrypt-staging`, `selfsigned` are available. Also, you can define your own. + customCertificate: + type: object + default: {} + properties: + secretName: + type: string + description: | + The name of the secret in the `d8-system` namespace to use with Kiali/metadata-exporter (including SPIFFE endpoint)/api-proxy. + + This secret must have the [kubernetes.io/tls](https://kubernetes.github.io/ingress-nginx/user-guide/tls/#tls-secrets) format. + default: "false" + highAvailability: + type: boolean + x-examples: [true] + description: | + Manually enable the high availability mode. + + By default, Deckhouse automatically decides whether to enable the HA mode. Click [here](/products/kubernetes-platform/documentation/v1/reference/api/global.html#parameters) to learn more about the HA mode for modules. + auth: + type: object + default: {} + x-examples: + - externalAuthentication: + authURL: "https://dex.d8.svc.cluster.local/dex/auth" + authSignInURL: "https://example.com/dex/sign_in" + allowedUserGroups: + - admins + description: Options related to authentication or authorization in the application. + properties: + externalAuthentication: + type: object + description: | + Parameters to enable external authentication based on the Ingress NGINX [external-auth](https://kubernetes.github.io/ingress-nginx/examples/auth/external-auth/) mechanism that uses the Nginx [auth_request](https://nginx.org/en/docs/http/ngx_http_auth_request_module.html) module. + + > External authentication is enabled automatically if the [user-authn](https://deckhouse.io/modules/user-authn/) module is enabled. + properties: + authURL: + type: string + x-examples: ["https://example.com/dex/auth"] + description: | + The URL of the authentication service. + + If the user is authenticated, the service should return an HTTP 200 response code. + authSignInURL: + type: string + x-examples: ["https://example.com/dex/sign_in"] + description: The URL to redirect the user for authentication (if the authentication service returned a non-200 HTTP response code). + allowedUserEmails: + type: array + items: + type: string + description: | + An array of emails of users that can access module's public web interfaces. + + This parameter is used if the `user-authn` module is enabled or the `externalAuthentication` parameter is set. + allowedUserGroups: + type: array + items: + type: string + description: | + An array of user groups that can access module's public web interfaces. + + This parameter is used if the `user-authn` module is enabled or the `externalAuthentication` parameter is set. + + **Caution!** Note that you must add those groups to the appropriate field in the DexProvider config if this module is used together with the [user-authn](https://deckhouse.io/modules/user-authn/) one. + whitelistSourceRanges: + type: array + items: + type: string + x-examples: + - [ "1.1.1.1/32" ] + description: An array if CIDRs that are allowed to authenticate in module's public web interfaces. + satisfyAny: + type: boolean + x-examples: [true] + default: false + description: | + Enables single authentication. + + If used together with the whitelistSourceRanges parameter, it authorizes all the users from above networks (no need to enter a username and password). diff --git a/test/module-config.yaml b/test/module-config.yaml new file mode 100644 index 00000000..9a891790 --- /dev/null +++ b/test/module-config.yaml @@ -0,0 +1,51 @@ +apiVersion: deckhouse.io/v1alpha1 +kind: ModuleConfig +metadata: + annotations: + argocd.argoproj.io/tracking-id: deckhouse-modules.customer-prod-cluster:deckhouse.io/ModuleConfig:lmru-devops-k8s-configuration/istio + creationTimestamp: "2022-12-14T16:00:45Z" + finalizers: + - modules.deckhouse.io/module-registered + generation: 10 + name: istio + resourceVersion: "5430247986" + uid: 2a3e8e67-ebfe-47e6-8879-81ec9bf8c066 +spec: + enabled: true + settings: + additionalVersions: + - "1.25" + alliance: + ingressGateway: + inlet: LoadBalancer + nodeSelector: + node-role/loadbalancer: "" + tolerations: + - operator: Exists + dataPlane: + trafficRedirectionSetupMode: CNIPlugin + federation: + enabled: true + globalVersion: "1.25" + multicluster: + enabled: true + sidecar: + resourcesManagement: + mode: Static + static: + limits: + cpu: "10" + memory: 10Gi + requests: + cpu: 100m + memory: 128Mi + tracing: + collector: + zipkin: + address: jaeger-collector.jaeger.svc.customer.p.mesh:9411 + enabled: true + kiali: + jaegerGRPCEndpoint: http://jaeger-query.jaeger.svc.customer.p.mesh:16685 + jaegerURLForUsers: https://jaeger-customer.apps.lmru.tech/ + sampling: 100 + version: 3 \ No newline at end of file diff --git a/test/validate_module_config_test.go b/test/validate_module_config_test.go new file mode 100644 index 00000000..4a704929 --- /dev/null +++ b/test/validate_module_config_test.go @@ -0,0 +1,400 @@ +package test + +import ( + "os" + "testing" + + . "github.com/onsi/gomega" + "gopkg.in/yaml.v3" + + "github.com/flant/addon-operator/pkg/module_manager/models/modules" + "github.com/flant/addon-operator/pkg/utils" +) + +// parseValuesWithYAMLv3 parses YAML using gopkg.in/yaml.v3, which preserves +// integer types (unlike sigs.k8s.io/yaml which converts everything through JSON, +// turning all numbers into float64). +// +// This matches the production code path in the ConfigMap backend +// (pkg/kube_config_manager/backend/configmap/configmap.go) which uses yaml.v3. +// +// The difference: +// - yaml.v3: `sampling: 100` -> Go int (triggers multipleOf bug) +// - sigs.k8s.io/yaml: `sampling: 100` -> Go float64 (masks the bug) +func parseValuesWithYAMLv3(yamlData string) (utils.Values, error) { + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &parsed); err != nil { + return nil, err + } + return utils.Values(parsed), nil +} + +// Test_Validate_RealModuleConfig_MultipleOf_YAMLv3 reproduces the production error: +// +// "factor MultipleOf declared for istio.tracing.sampling must be positive: 0" +// +// Root cause: when YAML value `sampling: 100` is parsed by gopkg.in/yaml.v3, +// it is stored as Go int. The go-openapi/validate library's MultipleOfNativeType +// function then calls MultipleOfInt(path, in, value, int64(multipleOf)), where +// int64(0.01) truncates to 0, triggering "must be positive" error. +// +// The tests use yaml.v3 to parse values (matching the production ConfigMap backend path) +// instead of sigs.k8s.io/yaml (used by utils.NewValuesFromBytes which converts all +// numbers to float64 and masks the bug). +func Test_Validate_RealModuleConfig_MultipleOf_YAMLv3(t *testing.T) { + configSchemaBytes, err := os.ReadFile("config-values.yaml") + if err != nil { + t.Fatalf("failed to read config-values.yaml: %v", err) + } + + tests := []struct { + name string + valuesYAML string + expectError bool + errorSubstr string + }{ + { + name: "sampling as integer 100 triggers multipleOf bug", + valuesYAML: ` +istio: + tracing: + sampling: 100 +`, + // yaml.v3 parses 100 as int → int64(0.01)=0 → "must be positive: 0" + expectError: true, + errorSubstr: "must be positive", + }, + { + name: "sampling as float 100.0 passes validation", + valuesYAML: ` +istio: + tracing: + sampling: 100.0 +`, + expectError: false, + }, + { + name: "sampling as float 50.05 passes validation", + valuesYAML: ` +istio: + tracing: + sampling: 50.05 +`, + expectError: false, + }, + { + name: "sampling as float 0.01 (minimum) passes validation", + valuesYAML: ` +istio: + tracing: + sampling: 0.01 +`, + expectError: false, + }, + { + name: "sampling as integer 1 triggers multipleOf bug", + valuesYAML: ` +istio: + tracing: + sampling: 1 +`, + // yaml.v3 parses 1 as int → int64(0.01)=0 → "must be positive: 0" + expectError: true, + errorSubstr: "must be positive", + }, + { + name: "sampling as float 1.0 passes validation", + valuesYAML: ` +istio: + tracing: + sampling: 1.0 +`, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + moduleValues, err := parseValuesWithYAMLv3(tt.valuesYAML) + g.Expect(err).ShouldNot(HaveOccurred()) + + valuesStorage, err := modules.NewValuesStorage("istio", nil, configSchemaBytes, nil) + g.Expect(err).ShouldNot(HaveOccurred()) + + mErr := valuesStorage.GetSchemaStorage().ValidateConfigValues("istio", moduleValues) + if tt.expectError { + g.Expect(mErr).Should(HaveOccurred(), "expected validation error for: %s", tt.name) + g.Expect(mErr.Error()).Should(ContainSubstring(tt.errorSubstr)) + } else { + g.Expect(mErr).ShouldNot(HaveOccurred(), "unexpected validation error: %v", mErr) + } + }) + } +} + +// Test_Validate_RealModuleConfig_FullSettings_YAMLv3 validates the exact production +// ModuleConfig settings against the istio OpenAPI schema using yaml.v3 parsing. +// This reproduces the exact production scenario that causes a fatal startup error. +func Test_Validate_RealModuleConfig_FullSettings_YAMLv3(t *testing.T) { + g := NewWithT(t) + + configSchemaBytes, err := os.ReadFile("config-values.yaml") + g.Expect(err).ShouldNot(HaveOccurred()) + + // These settings come from the production ModuleConfig resource. + // The key problematic field is `sampling: 100` (integer). + valuesYAML := ` +istio: + additionalVersions: + - "1.25" + alliance: + ingressGateway: + inlet: LoadBalancer + nodeSelector: + node-role/loadbalancer: "" + tolerations: + - operator: Exists + dataPlane: + trafficRedirectionSetupMode: CNIPlugin + federation: + enabled: true + globalVersion: "1.25" + multicluster: + enabled: true + sidecar: + resourcesManagement: + mode: Static + static: + limits: + cpu: "10" + memory: 10Gi + requests: + cpu: 100m + memory: 128Mi + tracing: + collector: + zipkin: + address: jaeger-collector.jaeger.svc.customer.p.mesh:9411 + enabled: true + kiali: + jaegerGRPCEndpoint: http://jaeger-query.jaeger.svc.customer.p.mesh:16685 + jaegerURLForUsers: https://jaeger-customer.apps.lmru.tech/ + sampling: 100 +` + + moduleValues, err := parseValuesWithYAMLv3(valuesYAML) + g.Expect(err).ShouldNot(HaveOccurred()) + + valuesStorage, err := modules.NewValuesStorage("istio", nil, configSchemaBytes, nil) + g.Expect(err).ShouldNot(HaveOccurred()) + + mErr := valuesStorage.GetSchemaStorage().ValidateConfigValues("istio", moduleValues) + g.Expect(mErr).Should(HaveOccurred(), "expected the multipleOf bug to trigger with yaml.v3 parsed integer value") + g.Expect(mErr.Error()).Should(ContainSubstring("must be positive")) +} + +// Test_Validate_MultipleOf_SmallSchema_YAMLv3 tests the multipleOf bug with a minimal +// schema using yaml.v3 parsing to isolate the integer type behavior. +func Test_Validate_MultipleOf_SmallSchema_YAMLv3(t *testing.T) { + tests := []struct { + name string + valuesYAML string + schemaYAML string + expectError bool + errorSubstr string + }{ + { + name: "integer value with fractional multipleOf fails (yaml.v3 preserves int type)", + valuesYAML: ` +moduleName: + sampling: 100 +`, + schemaYAML: ` +type: object +properties: + sampling: + type: number + minimum: 0.01 + maximum: 100.0 + multipleOf: 0.01 +`, + // yaml.v3: 100 → int → int64(0.01)=0 → "must be positive" + expectError: true, + errorSubstr: "must be positive", + }, + { + name: "float value with fractional multipleOf passes", + valuesYAML: ` +moduleName: + sampling: 100.0 +`, + schemaYAML: ` +type: object +properties: + sampling: + type: number + minimum: 0.01 + maximum: 100.0 + multipleOf: 0.01 +`, + expectError: false, + }, + { + name: "integer value with integer multipleOf passes", + valuesYAML: ` +moduleName: + count: 10 +`, + schemaYAML: ` +type: object +properties: + count: + type: integer + multipleOf: 5 +`, + expectError: false, + }, + { + name: "integer value not multiple of integer multipleOf fails", + valuesYAML: ` +moduleName: + count: 7 +`, + schemaYAML: ` +type: object +properties: + count: + type: integer + multipleOf: 5 +`, + expectError: true, + errorSubstr: "should be a multiple of 5", + }, + { + name: "integer value with multipleOf 0.1 fails (truncation bug)", + valuesYAML: ` +moduleName: + value: 5 +`, + schemaYAML: ` +type: object +properties: + value: + type: number + multipleOf: 0.1 +`, + // yaml.v3: 5 → int → int64(0.1)=0 → "must be positive" + expectError: true, + errorSubstr: "must be positive", + }, + { + name: "float value with multipleOf 0.1 passes", + valuesYAML: ` +moduleName: + value: 5.0 +`, + schemaYAML: ` +type: object +properties: + value: + type: number + multipleOf: 0.1 +`, + expectError: false, + }, + { + name: "integer 0 value with fractional multipleOf fails", + valuesYAML: ` +moduleName: + sampling: 0 +`, + schemaYAML: ` +type: object +properties: + sampling: + type: number + minimum: 0 + maximum: 100.0 + multipleOf: 0.01 +`, + // yaml.v3: 0 → int → int64(0.01)=0 → "must be positive" + expectError: true, + errorSubstr: "must be positive", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + moduleValues, err := parseValuesWithYAMLv3(tt.valuesYAML) + g.Expect(err).ShouldNot(HaveOccurred()) + + valuesStorage, err := modules.NewValuesStorage("moduleName", nil, []byte(tt.schemaYAML), nil) + g.Expect(err).ShouldNot(HaveOccurred()) + + mErr := valuesStorage.GetSchemaStorage().ValidateConfigValues("moduleName", moduleValues) + if tt.expectError { + g.Expect(mErr).Should(HaveOccurred(), "expected validation error for: %s", tt.name) + g.Expect(mErr.Error()).Should(ContainSubstring(tt.errorSubstr)) + } else { + g.Expect(mErr).ShouldNot(HaveOccurred(), "unexpected validation error: %v", mErr) + } + }) + } +} + +// Test_Validate_MultipleOf_ParsingDifference demonstrates the root cause: the difference +// between yaml.v3 and sigs.k8s.io/yaml when parsing integer values. +// yaml.v3 preserves int types, sigs.k8s.io/yaml converts all numbers to float64. +func Test_Validate_MultipleOf_ParsingDifference(t *testing.T) { + g := NewWithT(t) + + schemaYAML := ` +type: object +properties: + sampling: + type: number + minimum: 0.01 + maximum: 100.0 + multipleOf: 0.01 +` + valuesStorage, err := modules.NewValuesStorage("moduleName", nil, []byte(schemaYAML), nil) + g.Expect(err).ShouldNot(HaveOccurred()) + + // Test 1: yaml.v3 parsing (production path) — sampling: 100 is parsed as int + yamlV3Values, err := parseValuesWithYAMLv3("moduleName:\n sampling: 100\n") + g.Expect(err).ShouldNot(HaveOccurred()) + + mErr := valuesStorage.GetSchemaStorage().ValidateConfigValues("moduleName", yamlV3Values) + g.Expect(mErr).Should(HaveOccurred(), "yaml.v3 parsed int value should trigger multipleOf bug") + g.Expect(mErr.Error()).Should(ContainSubstring("must be positive")) + + // Test 2: sigs.k8s.io/yaml parsing (test helper path) — sampling: 100 is parsed as float64 + sigsYamlValues, err := utils.NewValuesFromBytes([]byte("moduleName:\n sampling: 100\n")) + g.Expect(err).ShouldNot(HaveOccurred()) + + mErr = valuesStorage.GetSchemaStorage().ValidateConfigValues("moduleName", sigsYamlValues) + g.Expect(mErr).ShouldNot(HaveOccurred(), "sigs.k8s.io/yaml parsed float64 value should pass validation") + + // Test 3: Manually constructed int value — directly prove the type matters + intValues := utils.Values{ + "moduleName": map[string]interface{}{ + "sampling": int(100), + }, + } + mErr = valuesStorage.GetSchemaStorage().ValidateConfigValues("moduleName", intValues) + g.Expect(mErr).Should(HaveOccurred(), "manually constructed int value should trigger multipleOf bug") + g.Expect(mErr.Error()).Should(ContainSubstring("must be positive")) + + // Test 4: Manually constructed float64 value — passes fine + floatValues := utils.Values{ + "moduleName": map[string]interface{}{ + "sampling": float64(100), + }, + } + mErr = valuesStorage.GetSchemaStorage().ValidateConfigValues("moduleName", floatValues) + g.Expect(mErr).ShouldNot(HaveOccurred(), "manually constructed float64 value should pass validation") +} From 6d60bdebb6f5b4a0bca9942ec95ca91f06eb5f58 Mon Sep 17 00:00:00 2001 From: Pavel Okhlopkov Date: Thu, 26 Feb 2026 18:00:15 +0300 Subject: [PATCH 2/2] bump Signed-off-by: Pavel Okhlopkov --- pkg/utils/values.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/utils/values.go b/pkg/utils/values.go index 95dca13e..bacf289d 100644 --- a/pkg/utils/values.go +++ b/pkg/utils/values.go @@ -9,7 +9,6 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/deckhouse/deckhouse/pkg/log" "github.com/ettle/strcase" - "gopkg.in/yaml.v3" k8syaml "sigs.k8s.io/yaml" utils_checksum "github.com/flant/shell-operator/pkg/utils/checksum" @@ -158,7 +157,7 @@ func (v Values) AsBytes(format string) ([]byte, error) { case "yaml": fallthrough default: - return yaml.Marshal(v) + return k8syaml.Marshal(v) } }