#1525 Support more fine-grained bootstrap/image configuration
Merged 3 years ago by praiskup. Opened 3 years ago by praiskup.
Unknown source bootstrap-rfe  into  master

@@ -472,16 +472,6 @@

          rlRun "find . -type f | grep 'build.log'" 0

          cd - && rm -r $MYTMPDIR

  

-         # test use_bootstrap_container setting

-         rlRun "copr-cli create ${NAME_PREFIX}BootstrapProject --use-bootstrap on --chroot $CHROOT"

-         rlAssertEquals "" `curl --silent ${FRONTEND_URL}/api/coprs/${NAME_PREFIX}BootstrapProject/detail/ |jq '.detail.use_bootstrap_container'` true

-         rlRun -s "copr-cli build ${NAME_PREFIX}BootstrapProject $HELLO --nowait"

-         rlRun "parse_build_id"

-         rlRun "copr watch-build $BUILD_ID"

-         rlRun "curl $BACKEND_URL/results/${NAME_PREFIX}BootstrapProject/$CHROOT/`printf %08d $BUILD_ID`-hello/configs.tar.gz | tar xz -O '*configs/child.cfg' | grep \"config_opts\['use_bootstrap'\] = True\""

-         rlRun "copr-cli modify ${NAME_PREFIX}BootstrapProject --use-bootstrap off"

-         rlAssertEquals "" `curl --silent ${FRONTEND_URL}/api/coprs/${NAME_PREFIX}BootstrapProject/detail/ |jq '.detail.use_bootstrap_container'` false

- 

          ## test building in copr dirs

          rlRun "copr-cli create --chroot $CHROOT ${NAME_PREFIX}CoprDirTest"

          rlRun "copr-cli add-package-scm ${NAME_PREFIX}CoprDirTest --name example --clone-url $COPR_HELLO_GIT" 0

file modified
+1 -1
@@ -6,7 +6,7 @@

  %global with_python2 1

  %endif

  

- %global min_python_copr_version 1.105.1.dev

+ %global min_python_copr_version 1.105.2.dev

  

  Name:       copr-cli

  Version:    1.89

@@ -11,6 +11,21 @@

  include('/etc/mock/{{chroot}}.cfg')

  

  config_opts['root'] = '{{ rootdir }}'

+ 

+ {%- if bootstrap == "on" %}

+ config_opts['use_bootstrap'] = True

+ config_opts['use_bootstrap_image'] = False

+ {%- elif bootstrap == "off" %}

+ config_opts['use_bootstrap'] = False

+ config_opts['use_bootstrap_image'] = False

+ {%- elif bootstrap in ["image", "custom_image"] %}

+ config_opts['use_bootstrap'] = True

+ config_opts['use_bootstrap_image'] = True

+ {%- if bootstrap_image %}

+ config_opts['bootstrap_image'] = "{{ bootstrap_image }}"

+ {%- endif %}

+ {%- endif %}

+ 

  {%- if additional_packages %}

  config_opts['chroot_additional_packages'] = '

  {%- for pkg in additional_packages -%}

file modified
+64 -8
@@ -55,6 +55,14 @@

      None: None,

  }

  

+ BOOTSTRAP_MAP = {

+     "default": "default",

+     "on": "on",

+     "off": "off",

+     "image": "image",

+     None: "default",

+ }

+ 

  no_config_warning = """

  ================= WARNING: =======================

  File '{0}' is missing or incorrect.
@@ -414,7 +422,7 @@

              enable_net=ON_OFF_MAP[args.enable_net],

              persistent=args.persistent,

              auto_prune=ON_OFF_MAP[args.auto_prune],

-             use_bootstrap_container=ON_OFF_MAP[args.use_bootstrap_container],

+             bootstrap=BOOTSTRAP_MAP[args.bootstrap],

              delete_after_days=args.delete_after_days,

              multilib=ON_OFF_MAP[args.multilib],

              module_hotfixes=ON_OFF_MAP[args.module_hotfixes],
@@ -436,7 +444,7 @@

              unlisted_on_hp=ON_OFF_MAP[args.unlisted_on_hp],

              enable_net=ON_OFF_MAP[args.enable_net],

              auto_prune=ON_OFF_MAP[args.auto_prune],

-             use_bootstrap_container=ON_OFF_MAP[args.use_bootstrap_container],

+             bootstrap=BOOTSTRAP_MAP[args.bootstrap],

              chroots=args.chroots,

              delete_after_days=args.delete_after_days,

              multilib=ON_OFF_MAP[args.multilib],
@@ -589,11 +597,15 @@

  

          :param args: argparse arguments provided by the user

          """

+ 

+         if args.bootstrap_image:

+             args.bootstrap = 'image'

          owner, copr, chroot = self.parse_chroot_path(args.coprchroot)

