From 1c4a3767586b8392527c950ca928563db756a22c Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 15 Mar 2021 12:31:59 -0400 Subject: [PATCH 1/6] Explicit db field for is_container_group We now have Container Groups that dont require a credential. --- awx/api/serializers.py | 12 +++++++-- .../management/commands/register_queue.py | 7 ++++- awx/main/managers.py | 3 ++- .../0132_instancegroup_is_container_group.py | 27 +++++++++++++++++++ awx/main/models/ha.py | 10 +++---- 5 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 awx/main/migrations/0132_instancegroup_is_container_group.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 8246ad8f1d..43d495637d 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -4776,8 +4776,7 @@ class InstanceGroupSerializer(BaseSerializer): ) is_container_group = serializers.BooleanField( help_text=_('Indicates whether instances in this group are containerized.' - 'Containerized groups have a designated Openshift or Kubernetes cluster.'), - read_only=True + 'Containerized groups have a designated Openshift or Kubernetes cluster.') ) # NOTE: help_text is duplicated from field definitions, no obvious way of # both defining field details here and also getting the field's help_text @@ -4853,6 +4852,15 @@ class InstanceGroupSerializer(BaseSerializer): raise serializers.ValidationError(_('Only Kubernetes credentials can be associated with an Instance Group')) return value + def validate(self, attrs): + attrs = super(InstanceGroupSerializer, self).validate(attrs) + + if attrs.get('credential') and not attrs.get('is_container_group'): + raise serializers.ValidationError({'is_container_group': _( + 'is_container_group must be True when associating a credential to an Instance Group')}) + + return attrs + def get_capacity_dict(self): # Store capacity values (globally computed) in the context if 'capacity_map' not in self.context: diff --git a/awx/main/management/commands/register_queue.py b/awx/main/management/commands/register_queue.py index edd8068b89..15891a771f 100644 --- a/awx/main/management/commands/register_queue.py +++ b/awx/main/management/commands/register_queue.py @@ -17,13 +17,14 @@ class InstanceNotFound(Exception): class RegisterQueue: - def __init__(self, queuename, controller, instance_percent, inst_min, hostname_list): + def __init__(self, queuename, controller, instance_percent, inst_min, hostname_list, is_container_group=None): self.instance_not_found_err = None self.queuename = queuename self.controller = controller self.instance_percent = instance_percent self.instance_min = inst_min self.hostname_list = hostname_list + self.is_container_group = is_container_group def get_create_update_instance_group(self): created = False @@ -36,6 +37,10 @@ class RegisterQueue: ig.policy_instance_minimum = self.instance_min changed = True + if self.is_container_group: + ig.is_container_group = self.is_container_group + changed = True + if changed: ig.save() diff --git a/awx/main/managers.py b/awx/main/managers.py index 1af57a9423..0d36515628 100644 --- a/awx/main/managers.py +++ b/awx/main/managers.py @@ -144,7 +144,8 @@ class InstanceManager(models.Manager): from awx.main.management.commands.register_queue import RegisterQueue pod_ip = os.environ.get('MY_POD_IP') registered = self.register(ip_address=pod_ip) - RegisterQueue('tower', None, 100, 0, []).register() + is_container_group = settings.IS_K8S + RegisterQueue('tower', None, 100, 0, [], is_container_group).register() return registered else: return (False, self.me()) diff --git a/awx/main/migrations/0132_instancegroup_is_container_group.py b/awx/main/migrations/0132_instancegroup_is_container_group.py new file mode 100644 index 0000000000..faffdc1af5 --- /dev/null +++ b/awx/main/migrations/0132_instancegroup_is_container_group.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.16 on 2021-03-13 14:53 + +from django.db import migrations, models + + +def migrate_existing_container_groups(apps, schema_editor): + InstanceGroup = apps.get_model('main', 'InstanceGroup') + + for group in InstanceGroup.objects.filter(credential__isnull=False).iterator(): + group.is_container_group = True + group.save(update_fields=['is_container_group']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0131_undo_org_polymorphic_ee'), + ] + + operations = [ + migrations.AddField( + model_name='instancegroup', + name='is_container_group', + field=models.BooleanField(default=False), + ), + migrations.RunPython(migrate_existing_container_groups, migrations.RunPython.noop), + ] diff --git a/awx/main/models/ha.py b/awx/main/models/ha.py index 94d4b8d462..eda3100b17 100644 --- a/awx/main/models/ha.py +++ b/awx/main/models/ha.py @@ -199,6 +199,9 @@ class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin): null=True, on_delete=models.CASCADE ) + is_container_group = models.BooleanField( + default=False + ) credential = models.ForeignKey( 'Credential', related_name='%(class)ss', @@ -253,13 +256,6 @@ class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin): def is_isolated(self): return bool(self.controller) - @property - def is_container_group(self): - if settings.IS_K8S: - return True - - return bool(self.credential and self.credential.kubernetes) - ''' RelatedJobsMixin ''' From 098ec639441350ac2bf82e87404300e6d461995e Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Mon, 15 Mar 2021 12:50:33 -0400 Subject: [PATCH 2/6] Add container group flag to add/edit data --- .../InstanceGroup/ContainerGroupAdd/ContainerGroupAdd.jsx | 1 + .../InstanceGroup/ContainerGroupAdd/ContainerGroupAdd.test.jsx | 2 ++ .../InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.jsx | 1 + .../ContainerGroupEdit/ContainerGroupEdit.test.jsx | 1 + 4 files changed, 5 insertions(+) diff --git a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupAdd/ContainerGroupAdd.jsx b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupAdd/ContainerGroupAdd.jsx index 6dffae83d3..7549a9d1cc 100644 --- a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupAdd/ContainerGroupAdd.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupAdd/ContainerGroupAdd.jsx @@ -33,6 +33,7 @@ function ContainerGroupAdd() { pod_spec_override: values.override ? getPodSpecValue(values.pod_spec_override) : null, + is_container_group: true, }); history.push(`/instance_groups/container_group/${response.id}/details`); } catch (error) { diff --git a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupAdd/ContainerGroupAdd.test.jsx b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupAdd/ContainerGroupAdd.test.jsx index b8bbb301ab..3b47a19d29 100644 --- a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupAdd/ContainerGroupAdd.test.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupAdd/ContainerGroupAdd.test.jsx @@ -26,6 +26,7 @@ const initialPodSpec = { }, ], }, + is_container_group: true, }, }; @@ -80,6 +81,7 @@ describe('', () => { expect(InstanceGroupsAPI.create).toHaveBeenCalledWith({ ...instanceGroupCreateData, credential: 71, + is_container_group: true, }); expect(wrapper.find('FormSubmitError').length).toBe(0); expect(history.location.pathname).toBe( diff --git a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.jsx b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.jsx index 9f4454c0a8..bd56246c18 100644 --- a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.jsx @@ -39,6 +39,7 @@ function ContainerGroupEdit({ instanceGroup }) { name: values.name, credential: values.credential ? values.credential.id : null, pod_spec_override: values.override ? values.pod_spec_override : null, + is_container_group: true, }); history.push(detailsIUrl); } catch (error) { diff --git a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.test.jsx b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.test.jsx index 860c6363c5..6d05e43472 100644 --- a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.test.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.test.jsx @@ -147,6 +147,7 @@ describe('', () => { ...updatedInstanceGroup, credential: 12, pod_spec_override: null, + is_container_group: true, }); expect(history.location.pathname).toEqual( '/instance_groups/container_group/123/details' From b15a75676deb04cffb57e4e916b53cbbed068172 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 15 Mar 2021 13:06:59 -0400 Subject: [PATCH 3/6] Fix container group tests --- awx/main/tests/functional/api/test_instance_group.py | 2 ++ .../tests/functional/task_management/test_container_groups.py | 1 + 2 files changed, 3 insertions(+) diff --git a/awx/main/tests/functional/api/test_instance_group.py b/awx/main/tests/functional/api/test_instance_group.py index 61c1054912..22ecf1a2f6 100644 --- a/awx/main/tests/functional/api/test_instance_group.py +++ b/awx/main/tests/functional/api/test_instance_group.py @@ -49,6 +49,7 @@ def isolated_instance_group(instance_group, instance): def containerized_instance_group(instance_group, kube_credential): ig = InstanceGroup(name="container") ig.credential = kube_credential + ig.is_container_group = True ig.save() return ig @@ -287,6 +288,7 @@ def test_containerized_group_default_fields(instance_group, kube_credential): assert ig.policy_instance_minimum == 5 assert ig.policy_instance_percentage == 5 ig.credential = kube_credential + ig.is_container_group = True ig.save() assert ig.policy_instance_list == [] assert ig.policy_instance_minimum == 0 diff --git a/awx/main/tests/functional/task_management/test_container_groups.py b/awx/main/tests/functional/task_management/test_container_groups.py index 84dcaf12d7..e739ff879b 100644 --- a/awx/main/tests/functional/task_management/test_container_groups.py +++ b/awx/main/tests/functional/task_management/test_container_groups.py @@ -13,6 +13,7 @@ from awx.main.utils import ( @pytest.fixture def containerized_job(default_instance_group, kube_credential, job_template_factory): default_instance_group.credential = kube_credential + default_instance_group.is_container_group = True default_instance_group.save() objects = job_template_factory('jt', organization='org1', project='proj', inventory='inv', credential='cred', From e6f06a95daf78a44da997929918f7b98ee6aca83 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 15 Mar 2021 13:30:31 -0400 Subject: [PATCH 4/6] Remove unnecessary code from launch script - Ansible is no longer installed on the control plane - We register the instance / instance group at dispatcher startup --- .../roles/dockerfile/files/launch_awx_task.sh | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/tools/ansible/roles/dockerfile/files/launch_awx_task.sh b/tools/ansible/roles/dockerfile/files/launch_awx_task.sh index 8b9774a477..4df5aefeb7 100755 --- a/tools/ansible/roles/dockerfile/files/launch_awx_task.sh +++ b/tools/ansible/roles/dockerfile/files/launch_awx_task.sh @@ -13,29 +13,4 @@ if [ -n "${AWX_KUBE_DEVEL}" ]; then export SDB_NOTIFY_HOST=$MY_POD_IP fi -source /etc/tower/conf.d/environment.sh - -ANSIBLE_REMOTE_TEMP=/tmp ANSIBLE_LOCAL_TEMP=/tmp ansible -i "127.0.0.1," -c local -v -m wait_for -a "host=$DATABASE_HOST port=$DATABASE_PORT" all -ANSIBLE_REMOTE_TEMP=/tmp ANSIBLE_LOCAL_TEMP=/tmp ansible -i "127.0.0.1," -c local -v -m postgresql_db --become-user $DATABASE_USER -a "name=$DATABASE_NAME owner=$DATABASE_USER login_user=$DATABASE_USER login_host=$DATABASE_HOST login_password=$DATABASE_PASSWORD port=$DATABASE_PORT" all - -if [ -z "$AWX_SKIP_MIGRATIONS" ]; then - echo "Running migrations..." - awx-manage migrate --noinput -fi - -if [ -z "$AWX_SKIP_PROVISION_INSTANCE" ]; then - awx-manage provision_instance --hostname=$(hostname) -fi - -if [ -z "$AWX_SKIP_REGISTERQUEUE" ]; then - awx-manage register_queue --queuename=tower --instance_percent=100 -fi - -if [ ! -z "$AWX_ADMIN_USER" ]&&[ ! -z "$AWX_ADMIN_PASSWORD" ]; then - echo "from django.contrib.auth.models import User; User.objects.create_superuser('$AWX_ADMIN_USER', 'root@localhost', '$AWX_ADMIN_PASSWORD')" | awx-manage shell -fi -echo 'from django.conf import settings; x = settings.AWX_TASK_ENV; x["HOME"] = "/var/lib/awx"; settings.AWX_TASK_ENV = x' | awx-manage shell - -unset $(cut -d = -f -1 /etc/tower/conf.d/environment.sh) - supervisord -c /etc/supervisord_task.conf From 4a4d25329b5289b19eae8846c891beae05b19fbd Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 15 Mar 2021 13:34:45 -0400 Subject: [PATCH 5/6] Update instance_group module with is_container_group --- awx_collection/plugins/modules/tower_instance_group.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/awx_collection/plugins/modules/tower_instance_group.py b/awx_collection/plugins/modules/tower_instance_group.py index f32b60aebf..e0beb6bc68 100644 --- a/awx_collection/plugins/modules/tower_instance_group.py +++ b/awx_collection/plugins/modules/tower_instance_group.py @@ -37,6 +37,11 @@ options: - Credential to authenticate with Kubernetes or OpenShift. Must be of type "Kubernetes/OpenShift API Bearer Token". required: False type: str + is_container_group: + description: + - Signifies that this InstanceGroup should act as a ContainerGroup. If no credential is specified, the underlying Pod's ServiceAccount will be used. + required: False + type: bool policy_instance_percentage: description: - Minimum percentage of all instances that will be automatically assigned to this group when new instances come online. @@ -100,6 +105,7 @@ def main(): name = module.params.get('name') new_name = module.params.get("new_name") credential = module.params.get('credential') + is_container_group = module.params.get('is_container_group') policy_instance_percentage = module.params.get('policy_instance_percentage') policy_instance_minimum = module.params.get('policy_instance_minimum') policy_instance_list = module.params.get('policy_instance_list') @@ -129,6 +135,8 @@ def main(): new_fields['name'] = new_name if new_name else (module.get_item_name(existing_item) if existing_item else name) if credential is not None: new_fields['credential'] = credential_id + if is_container_group is not None: + new_fields['is_container_group'] = is_container_group if policy_instance_percentage is not None: new_fields['policy_instance_percentage'] = policy_instance_percentage if policy_instance_minimum is not None: From 876d4316e1c8eafeeb44f34528fa04e7696f95e2 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 15 Mar 2021 14:14:03 -0400 Subject: [PATCH 6/6] Fix collection tests --- awx_collection/plugins/modules/tower_instance_group.py | 1 + awx_collection/test/awx/test_instance_group.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/awx_collection/plugins/modules/tower_instance_group.py b/awx_collection/plugins/modules/tower_instance_group.py index e0beb6bc68..d80e2e6ed1 100644 --- a/awx_collection/plugins/modules/tower_instance_group.py +++ b/awx_collection/plugins/modules/tower_instance_group.py @@ -90,6 +90,7 @@ def main(): name=dict(required=True), new_name=dict(), credential=dict(), + is_container_group=dict(type='bool', default=False), policy_instance_percentage=dict(type='int', default='0'), policy_instance_minimum=dict(type='int', default='0'), policy_instance_list=dict(type='list'), diff --git a/awx_collection/test/awx/test_instance_group.py b/awx_collection/test/awx/test_instance_group.py index 248d6f2d91..2516ce20ba 100644 --- a/awx_collection/test/awx/test_instance_group.py +++ b/awx_collection/test/awx/test_instance_group.py @@ -50,6 +50,7 @@ def test_container_group_create(run_module, admin_user, kube_credential): result = run_module('tower_instance_group', { 'name': 'foo-c-group', 'credential': kube_credential.id, + 'is_container_group': True, 'state': 'present' }, admin_user) assert not result.get('failed', False), result['msg'] @@ -61,6 +62,7 @@ def test_container_group_create(run_module, admin_user, kube_credential): result = run_module('tower_instance_group', { 'name': 'foo-c-group', 'credential': kube_credential.id, + 'is_container_group': True, 'pod_spec_override': pod_spec, 'state': 'present' }, admin_user)