From 1b4bdfd6dfcb22fb5674688e4699abea6e1501fc Mon Sep 17 00:00:00 2001
From: Tomas Kopecek <tkopecek@redhat.com>
Date: Feb 03 2022 11:40:19 +0000
Subject: mock.module_setup_commands tag extra option


Fixes: https://pagure.io/koji/issue/2483

---

diff --git a/builder/kojid b/builder/kojid
index 35a7e52..ab4c0e0 100755
--- a/builder/kojid
+++ b/builder/kojid
@@ -299,6 +299,8 @@ class BuildRoot(object):
                     'opts': self.config['extra']['mock.plugin_conf.sign_opts.opts'],
                 }
             }
+        if 'mock.module_setup_commands' in self.config['extra']:
+            opts['module_setup_commands'] = self.config['extra']['mock.module_setup_commands']
         if self.internal_dev_setup is not None:
             opts['internal_dev_setup'] = bool(self.internal_dev_setup)
         opts['tag_macros'] = {}
diff --git a/cli/koji_cli/commands.py b/cli/koji_cli/commands.py
index 014a509..354cd1a 100644
--- a/cli/koji_cli/commands.py
+++ b/cli/koji_cli/commands.py
@@ -1147,6 +1147,8 @@ def anon_handle_mock_config(goptions, session, args):
         opts['bootstrap_image'] = buildcfg['extra']['mock.bootstrap_image']
     if 'mock.use_bootstrap' in buildcfg['extra']:
         opts['use_bootstrap'] = buildcfg['extra']['mock.use_bootstrap']
+    if 'mock.module_setup_commands' in buildcfg['extra']:
+        opts['module_setup_commands'] = buildcfg['extra']['mock.module_setup_commands']
     opts['tag_macros'] = {}
     for key in buildcfg['extra']:
         if key.startswith('rpm.macro.'):