-         project_chroot = self.client.project_chroot_proxy.edit(

+         self.client.project_chroot_proxy.edit(

              ownername=owner, projectname=copr, chrootname=chroot,

              comps=args.upload_comps, delete_comps=args.delete_comps,

-             additional_packages=args.packages, additional_repos=args.repos

+             additional_packages=args.packages, additional_repos=args.repos,

+             bootstrap=args.bootstrap, bootstrap_image=args.bootstrap_image,

          )

          print("Edit chroot operation was successful.")

  
@@ -937,8 +949,20 @@

      parser_create.add_argument("--auto-prune", choices=["on", "off"], default="on",

                                 help="If auto-deletion of project's obsoleted builds should be enabled (default is on).\

                                 This option can only be specified by a COPR admin.")

-     parser_create.add_argument("--use-bootstrap", choices=["on", "off"], dest="use_bootstrap_container",

-                                help="If mock bootstrap container is used to initialize the buildroot.")

+ 

+     parser_create.add_argument(

+         "--bootstrap",

+         choices=["default", "on", "off", "image"],

+         help=(

+             "Configure Mock's bootstrap feature (consult 'man mock' for more "

+             "info).  'on'/'off' enables/disables bootstrap.  The 'default' "

+             "variant uses pre-configured setup from mock-core-configs.  The "

+             "'image' variant enforces the bootstrap initialization from "

+             "the pre-configured container image (defined in "

+             "mock-core-configs.rpm)."

+         ),

+     )

+ 

      parser_create.add_argument("--delete-after-days", default=None, metavar='DAYS',

                                 help="Delete the project after the specfied period of time")

      parser_create.add_argument("--module-hotfixes", choices=["on", "off"], default="off",
@@ -974,8 +998,13 @@

      parser_modify.add_argument("--auto-prune", choices=["on", "off"],

                                 help="If auto-deletion of project's obsoleted builds should be enabled.\

                                 This option can only be specified by a COPR admin.")

-     parser_modify.add_argument("--use-bootstrap", choices=["on", "off"], dest="use_bootstrap_container",

-                                help="If mock bootstrap container is used to initialize the buildroot.")

+ 

+     parser_modify.add_argument(

+         "--bootstrap",

+         choices=["default", "on", "off", "image"],

+         help=("Configure Mock's bootstrap feature, "

+               "See 'create --help' for more info."))

+ 

      parser_modify.add_argument("--delete-after-days", default=None, metavar='DAYS',

                                 help=("Delete the project after the specfied "

                                       "period of time, empty or -1 disables, "
@@ -1109,11 +1138,22 @@

      parser_build_parent.add_argument("--background", dest="background", action="store_true", default=False,

                                       help="Mark the build as a background job. It will have lesser priority than regular builds.")

  

+     parser_build_parent.add_argument(

+         "--bootstrap",

+         choices=["unchanged", "default", "on", "off", "image"],

+         help=("Configure Mock's bootstrap feature, "

+               "default is 'unchanged' so the configuration from Copr project "

+               "and Copr chroot is used for this build. "

+               "The 'default' variant resets the project/chroot configuration "

+               "to the pre-configured setup from mock-core-configs. "

+               "See 'create --help' for more info."))

+ 

      # create the parser for the "build" (url/upload) command

      parser_build = subparsers.add_parser("build", parents=[parser_build_parent],

                                           help="Build packages to a specified copr")

      parser_build.add_argument("pkgs", nargs="+",

                                help="filename of SRPM or URL of packages to build")

+ 

      parser_build.set_defaults(func="action_build")

  

      # create the parser for the "buildpypi" command
@@ -1208,6 +1248,22 @@

                                        help="space separated string of package names to be added to buildroot")

      parser_edit_chroot.add_argument("--repos",

                                        help="space separated string of additional repo urls for chroot")

+ 

+     parser_edit_chroot.add_argument(

+         "--bootstrap",

+         choices=["unchanged", "default", "on", "off", "image"],

+         help=("Configure Mock's bootstrap feature, "

+               "default is 'unchanged' so the configuration from Copr project "

+               "is used for this chroot. "

+               "The 'default' variant resets the project configuration "

+               "to the pre-configured setup from mock-core-configs. "

+               "See 'create --help' for more info."))

+ 

+     parser_edit_chroot.add_argument(

+         "--bootstrap-image",

+         help=("Use a custom container image for initializing Mock's "

+               "bootstrap (Implies --bootstrap=image)"))

+ 

      parser_edit_chroot.set_defaults(func="action_edit_chroot")

  

      parser_get_chroot = subparsers.add_parser("get-chroot", help="Get chroot of a project")

file modified
+2 -2
@@ -370,7 +370,7 @@

          "instructions": "instruction string", "chroots": ["f20", "f21"],

          "additional_repos": ["repo1", "repo2"],

          "unlisted_on_hp": None, "devel_mode": None, "enable_net": False,

-         "use_bootstrap_container": None,

+         "bootstrap": "default",

          "delete_after_days": None,

          "multilib": False,

          "module_hotfixes": False,
@@ -401,7 +401,7 @@

          "chroots": ["fedora-rawhide-x86_64", "fedora-rawhide-i386"],

          "additional_repos": ["repo1", "repo2"],

          "unlisted_on_hp": None, "devel_mode": None, "enable_net": False,

-         "use_bootstrap_container": None,

+         'bootstrap': 'default',

          "delete_after_days": None,

          "multilib": True,

          "module_hotfixes": False,

@@ -72,6 +72,7 @@

  BuildRequires: python3-click

  BuildRequires: python3-CommonMark

  BuildRequires: python3-blinker

+ BuildRequires: python3-beautifulsoup4

  BuildRequires: python3-copr-common >= 0.7

  BuildRequires: python3-dateutil

  BuildRequires: python3-decorator

@@ -0,0 +1,68 @@

+ """

+ add bootstrap-config columns

+ 

+ Revision ID: 63db6872060f

+ Revises: de903581465c

+ Create Date: 2020-06-29 08:59:07.525039

+ """

+ 

+ import sqlalchemy as sa

+ from alembic import op

+ 

+ 

+ revision = '63db6872060f'

+ down_revision = 'de903581465c'

+ 

+ def upgrade():

+     op.add_column('copr', sa.Column('bootstrap', sa.Text()))

+     op.add_column('copr_chroot', sa.Column('bootstrap', sa.Text()))

+     op.add_column('copr_chroot', sa.Column('bootstrap_image', sa.Text()))

+     op.add_column('build', sa.Column('bootstrap', sa.Text()))

+ 

+ 

+     op.execute("""

+     UPDATE

+         copr

+     SET

+         bootstrap = 'on'

+     WHERE

+         use_bootstrap_container = true

+     """)

+ 

+     op.execute("""

+     UPDATE

+         copr

+     SET

+         bootstrap = 'off'

+     WHERE

+         use_bootstrap_container = false

+     """)

+ 

+     op.drop_column('copr', 'use_bootstrap_container')

+ 

+ def downgrade():

+     op.add_column('copr', sa.Column('use_bootstrap_container', sa.Boolean(),

+                                     nullable=False, server_default='f'))

+ 

+     op.execute("""

+     UPDATE

+         copr

+     SET

+         use_bootstrap_container = true

+     WHERE

+         bootstrap = 'on'

+     """)

+ 

+     op.execute("""

+     UPDATE

+         copr

+     SET

+         use_bootstrap_container = false

+     WHERE

+         bootstrap = 'off'

+     """)

+ 

+     op.drop_column('copr', 'bootstrap')

+     op.drop_column('copr_chroot', 'bootstrap')

+     op.drop_column('copr_chroot', 'bootstrap_image')

+     op.drop_column('build', 'bootstrap')

@@ -59,6 +59,61 @@

          raise exceptions.UnknownSourceTypeException("Invalid source type")

  

  

+ def create_mock_bootstrap_field(level):

+     """

+     Select-box for the bootstrap configuration in chroot/project form

+     """

+ 

+     choices = []

+     default_choices = [

+         ('default', 'Use default configuration from mock-core-configs.rpm'),

+         ('off', 'Disable'),

+         ('on', 'Enable'),

+         ('image', 'Initialize by default pre-configured container image'),

+     ]

+ 

+     if level == 'chroot':

+         choices.append(("unchanged", "Use project settings"))

+         choices.extend(default_choices)

+         choices.append(('custom_image',

+                         'Initialize by custom bootstrap image (specified '

+                         'in the "Mock bootstrap image" field below)'))

+ 

+     elif level == 'build':

+         choices.append(("unchanged", "Use project/chroot settings"))

+         choices.extend(default_choices)

+ 

+     else:

+         choices.extend(default_choices)

+ 

+     return wtforms.SelectField(

+         "Mock bootstrap",

+         choices=choices,

+         validators=[wtforms.validators.Optional()],

+         # Replace "None" with None (needed on Fedora <= 32)

+         filters=[NoneFilter(None)],

+     )

+ 

+ 

+ def create_mock_bootstrap_image_field():

I think these create_*_field() functions could probably be done better with custom fields
https://wtforms.readthedocs.io/en/2.3.x/fields/#custom-fields
but this works too.

+     """

+     Mandatory bootstrap-image field when the bootstrap select-box is set to a

+     custom image option.

+     """

+     return wtforms.TextField(

+         "Mock bootstrap image",

+         validators=[

+             wtforms.validators.Optional(),

+             wtforms.validators.Regexp(

+                 r"^\w+(:\w+)?$",

+                 message=("Enter valid bootstrap image id "

+                          "(<name>[:<tag>], e.g. fedora:33)."))],

+         filters=[

+             lambda x: None if not x else x

+         ],

+     )

+ 

+ 

  class MultiCheckboxField(wtforms.SelectMultipleField):

      widget = wtforms.widgets.ListWidget(prefix_label=False)

      option_widget = wtforms.widgets.CheckboxInput()
@@ -277,6 +332,12 @@

              return helpers.PermissionEnum("request")

          return helpers.PermissionEnum("nothing")

  

+ def _optional_checkbox_filter(data):

+     if data in [True, 'true']:

+         return True

+     if data in [False, 'false']:

+         return False

+     return None

  

  class CoprFormFactory(object):

  
@@ -367,14 +428,12 @@

                      newer build (with respect to package version) and it is

                      older than 14 days""")

  

-             use_bootstrap_container = wtforms.BooleanField(

-                     "Enable mock's use_bootstrap_container experimental feature",

-                     description="""This will make the build slower but it has an

-                     advantage that the dnf _from_ the given chroot will be used

-                     to setup the chroot (otherwise host system dnf and rpm is

-                     used)""",

-                     default=False,

-                     false_values=FALSE_VALUES)

+             use_bootstrap_container = wtforms.StringField(

+                 "backward-compat-only: old bootstrap",

+                 validators=[wtforms.validators.Optional()],

+                 filters=[_optional_checkbox_filter])

+ 

+             bootstrap = create_mock_bootstrap_field("project")

  

              follow_fedora_branching = wtforms.BooleanField(

                      "Follow Fedora branching",
@@ -534,6 +593,7 @@

              enable_net = wtforms.BooleanField(false_values=FALSE_VALUES)

              background = wtforms.BooleanField(false_values=FALSE_VALUES)

              project_dirname = wtforms.StringField(default=None)

+             bootstrap = create_mock_bootstrap_field("build")

  

          F.chroots_list = list(map(lambda x: x.name, active_chroots))

          F.chroots_list.sort()
@@ -1046,6 +1106,7 @@

          F.enable_net = wtforms.BooleanField(false_values=FALSE_VALUES)

          F.background = wtforms.BooleanField(default=False, false_values=FALSE_VALUES)

          F.project_dirname = wtforms.StringField(default=None)

+         F.bootstrap = create_mock_bootstrap_field("build")

  

          # Overrides BasePackageForm.package_name, it is usually unused for

          # building
@@ -1199,6 +1260,20 @@

      with_opts = wtforms.StringField("With options")

      without_opts = wtforms.StringField("Without options")

  

+     bootstrap = create_mock_bootstrap_field("chroot")

+     bootstrap_image = create_mock_bootstrap_image_field()

+ 

+     def validate(self, *args, **kwargs):  # pylint: disable=signature-differs

+         """ We need to special-case custom_image configuration """

+         result = super().validate(*args, **kwargs)

+         if self.bootstrap.data != "custom_image":

+             return result

+         if not self.bootstrap_image.data:

+             self.bootstrap_image.errors.append(

+                 "Custom image is selected, but not specified")

+             return False

+         return result

+ 

  

  class CoprChrootExtend(FlaskForm):

      extend = wtforms.StringField("Chroot name")
@@ -1288,6 +1363,7 @@

      disable_createrepo = wtforms.BooleanField(validators=[wtforms.validators.Optional()], false_values=FALSE_VALUES)

      unlisted_on_hp = wtforms.BooleanField(validators=[wtforms.validators.Optional()], false_values=FALSE_VALUES)

      auto_prune = wtforms.BooleanField(validators=[wtforms.validators.Optional()], false_values=FALSE_VALUES)

+     bootstrap = create_mock_bootstrap_field("project")

      use_bootstrap_container = wtforms.BooleanField(validators=[wtforms.validators.Optional()], false_values=FALSE_VALUES)

      follow_fedora_branching = wtforms.BooleanField(validators=[wtforms.validators.Optional()], false_values=FALSE_VALUES)

      follow_fedora_branching = wtforms.BooleanField(default=True, false_values=FALSE_VALUES)

@@ -598,6 +598,7 @@

              batch=batch,

              srpm_url=srpm_url,

              copr_dirname=copr_dirname,

+             bootstrap=build_options.get("bootstrap"),

          )

  

          if "timeout" in build_options:
@@ -609,7 +610,7 @@

      def add(cls, user, pkgs, copr, source_type=None, source_json=None,

              repos=None, chroots=None, timeout=None, enable_net=True,

              git_hashes=None, skip_import=False, background=False, batch=None,

-             srpm_url=None, copr_dirname=None):

+             srpm_url=None, copr_dirname=None, bootstrap=None):

  

          if chroots is None:

              chroots = []
@@ -660,6 +661,7 @@

              batch=batch,

              srpm_url=srpm_url,

              copr_dir=copr_dir,

+             bootstrap=bootstrap,

          )

  

          if timeout:

@@ -388,6 +388,7 @@

          for i in copr.copr_chroots:

              if i.mock_chroot.name == chroot_id:

                  chroot = i

+                 break

          if not chroot:

              return {}

  
@@ -412,15 +413,35 @@

          repos.extend(cls.get_additional_repo_views(copr.repos_list, chroot_id))

          repos.extend(cls.get_additional_repo_views(chroot.repos_list, chroot_id))

  

-         return {

+         config_dict = {

              'project_id': copr.repo_id,

              'additional_packages': packages.split(),

              'repos': repos,

              'chroot': chroot_id,

-             'use_bootstrap_container': copr.use_bootstrap_container,

              'with_opts': chroot.with_opts.split(),

              'without_opts': chroot.without_opts.split(),

          }

+         config_dict.update(chroot.bootstrap_setup)

+         return config_dict

+ 

+     @classmethod

+     def build_bootstrap_setup(cls, build_config, build):

+         """ Get bootstrap setup from build_config, and override it by build """

+         build_record = {}

+         build_record["bootstrap"] = build_config.get("bootstrap", "default")

+         build_record["bootstrap_image"] = build_config.get("bootstrap_image")

+ 

+         # config overrides per-build

+         if build.bootstrap_set:

+             build_record["bootstrap"] = build.bootstrap

+ 

+         # drop unnecessary (default) fields

+         if build_record["bootstrap"] == "default":

+             del build_record['bootstrap']

+             del build_record['bootstrap_image']

+         elif build_record["bootstrap"] != "custom_image":

+             del build_record['bootstrap_image']

+         return build_record

  

      @classmethod

      def get_additional_repo_views(cls, repos_list, chroot_id):

@@ -218,7 +218,8 @@

      @classmethod

      def add(cls, user, name, selected_chroots, repos=None, description=None,

              instructions=None, check_for_duplicates=False, group=None, persistent=False,

-             auto_prune=True, use_bootstrap_container=False, follow_fedora_branching=False, **kwargs):

+             auto_prune=True, bootstrap=None, follow_fedora_branching=False,

+             **kwargs):

  

          if not flask.g.user.admin and flask.g.user != user:

              msg = ("You were authorized as '{0}' user without permissions to access "
@@ -242,7 +243,7 @@

                             created_on=int(time.time()),

                             persistent=persistent,

                             auto_prune=auto_prune,

-                            use_bootstrap_container=use_bootstrap_container,

+                            bootstrap=bootstrap,

                             follow_fedora_branching=follow_fedora_branching,

                             **kwargs)

  
@@ -652,7 +653,8 @@

      @classmethod

      def create_chroot(cls, user, copr, mock_chroot, buildroot_pkgs=None, repos=None, comps=None, comps_name=None,

                        with_opts="", without_opts="",

-                       delete_after=None, delete_notify=None, module_toggle=""):

+                       delete_after=None, delete_notify=None, module_toggle="",

+                       bootstrap=None, bootstrap_image=None):

          """

          :type user: models.User

          :type mock_chroot: models.MockChroot
@@ -667,7 +669,8 @@

  

          chroot = models.CoprChroot(copr=copr, mock_chroot=mock_chroot)

          cls._update_chroot(buildroot_pkgs, repos, comps, comps_name, chroot,

-                            with_opts, without_opts, delete_after, delete_notify, module_toggle)

+                            with_opts, without_opts, delete_after, delete_notify,

+                            module_toggle, bootstrap, bootstrap_image)

  

          # reassign old build_chroots, if the chroot is re-created

          get_old = logic.builds_logic.BuildChrootsLogic.by_copr_and_mock_chroot
@@ -678,7 +681,8 @@

  

      @classmethod

      def update_chroot(cls, user, copr_chroot, buildroot_pkgs=None, repos=None, comps=None, comps_name=None,

-                       with_opts="", without_opts="", delete_after=None, delete_notify=None, module_toggle=""):

+                       with_opts="", without_opts="", delete_after=None, delete_notify=None, module_toggle="",

+                       bootstrap=None, bootstrap_image=None):

          """

          :type user: models.User

          :type copr_chroot: models.CoprChroot
@@ -688,12 +692,14 @@

              "Only owners and admins may update their projects.")

  

          cls._update_chroot(buildroot_pkgs, repos, comps, comps_name,

-                            copr_chroot, with_opts, without_opts, delete_after, delete_notify, module_toggle)

+                            copr_chroot, with_opts, without_opts, delete_after, delete_notify, module_toggle,

+                            bootstrap, bootstrap_image)

          return copr_chroot

  

      @classmethod

      def _update_chroot(cls, buildroot_pkgs, repos, comps, comps_name,

-                        copr_chroot, with_opts, without_opts, delete_after, delete_notify, module_toggle):

+                        copr_chroot, with_opts, without_opts, delete_after, delete_notify, module_toggle,

+                        bootstrap, bootstrap_image):

          if buildroot_pkgs is not None:

              copr_chroot.buildroot_pkgs = buildroot_pkgs

  
@@ -720,6 +726,16 @@

          if module_toggle is not None:

              copr_chroot.module_toggle = module_toggle

  

+         if bootstrap is not None:

+             copr_chroot.bootstrap = bootstrap

+ 

+         if bootstrap_image is not None:

+             # By CLI/API we can set custom_image, and keep bootstrap unset.  In

+             # such case set also bootstrap to correct value.

+             if not bootstrap:

+                 copr_chroot.bootstrap = 'custom_image'

+             copr_chroot.bootstrap_image = bootstrap_image

+ 

          db.session.add(copr_chroot)

  

      @classmethod

@@ -296,8 +296,7 @@

      # if backend deletion script should be run for the project's builds

      auto_prune = db.Column(db.Boolean, default=True, nullable=False, server_default="1")

  

-     # use mock's bootstrap container feature

-     use_bootstrap_container = db.Column(db.Boolean, default=False, nullable=False, server_default="0")

+     bootstrap = db.Column(db.Text, default="default")

  

      # if chroots for the new branch should be auto-enabled and populated from rawhide ones

      follow_fedora_branching = db.Column(db.Boolean, default=True, nullable=False, server_default="1")
@@ -921,6 +920,8 @@

      source_status = db.Column(db.Integer, default=StatusEnum("waiting"))

      srpm_url = db.Column(db.Text)

  

+     bootstrap = db.Column(db.Text)

+ 

      # relations

      user_id = db.Column(db.Integer, db.ForeignKey("user.id"), index=True)

      user = db.relationship("User", backref=db.backref("builds"))
@@ -1315,6 +1316,13 @@

      def source_is_uploaded(self):

          return self.source_type == helpers.BuildSourceEnum('upload')

  

+     @property

+     def bootstrap_set(self):

+         """ Is bootstrap config from project/chroot overwritten by build? """

+         if not self.bootstrap:

+             return False

+         return self.bootstrap != "unchanged"

+ 

  

  class DistGitBranch(db.Model, helpers.Serializer):

      """
@@ -1452,6 +1460,9 @@

      delete_after = db.Column(db.DateTime, index=True)

      delete_notify = db.Column(db.DateTime, index=True)

  

+     bootstrap = db.Column(db.Text)

+     bootstrap_image = db.Column(db.Text)

+ 

      def update_comps(self, comps_xml):

          if isinstance(comps_xml, str):

              data = comps_xml.encode("utf-8")
@@ -1513,6 +1524,20 @@

          d["mock_chroot"] = self.mock_chroot.name

          return d

  

+     @property

+     def bootstrap_setup(self):

+         """ Get Copr+CoprChroot consolidated bootstrap configuration """

+         settings = {}

+         settings['bootstrap'] = self.copr.bootstrap

+ 

+         if self.bootstrap and self.bootstrap != 'unchanged':

+             # overwrite project default with chroot config

+             settings['bootstrap'] = self.bootstrap

+             if settings['bootstrap'] == 'custom_image':

+                 settings['bootstrap_image'] = self.bootstrap_image

+         if settings['bootstrap'] in [None, "default"]:

+             return {}

+         return settings

  

  class BuildChroot(db.Model, helpers.Serializer):

      """

@@ -546,11 +546,35 @@

  </table>

  {% endmacro %}

  

+ {% macro render_bootstrap_options(form, build=False) %}

+ {{ render_field(form.bootstrap, placeholder='default') }}

+ {% if form.bootstrap_image %}

+ <div id="bootstrap_image_wrapper">

+ {{ render_field(form.bootstrap_image, placeholder='Used when "custom image" is set in Mock Bootstrap. Enter in <distribution>:<version> format (e.g. fedora:32)') }}

+ </div>

+ <script>

+   $(document).ready(function() {

+     if ($("#bootstrap").val() != "custom_image") {

+       $("#bootstrap_image_wrapper").hide();

+     }

+     $("#bootstrap").change(function() {

+       if (this.value == "custom_image") {

+         $("#bootstrap_image_wrapper").show();

+       }

+       else {

+         $("#bootstrap_image_wrapper").hide();

+       }

+     });

+   });

+ </script>

+ {% endif %}

+ {% endmacro %}

+ 

  

  {% macro render_additional_build_options(form, copr) %}

    <div class="form-group">

-     <label class="col-sm-2 control-label" for="textInput-markup" style="text-align: left">

-       Chroots

+     <label class="col-sm-2 control-label" for="textInput-markup">

+       Chroots:

      </label>

      <div class="col-sm-10" id="chroots">

      {% for group_set, chs in form.chroots_sets.items() %}
@@ -569,8 +593,8 @@

    </div>

    {{ form.csrf_token }}

    <div class="form-group">

-     <label class="col-sm-2 control-label" for="textInput-markup" style="text-align: left">

-       Timeout

+     <label class="col-sm-2 control-label" for="textInput-markup" >

+       Timeout:

      </label>

      <div class="col-sm-6">

        <input id="timeout" class="form-control" name="timeout" type="text" value="{{ form.timeout.default }}" >
@@ -579,9 +603,12 @@

        </li>

      </div>

    </div>

+ 

+   {{ render_field(form.bootstrap, placeholder='default') }}

+ 

    <div class="form-group">

-     <label class="col-sm-2 control-label" for="textInput-markup" style="text-align: left">

-     Other Options

+     <label class="col-sm-2 control-label" for="textInput-markup">

+     Other options:

      </label>

      <div class="col-sm-10">

        <div class="checkbox">

@@ -1,4 +1,6 @@

- {% from "_helpers.html" import render_field, render_checkbox_field, render_form_errors, copr_details_href, copr_url %}

+ {% from "_helpers.html" import render_field, render_checkbox_field,

+ render_form_errors, copr_details_href, copr_url, render_dropdown_field,

+ render_bootstrap_options %}

  

  {% macro copr_form(form, view, copr = None, username = None, group = None, comments = None) %}

    {# if using for updating, we need to pass name to url_for, but otherwise we need to pass nothing #}
@@ -142,7 +144,6 @@

          [form.auto_prune, g.user.admin],

          [form.unlisted_on_hp],

          [form.persistent, g.user.admin],

-         [form.use_bootstrap_container],

          [form.follow_fedora_branching],

          [form.multilib],

          [form.module_hotfixes],
@@ -153,6 +154,9 @@

              placeholder='Optional',

              info='Delete the project after the specfied period of time (empty = disabled)') }}

  

+ 

+     {{ render_bootstrap_options(form) }}

+ 

      {{ render_field(form.runtime_dependencies, rows=5, cols=50, placeholder='Optional - URL to additional yum repos, which can be used as runtime dependencies. Space separated. This should be baseurl from .repo file. E.g.: http://copr-be.cloud.fedoraproject.org/results/rhughes/f20-gnome-3-12/fedora-$releasever-$basearch/') }}

      </div>

    </div>

@@ -1,5 +1,6 @@

  {% extends "coprs/detail.html" %}

- {% from "_helpers.html" import render_field, copr_url, copr_name %}

+ {% from "_helpers.html" import render_field, copr_url, copr_name,

+     render_bootstrap_options %}

  

  {% block title %}Editing {{ copr_name(copr) }}/{{ chroot.name }}{% endblock %}

  {%block project_breadcrumb%}
@@ -56,6 +57,8 @@

         )

      }}

  

+     {{ render_bootstrap_options(form) }}

+ 

      <div class="form-group">

        <label class="col-sm-2 control-label" for="textInput-markup">

          comps.xml

@@ -139,9 +139,14 @@

          if "auto_prune" in flask.request.form:

              auto_prune = form.auto_prune.data

  

-         use_bootstrap_container = True

+         # This is old route (apiv1) which nobody should use, so we don't

+         # implement the 'bootstrap' and 'bootstrap_image' options here.

+         use_bootstrap_container = None

          if "use_bootstrap_container" in flask.request.form:

              use_bootstrap_container = form.use_bootstrap_container.data

+         bootstrap = None

+         if use_bootstrap_container is not None:

+             bootstrap = "on" if use_bootstrap_container else "off"

  

          try:

              copr = CoprsLogic.add(
@@ -158,7 +163,7 @@

                  group=group,

                  persistent=form.persistent.data,

                  auto_prune=auto_prune,

-                 use_bootstrap_container=use_bootstrap_container,

+                 bootstrap=bootstrap,

              )

              infos.append("New project was successfully created.")

  
@@ -344,7 +349,7 @@

          "persistent": copr.persistent,

          "unlisted_on_hp": copr.unlisted_on_hp,

          "auto_prune": copr.auto_prune,

-         "use_bootstrap_container": copr.use_bootstrap_container,

+         "use_bootstrap_container": copr.bootstrap == "on",

      }

      return flask.jsonify(output)

  
@@ -522,6 +527,10 @@

  @api_req_with_copr

  def copr_new_build_scm(copr):

      form = forms.BuildFormScmFactory(copr.active_chroots)(meta={'csrf': False})

+     # We just want 'disable=duplicate-code' because this is a C&P with APIv3,

+     # though it doesn't actually work with PyLint, see

+     # https://github.com/PyCQA/pylint/issues/214

+     # pylint: disable=all

  

      def create_new_build():

          return BuildsLogic.create_new_from_scm(
@@ -697,7 +706,7 @@

      if "auto_prune" in flask.request.form:

          copr.auto_prune = form.auto_prune.data

      if "use_bootstrap_container" in flask.request.form:

-         copr.use_bootstrap_container = form.use_bootstrap_container.data

+         copr.bootstrap = "on" if form.use_bootstrap_container.data else "off"

      if "chroots" in  flask.request.form:

          coprs_logic.CoprChrootsLogic.update_from_names(

              flask.g.user, copr, form.chroots.data)

@@ -20,11 +20,10 @@

  def build_config(build_chroot):

      config = BuildConfigLogic.generate_build_config(build_chroot.build.copr, build_chroot.name)

      copr_chroot = CoprChrootsLogic.get_by_name_safe(build_chroot.build.copr, build_chroot.name)

-     return {

+     dict_data = {

          "repos": config.get("repos"),

          "additional_repos": BuildConfigLogic.generate_additional_repos(copr_chroot),

          "additional_packages": config.get("additional_packages"),

-         "use_bootstrap_container": config.get("use_bootstrap_container"),

          "with_opts": config.get("with_opts"),

          "without_opts": config.get("without_opts"),

          "memory_limit": build_chroot.build.memory_reqs,
@@ -32,6 +31,9 @@

          "enable_net": build_chroot.build.enable_net,

          "is_background": build_chroot.build.is_background,

      }

+     dict_data.update(

+         BuildConfigLogic.build_bootstrap_setup(config, build_chroot.build))

+     return dict_data

  

  

  @apiv3_ns.route("/build-chroot/<int:build_id>/<chrootname>", methods=GET)

@@ -130,16 +130,13 @@

      data = get_form_compatible_data()

      form = forms.BuildFormUrlFactory(copr.active_chroots)(data, meta={'csrf': False})

  

-     def create_new_build():

+     def create_new_build(options):

          # create separate build for each package

          pkgs = form.pkgs.data.split("\n")

          return [BuildsLogic.create_new_from_url(

              flask.g.user, copr,

              url=pkg,

-             chroot_names=form.selected_chroots,

-             background=form.background.data,

-             copr_dirname=form.project_dirname.data,

-             timeout=form.timeout.data,

+             **options,

          ) for pkg in pkgs]

      return process_creating_new_build(copr, form, create_new_build)

  
@@ -152,15 +149,12 @@

      data = get_form_compatible_data()

      form = forms.BuildFormUploadFactory(copr.active_chroots)(data, meta={'csrf': False})

  

-     def create_new_build():

+     def create_new_build(options):

          return BuildsLogic.create_new_from_upload(

              flask.g.user, copr,

              f_uploader=lambda path: form.pkgs.data.save(path),

              orig_filename=secure_filename(form.pkgs.data.filename),

-             chroot_names=form.selected_chroots,

-             background=form.background.data,

-             copr_dirname=form.project_dirname.data,

-             timeout=form.timeout.data,

+             **options,

          )

      return process_creating_new_build(copr, form, create_new_build)

  
@@ -172,7 +166,7 @@

      data = rename_fields(get_form_compatible_data())

      form = forms.BuildFormScmFactory(copr.active_chroots)(data, meta={'csrf': False})

  

-     def create_new_build():

+     def create_new_build(options):

          return BuildsLogic.create_new_from_scm(

              flask.g.user,

              copr,
@@ -182,10 +176,7 @@

              subdirectory=form.subdirectory.data,

              spec=form.spec.data,

              srpm_build_method=form.srpm_build_method.data,

-             chroot_names=form.selected_chroots,

-             background=form.background.data,

-             copr_dirname=form.project_dirname.data,

-             timeout=form.timeout.data,

+             **options,

          )

      return process_creating_new_build(copr, form, create_new_build)

  
@@ -200,7 +191,7 @@

      # pylint: disable=not-callable

      form = forms.BuildFormDistGitSimpleFactory(copr.active_chroots)(data, meta={'csrf': False})

  

-     def create_new_build():

+     def create_new_build(options):

          return BuildsLogic.create_new_from_distgit(

              flask.g.user,

              copr,
@@ -208,9 +199,7 @@

              distgit_name=form.distgit.data,

              distgit_namespace=form.namespace.data,

              committish=form.committish.data,

-             chroot_names=form.selected_chroots,

-             copr_dirname=form.project_dirname.data,

-             background=form.background.data,

+             **options,

          )

      return process_creating_new_build(copr, form, create_new_build)

  
@@ -225,7 +214,7 @@

      if not form.python_versions.data:

          form.python_versions.data = form.python_versions.default

  

-     def create_new_build():

+     def create_new_build(options):

          return BuildsLogic.create_new_from_pypi(

              flask.g.user,

              copr,
@@ -233,10 +222,7 @@

              form.pypi_package_version.data,

              form.spec_template.data,

              form.python_versions.data,

-             form.selected_chroots,

-             background=form.background.data,

-             copr_dirname=form.project_dirname.data,

-             timeout=form.timeout.data,

+             **options,

          )

      return process_creating_new_build(copr, form, create_new_build)

  
@@ -248,15 +234,12 @@

      data = get_form_compatible_data()

      form = forms.BuildFormRubyGemsFactory(copr.active_chroots)(data, meta={'csrf': False})

  

-     def create_new_build():

+     def create_new_build(options):

          return BuildsLogic.create_new_from_rubygems(

              flask.g.user,

              copr,

              form.gem_name.data,

-             form.selected_chroots,

-             background=form.background.data,

-             copr_dirname=form.project_dirname.data,

-             timeout=form.timeout.data,

+             **options,

          )

      return process_creating_new_build(copr, form, create_new_build)

  
@@ -268,7 +251,7 @@

      data = get_form_compatible_data()

      form = forms.BuildFormCustomFactory(copr.active_chroots)(data, meta={'csrf': False})

  

-     def create_new_build():

+     def create_new_build(options):

          return BuildsLogic.create_new_from_custom(

              flask.g.user,

              copr,
@@ -276,10 +259,7 @@

              form.chroot.data,

              form.builddeps.data,

              form.resultdir.data,

-             chroot_names=form.selected_chroots,

-             background=form.background.data,

-             copr_dirname=form.project_dirname.data,

-             timeout=form.timeout.data,

+             **options,

          )

      return process_creating_new_build(copr, form, create_new_build)

  
@@ -292,9 +272,17 @@

          raise AccessRestricted("User {} is not allowed to build in the copr: {}"

                                 .format(flask.g.user.username, copr.full_name))

  

+     generic_build_options = {

+         'chroot_names': form.selected_chroots,

+         'background': form.background.data,

+         'copr_dirname': form.project_dirname.data,

+         'timeout': form.timeout.data,

+         'bootstrap': form.bootstrap.data,

+     }

+ 

      # From URLs it can be created multiple builds at once

      # so it can return a list

-     build = create_new_build()

+     build = create_new_build(generic_build_options)

      db.session.commit()

  

      if type(build) == list:

@@ -25,16 +25,19 @@

  

  def to_build_config_dict(project_chroot):

      config = BuildConfigLogic.generate_build_config(project_chroot.copr, project_chroot.name)

-     return {

+     config_dict = {

          "chroot": project_chroot.name,

          "repos": config["repos"],

          "additional_repos": BuildConfigLogic.generate_additional_repos(project_chroot),

          "additional_packages": (project_chroot.buildroot_pkgs or "").split(),

-         "use_bootstrap_container": project_chroot.copr.use_bootstrap_container,

          "enable_net": project_chroot.copr.enable_net,

          "with_opts":  str_to_list(project_chroot.with_opts),

          "without_opts": str_to_list(project_chroot.without_opts),

      }

+     for option in ['bootstrap', 'bootstrap_image']:

+         if option in config:

+             config_dict[option] = config[option]

+     return config_dict

  

  

  def rename_fields(input):
@@ -101,6 +104,8 @@

          CoprChrootsLogic.remove_comps(flask.g.user, chroot)

      CoprChrootsLogic.update_chroot(

          flask.g.user, chroot, buildroot_pkgs, repos, comps=comps_xml, comps_name=comps_name,

-         with_opts=with_opts, without_opts=without_opts)

+         with_opts=with_opts, without_opts=without_opts,

+         bootstrap=form.bootstrap.data,

+         bootstrap_image=form.bootstrap_image.data)

      db.session.commit()

      return flask.jsonify(to_dict(chroot))

@@ -29,7 +29,7 @@

          "chroot_repos": CoprsLogic.get_yum_repos(copr, empty=True),

          "additional_repos": copr.repos_list,

          "enable_net": copr.build_enable_net,

-         "use_bootstrap_container": copr.use_bootstrap_container,

+         "bootstrap": copr.bootstrap,

          "module_hotfixes": copr.module_hotfixes,

      }

  
@@ -127,6 +127,14 @@

          raise BadRequest(form.errors)

      validate_chroots(get_input_dict(), MockChrootsLogic.get_multiple())

  

+     bootstrap = None

+     # backward compatibility

+     use_bootstrap_container = form.use_bootstrap_container.data

+     if use_bootstrap_container is not None:

+         bootstrap = "on" if use_bootstrap_container else "off"

+     if form.bootstrap.data is not None:

+         bootstrap = form.bootstrap.data

+ 

      try:

          copr = CoprsLogic.add(

              name=form.name.data.strip(),
@@ -141,7 +149,7 @@

              group=group,

              persistent=form.persistent.data,

              auto_prune=form.auto_prune.data,

-             use_bootstrap_container=form.use_bootstrap_container.data,

+             bootstrap=bootstrap,

              homepage=form.homepage.data,

              contact=form.contact.data,

              disable_createrepo=form.disable_createrepo.data,

@@ -125,10 +125,13 @@

          build_config = BuildConfigLogic.generate_build_config(task.build.copr, task.mock_chroot.name)

          build_record["repos"] = build_config.get("repos")

          build_record["buildroot_pkgs"] = build_config.get("additional_packages")

-         build_record["use_bootstrap_container"] = build_config.get("use_bootstrap_container")

          build_record["with_opts"] = build_config.get("with_opts")

          build_record["without_opts"] = build_config.get("without_opts")

  

+         bch_bootstrap = BuildConfigLogic.build_bootstrap_setup(

+             build_config, task.build)

+         build_record.update(bch_bootstrap)

+ 

      except Exception as err:

          app.logger.exception(err)

          return None

@@ -119,6 +119,7 @@

          build_options = {

              "enable_net": form.enable_net.data,

              "timeout": form.timeout.data,

+             "bootstrap": form.bootstrap.data,

          }

  

          try:

@@ -19,26 +19,28 @@

  @login_required

  @req_with_copr

  def chroot_edit(copr, chrootname):

-     return render_chroot_edit(copr, chrootname)

- 

+     """ Route for editing CoprChroot's """

+     chroot = ComplexLogic.get_copr_chroot_safe(copr, chrootname)

+     form = forms.ChrootForm(buildroot_pkgs=chroot.buildroot_pkgs, repos=chroot.repos,

+                             module_toggle=chroot.module_toggle, with_opts=chroot.with_opts,

+                             without_opts=chroot.without_opts,

+                             bootstrap=chroot.bootstrap,

+                             bootstrap_image=chroot.bootstrap_image)

+     return render_chroot_edit(form, copr, chroot)

  

- def render_chroot_edit(copr, chroot_name):

-     chroot = ComplexLogic.get_copr_chroot_safe(copr, chroot_name)

  

-     # todo: get COPR_chroot, not mock chroot, WTF?!

-     # form = forms.ChrootForm(buildroot_pkgs=copr.buildroot_pkgs(chroot))

+ def render_chroot_edit(form, copr, chroot):

+     """

+     Using a pre-filled form, copr and chroot instances, render the

+     edit_chroot.html template.

+     """

  

-     form = forms.ChrootForm(buildroot_pkgs=chroot.buildroot_pkgs, repos=chroot.repos,

-                             module_toggle=chroot.module_toggle, with_opts=chroot.with_opts,

-                             without_opts=chroot.without_opts)

-     # FIXME - test if chroot belongs to copr

      if flask.g.user.can_build_in(copr):

          return render_template("coprs/detail/edit_chroot.html",

                                 form=form, copr=copr, chroot=chroot)

-     else:

-         raise AccessRestricted(

-             "You are not allowed to modify chroots in project {0}."

-             .format(copr.name))

+     raise AccessRestricted(

+         "You are not allowed to modify chroots in project {0}."

+         .format(copr.name))

  

  

  @coprs_ns.route("/<username>/<coprname>/update_chroot/<chrootname>/", methods=["POST"])
@@ -46,11 +48,7 @@

  @login_required

  @req_with_copr

  def chroot_update(copr, chrootname):

-     return process_chroot_update(copr, chrootname)

- 

- 

- def process_chroot_update(copr, chroot_name):

- 

+     chroot_name = chrootname

      form = forms.ChrootForm()

      chroot = ComplexLogic.get_copr_chroot_safe(copr, chroot_name)

  
@@ -59,38 +57,39 @@

              "You are not allowed to modify chroots in project {0}."

              .format(copr.name))

  

-     if form.validate_on_submit():

-         if "submit" in flask.request.form:

-             action = flask.request.form["submit"]

-             if action == "update":

-                 comps_name = comps_xml = None

- 

-                 if form.comps.has_file():

-                     comps_xml = form.comps.data.stream.read()

-                     comps_name = form.comps.data.filename

- 

-                 coprs_logic.CoprChrootsLogic.update_chroot(

-                     flask.g.user, chroot,

-                     form.buildroot_pkgs.data,

-                     form.repos.data,

-                     comps=comps_xml, comps_name=comps_name,

-                     with_opts=form.with_opts.data, without_opts=form.without_opts.data,

-                     module_toggle=form.module_toggle.data

-                 )

- 

-             elif action == "delete_comps":

-                 CoprChrootsLogic.remove_comps(flask.g.user, chroot)

- 

-             flask.flash(

-                 "Buildroot {0} in project {1} has been updated successfully.".format(

-                     chroot_name, copr.name), 'success')

- 

-             db.session.commit()

-         return flask.redirect(url_for_copr_edit(copr))

- 

-     else:

-         flask.flash(form.errors, "error")

-         return render_chroot_edit(copr, chroot_name)

+     if not form.validate_on_submit():

+         flask.flash(form.errors, "error")  # pylint: disable=no-member

+         return render_chroot_edit(form, copr, chroot)

+ 

+     if "submit" in flask.request.form:

+         action = flask.request.form["submit"]

+         if action == "update":

+             comps_name = comps_xml = None

+ 

+             if form.comps.has_file():

+                 comps_xml = form.comps.data.stream.read()

+                 comps_name = form.comps.data.filename

+ 

+             coprs_logic.CoprChrootsLogic.update_chroot(

+                 flask.g.user, chroot,

+                 form.buildroot_pkgs.data,

+                 form.repos.data,

+                 comps=comps_xml, comps_name=comps_name,

+                 with_opts=form.with_opts.data, without_opts=form.without_opts.data,

+                 module_toggle=form.module_toggle.data,

+                 bootstrap=form.bootstrap.data,

+                 bootstrap_image=form.bootstrap_image.data,

+             )

+ 

+         elif action == "delete_comps":

+             CoprChrootsLogic.remove_comps(flask.g.user, chroot)

+ 

+         flask.flash(

+             "Buildroot {0} in project {1} has been updated successfully.".format(

+                 chroot_name, copr.name), 'success')

+ 

+         db.session.commit()

+     return flask.redirect(url_for_copr_edit(copr))

  

  

  @coprs_ns.route("/<username>/<coprname>/chroot/<chrootname>/comps/")

@@ -191,11 +191,11 @@

                  group=group,

                  persistent=form.persistent.data,

                  auto_prune=(form.auto_prune.data if flask.g.user.admin else True),

-                 use_bootstrap_container=form.use_bootstrap_container.data,

                  follow_fedora_branching=form.follow_fedora_branching.data,

                  delete_after_days=form.delete_after_days.data,

                  multilib=form.multilib.data,

                  runtime_dependencies=form.runtime_dependencies.data.replace("\n", " "),

+                 bootstrap=form.bootstrap.data,

              )

  

              db.session.commit()
@@ -503,12 +503,12 @@

      copr.disable_createrepo = form.disable_createrepo.data

      copr.build_enable_net = form.build_enable_net.data

      copr.unlisted_on_hp = form.unlisted_on_hp.data

-     copr.use_bootstrap_container = form.use_bootstrap_container.data

      copr.follow_fedora_branching = form.follow_fedora_branching.data

      copr.delete_after_days = form.delete_after_days.data

      copr.multilib = form.multilib.data

      copr.module_hotfixes = form.module_hotfixes.data

      copr.runtime_dependencies = form.runtime_dependencies.data.replace("\n", " ")

+     copr.bootstrap = form.bootstrap.data

      if flask.g.user.admin:

          copr.auto_prune = form.auto_prune.data

      else:

@@ -6,6 +6,7 @@

  from functools import wraps

  import datetime

  import uuid

+ from unittest import mock

  

  import pytest

  import decorator
@@ -19,13 +20,14 @@

  from coprs.logic.coprs_logic import BranchesLogic, CoprChrootsLogic

  from coprs.logic.dist_git_logic import DistGitLogic

  

- from unittest import mock

- 

+ from tests.request_test_api import WebUIRequests, API3Requests

  

  class CoprsTestCase(object):

  

-     # made available by TransactionDecorator

+     # These are made available by TransactionDecorator() decorator

      test_client = None

+     transaction_user = None

+     transaction_username = None

  

      original_config = coprs.app.config.copy()

  
@@ -70,6 +72,9 @@

          self.rmodel_TSE_coprs_general_mc.return_value.get_count.return_value = 0

          self.rmodel_TSE_coprs_general_mc.return_value.add_event.return_value = None

  

+         self.web_ui = WebUIRequests(self)

+         self.api3 = API3Requests(self)

+ 

      def teardown_method(self, method):

          # delete just data, not the tables

          self.db.session.rollback()
@@ -842,10 +847,12 @@

      def __call__(self, fn):

          @wraps(fn)

          def wrapper(fn, fn_self, *args):

-             username = getattr(fn_self, self.user).username

+             user = getattr(fn_self, self.user)

+             fn_self.transaction_user = user

+             fn_self.transaction_username = user.username

              with fn_self.tc as fn_self.test_client:

                  with fn_self.test_client.session_transaction() as session:

-                     session["openid"] = username

+                     session["openid"] = user.username

                  return fn(fn_self, *args)

          return decorator.decorator(wrapper, fn)

  

@@ -0,0 +1,214 @@

+ """

+ Library that simplifies interacting with Frontend routes.

+ """

+ 

+ from bs4 import BeautifulSoup

+ 

+ 

+ def parse_web_form_error(html_text):

+     """ return the list of form errors from failed form page """

+     soup = BeautifulSoup(html_text, "html.parser")

+     alerts = soup.findAll('div', class_='alert alert-danger')

+     assert len(alerts) == 1

+     div = alerts[0]

+     return [li.text for li in div.find_all("li")]

+ 

+ 

+ class _RequestsInterface:

+     success_expected = True

+ 

+     def __init__(self, test_class_object):

+         self.test_class_object = test_class_object

+ 

+     @property

+     def client(self):

+         """ Initialized flask http client """

+         return self.test_class_object.test_client

+ 

+     @property

+     def transaction_username(self):

+         """

+         The name of the user we work with;  this only works if

+         TransactionDecorator() is used

+         """

+         return self.test_class_object.transaction_username

+ 

+     def new_project(self, name, chroots, bootstrap=None):

+         """ Request Copr project creation.  Return the resonse. """

+         raise NotImplementedError

+ 

+     def edit_chroot(self, project, chroot, bootstrap=None,

+                     bootstrap_image=None, owner=None):

+         """ Modify CoprChroot """

+         raise NotImplementedError

+ 

+     def create_distgit_package(self, project, pkgname):

+         """ Modify CoprChroot """

+         raise NotImplementedError

+ 

+     def submit_url_build(self, project, urls=None, build_options=None):

+         """ Submit build using a Source RPM or SPEC URL """

+         if urls is None:

+             urls = "https://example.com/some.src.rpm"

+         return self._submit_url_build(project, urls, build_options)

+ 

+     def _submit_url_build(self, project, urls, build_options):

+         raise NotImplementedError

+ 

+ 

+ class WebUIRequests(_RequestsInterface):

+     """ Mimic Web UI request behavior """

+ 

+     def new_project(self, name, chroots, bootstrap=None):

+         data = {"name": name}

+         for ch in chroots:

+             data[ch] = 'y'

+         if bootstrap is not None:

+             data["bootstrap"] = bootstrap

+         resp = self.client.post(

+             "/coprs/{0}/new/".format(self.transaction_username),

+             data=data,

+             follow_redirects=False,

+         )

+         # Errors are shown on the same page (HTTP 200), while successful

+         # form submit is redirected (HTTP 302).

+         assert resp.status_code == 302 if self.success_expected else 200

+         return resp

+ 

+     def edit_chroot(self, project, chroot, bootstrap=None,

+                     bootstrap_image=None, owner=None):

+         """ Change CoprChroot using the web-UI """

+         route = "/coprs/{user}/{project}/update_chroot/{chroot}/".format(

+             user=owner or self.transaction_username,

+             project=project,

+             chroot=chroot,

+         )

+ 

+         # this is hack, submit needs to have a value, check

+         # the chroot_update() route for more info

+         data = {"submit": "update"}

+ 

+         if bootstrap is not None:

+             data["bootstrap"] = bootstrap

+         if bootstrap_image is not None:

+             data["bootstrap_image"] = bootstrap_image

+ 

+         resp = self.client.post(route, data=data)

+         if self.success_expected:

+             assert resp.status_code == 302

+         return resp

+ 

+     def create_distgit_package(self, project, pkgname):

+         data = {

+             "package_name": pkgname,

+         }

+         route = "/coprs/{user}/{project}/package/new/distgit".format(

+             user=self.transaction_username,

+             project=project,

+         )

+         resp = self.client.post(route, data=data)

+         assert resp.status_code == 302

+         return resp

+ 

+     @staticmethod

+     def _form_data_from_build_options(build_options):

+         form_data = {}

+         chroots = build_options.get("chroots")

+         if chroots:

+             for ch in chroots:

+                 form_data[ch] = 'y'

+ 

+         for attr in ["bootstrap"]:

+             value = build_options.get(attr)

+             if value is None:

+                 continue

+             form_data[attr] = value

+ 

+         return form_data

+ 

+     def _submit_url_build(self, project, urls, build_options):

+         """ Submit build by Web-UI from a src.rpm link """

+         form_data = self._form_data_from_build_options(build_options)

+         form_data["pkgs"] = urls

+         route = "/coprs/{user}/{project}/new_build/".format(

+             user=self.transaction_username,

+             project=project,

+         )

+         resp = self.client.post(route, data=form_data)

+         if resp.status_code != 302:

+             print(parse_web_form_error(resp.data))

+         assert resp.status_code == 302

+         return resp

+ 

+ class API3Requests(_RequestsInterface):

+     """

+     Mimic python-copr API requests

+ 

+     To successfully use this, the testing method needs to

+     - use the TransactionDecorator()

+     - use f_users_api fixture

+     """

+ 

+     def post(self, url, content):

+         """ Post API3 form under "user" """

+         return self.test_class_object.post_api3_with_auth(

+             url, content, self.test_class_object.transaction_user)

+ 

+     def get(self, url, content):

+         """ Get API3 url with authenticated user """

+         return self.test_class_object.get_api3_with_auth(

+             url, content, self.test_class_object.transaction_user)

+ 

+     def new_project(self, name, chroots, bootstrap=None):

+         route = "/api_3/project/add/{}".format(self.transaction_username)

+         data = {

+             "name": name,

+             "chroots": chroots,

+         }

+         if bootstrap is not None:

+             data["bootstrap"] = bootstrap

+         resp = self.post(route, data)

+         return resp

+ 

+     def edit_chroot(self, project, chroot, bootstrap=None,

+                     bootstrap_image=None, owner=None):

+         route = "/api_3/project-chroot/edit/{owner}/{project}/{chroot}".format(

+             owner=owner or self.transaction_username,

+             project=project,

+             chroot=chroot,

+         )

+         data = {}

+         if bootstrap is not None:

+             data["bootstrap"] = bootstrap

+         if bootstrap_image is not None:

+             data["bootstrap_image"] = bootstrap_image

+         resp = self.post(route, data)

+         return resp

+ 

+     @staticmethod

+     def _form_data_from_build_options(build_options):

+         form_data = {}

+         for arg in ["chroots", "bootstrap"]:

+             if arg not in build_options:

+                 continue

+             if build_options[arg] is None:

+                 continue

+             form_data[arg] = build_options[arg]

+         return form_data

+ 

+     def _submit_url_build(self, project, urls, build_options):

+         route = "/api_3/build/create/url"

+         data = {

+             "ownername": self.transaction_username,

+             "projectname": project,

+             "pkgs": urls,

+         }

+         data.update(self._form_data_from_build_options(build_options))

+         resp = self.post(route, data)

+         return resp

+ 

+     def create_distgit_package(self, project, pkgname):

+         route = "/api_3/package/add/{}/{}/{}/distgit".format(

+             self.transaction_username, project, pkgname)

+         resp = self.post(route, {"package_name": pkgname})

+         return resp

@@ -0,0 +1,71 @@

+ """

+ Coverage for stuff related to CoprChroots

+ """

+ 

+ from bs4 import BeautifulSoup

+ import pytest

+ 

+ from coprs import db

+ from coprs.models import Copr

+ 

+ from tests.coprs_test_case import CoprsTestCase, TransactionDecorator

+ 

+ 

+ class TestCoprChroots(CoprsTestCase):

+     @TransactionDecorator("u1")

+     @pytest.mark.usefixtures("f_users", "f_users_api", "f_mock_chroots", "f_db")

+     def test_edit_chroot_form(self):

+         chroot = "fedora-rawhide-i386"

+         project = "test"

+         self.web_ui.new_project(project, [chroot],

+                                 bootstrap="on")

+         route = "/coprs/{}/{}/edit_chroot/{}/".format(

+             self.transaction_username, project, chroot,

+         )

+         def get_selected(html):

+             soup = BeautifulSoup(html, "html.parser")

+             return (soup.find("select", id="bootstrap")

+                     .find("option", attrs={'selected': True}))

+ 

+         resp = self.test_client.get(route)

+         assert get_selected(resp.data) is None

+ 

+         self.web_ui.edit_chroot("test", chroot, bootstrap="on")

+ 

+         resp = self.test_client.get(route)

+         assert get_selected(resp.data)["value"] == "on"

+ 

+     @TransactionDecorator("u2")

+     @pytest.mark.usefixtures("f_users", "f_users_api", "f_mock_chroots", "f_db")

+     def test_edit_chroot_permission(self):

+         chroot = "fedora-rawhide-i386"

+         project = "test"

+         self.web_ui.new_project(project, [chroot],

+                                 bootstrap="on")

+         copr = Copr.query.one()

+         copr.user = self.u1

+         db.session.commit()

+         route = "/coprs/{}/{}/edit_chroot/{}/".format(

+             self.u1.username, project, chroot,

+         )

+         resp = self.test_client.get(route)

+         assert resp.status_code == 403

+ 

+         self.web_ui.success_expected = False

+         resp = self.web_ui.edit_chroot(project, chroot, bootstrap="off",

+                                        owner=self.u1.username)

+         assert resp.status_code == 403

+ 

+     @TransactionDecorator("u2")

+     @pytest.mark.usefixtures("f_users", "f_users_api", "f_mock_chroots", "f_db")

+     def test_edit_chroot_form_error(self):

+         chroot = "fedora-rawhide-i386"

+         project = "test"

+         self.web_ui.new_project(project, [chroot],

+                                 bootstrap="on")

+         self.web_ui.success_expected = False

+         resp = self.web_ui.edit_chroot(project, chroot, bootstrap="invalid")

+         assert resp.status_code == 200

+         soup = BeautifulSoup(resp.data, "html.parser")

+         div = soup.find("div", class_="alert alert-danger alert-dismissable")

+         assert "Not a valid choice" in div.text

@@ -1,11 +1,11 @@

  import json

  from unittest.mock import patch, MagicMock

  

- from coprs.models import User, Copr

+ import pytest

  

- from tests.coprs_test_case import CoprsTestCase

+ from coprs.models import User, Copr

  

- from coprs.views.apiv3_ns import apiv3_projects

+ from tests.coprs_test_case import CoprsTestCase, TransactionDecorator

  

  

  class TestApiV3Permissions(CoprsTestCase):
@@ -224,3 +224,16 @@

          r = self.auth_post('/request/user2/barcopr', permissions, u)

          assert r.status_code == 200

          assert len(calls) == 2

+ 

+ 

+     @TransactionDecorator("u1")

+     @pytest.mark.usefixtures("f_users", "f_users_api", "f_mock_chroots", "f_db")

+     @pytest.mark.parametrize("store, read", [(True, "on"), (False, "off")])

+     def test_compat_bootstrap_config(self, store, read):

+         route = "/api_3/project/add/{}".format(self.transaction_username)

+         self.api3.post(route, {

+             "name": "test-compat-bootstrap",

+             "chroots": ["fedora-rawhide-i386"],

+             "use_bootstrap_container": store,

+         })

+         assert Copr.query.one().bootstrap == read

@@ -0,0 +1,129 @@

+ """

+ Test variants of Mock bootstrap configuration in Copr project, chroot and a

+ concrete build.

+ """

+ 

+ import json

+ 

+ import pytest

+ 

+ from coprs import models

+ 

+ from tests.coprs_test_case import (CoprsTestCase, TransactionDecorator)

+ 

+ # the None value means "unset"

+ VALID_BOOTSTRAP_CONFIG_CASES = [{

+     "project": "image",

+     "chroots": {

+         "fedora-18-x86_64": ("custom_image", "fedora"),

+         "fedora-17-i386": ("off", "fedora"),

+         # the effect here is as if "custom_image" was set

+         "fedora-17-x86_64": (None, "centos"),

+         "fedora-rawhide-i386": ("on", None),

+     },

+     "build": None,

+     "expected": {

+         "fedora-18-x86_64": ("custom_image", "fedora"),

+         "fedora-17-i386": ("off", None),

+         "fedora-17-x86_64": ("custom_image", "centos"),

+         "fedora-rawhide-i386": ("on", None),

+     }

+ }, {

+     "project": "off",

+     "chroots": {

+         "fedora-18-x86_64": ("custom_image", "fedora"),

+         "fedora-rawhide-i386": ("on", None),

+     },

+     "build": "image",

+     "expected": {

+         "fedora-18-x86_64": ("image", None),

+         "fedora-rawhide-i386": ("image", None),

+     }

+ }, {

+     "project": "on",

+     "chroots": {

+         "fedora-18-x86_64": (None, None),

+         "fedora-rawhide-i386": ("image", None),

+     },

+     "build": "unchanged",

+     "expected": {

+         "fedora-18-x86_64": ("on", None),

+         "fedora-rawhide-i386": ("image", None),

+     }

+ }, {

+     "project": None,

+     "chroots": {

+         "fedora-18-x86_64": ("custom_image", "fedora:18"),

+         "fedora-rawhide-i386": (None, None),

+     },

+     "build": None,

+     "expected": {

+         "fedora-18-x86_64": ("custom_image", "fedora:18"),

+         "fedora-rawhide-i386": (None, None),

+     }

+ }, {

+     "project": "default",

+     "chroots": {

+         "fedora-18-x86_64": (None, None),

+         "fedora-rawhide-i386": ("on", None),

+     },

+     "build": "unchanged",

+     "expected": {

+         "fedora-18-x86_64": (None, None),

+         "fedora-rawhide-i386": ("on", None),

+     }

+ }]

+ 

+ 

+ class TestConfigOverrides(CoprsTestCase):

+ 

+     @TransactionDecorator("u1")

+     @pytest.mark.usefixtures("f_users", "f_users_api", "f_mock_chroots", "f_db")

+     @pytest.mark.parametrize("request_type", ["api", "webui"])

+     @pytest.mark.parametrize("case", VALID_BOOTSTRAP_CONFIG_CASES)

+     def test_valid_configuration_cases(self, case, request_type):

+         client = self.api3 if request_type == "api" else self.web_ui

+         client.new_project("test-bootstrap", list(case["chroots"]),

+                            bootstrap=case["project"])

+         project = models.Copr.query.one()

+         assert project.bootstrap == case["project"] or "default"

+         for chroot in case["chroots"]:

+             bootstrap, bootstrap_image = case["chroots"][chroot]

+             kwargs = {}

+             if bootstrap:

+                 kwargs["bootstrap"] = bootstrap

+             if bootstrap_image:

+                 kwargs["bootstrap_image"] = bootstrap_image

+             if not kwargs:

+                 continue

+             client.edit_chroot("test-bootstrap", chroot, **kwargs)

+ 

+         # create package so we can assign the build to that

+         client.create_distgit_package("test-bootstrap", "tar")

+ 

+         client.submit_url_build("test-bootstrap",

+                                 build_options={

+                                     "chroots": list(case["chroots"]),

+                                     "bootstrap": case["build"]})

+ 

+         # Assign Package to Build, so we can query the (build/chroot) configs

+         build = models.Build.query.one()

+         build.package = models.Package.query.one()

+         self.db.session.add(build)

+         self.db.session.commit()

+ 

+         for chroot in case["expected"]:

+             for url in ["/api_3/build-chroot/build-config/1/{0}".format(chroot),

+                         "/backend/get-build-task/1-{}".format(chroot)]:

+                 bootstrap, bootstrap_image = case["expected"][chroot]

+                 response = self.test_client.get(url)

+                 assert response.status_code == 200

+                 result_dict = json.loads(response.data)

+ 

+                 if not bootstrap:

+                     assert "bootstrap" not in result_dict

+                 if not bootstrap_image:

+                     assert "bootstrap_image" not in result_dict

+ 

+                 assert result_dict.get("bootstrap") == bootstrap

+                 assert result_dict.get("bootstrap_image") == bootstrap_image

@@ -1,10 +1,19 @@

  from __future__ import absolute_import

  

+ import warnings

+ 

  from . import BaseProxy

  from ..requests import Request, munchify, POST, GET, PUT

  from ..helpers import for_all_methods, bind_proxy

  

  

+ def _compat_use_bootstrap_container(data, value):

+     if value is None:

+         return

+     data["bootstrap"] = "on" if value else "off"

+     warnings.warn("The 'use_bootstrap_container' argument is obsoleted by "

+                   "'bootstrap' and 'bootstrap_image'")

+ 

  @for_all_methods(bind_proxy)

  class ProjectProxy(BaseProxy):

  
@@ -61,8 +70,9 @@

  

      def add(self, ownername, projectname, chroots, description=None, instructions=None, homepage=None,

              contact=None, additional_repos=None, unlisted_on_hp=False, enable_net=True, persistent=False,

-             auto_prune=True, use_bootstrap_container=False, devel_mode=False,

-             delete_after_days=None, multilib=False, module_hotfixes=False):

+             auto_prune=True, use_bootstrap_container=None, devel_mode=False,

+             delete_after_days=None, multilib=False, module_hotfixes=False,

+             bootstrap=None, bootstrap_image=None):

          """

          Create a project

  
@@ -78,11 +88,16 @@

          :param bool enable_net: if builder can access net for builds in this project

          :param bool persistent: if builds and the project are undeletable

          :param bool auto_prune: if backend auto-deletion script should be run for the project

-         :param bool use_bootstrap_container: if mock bootstrap container is used to initialize the buildroot

+         :param bool use_bootstrap_container: obsoleted, use the 'bootstrap'

+             argument and/or the 'bootstrap_image'.

          :param bool devel_mode: if createrepo should run automatically

          :param int delete_after_days: delete the project after the specfied period of time

          :param bool module_hotfixes: make packages from this project available

                                       on along with packages from the active module streams.

+         :param str bootstrap: Mock bootstrap feature setup.

+             Possible values are 'default', 'on', 'off', 'image'.

+         :param str bootstrap_image: Name of the container image to initialize

+             the bootstrap chroot from.  This also implies 'bootstrap=image'.

          :return: Munch

          """

          endpoint = "/project/add/{ownername}"
