Run collection sanity tests in CI (#13356)

* Run collection sanity tests in CI

This requires adding a Makefile install of ansible-core

Fake the version to make semver check happy

* Fixes from ansible-test sanity failures

* Exclude the export module due to awxkit requirement

* Fix broken ansible-test rule exceptions

remove Ansible 2.14 exclusions that make ansible-test ERROR, saying they are not needed
This commit is contained in:
Alan Rominger
2022-12-20 16:06:25 -05:00
committed by GitHub
parent 8f6849fc22
commit ac8cff75ce
22 changed files with 73 additions and 122 deletions

View File

@@ -28,6 +28,9 @@ jobs:
- name: awx-collection - name: awx-collection
command: /start_tests.sh test_collection_all command: /start_tests.sh test_collection_all
label: Run Collection Tests label: Run Collection Tests
- name: awx-collection-sanity
command: /start_tests.sh test_collection_sanity
label: Run Ansible core Collection Sanity tests
- name: api-schema - name: api-schema
label: Check API Schema label: Check API Schema
command: /start_tests.sh detect-schema-change SCHEMA_DIFF_BASE_BRANCH=${{ github.event.pull_request.base.ref }} command: /start_tests.sh detect-schema-change SCHEMA_DIFF_BASE_BRANCH=${{ github.event.pull_request.base.ref }}

View File

@@ -6,7 +6,9 @@ CHROMIUM_BIN=/tmp/chrome-linux/chrome
GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
MANAGEMENT_COMMAND ?= awx-manage MANAGEMENT_COMMAND ?= awx-manage
VERSION := $(shell $(PYTHON) tools/scripts/scm_version.py) VERSION := $(shell $(PYTHON) tools/scripts/scm_version.py)
COLLECTION_VERSION := $(shell $(PYTHON) tools/scripts/scm_version.py | cut -d . -f 1-3)
# ansible-test requires semver compatable version, so we allow overrides to hack it
COLLECTION_VERSION ?= $(shell $(PYTHON) tools/scripts/scm_version.py | cut -d . -f 1-3)
# NOTE: This defaults the container image version to the branch that's active # NOTE: This defaults the container image version to the branch that's active
COMPOSE_TAG ?= $(GIT_BRANCH) COMPOSE_TAG ?= $(GIT_BRANCH)
@@ -300,7 +302,8 @@ test_collection:
if [ "$(VENV_BASE)" ]; then \ if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/awx/bin/activate; \ . $(VENV_BASE)/awx/bin/activate; \
fi && \ fi && \
pip install ansible-core && \ if ! [ -x "$(shell command -v ansible-playbook)" ]; then pip install ansible-core; fi
ansible --version
py.test $(COLLECTION_TEST_DIRS) -v py.test $(COLLECTION_TEST_DIRS) -v
# The python path needs to be modified so that the tests can find Ansible within the container # The python path needs to be modified so that the tests can find Ansible within the container
# First we will use anything expility set as PYTHONPATH # First we will use anything expility set as PYTHONPATH
@@ -330,8 +333,13 @@ install_collection: build_collection
rm -rf $(COLLECTION_INSTALL) rm -rf $(COLLECTION_INSTALL)
ansible-galaxy collection install awx_collection_build/$(COLLECTION_NAMESPACE)-$(COLLECTION_PACKAGE)-$(COLLECTION_VERSION).tar.gz ansible-galaxy collection install awx_collection_build/$(COLLECTION_NAMESPACE)-$(COLLECTION_PACKAGE)-$(COLLECTION_VERSION).tar.gz
test_collection_sanity: install_collection test_collection_sanity:
cd $(COLLECTION_INSTALL) && ansible-test sanity rm -rf awx_collection_build/
rm -rf $(COLLECTION_INSTALL)
if ! [ -x "$(shell command -v ansible-test)" ]; then pip install ansible-core; fi
ansible --version
COLLECTION_VERSION=1.0.0 make install_collection
cd $(COLLECTION_INSTALL) && ansible-test sanity --exclude=plugins/modules/export.py
test_collection_integration: install_collection test_collection_integration: install_collection
cd $(COLLECTION_INSTALL) && ansible-test integration $(COLLECTION_TEST_TARGET) cd $(COLLECTION_INSTALL) && ansible-test integration $(COLLECTION_TEST_TARGET)

View File

@@ -7,7 +7,6 @@ __metaclass__ = type
DOCUMENTATION = ''' DOCUMENTATION = '''
name: controller name: controller
plugin_type: inventory
author: author:
- Matthew Jones (@matburt) - Matthew Jones (@matburt)
- Yunfan Zhang (@YunfanZhang42) - Yunfan Zhang (@YunfanZhang42)

View File

@@ -5,7 +5,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = """ DOCUMENTATION = """
lookup: controller_api name: controller_api
author: John Westcott IV (@john-westcott-iv) author: John Westcott IV (@john-westcott-iv)
short_description: Search the API for objects short_description: Search the API for objects
requirements: requirements:

View File

@@ -5,7 +5,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = """ DOCUMENTATION = """
lookup: schedule_rrule name: schedule_rrule
author: John Westcott IV (@john-westcott-iv) author: John Westcott IV (@john-westcott-iv)
short_description: Generate an rrule string which can be used for Schedules short_description: Generate an rrule string which can be used for Schedules
requirements: requirements:

View File

@@ -5,7 +5,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = """ DOCUMENTATION = """
lookup: schedule_rruleset name: schedule_rruleset
author: John Westcott IV (@john-westcott-iv) author: John Westcott IV (@john-westcott-iv)
short_description: Generate an rruleset string short_description: Generate an rruleset string
requirements: requirements:
@@ -31,7 +31,8 @@ DOCUMENTATION = """
rules: rules:
description: description:
- Array of rules in the rruleset - Array of rules in the rruleset
type: array type: list
elements: dict
required: True required: True
suboptions: suboptions:
frequency: frequency:
@@ -192,14 +193,14 @@ class LookupModule(LookupBase):
# something: [1,2,3] - A list of ints # something: [1,2,3] - A list of ints
return_values = [] return_values = []
# If they give us a single int, lets make it a list of ints # If they give us a single int, lets make it a list of ints
if type(rule[field_name]) == int: if isinstance(rule[field_name], int):
rule[field_name] = [rule[field_name]] rule[field_name] = [rule[field_name]]
# If its not a list, we need to split it into a list # If its not a list, we need to split it into a list
if type(rule[field_name]) != list: if isinstance(rule[field_name], list):
rule[field_name] = rule[field_name].split(',') rule[field_name] = rule[field_name].split(',')
for value in rule[field_name]: for value in rule[field_name]:
# If they have a list of strs we want to strip the str incase its space delineated # If they have a list of strs we want to strip the str incase its space delineated
if type(value) == str: if isinstance(value, str):
value = value.strip() value = value.strip()
# If value happens to be an int (from a list of ints) we need to coerce it into a str for the re.match # If value happens to be an int (from a list of ints) we need to coerce it into a str for the re.match
if not re.match(r"^\d+$", str(value)) or int(value) < min_value or int(value) > max_value: if not re.match(r"^\d+$", str(value)) or int(value) < min_value or int(value) > max_value:
@@ -209,7 +210,7 @@ class LookupModule(LookupBase):
def process_list(self, field_name, rule, valid_list, rule_number): def process_list(self, field_name, rule, valid_list, rule_number):
return_values = [] return_values = []
if type(rule[field_name]) != list: if isinstance(rule[field_name], list):
rule[field_name] = rule[field_name].split(',') rule[field_name] = rule[field_name].split(',')
for value in rule[field_name]: for value in rule[field_name]:
value = value.strip() value = value.strip()

View File

@@ -4,6 +4,7 @@ __metaclass__ = type
from ansible.module_utils.basic import AnsibleModule, env_fallback from ansible.module_utils.basic import AnsibleModule, env_fallback
from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError
from ansible.module_utils.parsing.convert_bool import boolean as strtobool
from ansible.module_utils.six import PY2 from ansible.module_utils.six import PY2
from ansible.module_utils.six import raise_from, string_types from ansible.module_utils.six import raise_from, string_types
from ansible.module_utils.six.moves import StringIO from ansible.module_utils.six.moves import StringIO
@@ -11,14 +12,21 @@ from ansible.module_utils.six.moves.urllib.error import HTTPError
from ansible.module_utils.six.moves.http_cookiejar import CookieJar from ansible.module_utils.six.moves.http_cookiejar import CookieJar
from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode
from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError
from distutils.version import LooseVersion as Version
from socket import getaddrinfo, IPPROTO_TCP from socket import getaddrinfo, IPPROTO_TCP
import time import time
import re import re
from json import loads, dumps from json import loads, dumps
from os.path import isfile, expanduser, split, join, exists, isdir from os.path import isfile, expanduser, split, join, exists, isdir
from os import access, R_OK, getcwd from os import access, R_OK, getcwd
from distutils.util import strtobool
try:
from ansible.module_utils.compat.version import LooseVersion as Version
except ImportError:
try:
from distutils.version import LooseVersion as Version
except ImportError:
raise AssertionError('To use this plugin or module with ansible-core 2.11, you need to use Python < 3.12 with distutils.version present')
try: try:
import yaml import yaml

View File

@@ -55,7 +55,6 @@ options:
description: description:
- The arguments to pass to the module. - The arguments to pass to the module.
type: str type: str
default: ""
forks: forks:
description: description:
- The number of forks to use for this ad hoc execution. - The number of forks to use for this ad hoc execution.

View File

@@ -42,6 +42,7 @@ options:
- Maximum time in seconds to wait for a job to finish. - Maximum time in seconds to wait for a job to finish.
- Not specifying means the task will wait until the controller cancels the command. - Not specifying means the task will wait until the controller cancels the command.
type: int type: int
default: 0
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''

View File

@@ -80,9 +80,9 @@ def main():
name=dict(required=True), name=dict(required=True),
new_name=dict(), new_name=dict(),
image=dict(required=True), image=dict(required=True),
description=dict(default=''), description=dict(),
organization=dict(), organization=dict(),
credential=dict(default=''), credential=dict(),
state=dict(choices=['present', 'absent'], default='present'), state=dict(choices=['present', 'absent'], default='present'),
pull=dict(choices=['always', 'missing', 'never'], default='missing'), pull=dict(choices=['always', 'missing', 'never'], default='missing'),
) )

View File

@@ -86,6 +86,16 @@ options:
- workflow names to export - workflow names to export
type: list type: list
elements: str elements: str
applications:
description:
- OAuth2 application names to export
type: list
elements: str
schedules:
description:
- schedule names to export
type: list
elements: str
requirements: requirements:
- "awxkit >= 9.3.0" - "awxkit >= 9.3.0"
notes: notes:

View File

@@ -266,6 +266,7 @@ options:
description: description:
- Maximum time in seconds to wait for a job to finish (server-side). - Maximum time in seconds to wait for a job to finish (server-side).
type: int type: int
default: 0
job_slice_count: job_slice_count:
description: description:
- The number of jobs to slice into at runtime. Will cause the Job Template to launch a workflow if value is greater than 1. - The number of jobs to slice into at runtime. Will cause the Job Template to launch a workflow if value is greater than 1.
@@ -287,7 +288,6 @@ options:
description: description:
- Branch to use in job run. Project default used if blank. Only allowed if project allow_override field is set to true. - Branch to use in job run. Project default used if blank. Only allowed if project allow_override field is set to true.
type: str type: str
default: ''
labels: labels:
description: description:
- The labels applied to this job template - The labels applied to this job template

View File

@@ -60,12 +60,10 @@ options:
description: description:
- The branch to use for the SCM resource. - The branch to use for the SCM resource.
type: str type: str
default: ''
scm_refspec: scm_refspec:
description: description:
- The refspec to use for the SCM resource. - The refspec to use for the SCM resource.
type: str type: str
default: ''
credential: credential:
description: description:
- Name of the credential to use with this SCM resource. - Name of the credential to use with this SCM resource.

View File

@@ -51,7 +51,6 @@ options:
- Specify C(extra_vars) for the template. - Specify C(extra_vars) for the template.
required: False required: False
type: dict type: dict
default: {}
forks: forks:
description: description:
- Forks applied as a prompt, assuming job template prompts for forks - Forks applied as a prompt, assuming job template prompts for forks

View File

@@ -39,6 +39,7 @@ options:
- Note This is a client side search, not an API side search - Note This is a client side search, not an API side search
required: False required: False
type: dict type: dict
default: {}
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''

View File

@@ -35,7 +35,6 @@ options:
- Optional description of this access token. - Optional description of this access token.
required: False required: False
type: str type: str
default: ''
application: application:
description: description:
- The application tied to this token. - The application tied to this token.

View File

@@ -214,7 +214,8 @@ options:
type: int type: int
job_slice_count: job_slice_count:
description: description:
- The number of jobs to slice into at runtime, if job template prompts for job slices. Will cause the Job Template to launch a workflow if value is greater than 1. - The number of jobs to slice into at runtime, if job template prompts for job slices.
- Will cause the Job Template to launch a workflow if value is greater than 1.
type: int type: int
default: '1' default: '1'
timeout: timeout:
@@ -328,42 +329,46 @@ options:
- Nodes that will run after this node completes. - Nodes that will run after this node completes.
- List of node identifiers. - List of node identifiers.
type: list type: list
elements: dict
suboptions: suboptions:
identifier: identifier:
description: description:
- Identifier of Node that will run after this node completes given this option. - Identifier of Node that will run after this node completes given this option.
elements: str type: str
success_nodes: success_nodes:
description: description:
- Nodes that will run after this node on success. - Nodes that will run after this node on success.
- List of node identifiers. - List of node identifiers.
type: list type: list
elements: dict
suboptions: suboptions:
identifier: identifier:
description: description:
- Identifier of Node that will run after this node completes given this option. - Identifier of Node that will run after this node completes given this option.
elements: str type: str
failure_nodes: failure_nodes:
description: description:
- Nodes that will run after this node on failure. - Nodes that will run after this node on failure.
- List of node identifiers. - List of node identifiers.
type: list type: list
elements: dict
suboptions: suboptions:
identifier: identifier:
description: description:
- Identifier of Node that will run after this node completes given this option. - Identifier of Node that will run after this node completes given this option.
elements: str type: str
credentials: credentials:
description: description:
- Credentials to be applied to job as launch-time prompts. - Credentials to be applied to job as launch-time prompts.
- List of credential names. - List of credential names.
- Uniqueness is not handled rigorously. - Uniqueness is not handled rigorously.
type: list type: list
elements: dict
suboptions: suboptions:
name: name:
description: description:
- Name Credentials to be applied to job as launch-time prompts. - Name Credentials to be applied to job as launch-time prompts.
elements: str type: str
organization: organization:
description: description:
- Name of key for use in model for organizational reference - Name of key for use in model for organizational reference
@@ -379,11 +384,12 @@ options:
- List of Label names. - List of Label names.
- Uniqueness is not handled rigorously. - Uniqueness is not handled rigorously.
type: list type: list
elements: dict
suboptions: suboptions:
name: name:
description: description:
- Name Labels to be applied to job as launch-time prompts. - Name Labels to be applied to job as launch-time prompts.
elements: str type: str
organization: organization:
description: description:
- Name of key for use in model for organizational reference - Name of key for use in model for organizational reference
@@ -399,11 +405,12 @@ options:
- List of Instance group names. - List of Instance group names.
- Uniqueness is not handled rigorously. - Uniqueness is not handled rigorously.
type: list type: list
elements: dict
suboptions: suboptions:
name: name:
description: description:
- Name of Instance groups to be applied to job as launch-time prompts. - Name of Instance groups to be applied to job as launch-time prompts.
elements: str type: str
destroy_current_nodes: destroy_current_nodes:
description: description:
- Set in order to destroy current workflow_nodes on the workflow. - Set in order to destroy current workflow_nodes on the workflow.
@@ -789,7 +796,7 @@ def main():
allow_simultaneous=dict(type='bool'), allow_simultaneous=dict(type='bool'),
ask_variables_on_launch=dict(type='bool'), ask_variables_on_launch=dict(type='bool'),
ask_labels_on_launch=dict(type='bool', aliases=['ask_labels']), ask_labels_on_launch=dict(type='bool', aliases=['ask_labels']),
ask_tags_on_launch=dict(type='bool'), ask_tags_on_launch=dict(type='bool', aliases=['ask_tags']),
ask_skip_tags_on_launch=dict(type='bool', aliases=['ask_skip_tags']), ask_skip_tags_on_launch=dict(type='bool', aliases=['ask_skip_tags']),
inventory=dict(), inventory=dict(),
limit=dict(), limit=dict(),

View File

@@ -30,7 +30,6 @@ options:
- Variables to apply at launch time. - Variables to apply at launch time.
- Will only be accepted if job template prompts for vars or has a survey asking for those vars. - Will only be accepted if job template prompts for vars or has a survey asking for those vars.
type: dict type: dict
default: {}
inventory: inventory:
description: description:
- Inventory applied as a prompt, if job template prompts for inventory - Inventory applied as a prompt, if job template prompts for inventory

View File

@@ -159,7 +159,7 @@ def run_module(request, collection_import):
elif getattr(resource_module, 'TowerLegacyModule', None): elif getattr(resource_module, 'TowerLegacyModule', None):
resource_class = resource_module.TowerLegacyModule resource_class = resource_module.TowerLegacyModule
else: else:
raise ("The module has neither a TowerLegacyModule, ControllerAWXKitModule or a ControllerAPIModule") raise RuntimeError("The module has neither a TowerLegacyModule, ControllerAWXKitModule or a ControllerAPIModule")
with mock.patch.object(resource_class, '_load_params', new=mock_load_params): with mock.patch.object(resource_class, '_load_params', new=mock_load_params):
# Call the test utility (like a mock server) instead of issuing HTTP requests # Call the test utility (like a mock server) instead of issuing HTTP requests

View File

@@ -14,7 +14,7 @@ def test_create_project(run_module, admin_user, organization, silence_warning):
dict(name='foo', organization=organization.name, scm_type='git', scm_url='https://foo.invalid', wait=False, scm_update_cache_timeout=5), dict(name='foo', organization=organization.name, scm_type='git', scm_url='https://foo.invalid', wait=False, scm_update_cache_timeout=5),
admin_user, admin_user,
) )
silence_warning.assert_called_once_with('scm_update_cache_timeout will be ignored since scm_update_on_launch ' 'was not set to true') silence_warning.assert_called_once_with('scm_update_cache_timeout will be ignored since scm_update_on_launch was not set to true')
assert result.pop('changed', None), result assert result.pop('changed', None), result

View File

@@ -1,88 +0,0 @@
plugins/module_utils/awxkit.py import-3.9
plugins/module_utils/controller_api.py import-3.9
plugins/modules/ad_hoc_command.py import-3.9
plugins/modules/ad_hoc_command_cancel.py import-3.9
plugins/modules/ad_hoc_command_wait.py import-3.9
plugins/modules/application.py import-3.9
plugins/modules/controller_meta.py import-3.9
plugins/modules/credential.py import-3.92
plugins/modules/credential_input_source.py import-3.9
plugins/modules/credential_type.py import-3.9
plugins/modules/execution_environment.py import-3.9
plugins/modules/export.py import-3.9
plugins/modules/group.py import-3.9
plugins/modules/host.py import-3.9
plugins/modules/import.py import-3.9
plugins/modules/instance.py import-3.9
plugins/modules/instance_group.py import-3.9
plugins/modules/inventory.py import-3.9
plugins/modules/inventory_source.py import-3.9
plugins/modules/inventory_source_update.py import-3.9
plugins/modules/job_cancel.py import-3.9
plugins/modules/job_launch.py import-3.9
plugins/modules/job_list.py import-3.9
plugins/modules/job_template.py import-3.93
plugins/modules/job_wait.py import-3.9
plugins/modules/label.py import-3.9
plugins/modules/license.py import-3.9
plugins/modules/notification_template.py import-3.9
plugins/modules/organization.py import-3.9
plugins/modules/project.py import-3.92
plugins/modules/project_update.py import-3.9
plugins/modules/role.py import-3.9
plugins/modules/schedule.py import-3.9
plugins/modules/settings.py import-3.9
plugins/modules/subscriptions.py import-3.9
plugins/modules/team.py import-3.9
plugins/modules/token.py import-3.9
plugins/modules/user.py import-3.9
plugins/modules/workflow_approval.py import-3.9
plugins/modules/workflow_job_template.py import-3.9
plugins/modules/workflow_job_template_node.py import-3.9
plugins/modules/workflow_launch.py import-3.9
plugins/modules/workflow_node_wait.py import-3.9
plugins/inventory/controller.py import-3.10
plugins/lookup/controller_api.py import-3.10
plugins/module_utils/awxkit.py import-3.10
plugins/module_utils/controller_api.py import-3.10
plugins/modules/ad_hoc_command.py import-3.10
plugins/modules/ad_hoc_command_cancel.py import-3.10
plugins/modules/ad_hoc_command_wait.py import-3.10
plugins/modules/application.py import-3.10
plugins/modules/controller_meta.py import-3.10
plugins/modules/credential.py import-3.10
plugins/modules/credential_input_source.py import-3.10
plugins/modules/credential_type.py import-3.10
plugins/modules/execution_environment.py import-3.10
plugins/modules/export.py import-3.10
plugins/modules/group.py import-3.10
plugins/modules/host.py import-3.10
plugins/modules/import.py import-3.10
plugins/modules/instance.py import-3.10
plugins/modules/instance_group.py import-3.10
plugins/modules/inventory.py import-3.10
plugins/modules/inventory_source.py import-3.10
plugins/modules/inventory_source_update.py import-3.10
plugins/modules/job_cancel.py import-3.10
plugins/modules/job_launch.py import-3.10
plugins/modules/job_list.py import-3.10
plugins/modules/job_template.py import-3.10
plugins/modules/job_wait.py import-3.10
plugins/modules/label.py import-3.10
plugins/modules/license.py import-3.10
plugins/modules/notification_template.py import-3.10
plugins/modules/organization.py import-3.10
plugins/modules/project.py import-3.10
plugins/modules/project_update.py import-3.10
plugins/modules/role.py import-3.10
plugins/modules/schedule.py import-3.10
plugins/modules/settings.py import-3.10
plugins/modules/subscriptions.py import-3.10
plugins/modules/team.py import-3.10
plugins/modules/token.py import-3.10
plugins/modules/user.py import-3.10
plugins/modules/workflow_approval.py import-3.10
plugins/modules/workflow_job_template.py import-3.10
plugins/modules/workflow_job_template_node.py import-3.10
plugins/modules/workflow_launch.py import-3.10
plugins/modules/workflow_node_wait.py import-3.10

View File

@@ -1,4 +1,11 @@
--- ---
- name: Sanity assertions, that some variables have a non-blank value
assert:
that:
- collection_version
- collection_package
- collection_path
- name: Set the collection version in the controller_api.py file - name: Set the collection version in the controller_api.py file
replace: replace:
path: "{{ collection_path }}/plugins/module_utils/controller_api.py" path: "{{ collection_path }}/plugins/module_utils/controller_api.py"