mirror of
https://github.com/ansible/awx.git
synced 2026-03-26 05:15:02 -02:30
Compare commits
31 Commits
constructe
...
revert-136
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44fa30f91b | ||
|
|
2902b40084 | ||
|
|
9669b9dd2f | ||
|
|
d27aada817 | ||
|
|
2fca07ee4c | ||
|
|
f4bcc03ac7 | ||
|
|
5e28f5dca1 | ||
|
|
d088d36448 | ||
|
|
89e41597a6 | ||
|
|
283adc30a8 | ||
|
|
019e6a52fe | ||
|
|
35e5610642 | ||
|
|
3a303875bb | ||
|
|
4499a50019 | ||
|
|
3fe46e2e27 | ||
|
|
6d3f39fe92 | ||
|
|
a3233b5fdd | ||
|
|
fe3aa6ce2b | ||
|
|
77ec46f6cf | ||
|
|
b5f240ce70 | ||
|
|
fb2647ff7b | ||
|
|
23a34c5dc9 | ||
|
|
bef3da6fb2 | ||
|
|
7f50679e68 | ||
|
|
52d071f9d1 | ||
|
|
26a888547d | ||
|
|
eb9431ee1f | ||
|
|
fd6605932a | ||
|
|
5d96ee084d | ||
|
|
e2cee10767 | ||
|
|
31c2e1a450 |
1
.github/workflows/promote.yml
vendored
1
.github/workflows/promote.yml
vendored
@@ -10,6 +10,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
promote:
|
promote:
|
||||||
|
if: endsWith(github.repository, '/awx')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout awx
|
- name: Checkout awx
|
||||||
|
|||||||
1
.github/workflows/stage.yml
vendored
1
.github/workflows/stage.yml
vendored
@@ -21,6 +21,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stage:
|
stage:
|
||||||
|
if: endsWith(github.repository, '/awx')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from rest_framework import generics
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework import views
|
from rest_framework import views
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.renderers import StaticHTMLRenderer
|
from rest_framework.renderers import StaticHTMLRenderer
|
||||||
from rest_framework.negotiation import DefaultContentNegotiation
|
from rest_framework.negotiation import DefaultContentNegotiation
|
||||||
|
|
||||||
@@ -822,7 +822,7 @@ def trigger_delayed_deep_copy(*args, **kwargs):
|
|||||||
|
|
||||||
class CopyAPIView(GenericAPIView):
|
class CopyAPIView(GenericAPIView):
|
||||||
serializer_class = CopySerializer
|
serializer_class = CopySerializer
|
||||||
permission_classes = (AllowAny,)
|
permission_classes = (IsAuthenticated,)
|
||||||
copy_return_serializer_class = None
|
copy_return_serializer_class = None
|
||||||
new_in_330 = True
|
new_in_330 = True
|
||||||
new_in_api_v2 = True
|
new_in_api_v2 = True
|
||||||
|
|||||||
@@ -4288,7 +4288,7 @@ class WorkflowApprovalTemplateJobsList(SubListAPIView):
|
|||||||
parent_key = 'workflow_approval_template'
|
parent_key = 'workflow_approval_template'
|
||||||
|
|
||||||
|
|
||||||
class WorkflowApprovalList(ListCreateAPIView):
|
class WorkflowApprovalList(ListAPIView):
|
||||||
model = models.WorkflowApproval
|
model = models.WorkflowApproval
|
||||||
serializer_class = serializers.WorkflowApprovalListSerializer
|
serializer_class = serializers.WorkflowApprovalListSerializer
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.timezone import now
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('awx.conf.migrations')
|
||||||
|
|
||||||
|
|
||||||
def fill_ldap_group_type_params(apps, schema_editor):
|
def fill_ldap_group_type_params(apps, schema_editor):
|
||||||
@@ -15,7 +19,7 @@ def fill_ldap_group_type_params(apps, schema_editor):
|
|||||||
entry = qs[0]
|
entry = qs[0]
|
||||||
group_type_params = entry.value
|
group_type_params = entry.value
|
||||||
else:
|
else:
|
||||||
entry = Setting(key='AUTH_LDAP_GROUP_TYPE_PARAMS', value=group_type_params, created=now(), modified=now())
|
return # for new installs we prefer to use the default value
|
||||||
|
|
||||||
init_attrs = set(inspect.getfullargspec(group_type.__init__).args[1:])
|
init_attrs = set(inspect.getfullargspec(group_type.__init__).args[1:])
|
||||||
for k in list(group_type_params.keys()):
|
for k in list(group_type_params.keys()):
|
||||||
@@ -23,4 +27,5 @@ def fill_ldap_group_type_params(apps, schema_editor):
|
|||||||
del group_type_params[k]
|
del group_type_params[k]
|
||||||
|
|
||||||
entry.value = group_type_params
|
entry.value = group_type_params
|
||||||
|
logger.warning(f'Migration updating AUTH_LDAP_GROUP_TYPE_PARAMS with value {entry.value}')
|
||||||
entry.save()
|
entry.save()
|
||||||
|
|||||||
25
awx/conf/tests/functional/test_migrations.py
Normal file
25
awx/conf/tests/functional/test_migrations.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from awx.conf.migrations._ldap_group_type import fill_ldap_group_type_params
|
||||||
|
from awx.conf.models import Setting
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_fill_group_type_params_no_op():
|
||||||
|
fill_ldap_group_type_params(apps, 'dont-use-me')
|
||||||
|
assert Setting.objects.count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_keep_old_setting_with_default_value():
|
||||||
|
Setting.objects.create(key='AUTH_LDAP_GROUP_TYPE', value={'name_attr': 'cn', 'member_attr': 'member'})
|
||||||
|
fill_ldap_group_type_params(apps, 'dont-use-me')
|
||||||
|
assert Setting.objects.count() == 1
|
||||||
|
s = Setting.objects.first()
|
||||||
|
assert s.value == {'name_attr': 'cn', 'member_attr': 'member'}
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: would be good to test the removal of attributes by migration
|
||||||
|
# but this requires fighting with the validator and is not done here
|
||||||
@@ -27,8 +27,8 @@ class AWXProtocolTypeRouter(ProtocolTypeRouter):
|
|||||||
|
|
||||||
|
|
||||||
websocket_urlpatterns = [
|
websocket_urlpatterns = [
|
||||||
re_path(r'websocket/$', consumers.EventConsumer.as_asgi()),
|
re_path(r'websocket/', consumers.EventConsumer.as_asgi()),
|
||||||
re_path(r'websocket/broadcast/$', consumers.BroadcastConsumer.as_asgi()),
|
re_path(r'websocket/broadcast/', consumers.BroadcastConsumer.as_asgi()),
|
||||||
]
|
]
|
||||||
|
|
||||||
application = AWXProtocolTypeRouter(
|
application = AWXProtocolTypeRouter(
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ class BaseTask(object):
|
|||||||
env['AWX_PRIVATE_DATA_DIR'] = private_data_dir
|
env['AWX_PRIVATE_DATA_DIR'] = private_data_dir
|
||||||
|
|
||||||
if self.instance.execution_environment is None:
|
if self.instance.execution_environment is None:
|
||||||
raise RuntimeError('The project could not sync because there is no Execution Environment.')
|
raise RuntimeError(f'The {self.model.__name__} could not run because there is no Execution Environment.')
|
||||||
|
|
||||||
return env
|
return env
|
||||||
|
|
||||||
|
|||||||
@@ -2008,7 +2008,7 @@ def test_project_update_no_ee(mock_me):
|
|||||||
with pytest.raises(RuntimeError) as e:
|
with pytest.raises(RuntimeError) as e:
|
||||||
task.build_env(job, {})
|
task.build_env(job, {})
|
||||||
|
|
||||||
assert 'The project could not sync because there is no Execution Environment' in str(e.value)
|
assert 'The ProjectUpdate could not run because there is no Execution Environment' in str(e.value)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -6,8 +7,15 @@ from django.conf import settings
|
|||||||
from awx.main.models.execution_environments import ExecutionEnvironment
|
from awx.main.models.execution_environments import ExecutionEnvironment
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_control_plane_execution_environment():
|
def get_control_plane_execution_environment():
|
||||||
return ExecutionEnvironment.objects.filter(organization=None, managed=True).first()
|
ee = ExecutionEnvironment.objects.filter(organization=None, managed=True).first()
|
||||||
|
if ee == None:
|
||||||
|
logger.error('Failed to find control plane ee, there are no managed EEs without organizations')
|
||||||
|
raise RuntimeError("Failed to find default control plane EE")
|
||||||
|
return ee
|
||||||
|
|
||||||
|
|
||||||
def get_default_execution_environment():
|
def get_default_execution_environment():
|
||||||
|
|||||||
@@ -385,10 +385,10 @@ def on_populate_user(sender, **kwargs):
|
|||||||
logger.warning('LDAP user {} has {} > max {} characters'.format(user.username, field, max_len))
|
logger.warning('LDAP user {} has {} > max {} characters'.format(user.username, field, max_len))
|
||||||
|
|
||||||
org_map = getattr(backend.settings, 'ORGANIZATION_MAP', {})
|
org_map = getattr(backend.settings, 'ORGANIZATION_MAP', {})
|
||||||
team_map = getattr(backend.settings, 'TEAM_MAP', {})
|
team_map_settings = getattr(backend.settings, 'TEAM_MAP', {})
|
||||||
orgs_list = list(org_map.keys())
|
orgs_list = list(org_map.keys())
|
||||||
team_map = {}
|
team_map = {}
|
||||||
for team_name, team_opts in team_map.items():
|
for team_name, team_opts in team_map_settings.items():
|
||||||
if not team_opts.get('organization', None):
|
if not team_opts.get('organization', None):
|
||||||
# You can't save the LDAP config in the UI w/o an org (or '' or null as the org) so if we somehow got this condition its an error
|
# You can't save the LDAP config in the UI w/o an org (or '' or null as the org) so if we somehow got this condition its an error
|
||||||
logger.error("Team named {} in LDAP team map settings is invalid due to missing organization".format(team_name))
|
logger.error("Team named {} in LDAP team map settings is invalid due to missing organization".format(team_name))
|
||||||
@@ -416,7 +416,7 @@ def on_populate_user(sender, **kwargs):
|
|||||||
|
|
||||||
# Compute in memory what the state is of the different LDAP teams
|
# Compute in memory what the state is of the different LDAP teams
|
||||||
desired_team_states = {}
|
desired_team_states = {}
|
||||||
for team_name, team_opts in team_map.items():
|
for team_name, team_opts in team_map_settings.items():
|
||||||
if 'organization' not in team_opts:
|
if 'organization' not in team_opts:
|
||||||
continue
|
continue
|
||||||
users_opts = team_opts.get('users', None)
|
users_opts = team_opts.get('users', None)
|
||||||
|
|||||||
@@ -57,7 +57,15 @@ extends_documentation_fragment: awx.awx.auth
|
|||||||
|
|
||||||
|
|
||||||
EXAMPLES = """
|
EXAMPLES = """
|
||||||
- name: Launch a workflow with a timeout of 10 seconds
|
- name: Create a workflow approval node
|
||||||
|
workflow_job_template_node:
|
||||||
|
identifier: approval_test
|
||||||
|
approval_node:
|
||||||
|
name: approval_jt_name
|
||||||
|
timeout: 900
|
||||||
|
workflow: "Test Workflow"
|
||||||
|
|
||||||
|
- name: Launch the workflow with a timeout of 10 seconds
|
||||||
workflow_launch:
|
workflow_launch:
|
||||||
workflow_template: "Test Workflow"
|
workflow_template: "Test Workflow"
|
||||||
wait: False
|
wait: False
|
||||||
@@ -66,7 +74,7 @@ EXAMPLES = """
|
|||||||
- name: Wait for approval node to activate and approve
|
- name: Wait for approval node to activate and approve
|
||||||
workflow_approval:
|
workflow_approval:
|
||||||
workflow_job_id: "{{ workflow.id }}"
|
workflow_job_id: "{{ workflow.id }}"
|
||||||
name: Approve Me
|
name: approval_jt_name
|
||||||
interval: 10
|
interval: 10
|
||||||
timeout: 20
|
timeout: 20
|
||||||
action: deny
|
action: deny
|
||||||
|
|||||||
@@ -183,7 +183,21 @@ options:
|
|||||||
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
|
||||||
type: str
|
type: dict
|
||||||
|
suboptions:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name Inventory to be applied to job as launch-time prompts.
|
||||||
|
type: str
|
||||||
|
organization:
|
||||||
|
description:
|
||||||
|
- Name of key for use in model for organizational reference
|
||||||
|
type: dict
|
||||||
|
suboptions:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- The organization of the credentials exists in.
|
||||||
|
type: str
|
||||||
scm_branch:
|
scm_branch:
|
||||||
description:
|
description:
|
||||||
- SCM branch applied as a prompt, if job template prompts for SCM branch
|
- SCM branch applied as a prompt, if job template prompts for SCM branch
|
||||||
@@ -544,6 +558,10 @@ EXAMPLES = '''
|
|||||||
type: job_template
|
type: job_template
|
||||||
execution_environment:
|
execution_environment:
|
||||||
name: My EE
|
name: My EE
|
||||||
|
inventory:
|
||||||
|
name: Test inventory
|
||||||
|
organization:
|
||||||
|
name: Default
|
||||||
related:
|
related:
|
||||||
credentials:
|
credentials:
|
||||||
- name: cyberark
|
- name: cyberark
|
||||||
@@ -613,10 +631,6 @@ def create_workflow_nodes(module, response, workflow_nodes, workflow_id):
|
|||||||
if workflow_node['unified_job_template']['type'] != 'workflow_approval':
|
if workflow_node['unified_job_template']['type'] != 'workflow_approval':
|
||||||
module.fail_json(msg="Unable to Find unified_job_template: {0}".format(search_fields))
|
module.fail_json(msg="Unable to Find unified_job_template: {0}".format(search_fields))
|
||||||
|
|
||||||
inventory = workflow_node.get('inventory')
|
|
||||||
if inventory:
|
|
||||||
workflow_node_fields['inventory'] = module.resolve_name_to_id('inventories', inventory)
|
|
||||||
|
|
||||||
# Lookup Values for other fields
|
# Lookup Values for other fields
|
||||||
|
|
||||||
for field_name in (
|
for field_name in (
|
||||||
@@ -645,6 +659,17 @@ def create_workflow_nodes(module, response, workflow_nodes, workflow_id):
|
|||||||
'execution_environments', name_or_id=workflow_node['execution_environment']['name']
|
'execution_environments', name_or_id=workflow_node['execution_environment']['name']
|
||||||
)['id']
|
)['id']
|
||||||
|
|
||||||
|
# Two lookup methods are used based on a fix added in 21.11.0, and the awx export model
|
||||||
|
if 'inventory' in workflow_node:
|
||||||
|
if 'name' in workflow_node['inventory']:
|
||||||
|
inv_lookup_data = {}
|
||||||
|
if 'organization' in workflow_node['inventory']:
|
||||||
|
inv_lookup_data['organization'] = module.resolve_name_to_id('organizations', workflow_node['inventory']['organization']['name'])
|
||||||
|
workflow_node_fields['inventory'] = module.get_one(
|
||||||
|
'inventories', name_or_id=workflow_node['inventory']['name'], data=inv_lookup_data)['id']
|
||||||
|
else:
|
||||||
|
workflow_node_fields['inventory'] = module.get_one('inventories', name_or_id=workflow_node['inventory'])['id']
|
||||||
|
|
||||||
# Set Search fields
|
# Set Search fields
|
||||||
search_fields['workflow_job_template'] = workflow_node_fields['workflow_job_template'] = workflow_id
|
search_fields['workflow_job_template'] = workflow_node_fields['workflow_job_template'] = workflow_id
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import glob
|
|||||||
# Normally a read-only endpoint should not have a module (i.e. /api/v2/me) but sometimes we reuse a name
|
# Normally a read-only endpoint should not have a module (i.e. /api/v2/me) but sometimes we reuse a name
|
||||||
# For example, we have a role module but /api/v2/roles is a read only endpoint.
|
# For example, we have a role module but /api/v2/roles is a read only endpoint.
|
||||||
# This list indicates which read-only endpoints have associated modules with them.
|
# This list indicates which read-only endpoints have associated modules with them.
|
||||||
read_only_endpoints_with_modules = ['settings', 'role', 'project_update']
|
read_only_endpoints_with_modules = ['settings', 'role', 'project_update', 'workflow_approval']
|
||||||
|
|
||||||
# If a module should not be created for an endpoint and the endpoint is not read-only add it here
|
# If a module should not be created for an endpoint and the endpoint is not read-only add it here
|
||||||
# THINK HARD ABOUT DOING THIS
|
# THINK HARD ABOUT DOING THIS
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
- name: Generate a random string for names
|
||||||
|
set_fact:
|
||||||
|
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||||
|
test_prefix: AWX-Collection-tests-workflow_approval
|
||||||
|
|
||||||
|
- name: Generate random names for test objects
|
||||||
|
set_fact:
|
||||||
|
org_name: "{{ test_prefix }}-org-{{ test_id }}"
|
||||||
|
approval_node_name: "{{ test_prefix }}-node-{{ test_id }}"
|
||||||
|
wfjt_name: "{{ test_prefix }}-wfjt-{{ test_id }}"
|
||||||
|
|
||||||
|
- block:
|
||||||
|
- name: Create a new organization for test isolation
|
||||||
|
organization:
|
||||||
|
name: "{{ org_name }}"
|
||||||
|
|
||||||
|
- name: Create a workflow job template
|
||||||
|
workflow_job_template:
|
||||||
|
name: "{{ wfjt_name }}"
|
||||||
|
organization: "{{ org_name }}"
|
||||||
|
|
||||||
|
- name: Create approval node
|
||||||
|
workflow_job_template_node:
|
||||||
|
identifier: approval_test
|
||||||
|
approval_node:
|
||||||
|
name: "{{ approval_node_name }}" # Referenced later on
|
||||||
|
timeout: 900
|
||||||
|
workflow: "{{ wfjt_name }}"
|
||||||
|
|
||||||
|
# Launch and approve the workflow
|
||||||
|
- name: Launch the workflow
|
||||||
|
workflow_launch:
|
||||||
|
workflow_template: "{{ wfjt_name }}"
|
||||||
|
wait: False
|
||||||
|
register: workflow_job
|
||||||
|
|
||||||
|
- name: Wait for approval node to activate and approve
|
||||||
|
workflow_approval:
|
||||||
|
workflow_job_id: "{{ workflow_job.id }}"
|
||||||
|
name: "{{ approval_node_name }}"
|
||||||
|
interval: 10
|
||||||
|
timeout: 20
|
||||||
|
action: approve
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "result is changed"
|
||||||
|
- "result is not failed"
|
||||||
|
|
||||||
|
always:
|
||||||
|
- name: Delete the workflow job template
|
||||||
|
workflow_job_template:
|
||||||
|
name: "{{ wfjt_name }}"
|
||||||
|
state: absent
|
||||||
|
ignore_errors: True
|
||||||
@@ -493,6 +493,7 @@
|
|||||||
workflow_job_template:
|
workflow_job_template:
|
||||||
name: "copy_{{ wfjt_name }}"
|
name: "copy_{{ wfjt_name }}"
|
||||||
organization: Default
|
organization: Default
|
||||||
|
ask_inventory_on_launch: true
|
||||||
survey_spec:
|
survey_spec:
|
||||||
name: Basic Survey
|
name: Basic Survey
|
||||||
description: Basic Survey
|
description: Basic Survey
|
||||||
@@ -737,6 +738,10 @@
|
|||||||
timeout: 23
|
timeout: 23
|
||||||
execution_environment:
|
execution_environment:
|
||||||
name: "{{ ee1 }}"
|
name: "{{ ee1 }}"
|
||||||
|
inventory:
|
||||||
|
name: Test inventory
|
||||||
|
organization:
|
||||||
|
name: Default
|
||||||
related:
|
related:
|
||||||
credentials:
|
credentials:
|
||||||
- name: "{{ scm_cred_name }}"
|
- name: "{{ scm_cred_name }}"
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ In the root of awx-operator:
|
|||||||
-e image_version=devel \
|
-e image_version=devel \
|
||||||
-e image_pull_policy=Always \
|
-e image_pull_policy=Always \
|
||||||
-e service_type=nodeport \
|
-e service_type=nodeport \
|
||||||
-e namespace=awx
|
-e namespace=awx \
|
||||||
|
-e nodeport_port=30080
|
||||||
```
|
```
|
||||||
Check the operator with the following commands:
|
Check the operator with the following commands:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user