@@ -5305,7 +5307,7 @@ def handle_edit_tag(goptions, session, args):
     parser.add_option("--no-include-all", action="store_true",
                       help="Do not include all packages in this tag when generating Maven repos")
     parser.add_option("-x", "--extra", action="append", default=[], metavar="key=value",
-                      help="Set tag extra option")
+                      help="Set tag extra option. JSON-encoded or simple value")
     parser.add_option("-r", "--remove-extra", action="append", default=[], metavar="key",
                       help="Remove tag extra option")
     parser.add_option("-b", "--block-extra", action="append", default=[], metavar="key",
@@ -5340,8 +5342,9 @@ def handle_edit_tag(goptions, session, args):
         extra = {}
         for xopt in options.extra:
             key, value = xopt.split('=', 1)
-            value = arg_filter(value)
-            extra[key] = value
+            if key in extra:
+                parser.error("Duplicate extra key: %s" % key)
+            extra[key] = arg_filter(value, parse_json=True)
         opts['extra'] = extra
     opts['remove_extra'] = options.remove_extra
     opts['block_extra'] = options.block_extra
diff --git a/cli/koji_cli/lib.py b/cli/koji_cli/lib.py
index 90a0a2a..31a2d87 100644
--- a/cli/koji_cli/lib.py
+++ b/cli/koji_cli/lib.py
@@ -2,6 +2,7 @@
 from __future__ import absolute_import, division
 
 import hashlib
+import json
 import optparse
 import os
 import random
@@ -9,7 +10,6 @@ import socket
 import string
 import sys
 import time
-import json
 from contextlib import closing
 from copy import copy
 
@@ -86,7 +86,7 @@ ARGMAP = {'None': None,
           'False': False}
 
 
-def arg_filter(arg):
+def arg_filter(arg, parse_json=False):
     try:
         return int(arg)
     except ValueError:
@@ -98,6 +98,11 @@ def arg_filter(arg):
     if arg in ARGMAP:
         return ARGMAP[arg]
     # handle lists/dicts?
+    if parse_json:
+        try:
+            return json.loads(arg)
+        except Exception:  # ValueError < 2.7, JSONDecodeError > 3.5
+            pass
     return arg
 
 
diff --git a/docs/source/using_the_koji_build_system.rst b/docs/source/using_the_koji_build_system.rst
index 6570c80..7d8887b 100644
--- a/docs/source/using_the_koji_build_system.rst
+++ b/docs/source/using_the_koji_build_system.rst
@@ -321,7 +321,7 @@ line tool will print a list of valid commands and each command supports
     cancel-task          Cancel a task
     help                 List available commands
     latest-build         Print the latest builds for a tag
-    [...] 
+    [...]
 
 ::
 
@@ -335,7 +335,7 @@ line tool will print a list of valid commands and each command supports
     --skip-tag            Do not attempt to tag package
     --scratch             Perform a scratch build
     --nowait              Don't wait on build
-    [...] 
+    [...]
 
 Using koji to generate a mock config to replicate a buildroot
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -428,6 +428,11 @@ environment follows:
 
   - this option will automatically turn ``mock.use_bootstrap`` (this is how
     it is implemented in mock)
+* ``mock.module_setup_commands`` - commands for configuring the modules active
+  in a buildroot. Available in `mock 2.4
+  <https://github.com/rpm-software-management/mock/wiki/Release-Notes-2.4>`__.
+* ``mock.yum.best`` - 0/1 value. If set yum/dnf will use highest available rpm
+  version (see man yum.conf)
 * ``mock.yum.module_hotfixes`` - 0/1 value. If set, yum/dnf will use packages
   regardless if they come from modularity repo or not. It makes sense only for
   tags with external repositories. (See dnf `docs
diff --git a/koji/__init__.py b/koji/__init__.py
index 0278e30..33071ef 100644
--- a/koji/__init__.py
+++ b/koji/__init__.py
@@ -1649,6 +1649,8 @@ def genMockConfig(name, arch, managed=False, repoid=None, tag_name=None, **opts)
         config_opts['bootstrap_image'] = opts['bootstrap_image']
     if 'use_bootstrap' in opts:
         config_opts['use_bootstrap'] = bool(opts['use_bootstrap'])
+    if 'module_setup_commands' in opts:
+        config_opts['module_setup_commands'] = opts['module_setup_commands']
 
     # bind_opts are used to mount parts (or all of) /dev if needed.
     # See kojid::LiveCDTask for a look at this option in action.
diff --git a/tests/test_cli/test_arg_filter.py b/tests/test_cli/test_arg_filter.py
new file mode 100644
index 0000000..7e9b75e
--- /dev/null
+++ b/tests/test_cli/test_arg_filter.py
@@ -0,0 +1,19 @@
+import unittest
+
+from koji_cli.lib import arg_filter
+
+class TestArgFilter(unittest.TestCase):
+    def test_valid_values(self):
+        for parse_json in (True, False):
+            self.assertEqual(arg_filter("1", parse_json=parse_json), 1)
+            self.assertEqual(arg_filter("1.123", parse_json=parse_json), 1.123)
+            self.assertEqual(arg_filter("True", parse_json=parse_json), True)
+            self.assertEqual(arg_filter("False", parse_json=parse_json), False)
+            self.assertEqual(arg_filter("None", parse_json=parse_json), None)
+
+        # non/json
+        self.assertEqual(arg_filter('{"a": 1}'), '{"a": 1}')
+        self.assertDictEqual(arg_filter('{"a": 1}', parse_json=True), {"a": 1})
+
+        # invalid json
+        self.assertEqual(arg_filter("{'a': 1}", parse_json=True), "{'a': 1}")
diff --git a/tests/test_cli/test_edit_tag.py b/tests/test_cli/test_edit_tag.py
index 41144c6..6fe5ce8 100644
--- a/tests/test_cli/test_edit_tag.py
+++ b/tests/test_cli/test_edit_tag.py
@@ -130,7 +130,7 @@ Options:
   --no-include-all      Do not include all packages in this tag when
                         generating Maven repos
   -x key=value, --extra=key=value
-                        Set tag extra option
+                        Set tag extra option. JSON-encoded or simple value
   -r key, --remove-extra=key
                         Remove tag extra option
   -b key, --block-extra=key

