diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 002c6929fb..c2f5dfca23 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -5471,6 +5471,8 @@ class InstanceGroupSerializer(BaseSerializer): res = super(InstanceGroupSerializer, self).get_related(obj) res['jobs'] = self.reverse('api:instance_group_unified_jobs_list', kwargs={'pk': obj.pk}) res['instances'] = self.reverse('api:instance_group_instance_list', kwargs={'pk': obj.pk}) + res['access_list'] = self.reverse('api:instance_group_access_list', kwargs={'pk': obj.pk}) + res['object_roles'] = self.reverse('api:instance_group_object_role_list', kwargs={'pk': obj.pk}) if obj.credential: res['credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.credential_id}) diff --git a/awx/api/urls/instance_group.py b/awx/api/urls/instance_group.py index de8cf8b52a..a37d0840a8 100644 --- a/awx/api/urls/instance_group.py +++ b/awx/api/urls/instance_group.py @@ -3,7 +3,14 @@ from django.urls import re_path -from awx.api.views import InstanceGroupList, InstanceGroupDetail, InstanceGroupUnifiedJobsList, InstanceGroupInstanceList +from awx.api.views import ( + InstanceGroupList, + InstanceGroupDetail, + InstanceGroupUnifiedJobsList, + InstanceGroupInstanceList, + InstanceGroupAccessList, + InstanceGroupObjectRolesList, +) urls = [ @@ -11,6 +18,8 @@ urls = [ re_path(r'^(?P[0-9]+)/$', InstanceGroupDetail.as_view(), name='instance_group_detail'), re_path(r'^(?P[0-9]+)/jobs/$', InstanceGroupUnifiedJobsList.as_view(), name='instance_group_unified_jobs_list'), re_path(r'^(?P[0-9]+)/instances/$', InstanceGroupInstanceList.as_view(), name='instance_group_instance_list'), + re_path(r'^(?P[0-9]+)/access_list/$', InstanceGroupAccessList.as_view(), name='instance_group_access_list'), + re_path(r'^(?P[0-9]+)/object_roles/$', InstanceGroupObjectRolesList.as_view(), name='instance_group_object_role_list'), ] __all__ = ['urls'] diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index b17986bb3e..fc3fe52610 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -466,6 +466,23 @@ class InstanceGroupUnifiedJobsList(SubListAPIView): relationship = "unifiedjob_set" +class InstanceGroupAccessList(ResourceAccessList): + model = models.User # needs to be User for AccessLists + parent_model = models.InstanceGroup + + +class InstanceGroupObjectRolesList(SubListAPIView): + model = models.Role + serializer_class = serializers.RoleSerializer + parent_model = models.InstanceGroup + search_fields = ('role_field', 'content_type__model') + + def get_queryset(self): + po = self.get_parent_object() + content_type = ContentType.objects.get_for_model(self.parent_model) + return models.Role.objects.filter(content_type=content_type, object_id=po.pk) + + class InstanceGroupInstanceList(InstanceGroupMembershipMixin, SubListAttachDetachAPIView): name = _("Instance Group's Instances") model = models.Instance diff --git a/awx/main/access.py b/awx/main/access.py index ef974f0f4c..5d51ab3b91 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -588,17 +588,39 @@ class InstanceAccess(BaseAccess): class InstanceGroupAccess(BaseAccess): + """ + I can see Instance Groups when I am: + - a superuser(system administrator) + - at least read_role on the instance group + I can edit Instance Groups when I am: + - a superuser + - admin role on the Instance group + I can add/delete Instance Groups: + - a superuser(system administrator) + I can use Instance Groups when I have: + - use_role on the instance group + """ + model = InstanceGroup prefetch_related = ('instances',) def filtered_queryset(self): - return InstanceGroup.objects.filter(organization__in=Organization.accessible_pk_qs(self.user, 'admin_role')).distinct() + return self.model.accessible_objects(self.user, 'read_role') + + @check_superuser + def can_use(self, obj): + return self.user in obj.use_role def can_add(self, data): return self.user.is_superuser + @check_superuser def can_change(self, obj, data): - return self.user.is_superuser + return self.can_admin(obj) + + @check_superuser + def can_admin(self, obj): + return self.user in obj.admin_role def can_delete(self, obj): if obj.name in [settings.DEFAULT_EXECUTION_QUEUE_NAME, settings.DEFAULT_CONTROL_PLANE_QUEUE_NAME]: @@ -845,7 +867,7 @@ class OrganizationAccess(NotificationAttachMixin, BaseAccess): return RoleAccess(self.user).can_attach(rel_role, sub_obj, 'members', *args, **kwargs) if relationship == "instance_groups": - if self.user.is_superuser: + if self.user in obj.admin_role and self.user in sub_obj.use_role: return True return False return super(OrganizationAccess, self).can_attach(obj, sub_obj, relationship, *args, **kwargs) @@ -934,7 +956,7 @@ class InventoryAccess(BaseAccess): def can_attach(self, obj, sub_obj, relationship, *args, **kwargs): if relationship == "instance_groups": - if self.user.can_access(type(sub_obj), "read", sub_obj) and self.user in obj.organization.admin_role: + if self.user in sub_obj.use_role and self.user in obj.admin_role: return True return False return super(InventoryAccess, self).can_attach(obj, sub_obj, relationship, *args, **kwargs) @@ -1671,11 +1693,12 @@ class JobTemplateAccess(NotificationAttachMixin, UnifiedCredentialsMixin, BaseAc return self.user.is_superuser or self.user in obj.admin_role @check_superuser + # object here is the job template. sub_object here is what is being attached def can_attach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False): if relationship == "instance_groups": if not obj.organization: return False - return self.user.can_access(type(sub_obj), "read", sub_obj) and self.user in obj.organization.admin_role + return self.user in sub_obj.use_role and self.user in obj.admin_role return super(JobTemplateAccess, self).can_attach(obj, sub_obj, relationship, data, skip_sub_obj_read_check=skip_sub_obj_read_check) @check_superuser @@ -1852,8 +1875,6 @@ class JobLaunchConfigAccess(UnifiedCredentialsMixin, BaseAccess): def _related_filtered_queryset(self, cls): if cls is Label: return LabelAccess(self.user).filtered_queryset() - elif cls is InstanceGroup: - return InstanceGroupAccess(self.user).filtered_queryset() else: return cls._accessible_pk_qs(cls, self.user, 'use_role') diff --git a/awx/main/migrations/0177_instance_group_role_addition.py b/awx/main/migrations/0177_instance_group_role_addition.py new file mode 100644 index 0000000000..c25a43845b --- /dev/null +++ b/awx/main/migrations/0177_instance_group_role_addition.py @@ -0,0 +1,48 @@ +# Generated by Django 3.2.16 on 2023-02-17 02:45 + +import awx.main.fields +from django.db import migrations +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ('main', '0176_inventorysource_scm_branch'), + ] + + operations = [ + migrations.AddField( + model_name='instancegroup', + name='admin_role', + field=awx.main.fields.ImplicitRoleField( + editable=False, + null='True', + on_delete=django.db.models.deletion.CASCADE, + parent_role=['singleton:system_administrator'], + related_name='+', + to='main.role', + ), + preserve_default='True', + ), + migrations.AddField( + model_name='instancegroup', + name='read_role', + field=awx.main.fields.ImplicitRoleField( + editable=False, + null='True', + on_delete=django.db.models.deletion.CASCADE, + parent_role=['singleton:system_auditor', 'use_role', 'admin_role'], + related_name='+', + to='main.role', + ), + preserve_default='True', + ), + migrations.AddField( + model_name='instancegroup', + name='use_role', + field=awx.main.fields.ImplicitRoleField( + editable=False, null='True', on_delete=django.db.models.deletion.CASCADE, parent_role=['admin_role'], related_name='+', to='main.role' + ), + preserve_default='True', + ), + ] diff --git a/awx/main/migrations/0178_instance_group_admin_migration.py b/awx/main/migrations/0178_instance_group_admin_migration.py new file mode 100644 index 0000000000..1ada858499 --- /dev/null +++ b/awx/main/migrations/0178_instance_group_admin_migration.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2023-02-17 02:45 + +from django.db import migrations +from awx.main.migrations import _rbac as rbac +from awx.main.migrations import _migration_utils as migration_utils +from awx.main.migrations import _OrgAdmin_to_use_ig as oamigrate +from awx.main.migrations import ActivityStreamDisabledMigration + + +class Migration(ActivityStreamDisabledMigration): + dependencies = [ + ('main', '0177_instance_group_role_addition'), + ] + operations = [ + migrations.RunPython(migration_utils.set_current_apps_for_migrations), + migrations.RunPython(rbac.create_roles), + migrations.RunPython(oamigrate.migrate_org_admin_to_use), + ] diff --git a/awx/main/migrations/_OrgAdmin_to_use_ig.py b/awx/main/migrations/_OrgAdmin_to_use_ig.py new file mode 100644 index 0000000000..3cbf42d5bd --- /dev/null +++ b/awx/main/migrations/_OrgAdmin_to_use_ig.py @@ -0,0 +1,20 @@ +import logging + +from awx.main.models import Organization + +logger = logging.getLogger('awx.main.migrations') + + +def migrate_org_admin_to_use(apps, schema_editor): + logger.info('Initiated migration from Org admin to use role') + roles_added = 0 + for org in Organization.objects.prefetch_related('admin_role__members').iterator(): + igs = list(org.instance_groups.all()) + if not igs: + continue + for admin in org.admin_role.members.filter(is_superuser=False): + for ig in igs: + ig.use_role.members.add(admin) + roles_added += 1 + if roles_added: + logger.info(f'Migration converted {roles_added} from organization admin to use role') diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index 96e6334d83..9bbbcd4d4e 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -29,6 +29,7 @@ def create_roles(apps, schema_editor): 'Project', 'Credential', 'JobTemplate', + 'InstanceGroup', ] ] diff --git a/awx/main/models/ha.py b/awx/main/models/ha.py index a706063630..de0861c909 100644 --- a/awx/main/models/ha.py +++ b/awx/main/models/ha.py @@ -17,15 +17,20 @@ from django.db.models import Sum import redis from solo.models import SingletonModel +# AWX from awx import __version__ as awx_application_version from awx.api.versioning import reverse -from awx.main.fields import JSONBlob +from awx.main.fields import JSONBlob, ImplicitRoleField from awx.main.managers import InstanceManager, UUID_DEFAULT from awx.main.constants import JOB_FOLDER_PREFIX from awx.main.models.base import BaseModel, HasEditsMixin, prevent_search +from awx.main.models.rbac import ( + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, + ROLE_SINGLETON_SYSTEM_AUDITOR, +) from awx.main.models.unified_jobs import UnifiedJob from awx.main.utils.common import get_corrected_cpu, get_cpu_effective_capacity, get_corrected_memory, get_mem_effective_capacity -from awx.main.models.mixins import RelatedJobsMixin +from awx.main.models.mixins import RelatedJobsMixin, ResourceMixin # ansible-runner from ansible_runner.utils.capacity import get_cpu_count, get_mem_in_bytes @@ -352,7 +357,7 @@ class Instance(HasPolicyEditsMixin, BaseModel): self.save_health_data(awx_application_version, get_cpu_count(), get_mem_in_bytes(), update_last_seen=True, errors=errors) -class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin): +class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin, ResourceMixin): """A model representing a Queue/Group of AWX Instances.""" name = models.CharField(max_length=250, unique=True) @@ -379,6 +384,24 @@ class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin): default='', ) ) + admin_role = ImplicitRoleField( + parent_role=[ + 'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, + ] + ) + use_role = ImplicitRoleField( + parent_role=[ + 'admin_role', + ] + ) + read_role = ImplicitRoleField( + parent_role=[ + 'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, + 'use_role', + 'admin_role', + ] + ) + max_concurrent_jobs = models.IntegerField(default=0, help_text=_("Maximum number of concurrent jobs to run on this group. Zero means no limit.")) max_forks = models.IntegerField(default=0, help_text=_("Max forks to execute on this group. Zero means no limit.")) policy_instance_percentage = models.IntegerField(default=0, help_text=_("Percentage of Instances to automatically assign to this group")) diff --git a/awx/main/tests/functional/test_instances.py b/awx/main/tests/functional/test_instances.py index df6d177868..9db370225c 100644 --- a/awx/main/tests/functional/test_instances.py +++ b/awx/main/tests/functional/test_instances.py @@ -99,12 +99,12 @@ def test_instance_dup(org_admin, organization, project, instance_factory, instan list_response = get(reverse('api:instance_list'), user=system_auditor) api_num_instances_auditor = list(list_response.data.items())[0][1] + ig_all.read_role.members.add(org_admin) list_response2 = get(reverse('api:instance_list'), user=org_admin) api_num_instances_oa = list(list_response2.data.items())[0][1] assert api_num_instances_auditor == actual_num_instances - # Note: The org_admin will not see the default 'tower' node - # (instance fixture) because it is not in its group, as expected + # Note: The org_admin will not see instances unless at least read_role to the IG has been assigned assert api_num_instances_oa == (actual_num_instances - 1) diff --git a/awx/main/tests/functional/test_org_admin_migration.py b/awx/main/tests/functional/test_org_admin_migration.py new file mode 100644 index 0000000000..84bed9ac88 --- /dev/null +++ b/awx/main/tests/functional/test_org_admin_migration.py @@ -0,0 +1,16 @@ +import pytest + +from django.apps import apps + +from awx.main.models import InstanceGroup +from awx.main.migrations import _OrgAdmin_to_use_ig as orgadmin + + +@pytest.mark.django_db +def test_migrate_admin_role(org_admin, organization): + instance_group = InstanceGroup.objects.create(name='test') + organization.admin_role.members.add(org_admin) + organization.instance_groups.add(instance_group) + orgadmin.migrate_org_admin_to_use(apps, None) + assert org_admin in instance_group.use_role.members.all() + assert instance_group.use_role.members.count() == 1 diff --git a/awx/main/tests/functional/test_rbac_instance_groups.py b/awx/main/tests/functional/test_rbac_instance_groups.py index 402040ea21..418e5a351a 100644 --- a/awx/main/tests/functional/test_rbac_instance_groups.py +++ b/awx/main/tests/functional/test_rbac_instance_groups.py @@ -6,7 +6,47 @@ from awx.main.access import ( InventoryAccess, JobTemplateAccess, ) -from awx.main.models import Organization + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "obj_perm,allowed,readonly,partial", [("admin_role", True, True, True), ("use_role", False, True, True), ("read_role", False, True, False)] +) +def test_ig_role_base_visibility(default_instance_group, rando, obj_perm, allowed, partial, readonly): + if obj_perm: + getattr(default_instance_group, obj_perm).members.add(rando) + + assert readonly == InstanceGroupAccess(rando).can_read(default_instance_group) + assert partial == InstanceGroupAccess(rando).can_use(default_instance_group) + assert not InstanceGroupAccess(rando).can_add(default_instance_group) + assert allowed == InstanceGroupAccess(rando).can_admin(default_instance_group) + assert allowed == InstanceGroupAccess(rando).can_change(default_instance_group, {'name': 'New Name'}) + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "obj_perm,subobj_perm,allowed", [('admin_role', 'use_role', True), ('admin_role', 'read_role', False), ('admin_role', 'admin_role', True)] +) +def test_ig_role_based_associability(default_instance_group, rando, organization, job_template_factory, obj_perm, subobj_perm, allowed): + objects = job_template_factory('jt', organization=organization, project='p', inventory='i', credential='c') + if obj_perm: + getattr(objects.job_template, obj_perm).members.add(rando) + getattr(objects.inventory, obj_perm).members.add(rando) + getattr(objects.organization, obj_perm).members.add(rando) + if subobj_perm: + getattr(default_instance_group, subobj_perm).members.add(rando) + + assert allowed == JobTemplateAccess(rando).can_attach(objects.job_template, default_instance_group, 'instance_groups', None) + assert allowed == InventoryAccess(rando).can_attach(objects.inventory, default_instance_group, 'instance_groups', None) + assert allowed == OrganizationAccess(rando).can_attach(objects.organization, default_instance_group, 'instance_groups', None) + + +@pytest.mark.django_db +def test_ig_use_with_org_admin(default_instance_group, rando, org_admin): + default_instance_group.use_role.members.add(rando) + + assert list(InstanceGroupAccess(org_admin).get_queryset()) != [default_instance_group] + assert list(InstanceGroupAccess(rando).get_queryset()) == [default_instance_group] @pytest.mark.django_db @@ -24,7 +64,7 @@ def test_ig_admin_user_visibility(organization, default_instance_group, admin, s assert len(InstanceGroupAccess(system_auditor).get_queryset()) == 1 assert len(InstanceGroupAccess(org_admin).get_queryset()) == 0 organization.instance_groups.add(default_instance_group) - assert len(InstanceGroupAccess(org_admin).get_queryset()) == 1 + assert len(InstanceGroupAccess(org_admin).get_queryset()) == 0 @pytest.mark.django_db @@ -37,16 +77,6 @@ def test_ig_normal_user_associability(organization, default_instance_group, user assert not access.can_attach(organization, default_instance_group, 'instance_groups', None) -@pytest.mark.django_db -def test_access_via_two_organizations(rando, default_instance_group): - for org_name in ['org1', 'org2']: - org = Organization.objects.create(name=org_name) - org.instance_groups.add(default_instance_group) - org.admin_role.members.add(rando) - access = InstanceGroupAccess(rando) - assert list(access.get_queryset()) == [default_instance_group] - - @pytest.mark.django_db def test_ig_associability(organization, default_instance_group, admin, system_auditor, org_admin, org_member, job_template_factory): admin_access = OrganizationAccess(admin) @@ -72,7 +102,7 @@ def test_ig_associability(organization, default_instance_group, admin, system_au omember_access = InventoryAccess(org_member) assert admin_access.can_attach(objects.inventory, default_instance_group, 'instance_groups', None) - assert oadmin_access.can_attach(objects.inventory, default_instance_group, 'instance_groups', None) + assert not oadmin_access.can_attach(objects.inventory, default_instance_group, 'instance_groups', None) assert not auditor_access.can_attach(objects.inventory, default_instance_group, 'instance_groups', None) assert not omember_access.can_attach(objects.inventory, default_instance_group, 'instance_groups', None) @@ -82,6 +112,6 @@ def test_ig_associability(organization, default_instance_group, admin, system_au omember_access = JobTemplateAccess(org_member) assert admin_access.can_attach(objects.job_template, default_instance_group, 'instance_groups', None) - assert oadmin_access.can_attach(objects.job_template, default_instance_group, 'instance_groups', None) + assert not oadmin_access.can_attach(objects.job_template, default_instance_group, 'instance_groups', None) assert not auditor_access.can_attach(objects.job_template, default_instance_group, 'instance_groups', None) assert not omember_access.can_attach(objects.job_template, default_instance_group, 'instance_groups', None) diff --git a/awx/main/tests/functional/test_rbac_workflow.py b/awx/main/tests/functional/test_rbac_workflow.py index 4c29907519..c2022783ac 100644 --- a/awx/main/tests/functional/test_rbac_workflow.py +++ b/awx/main/tests/functional/test_rbac_workflow.py @@ -148,7 +148,7 @@ class TestWorkflowJobTemplateNodeAccess: elif permission_type == 'instance_groups': sub_obj = InstanceGroup.objects.create() org = Organization.objects.create() - org.admin_role.members.add(rando) # only admins can see IGs + sub_obj.use_role.members.add(rando) # only admins can see IGs org.instance_groups.add(sub_obj) access = WorkflowJobTemplateNodeAccess(rando) diff --git a/awx/ui/src/api/models/InstanceGroups.js b/awx/ui/src/api/models/InstanceGroups.js index e28a1694e4..082268fa12 100644 --- a/awx/ui/src/api/models/InstanceGroups.js +++ b/awx/ui/src/api/models/InstanceGroups.js @@ -8,6 +8,7 @@ class InstanceGroups extends Base { this.associateInstance = this.associateInstance.bind(this); this.disassociateInstance = this.disassociateInstance.bind(this); this.readInstanceOptions = this.readInstanceOptions.bind(this); + this.readInstanceGroupOptions = this.readInstanceGroupOptions.bind(this); this.readInstances = this.readInstances.bind(this); this.readJobs = this.readJobs.bind(this); } @@ -33,6 +34,10 @@ class InstanceGroups extends Base { return this.http.options(`${this.baseUrl}${id}/instances/`); } + readInstanceGroupOptions(id) { + return this.http.options(`${this.baseUrl}${id}/`); + } + readJobs(id) { return this.http.get(`${this.baseUrl}${id}/jobs/`); } diff --git a/awx/ui/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js b/awx/ui/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js index 84fbe8eeca..98e294976e 100644 --- a/awx/ui/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js +++ b/awx/ui/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js @@ -6,6 +6,7 @@ import { InventoriesAPI, ProjectsAPI, OrganizationsAPI, + InstanceGroupsAPI, } from 'api'; export default function getResourceAccessConfig() { @@ -210,5 +211,32 @@ export default function getResourceAccessConfig() { fetchItems: (queryParams) => OrganizationsAPI.read(queryParams), fetchOptions: () => OrganizationsAPI.readOptions(), }, + { + selectedResource: 'Instance Groups', + label: t`Instance Groups`, + searchColumns: [ + { + name: t`Name`, + key: 'name__icontains', + isDefault: true, + }, + { + name: t`Created By (Username)`, + key: 'created_by__username__icontains', + }, + { + name: t`Modified By (Username)`, + key: 'modified_by__username__icontains', + }, + ], + sortColumns: [ + { + name: t`Name`, + key: 'name', + }, + ], + fetchItems: (queryParams) => InstanceGroupsAPI.read(queryParams), + fetchOptions: () => InstanceGroupsAPI.readOptions(), + }, ]; } diff --git a/awx/ui/src/routeConfig.js b/awx/ui/src/routeConfig.js index c5b4728107..602e804d2b 100644 --- a/awx/ui/src/routeConfig.js +++ b/awx/ui/src/routeConfig.js @@ -184,7 +184,6 @@ function getRouteConfig(userProfile = {}) { deleteRouteGroup('settings'); deleteRoute('management_jobs'); if (userProfile?.isOrgAdmin) return routeConfig; - deleteRoute('instance_groups'); deleteRoute('topology_view'); deleteRoute('instances'); if (!userProfile?.isNotificationAdmin) deleteRoute('notification_templates'); diff --git a/awx/ui/src/routeConfig.test.js b/awx/ui/src/routeConfig.test.js index 6210b5bc32..0b84c670c3 100644 --- a/awx/ui/src/routeConfig.test.js +++ b/awx/ui/src/routeConfig.test.js @@ -127,6 +127,7 @@ describe('getRouteConfig', () => { '/teams', '/credential_types', '/notification_templates', + '/instance_groups', '/applications', '/execution_environments', ]); @@ -150,6 +151,7 @@ describe('getRouteConfig', () => { '/users', '/teams', '/credential_types', + '/instance_groups', '/applications', '/execution_environments', ]); @@ -173,6 +175,7 @@ describe('getRouteConfig', () => { '/users', '/teams', '/credential_types', + '/instance_groups', '/applications', '/execution_environments', ]); @@ -201,6 +204,7 @@ describe('getRouteConfig', () => { '/teams', '/credential_types', '/notification_templates', + '/instance_groups', '/applications', '/execution_environments', ]); diff --git a/awx/ui/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.js b/awx/ui/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.js index 795f7a33b3..66be9eecf1 100644 --- a/awx/ui/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.js +++ b/awx/ui/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.js @@ -21,9 +21,11 @@ function ContainerGroupEdit({ instanceGroup }) { result: initialPodSpec, } = useRequest( useCallback(async () => { - const { data } = await InstanceGroupsAPI.readOptions(); - return data.actions.POST.pod_spec_override.default; - }, []), + const { data } = await InstanceGroupsAPI.readInstanceGroupOptions( + instanceGroup.id + ); + return data.actions.PUT.pod_spec_override.default; + }, [instanceGroup.id]), { initialPodSpec: {}, }