@@ -101,12 +116,16 @@

              "enable_net": enable_net,

              "persistent": persistent,

              "auto_prune": auto_prune,

-             "use_bootstrap_container": use_bootstrap_container,

+             "bootstrap": bootstrap,

+             "bootstrap_image": bootstrap_image,

              "devel_mode": devel_mode,

              "delete_after_days": delete_after_days,

              "multilib": multilib,

              "module_hotfixes": module_hotfixes,

          }

+ 

+         _compat_use_bootstrap_container(data, use_bootstrap_container)

+ 

          request = Request(endpoint, api_base_url=self.api_base_url, method=POST,

                            params=params, data=data, auth=self.auth)

          response = request.send()
@@ -115,7 +134,8 @@

      def edit(self, ownername, projectname, chroots=None, description=None, instructions=None, homepage=None,

               contact=None, additional_repos=None, unlisted_on_hp=None, enable_net=None,

               auto_prune=None, use_bootstrap_container=None, devel_mode=None,

-              delete_after_days=None, multilib=None, module_hotfixes=None):

+              delete_after_days=None, multilib=None, module_hotfixes=None,

+              bootstrap=None, bootstrap_image=None):

          """

          Edit a project

  
@@ -130,11 +150,16 @@

          :param bool unlisted_on_hp: project will not be shown on Copr homepage

          :param bool enable_net: if builder can access net for builds in this project

          :param bool auto_prune: if backend auto-deletion script should be run for the project

-         :param bool use_bootstrap_container: if mock bootstrap container is used to initialize the buildroot

+         :param bool use_bootstrap_container: obsoleted, use the 'bootstrap'

+             argument and/or the 'bootstrap_image'.

          :param bool devel_mode: if createrepo should run automatically

          :param int delete_after_days: delete the project after the specfied period of time

          :param bool module_hotfixes: make packages from this project available

                                       on along with packages from the active module streams.

+         :param str bootstrap: Mock bootstrap feature setup.

+             Possible values are 'default', 'on', 'off', 'image'.

+         :param str bootstrap_image: Name of the container image to initialize

+             the bootstrap chroot from.  This also implies 'bootstrap=image'.

          :return: Munch

          """

          endpoint = "/project/edit/{ownername}/{projectname}"
