diff --git a/molecule/default/tasks/service_configuration_test.yml b/molecule/default/tasks/service_configuration_test.yml new file mode 100644 index 000000000..96d86360c --- /dev/null +++ b/molecule/default/tasks/service_configuration_test.yml @@ -0,0 +1,276 @@ +--- +# Service Configuration Test Suite +# Tests for different service types and configurations + +- name: Test ClusterIP service (default) + block: + - name: Apply AWX with ClusterIP service + k8s: + state: present + definition: + apiVersion: awx.ansible.com/v1beta1 + kind: AWX + metadata: + name: test-clusterip-awx + namespace: "{{ namespace }}" + spec: + service_type: ClusterIP + service_labels: | + test-type: clusterip + service_annotations: | + test.annotation/type: clusterip + + - name: Wait for AWX ClusterIP service to be created + k8s_info: + namespace: "{{ namespace }}" + api_version: v1 + kind: Service + name: test-clusterip-awx-service + wait: true + wait_condition: + type: Ready + status: "True" + wait_timeout: 300 + register: clusterip_service + + - name: Validate ClusterIP service configuration + ansible.builtin.assert: + that: + - clusterip_service.resources[0].spec.type == "ClusterIP" + - clusterip_service.resources[0].spec.ports[0].port == 80 + - clusterip_service.resources[0].spec.ports[0].targetPort == 8052 + - clusterip_service.resources[0].spec.ports[0].name == "http" + - "'test-type: clusterip' in clusterip_service.resources[0].metadata.labels" + - "'test.annotation/type: clusterip' in clusterip_service.resources[0].metadata.annotations" + fail_msg: "ClusterIP service validation failed" + +- name: Test NodePort service + block: + - name: Apply AWX with NodePort service + k8s: + state: present + definition: + apiVersion: awx.ansible.com/v1beta1 + kind: AWX + metadata: + name: test-nodeport-awx + namespace: "{{ namespace }}" + spec: + service_type: NodePort + nodeport_port: 30080 + service_labels: | + test-type: nodeport + + - name: Wait for AWX NodePort service to be created + k8s_info: + namespace: "{{ namespace }}" + api_version: v1 + kind: Service + name: test-nodeport-awx-service + wait: true + wait_condition: + type: Ready + status: "True" + wait_timeout: 300 + register: nodeport_service + + - name: Validate NodePort service configuration + ansible.builtin.assert: + that: + - nodeport_service.resources[0].spec.type == "NodePort" + - nodeport_service.resources[0].spec.ports[0].port == 80 + - nodeport_service.resources[0].spec.ports[0].targetPort == 8052 + - nodeport_service.resources[0].spec.ports[0].nodePort == 30080 + - nodeport_service.resources[0].spec.ports[0].name == "http" + - "'test-type: nodeport' in nodeport_service.resources[0].metadata.labels" + fail_msg: "NodePort service validation failed" + +- name: Test LoadBalancer service with HTTP + block: + - name: Apply AWX with LoadBalancer service (HTTP) + k8s: + state: present + definition: + apiVersion: awx.ansible.com/v1beta1 + kind: AWX + metadata: + name: test-lb-http-awx + namespace: "{{ namespace }}" + spec: + service_type: LoadBalancer + loadbalancer_port: 8080 + loadbalancer_protocol: http + loadbalancer_class: "nginx" + service_labels: | + test-type: loadbalancer-http + + - name: Wait for AWX LoadBalancer HTTP service to be created + k8s_info: + namespace: "{{ namespace }}" + api_version: v1 + kind: Service + name: test-lb-http-awx-service + wait: true + wait_condition: + type: Ready + status: "True" + wait_timeout: 300 + register: lb_http_service + + - name: Validate LoadBalancer HTTP service configuration + ansible.builtin.assert: + that: + - lb_http_service.resources[0].spec.type == "LoadBalancer" + - lb_http_service.resources[0].spec.ports[0].port == 8080 + - lb_http_service.resources[0].spec.ports[0].targetPort == 8052 + - lb_http_service.resources[0].spec.ports[0].name == "http" + - lb_http_service.resources[0].spec.loadBalancerClass == "nginx" + - "'test-type: loadbalancer-http' in lb_http_service.resources[0].metadata.labels" + fail_msg: "LoadBalancer HTTP service validation failed" + +- name: Test LoadBalancer service with HTTPS + block: + - name: Apply AWX with LoadBalancer service (HTTPS) + k8s: + state: present + definition: + apiVersion: awx.ansible.com/v1beta1 + kind: AWX + metadata: + name: test-lb-https-awx + namespace: "{{ namespace }}" + spec: + service_type: LoadBalancer + loadbalancer_port: 8443 + loadbalancer_protocol: https + loadbalancer_ip: "192.168.1.100" + service_labels: | + test-type: loadbalancer-https + + - name: Wait for AWX LoadBalancer HTTPS service to be created + k8s_info: + namespace: "{{ namespace }}" + api_version: v1 + kind: Service + name: test-lb-https-awx-service + wait: true + wait_condition: + type: Ready + status: "True" + wait_timeout: 300 + register: lb_https_service + + - name: Validate LoadBalancer HTTPS service configuration + ansible.builtin.assert: + that: + - lb_https_service.resources[0].spec.type == "LoadBalancer" + - lb_https_service.resources[0].spec.ports[0].port == 8443 + - lb_https_service.resources[0].spec.ports[0].targetPort == 8052 + - lb_https_service.resources[0].spec.ports[0].name == "https" + - lb_https_service.resources[0].spec.loadBalancerIP == "192.168.1.100" + - "'test-type: loadbalancer-https' in lb_https_service.resources[0].metadata.labels" + fail_msg: "LoadBalancer HTTPS service validation failed" + +- name: Test Route passthrough HTTPS configuration + block: + - name: Apply AWX with Route passthrough + k8s: + state: present + definition: + apiVersion: awx.ansible.com/v1beta1 + kind: AWX + metadata: + name: test-route-passthrough-awx + namespace: "{{ namespace }}" + spec: + service_type: ClusterIP + ingress_type: route + route_tls_termination_mechanism: passthrough + service_labels: | + test-type: route-passthrough + + - name: Wait for AWX Route passthrough service to be created + k8s_info: + namespace: "{{ namespace }}" + api_version: v1 + kind: Service + name: test-route-passthrough-awx-service + wait: true + wait_condition: + type: Ready + status: "True" + wait_timeout: 300 + register: route_passthrough_service + + - name: Validate Route passthrough service has HTTPS port + ansible.builtin.assert: + that: + - route_passthrough_service.resources[0].spec.type == "ClusterIP" + # Should have both HTTP and HTTPS ports for passthrough + - route_passthrough_service.resources[0].spec.ports | length >= 1 + # Check if HTTPS port (443 -> 8053) exists for passthrough + - route_passthrough_service.resources[0].spec.ports | selectattr('port', 'equalto', 443) | list | length == 1 + - (route_passthrough_service.resources[0].spec.ports | selectattr('port', 'equalto', 443) | first).targetPort == 8053 + - (route_passthrough_service.resources[0].spec.ports | selectattr('port', 'equalto', 443) | first).name == "https" + fail_msg: "Route passthrough service validation failed" + +- name: Test service template field validation (negative tests) + block: + # Test invalid NodePort range + - name: Test invalid NodePort (should fail) + k8s: + state: present + definition: + apiVersion: awx.ansible.com/v1beta1 + kind: AWX + metadata: + name: test-invalid-nodeport-awx + namespace: "{{ namespace }}" + spec: + service_type: NodePort + nodeport_port: 25000 # Below valid range + register: invalid_nodeport_result + failed_when: false + + - name: Verify invalid NodePort was rejected + ansible.builtin.assert: + that: + - invalid_nodeport_result is failed + fail_msg: "Invalid NodePort should have been rejected" + + rescue: + - name: Clean up test resources on failure + k8s: + state: absent + api_version: awx.ansible.com/v1beta1 + kind: AWX + name: "{{ item }}" + namespace: "{{ namespace }}" + loop: + - test-clusterip-awx + - test-nodeport-awx + - test-lb-http-awx + - test-lb-https-awx + - test-route-passthrough-awx + - test-invalid-nodeport-awx + ignore_errors: yes + + - name: Re-emit failure + fail: + msg: "Service configuration tests failed: {{ ansible_failed_result }}" + +- name: Clean up test resources + k8s: + state: absent + api_version: awx.ansible.com/v1beta1 + kind: AWX + name: "{{ item }}" + namespace: "{{ namespace }}" + loop: + - test-clusterip-awx + - test-nodeport-awx + - test-lb-http-awx + - test-lb-https-awx + - test-route-passthrough-awx + - test-invalid-nodeport-awx + ignore_errors: yes diff --git a/molecule/default/tasks/service_template_unit_test.yml b/molecule/default/tasks/service_template_unit_test.yml new file mode 100644 index 000000000..256c90ec1 --- /dev/null +++ b/molecule/default/tasks/service_template_unit_test.yml @@ -0,0 +1,233 @@ +--- +# Service Template Unit Tests +# Tests Jinja2 template rendering with different variable combinations + +- name: Test service template rendering + block: + - name: Create temporary directory for test templates + ansible.builtin.tempfile: + state: directory + suffix: service_template_test + register: temp_dir + + - name: Copy service template for testing + ansible.builtin.copy: + src: "{{ playbook_dir }}/../../roles/installer/templates/networking/service.yaml.j2" + dest: "{{ temp_dir.path }}/service.yaml.j2" + + - name: Test ClusterIP template rendering + ansible.builtin.template: + src: "{{ temp_dir.path }}/service.yaml.j2" + dest: "{{ temp_dir.path }}/clusterip_service.yaml" + vars: + ansible_operator_meta: + name: test-awx + namespace: test-namespace + deployment_type: awx + service_type: ClusterIP + service_labels: | + app: test-awx + environment: test + service_annotations: | + service.beta.kubernetes.io/aws-load-balancer-type: nlb + + - name: Validate ClusterIP rendered template + ansible.builtin.slurp: + src: "{{ temp_dir.path }}/clusterip_service.yaml" + register: clusterip_rendered + + - name: Parse ClusterIP YAML + ansible.builtin.set_fact: + clusterip_service_yaml: "{{ clusterip_rendered.content | b64decode | from_yaml }}" + + - name: Assert ClusterIP service properties + ansible.builtin.assert: + that: + - clusterip_service_yaml.apiVersion == "v1" + - clusterip_service_yaml.kind == "Service" + - clusterip_service_yaml.metadata.name == "test-awx-service" + - clusterip_service_yaml.metadata.namespace == "test-namespace" + - clusterip_service_yaml.spec.type == "ClusterIP" + - clusterip_service_yaml.spec.ports[0].port == 80 + - clusterip_service_yaml.spec.ports[0].targetPort == 8052 + - clusterip_service_yaml.spec.ports[0].name == "http" + fail_msg: "ClusterIP template rendering validation failed" + + - name: Test NodePort template rendering + ansible.builtin.template: + src: "{{ temp_dir.path }}/service.yaml.j2" + dest: "{{ temp_dir.path }}/nodeport_service.yaml" + vars: + ansible_operator_meta: + name: test-awx + namespace: test-namespace + deployment_type: awx + service_type: NodePort + nodeport_port: 30080 + + - name: Validate NodePort rendered template + ansible.builtin.slurp: + src: "{{ temp_dir.path }}/nodeport_service.yaml" + register: nodeport_rendered + + - name: Parse NodePort YAML + ansible.builtin.set_fact: + nodeport_service_yaml: "{{ nodeport_rendered.content | b64decode | from_yaml }}" + + - name: Assert NodePort service properties + ansible.builtin.assert: + that: + - nodeport_service_yaml.spec.type == "NodePort" + - nodeport_service_yaml.spec.ports[0].port == 80 + - nodeport_service_yaml.spec.ports[0].targetPort == 8052 + - nodeport_service_yaml.spec.ports[0].nodePort == 30080 + - nodeport_service_yaml.spec.ports[0].name == "http" + fail_msg: "NodePort template rendering validation failed" + + - name: Test LoadBalancer HTTP template rendering + ansible.builtin.template: + src: "{{ temp_dir.path }}/service.yaml.j2" + dest: "{{ temp_dir.path }}/loadbalancer_http_service.yaml" + vars: + ansible_operator_meta: + name: test-awx + namespace: test-namespace + deployment_type: awx + service_type: LoadBalancer + loadbalancer_port: 8080 + loadbalancer_protocol: http + loadbalancer_ip: "192.168.1.100" + loadbalancer_class: "nginx" + + - name: Validate LoadBalancer HTTP rendered template + ansible.builtin.slurp: + src: "{{ temp_dir.path }}/loadbalancer_http_service.yaml" + register: lb_http_rendered + + - name: Parse LoadBalancer HTTP YAML + ansible.builtin.set_fact: + lb_http_service_yaml: "{{ lb_http_rendered.content | b64decode | from_yaml }}" + + - name: Assert LoadBalancer HTTP service properties + ansible.builtin.assert: + that: + - lb_http_service_yaml.spec.type == "LoadBalancer" + - lb_http_service_yaml.spec.ports[0].port == 8080 + - lb_http_service_yaml.spec.ports[0].targetPort == 8052 + - lb_http_service_yaml.spec.ports[0].name == "http" + - lb_http_service_yaml.spec.loadBalancerIP == "192.168.1.100" # Test the critical field fix + - lb_http_service_yaml.spec.loadBalancerClass == "nginx" + fail_msg: "LoadBalancer HTTP template rendering validation failed" + + - name: Test LoadBalancer HTTPS template rendering + ansible.builtin.template: + src: "{{ temp_dir.path }}/service.yaml.j2" + dest: "{{ temp_dir.path }}/loadbalancer_https_service.yaml" + vars: + ansible_operator_meta: + name: test-awx + namespace: test-namespace + deployment_type: awx + service_type: LoadBalancer + loadbalancer_port: 8443 + loadbalancer_protocol: https + + - name: Validate LoadBalancer HTTPS rendered template + ansible.builtin.slurp: + src: "{{ temp_dir.path }}/loadbalancer_https_service.yaml" + register: lb_https_rendered + + - name: Parse LoadBalancer HTTPS YAML + ansible.builtin.set_fact: + lb_https_service_yaml: "{{ lb_https_rendered.content | b64decode | from_yaml }}" + + - name: Assert LoadBalancer HTTPS service properties + ansible.builtin.assert: + that: + - lb_https_service_yaml.spec.type == "LoadBalancer" + - lb_https_service_yaml.spec.ports[0].port == 8443 + - lb_https_service_yaml.spec.ports[0].targetPort == 8052 + - lb_https_service_yaml.spec.ports[0].name == "https" + fail_msg: "LoadBalancer HTTPS template rendering validation failed" + + - name: Test Route passthrough template rendering + ansible.builtin.template: + src: "{{ temp_dir.path }}/service.yaml.j2" + dest: "{{ temp_dir.path }}/route_passthrough_service.yaml" + vars: + ansible_operator_meta: + name: test-awx + namespace: test-namespace + deployment_type: awx + service_type: ClusterIP + ingress_type: route + route_tls_termination_mechanism: passthrough + + - name: Validate Route passthrough rendered template + ansible.builtin.slurp: + src: "{{ temp_dir.path }}/route_passthrough_service.yaml" + register: route_passthrough_rendered + + - name: Parse Route passthrough YAML + ansible.builtin.set_fact: + route_passthrough_service_yaml: "{{ route_passthrough_rendered.content | b64decode | from_yaml }}" + + - name: Assert Route passthrough service has HTTPS port + ansible.builtin.assert: + that: + - route_passthrough_service_yaml.spec.type == "ClusterIP" + # Should have HTTPS port for passthrough (443 -> 8053) + - route_passthrough_service_yaml.spec.ports | selectattr('port', 'equalto', 443) | list | length == 1 + - (route_passthrough_service_yaml.spec.ports | selectattr('port', 'equalto', 443) | first).targetPort == 8053 + - (route_passthrough_service_yaml.spec.ports | selectattr('port', 'equalto', 443) | first).name == "https" + fail_msg: "Route passthrough template rendering validation failed" + + # Test validation logic (this should fail template rendering) + - name: Test template validation - invalid NodePort range + ansible.builtin.template: + src: "{{ temp_dir.path }}/service.yaml.j2" + dest: "{{ temp_dir.path }}/invalid_nodeport_service.yaml" + vars: + ansible_operator_meta: + name: test-awx + namespace: test-namespace + deployment_type: awx + service_type: NodePort + nodeport_port: 25000 # Invalid range + register: invalid_nodeport_result + failed_when: false + + - name: Assert that invalid NodePort was caught + ansible.builtin.assert: + that: + - invalid_nodeport_result is failed + - "'nodeport_port must be between 30000 and 32767' in invalid_nodeport_result.msg" + fail_msg: "Template validation should have caught invalid NodePort range" + + - name: Test template validation - missing LoadBalancer port + ansible.builtin.template: + src: "{{ temp_dir.path }}/service.yaml.j2" + dest: "{{ temp_dir.path }}/missing_lb_port_service.yaml" + vars: + ansible_operator_meta: + name: test-awx + namespace: test-namespace + deployment_type: awx + service_type: LoadBalancer + # Missing loadbalancer_port + register: missing_lb_port_result + failed_when: false + + - name: Assert that missing LoadBalancer port was caught + ansible.builtin.assert: + that: + - missing_lb_port_result is failed + - "'loadbalancer_port must be defined when service_type is LoadBalancer' in missing_lb_port_result.msg" + fail_msg: "Template validation should have caught missing LoadBalancer port" + + always: + - name: Clean up temporary directory + ansible.builtin.file: + path: "{{ temp_dir.path }}" + state: absent + when: temp_dir is defined diff --git a/molecule/default/verify.yml b/molecule/default/verify.yml index 31b95d3f3..cbd524a7e 100644 --- a/molecule/default/verify.yml +++ b/molecule/default/verify.yml @@ -17,6 +17,8 @@ with_fileglob: - tasks/awx_test.yml - tasks/awx_replicas_test.yml + - tasks/service_configuration_test.yml + - tasks/service_template_unit_test.yml tags: - always rescue: diff --git a/roles/installer/templates/networking/service.yaml.j2 b/roles/installer/templates/networking/service.yaml.j2 index 00117b505..d59c2abb9 100644 --- a/roles/installer/templates/networking/service.yaml.j2 +++ b/roles/installer/templates/networking/service.yaml.j2 @@ -1,4 +1,13 @@ --- +{# + AWX Service Template + + Configures the main service for AWX web interface with support for: + - ClusterIP (default) + - NodePort (with optional custom port) + - LoadBalancer (with configurable protocol and port) + - Route passthrough termination for HTTPS +#} apiVersion: v1 kind: Service metadata: @@ -6,13 +15,23 @@ metadata: namespace: '{{ ansible_operator_meta.namespace }}' labels: {{ lookup("template", "../common/templates/labels/common.yaml.j2") | indent(width=4) | trim }} +{% if service_labels %} {{ service_labels | indent(width=4) }} +{% endif %} {% if service_annotations %} annotations: {{ service_annotations | indent(width=4) }} {% endif %} +{# Variable validation for service configurations #} +{% if service_type | lower == 'loadbalancer' and not (loadbalancer_port is defined) %} + {% set _ = ansible_fail(msg="loadbalancer_port must be defined when service_type is LoadBalancer") %} +{% endif %} +{% if service_type | lower == 'nodeport' and nodeport_port is defined and (nodeport_port | int < 30000 or nodeport_port | int > 32767) %} + {% set _ = ansible_fail(msg="nodeport_port must be between 30000 and 32767") %} +{% endif %} spec: ports: +{# HTTP Port Configuration #} {% if service_type | lower == "nodeport" %} - port: 80 protocol: TCP @@ -21,39 +40,40 @@ spec: {% if nodeport_port is defined %} nodePort: {{ nodeport_port }} {% endif %} -{% elif service_type | lower != 'loadbalancer' and loadbalancer_protocol | lower != 'https' %} +{% elif service_type | lower == 'loadbalancer' %} +{% set lb_port = loadbalancer_port | default(80) %} +{% set lb_protocol = loadbalancer_protocol | lower | default('http') %} + - port: {{ lb_port }} + protocol: TCP + targetPort: 8052 + name: {{ 'https' if lb_protocol == 'https' else 'http' }} +{% else %} +{# ClusterIP service - only add HTTP port if not using HTTPS load balancer protocol #} +{% if not (loadbalancer_protocol is defined and loadbalancer_protocol | lower == 'https') %} - port: 80 protocol: TCP targetPort: 8052 name: http {% endif %} +{% endif %} +{# HTTPS Port for Route Passthrough #} {% if ingress_type | lower == 'route' and route_tls_termination_mechanism | lower == 'passthrough' %} - port: 443 protocol: TCP targetPort: 8053 name: https -{% endif %} -{% if service_type | lower == 'loadbalancer' and loadbalancer_protocol | lower == 'https' %} - - port: {{ loadbalancer_port }} - protocol: TCP - targetPort: 8052 - name: https -{% elif service_type | lower == 'loadbalancer' and loadbalancer_protocol | lower != 'https' %} - - port: {{ loadbalancer_port }} - protocol: TCP - targetPort: 8052 - name: http {% endif %} selector: app.kubernetes.io/name: '{{ ansible_operator_meta.name }}-web' app.kubernetes.io/managed-by: '{{ deployment_type }}-operator' app.kubernetes.io/component: '{{ deployment_type }}' +{# Service Type Configuration - supports ClusterIP (default), NodePort, and LoadBalancer #} {% if service_type | lower == "nodeport" %} type: NodePort {% elif service_type | lower == "loadbalancer" %} type: LoadBalancer {% if loadbalancer_ip is defined and loadbalancer_ip | length %} - loadbalancerip: '{{ loadbalancer_ip }}' + loadBalancerIP: '{{ loadbalancer_ip }}' {% endif %} {% if loadbalancer_class is defined and loadbalancer_class | length %} loadBalancerClass: {{ loadbalancer_class }}