diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 6e23f3298f..054180b51e 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2000,6 +2000,7 @@ class InventorySourceOptionsSerializer(BaseSerializer): 'source', 'source_path', 'source_vars', + 'scm_branch', 'credential', 'enabled_var', 'enabled_value', @@ -2164,10 +2165,14 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt if ('source' in attrs or 'source_project' in attrs) and get_field_from_model_or_attrs('source_project') is None: raise serializers.ValidationError({"source_project": _("Project required for scm type sources.")}) else: - redundant_scm_fields = list(filter(lambda x: attrs.get(x, None), ['source_project', 'source_path'])) + redundant_scm_fields = list(filter(lambda x: attrs.get(x, None), ['source_project', 'source_path', 'scm_branch'])) if redundant_scm_fields: raise serializers.ValidationError({"detail": _("Cannot set %s if not SCM type." % ' '.join(redundant_scm_fields))}) + project = get_field_from_model_or_attrs('source_project') + if get_field_from_model_or_attrs('scm_branch') and not project.allow_override: + raise serializers.ValidationError({'scm_branch': _('Project does not allow overriding branch.')}) + attrs = super(InventorySourceSerializer, self).validate(attrs) # Check type consistency of source and cloud credential, if provided diff --git a/awx/main/migrations/0175_inventorysource_scm_branch.py b/awx/main/migrations/0175_inventorysource_scm_branch.py new file mode 100644 index 0000000000..56eed2da7f --- /dev/null +++ b/awx/main/migrations/0175_inventorysource_scm_branch.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.16 on 2023-03-03 20:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('main', '0174_ensure_org_ee_admin_roles'), + ] + + operations = [ + migrations.AddField( + model_name='inventorysource', + name='scm_branch', + field=models.CharField( + blank=True, + default='', + help_text='Inventory source SCM branch. Project default used if blank. Only allowed if project allow_override field is set to true.', + max_length=1024, + ), + ), + migrations.AddField( + model_name='inventoryupdate', + name='scm_branch', + field=models.CharField( + blank=True, + default='', + help_text='Inventory source SCM branch. Project default used if blank. Only allowed if project allow_override field is set to true.', + max_length=1024, + ), + ), + ] diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index a0df4dfde4..7b55c51851 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -872,6 +872,12 @@ class InventorySourceOptions(BaseModel): default='', help_text=_('Inventory source variables in YAML or JSON format.'), ) + scm_branch = models.CharField( + max_length=1024, + default='', + blank=True, + help_text=_('Inventory source SCM branch. Project default used if blank. Only allowed if project allow_override field is set to true.'), + ) enabled_var = models.TextField( blank=True, default='', diff --git a/awx/main/tasks/jobs.py b/awx/main/tasks/jobs.py index 369e045e6f..9da4bc074b 100644 --- a/awx/main/tasks/jobs.py +++ b/awx/main/tasks/jobs.py @@ -759,7 +759,7 @@ class SourceControlMixin(BaseTask): def sync_and_copy(self, project, private_data_dir, scm_branch=None): self.acquire_lock(project, self.instance.id) - + is_commit = False try: original_branch = None failed_reason = project.get_reason_if_failed() @@ -771,6 +771,7 @@ class SourceControlMixin(BaseTask): if os.path.exists(project_path): git_repo = git.Repo(project_path) if git_repo.head.is_detached: + is_commit = True original_branch = git_repo.head.commit else: original_branch = git_repo.active_branch @@ -782,7 +783,11 @@ class SourceControlMixin(BaseTask): # for git project syncs, non-default branches can be problems # restore to branch the repo was on before this run try: - original_branch.checkout() + if is_commit: + git_repo.head.set_commit(original_branch) + git_repo.head.reset(index=True, working_tree=True) + else: + original_branch.checkout() except Exception: # this could have failed due to dirty tree, but difficult to predict all cases logger.exception(f'Failed to restore project repo to prior state after {self.instance.id}') @@ -1581,7 +1586,7 @@ class RunInventoryUpdate(SourceControlMixin, BaseTask): if inventory_update.source == 'scm': if not source_project: raise RuntimeError('Could not find project to run SCM inventory update from.') - self.sync_and_copy(source_project, private_data_dir) + self.sync_and_copy(source_project, private_data_dir, scm_branch=inventory_update.inventory_source.scm_branch) else: # If source is not SCM make an empty project directory, content is built inside inventory folder super(RunInventoryUpdate, self).build_project_dir(inventory_update, private_data_dir) diff --git a/awx_collection/plugins/modules/inventory_source.py b/awx_collection/plugins/modules/inventory_source.py index f3000ee9ec..1e6939df3f 100644 --- a/awx_collection/plugins/modules/inventory_source.py +++ b/awx_collection/plugins/modules/inventory_source.py @@ -105,6 +105,11 @@ options: description: - Project to use as source with scm option type: str + scm_branch: + description: + - Inventory source SCM branch. + - Project must have branch override enabled. + type: str state: description: - Desired state of the resource. @@ -178,6 +183,7 @@ def main(): update_on_launch=dict(type='bool'), update_cache_timeout=dict(type='int'), source_project=dict(), + scm_branch=dict(type='str'), notification_templates_started=dict(type="list", elements='str'), notification_templates_success=dict(type="list", elements='str'), notification_templates_error=dict(type="list", elements='str'), @@ -272,6 +278,7 @@ def main(): 'enabled_var', 'enabled_value', 'host_filter', + 'scm_branch', ) # Layer in all remaining optional information diff --git a/awx_collection/test/awx/test_inventory_source.py b/awx_collection/test/awx/test_inventory_source.py index bebd3fc00b..c3123ae9e5 100644 --- a/awx_collection/test/awx/test_inventory_source.py +++ b/awx_collection/test/awx/test_inventory_source.py @@ -112,6 +112,7 @@ def test_falsy_value(run_module, admin_user, base_inventory): # credential ? ? o o r r r r r r r o # source_project ? ? r - - - - - - - - - # source_path ? ? r - - - - - - - - - +# scm_branch ? ? r - - - - - - - - - # verbosity ? ? o o o o o o o o o o # overwrite ? ? o o o o o o o o o o # overwrite_vars ? ? o o o o o o o o o o diff --git a/awxkit/awxkit/api/pages/inventory.py b/awxkit/awxkit/api/pages/inventory.py index 1ff9e02960..eeace96bd6 100644 --- a/awxkit/awxkit/api/pages/inventory.py +++ b/awxkit/awxkit/api/pages/inventory.py @@ -319,6 +319,7 @@ class InventorySource(HasCreate, HasNotifications, UnifiedJobTemplate): optional_fields = ( 'source_path', 'source_vars', + 'scm_branch', 'timeout', 'overwrite', 'overwrite_vars',