@@ -152,12 +177,16 @@

              "unlisted_on_hp": unlisted_on_hp,

              "enable_net": enable_net,

              "auto_prune": auto_prune,

-             "use_bootstrap_container": use_bootstrap_container,

+             "bootstrap": bootstrap,

+             "bootstrap_image": bootstrap_image,

              "devel_mode": devel_mode,

              "delete_after_days": delete_after_days,

              "multilib": multilib,

              "module_hotfixes": module_hotfixes,

          }

+ 

+         _compat_use_bootstrap_container(data, use_bootstrap_container)

+ 

          request = Request(endpoint, api_base_url=self.api_base_url, method=POST,

                            params=params, data=data, auth=self.auth)

          response = request.send()

@@ -47,8 +47,10 @@

          response = request.send()

          return munchify(response)

  

+     # pylint: disable=too-many-arguments

      def edit(self, ownername, projectname, chrootname, additional_packages=None, additional_repos=None,

-              comps=None, delete_comps=False, with_opts=None, without_opts=None):

+              comps=None, delete_comps=False, with_opts=None, without_opts=None,

+              bootstrap=None, bootstrap_image=None):

          """

          Edit a chroot configuration in a project

  
@@ -61,6 +63,9 @@

          :param bool delete_comps: if True, current comps.xml will be removed

          :param list with_opts: Mock --with option

          :param list without_opts: Mock --without option

+         :param str bootstrap: Allowed values 'on', 'off', 'image', 'default',

+                               'untouched' (equivalent to None)

+         :param str bootstrap_image: Implies 'bootstrap=image'.

          :return: Munch

          """

          endpoint = "/project-chroot/edit/{ownername}/{projectname}/{chrootname}"
@@ -69,12 +74,18 @@

              "projectname": projectname,

              "chrootname": chrootname,

          }

+ 

+         if bootstrap_image:

+             bootstrap = 'custom_image'

+ 

          data = {

              "additional_repos": additional_repos,

              "additional_packages": additional_packages,

              "delete_comps": delete_comps,

              "with_opts": with_opts,

              "without_opts": without_opts,

+             "bootstrap": bootstrap,

+             "bootstrap_image": bootstrap_image,

          }

          files = {}

          if comps:

file modified
+1 -1
@@ -15,7 +15,7 @@

  %endif

  

  Name:       python-copr

- Version:    1.105.1.dev

+ Version:    1.105.2.dev

  Release:    1%{?dist}

  Summary:    Python interface for Copr

  

@@ -20,7 +20,8 @@

          self.buildroot_pkgs = task.get("buildroot_pkgs")

          self.enable_net = task.get("enable_net")

          self.repos = task.get("repos")

-         self.use_bootstrap_container = task.get("use_bootstrap_container")

+         self.bootstrap = task.get("bootstrap")

+         self.bootstrap_image = task.get("bootstrap_image")

          self.timeout = task.get("timeout", 3600)

          self.with_opts = task.get("with_opts", [])

          self.without_opts = task.get("without_opts", [])
@@ -65,7 +66,9 @@

          jinja_env = Environment(loader=FileSystemLoader(CONF_DIRS))

          template = jinja_env.get_template("mock.cfg.j2")

          return template.render(chroot=self.chroot, task_id=self.task_id, buildroot_pkgs=self.buildroot_pkgs,

-                                enable_net=self.enable_net, use_bootstrap_container=self.use_bootstrap_container,

+                                enable_net=self.enable_net,

+                                bootstrap=self.bootstrap,

+                                bootstrap_image=self.bootstrap_image,

                                 repos=self.repos,

                                 copr_username=self.copr_username, copr_projectname=self.copr_projectname,

                                 modules=self.enable_modules,
@@ -100,11 +103,12 @@

              raise RuntimeError("Mock build failed")

  

      def mock_clean(self):

-         """ Do best effort /var/mock/cache cleanup. """

+         """ Do a best effort Mock cleanup. """

          cmd = MOCK_CALL + [

              "-r", self.mock_config_file,

              "--scrub", "bootstrap",

              "--scrub", "chroot",

+             "--scrub", "root-cache",

              "--quiet",

          ]

          subprocess.call(cmd) # ignore failure here, if any

file modified
+13 -7
@@ -17,13 +17,19 @@

  # Build-system's (or build) ID

  config_opts['macros']['%buildtag'] = '.copr{{ copr_build_id }}'

  

- config_opts['use_bootstrap'] = {{ 'True' if use_bootstrap_container else 'False' }}

- 

- {% if use_bootstrap_container %}

- config_opts['bootstrap_chroot_additional_packages'] = []

- config_opts['bootstrap_module_enable'] = []

- config_opts['bootstrap_module_install'] = []

- {% endif %}

+ {%- if bootstrap == "on" %}

+ config_opts['use_bootstrap'] = True

+ config_opts['use_bootstrap_image'] = False

+ {%- elif bootstrap == "off" %}

+ config_opts['use_bootstrap'] = False

+ config_opts['use_bootstrap_image'] = False

+ {%- elif bootstrap in ["image", "custom_image"] %}

+ config_opts['use_bootstrap'] = True

+ config_opts['use_bootstrap_image'] = True

+ {%- if bootstrap_image %}

+ config_opts['bootstrap_image'] = "{{ bootstrap_image }}"

+ {%- endif %}

+ {%- endif %}

  {% for module in modules %}

  config_opts["module_enable"] += ["{{ module }}"]

  {%- endfor %}

file modified
+6 -10
@@ -69,7 +69,6 @@

              "submitter": "clime",

              "task_id": "10-fedora-24-x86_64",

              "timeout": 21600,

-             "use_bootstrap_container": False,

              "with_opts": [],

              "without_opts": [],

          }
@@ -104,7 +103,8 @@

          assert builder.buildroot_pkgs == ["pkg1", "pkg2", "pkg3"]

          assert builder.enable_net

          assert builder.repos == []

-         assert not builder.use_bootstrap_container

+         assert not builder.bootstrap

+         assert not builder.bootstrap_image

  

      def test_render_config_template(self):

          confdirs = [dirname(dirname(realpath(__file__)))]
@@ -123,7 +123,9 @@

  

          assert config_opts["chroot_additional_packages"] == "pkg1 pkg2 pkg3"

          assert config_opts["rpmbuild_networking"]

-         assert not config_opts["use_bootstrap"]

+         assert "use_bootstrap" not in config_opts

+         assert "bootstrap" not in config_opts

+         assert "bootstrap_image" not in config_opts

          assert config_opts["macros"]["%copr_username"] == "@copr"

          assert config_opts["macros"]["%copr_projectname"] == "copr-dev"

          assert config_opts["yum.conf"] == []
@@ -153,10 +155,6 @@

  # Build-system's (or build) ID

  config_opts['macros']['%buildtag'] = '.copr10'

  

- config_opts['use_bootstrap'] = False

- 

- 

- 

  """  # TODO: make the output nicer

  

      @mock.patch("copr_rpmbuild.builders.mock.MockBuilder.prepare_configs")
@@ -223,9 +221,7 @@

          assert call[0][0] == self.mock_rpm_call

  

          part_of_expected_output = (

-             "config_opts['use_bootstrap'] = False\n"

-             "\n"

-             "\n"

+             "config_opts['macros']['%buildtag'] = '.copr10'\n"

              "\n"

              "{0}\n"

          ).format('\n'.join(

Metadata Update from @praiskup:
- Pull-request tagged with: needs-tests, needs-work

3 years ago

3 new commits added

  • frontend, cli, python, rpmbuild: better bootstrap config
  • beaker-tests, cli, frontend, python, rpmbuild: add option to config bootstrap
  • frontend: exception handlers fix once more
3 years ago

pretty please pagure-ci rebuild

3 years ago

3 new commits added

  • frontend, cli, python, rpmbuild: better bootstrap config
  • beaker-tests, cli, frontend, python, rpmbuild: add option to config bootstrap
  • frontend: exception handlers fix once more
3 years ago

rebased onto 1aa6c8f3c74587b30dfc6f1e34419b972a17363d

3 years ago

2 new commits added

  • frontend: short-cut the loop in build_config chroot search
  • chroot tests
3 years ago

1 new commit added

  • lint
3 years ago

4 new commits added

  • frontend: short-cut the loop in build_config chroot search
  • chroot tests
  • frontend, cli, python, rpmbuild: better bootstrap config
  • beaker-tests, cli, frontend, python, rpmbuild: add option to config bootstrap
3 years ago

7 new commits added

  • frontend: test chroot-edit 403 and form errors
  • frontend: fix-up the CoprChroot form rendering
  • frontend: de-duplicate work with build form
  • frontend: merge two methods which were split needlessly
  • frontend: short-cut the loop in build_config chroot search
  • frontend, cli, python, rpmbuild: better bootstrap config
  • beaker-tests, cli, frontend, python, rpmbuild: add option to config bootstrap
3 years ago

7 new commits added

  • frontend: test chroot-edit 403 and form errors
  • frontend: fix-up the CoprChroot form rendering
  • frontend: de-duplicate work with build form
  • frontend: merge two methods which were split needlessly
  • frontend: short-cut the loop in build_config chroot search
  • frontend, cli, python, rpmbuild: better bootstrap config
  • beaker-tests, cli, frontend, python, rpmbuild: add option to config bootstrap
3 years ago

Metadata Update from @praiskup:
- Pull-request untagged with: needs-tests, needs-work

3 years ago

rebased onto cd24a6c5d9a071b4325d5d4a4823933ee0383f0b

3 years ago

pretty please pagure-ci rebuild

3 years ago

1 new commit added

  • cli: move the build --bootstrap option to a correct parser
3 years ago

Please take a look at this PR, I'd like to use some of the logic here also in PR#1535

I think these create_*_field() functions could probably be done better with custom fields
https://wtforms.readthedocs.io/en/2.3.x/fields/#custom-fields
but this works too.

That gif is great, we should definitely use that in release notes

I took a look, sorry for taking it too long, the PR is huge.
+1

Also, I like some of the refactorings and code clean-ups.

Thank you for the review!

That gif is great, we should definitely use that in release notes

Heh, such tutorial would probably deserve some compressed video including spoken instructions... Or at least more attention to detail :-) ... not a bad idea.

rebased onto b3e927d

3 years ago

Merge button is broken after some time again. Merging manually.

Commit 5e6f7e1 fixes this pull-request

Pull-Request has been merged by praiskup

3 years ago
Metadata
Flags
Copr build
success (100%)
#1699860
3 years ago
Copr build
success (100%)
#1699859
3 years ago
Copr build
success (100%)
#1699858
3 years ago
Copr build
success (100%)
#1699857
3 years ago
Copr build
success (100%)
#1695015
3 years ago
Copr build
success (100%)
#1695014
3 years ago
Copr build
success (100%)
#1695013
3 years ago
Copr build
success (100%)
#1695012
3 years ago
Copr build
success (100%)
#1693974
3 years ago
Copr build
success (100%)
#1693973
3 years ago
Copr build
success (100%)
#1693972
3 years ago
Copr build
success (100%)
#1693971
3 years ago
Copr build
success (100%)
#1693970
3 years ago
Copr build
success (100%)
#1693969
3 years ago
Copr build
success (100%)
#1693968
3 years ago
Copr build
success (100%)
#1693967
3 years ago
Copr build
success (100%)
#1693947
3 years ago
Copr build
success (100%)
#1693946
3 years ago
Copr build
success (100%)
#1693945
3 years ago
Copr build
success (100%)
#1693944
3 years ago
jenkins
success (100%)
Build #540 successful (commit: e3c33266)
3 years ago
Copr build
success (100%)
#1691971
3 years ago
Copr build
success (100%)
#1691970
3 years ago
Copr build
success (100%)
#1691969
3 years ago
Copr build
success (100%)
#1691968
3 years ago
jenkins
success (100%)
Build #539 successful (commit: dddebd4d)
3 years ago
Copr build
success (100%)
#1691950
3 years ago
Copr build
success (100%)
#1691949
3 years ago
Copr build
success (100%)
#1691948
3 years ago
Copr build
pending (50%)
#1691947
3 years ago
jenkins
failure
Build #538 failed (commit: b324bd81)
3 years ago
Copr build
success (100%)
#1691858
3 years ago
Copr build
success (100%)
#1691857
3 years ago
Copr build
success (100%)
#1691856
3 years ago
Copr build
failure
#1691855
3 years ago
jenkins
success (100%)
Build #537 successful (commit: 5ebff6f3)
3 years ago
Copr build
success (100%)
#1691616
3 years ago
Copr build
success (100%)
#1691615
3 years ago
Copr build
success (100%)
#1691614
3 years ago
Copr build
success (100%)
#1691613
3 years ago
jenkins
success (100%)
Build #536 successful (commit: 5ebff6f3)
3 years ago
Copr build
success (100%)
#1691036
3 years ago
Copr build
success (100%)
#1691035
3 years ago
Copr build
success (100%)
#1691034
3 years ago
Copr build
success (100%)
#1691033
3 years ago
jenkins
failure
Build #535 failed (commit: 086e6ccd)
3 years ago
Copr build
success (100%)
#1690663
3 years ago
Copr build
success (100%)
#1690662
3 years ago
Copr build
success (100%)
#1690661
3 years ago
Copr build
success (100%)
#1690660
3 years ago
Copr build
failure
#1690650
3 years ago
Copr build
success (100%)
#1690649
3 years ago
Copr build
success (100%)
#1690648
3 years ago
Copr build
success (100%)
#1690647
3 years ago
Copr build
failure
#1690643
3 years ago
Copr build
success (100%)
#1690642
3 years ago
Copr build
success (100%)
#1690640
3 years ago
Copr build
success (100%)
#1690639
3 years ago
Changes Summary 35
+0 -10
file changed
beaker-tests/Sanity/copr-cli-basic-operations/runtest.sh
+1 -1
file changed
cli/copr-cli.spec
+15 -0
file changed
cli/copr_cli/build_config.py
+64 -8
file changed
cli/copr_cli/main.py
+2 -2
file changed
cli/tests/test_cli.py
+1 -0
file changed
frontend/copr-frontend.spec
+68
file added
frontend/coprs_frontend/alembic/versions/63db6872060f_add_bootstrap_config_columns.py
+84 -8
file changed
frontend/coprs_frontend/coprs/forms.py
+3 -1
file changed
frontend/coprs_frontend/coprs/logic/builds_logic.py
+23 -2
file changed
frontend/coprs_frontend/coprs/logic/complex_logic.py
+23 -7
file changed
frontend/coprs_frontend/coprs/logic/coprs_logic.py
+27 -2
file changed
frontend/coprs_frontend/coprs/models.py
+33 -6
file changed
frontend/coprs_frontend/coprs/templates/_helpers.html
+6 -2
file changed
frontend/coprs_frontend/coprs/templates/coprs/_coprs_forms.html
+4 -1
file changed
frontend/coprs_frontend/coprs/templates/coprs/detail/edit_chroot.html
+13 -4
file changed
frontend/coprs_frontend/coprs/views/api_ns/api_general.py
+4 -2
file changed
frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_build_chroots.py
+23 -35
file changed
frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_builds.py
+8 -3
file changed
frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_project_chroots.py
+10 -2
file changed
frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_projects.py
+4 -1
file changed
frontend/coprs_frontend/coprs/views/backend_ns/backend_general.py
+1 -0
file changed
frontend/coprs_frontend/coprs/views/coprs_ns/coprs_builds.py
+50 -51
file changed
frontend/coprs_frontend/coprs/views/coprs_ns/coprs_chroots.py
+2 -2
file changed
frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py
+12 -5
file changed
frontend/coprs_frontend/tests/coprs_test_case.py
+214
file added
frontend/coprs_frontend/tests/request_test_api.py
+71
file added
frontend/coprs_frontend/tests/test_apiv3/test_copr_chroot.py
+16 -3
file changed
frontend/coprs_frontend/tests/test_apiv3/test_projects.py
+129
file added
frontend/coprs_frontend/tests/test_config_overrides.py
+36 -7
file changed
python/copr/v3/proxies/project.py
+12 -1
file changed
python/copr/v3/proxies/project_chroot.py
+1 -1
file changed
python/python-copr.spec
+7 -3
file changed
rpmbuild/copr_rpmbuild/builders/mock.py
+13 -7
file changed
rpmbuild/mock.cfg.j2
+6 -10
file changed
rpmbuild/tests/test_mock.py