From fdacb5ddec8973c48f9d9aa0ad0542de693d9572 Mon Sep 17 00:00:00 2001 From: Adam Miller Date: Dec 06 2016 15:58:40 +0000 Subject: update to latest upstream, patch for koji krb5 and site customizations --- diff --git a/.gitignore b/.gitignore index bf78f8e..405d774 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ /osbs-client-0112f3773b0d29b38df1e6e6373c811383b1d3d5.tar.gz /osbs-client-dd5b0ea3f0b77fd3f9f2d55f607dbf4323b4d167.tar.gz /osbs-client-5021224746eb0e6ac12c54c397397b629167030b.tar.gz +/osbs-client-696505ae4da4898a4e3689ef18e1f6791b04ea09.tar.gz diff --git a/osbs-client-koji_krb5.patch b/osbs-client-koji_krb5.patch new file mode 100644 index 0000000..a65b900 --- /dev/null +++ b/osbs-client-koji_krb5.patch @@ -0,0 +1,181 @@ +diff --git a/docs/configuration_file.md b/docs/configuration_file.md +index 3ea34af..c200e86 100644 +--- a/docs/configuration_file.md ++++ b/docs/configuration_file.md +@@ -53,6 +53,12 @@ Some options are also mandatory. + + * `koji_certs_secret` (*optional*, `string`) — name of [kubernetes secret](https://github.com/kubernetes/kubernetes/blob/master/docs/design/secrets.md) to use for koji authentication + ++* `koji_use_kerberos` (*optional*, `boolean`) — will set [atomic-reactor](https://github.com/projectatomic/atomic-reactor) plugins to use kerberos to authenticate to koji. ++ ++* `koji_kerberos_keytab` (*optional*, `string`) - location of the keytab that will be used to initialize kerberos credentials for [atomic-reactor](https://github.com/projectatomic/atomic-reactor) plugins - usually in the form `FILE:`, see [kerberos documentation](http://web.mit.edu/Kerberos/krb5-latest/doc/basic/keytab_def.html) for other possible values ++ ++* `koji_kerberos_principal` (*optional*, `string`) - kerberos principal for the keytab provided in `koji_kerberos_keytab` ++ + * `sources_command` (*optional*, `string`) — command to use to get dist-git artifacts from lookaside cache (e.g. `fedpkg sources`) + + * `username`, `password` (*optional*, `string`) — when OpenShift is hidden behind authentication proxy, you can specify username and password for basic authentication +diff --git a/inputs/prod_inner.json b/inputs/prod_inner.json +index 306ac89..d2b5b4e 100644 +--- a/inputs/prod_inner.json ++++ b/inputs/prod_inner.json +@@ -154,7 +154,9 @@ + "kojihub": "{{KOJI_HUB}}", + "url": "{{OPENSHIFT_URI}}", + "verify_ssl": false, +- "blocksize": 10485760 ++ "blocksize": 10485760, ++ "koji_keytab": false, ++ "koji_principal": false + } + }, + { +diff --git a/osbs/api.py b/osbs/api.py +index 1a8e5c5..8d1883c 100644 +--- a/osbs/api.py ++++ b/osbs/api.py +@@ -393,6 +393,9 @@ class OSBS(object): + koji_target=target, + koji_certs_secret=self.build_conf.get_koji_certs_secret(), + koji_task_id=koji_task_id, ++ koji_use_kerberos=self.build_conf.get_koji_use_kerberos(), ++ koji_kerberos_keytab=self.build_conf.get_koji_kerberos_keytab(), ++ koji_kerberos_principal=self.build_conf.get_koji_kerberos_principal(), + architecture=architecture, + vendor=self.build_conf.get_vendor(), + build_host=self.build_conf.get_build_host(), +diff --git a/osbs/build/build_request.py b/osbs/build/build_request.py +index a890d0b..9f9824a 100644 +--- a/osbs/build/build_request.py ++++ b/osbs/build/build_request.py +@@ -486,7 +486,13 @@ class BuildRequest(object): + + if use_auth is not None: + self.dj.dock_json_set_arg('exit_plugins', 'koji_promote', +- 'use_auth', use_auth) ++ 'use_auth', use_auth) ++ ++ if self.spec.koji_use_kerberos.value: ++ self.dj.dock_json_set_arg('exit_plugins', 'koji_promote', ++ 'koji_principal', self.spec.koji_kerberos_principal.value) ++ self.dj.dock_json_set_arg('exit_plugins', 'koji_promote', ++ 'koji_keytab', self.spec.koji_kerberos_keytab.value) + else: + logger.info("removing koji_promote from request as no kojihub " + "specified") +diff --git a/osbs/build/spec.py b/osbs/build/spec.py +index 1f68281..bcab0fe 100644 +--- a/osbs/build/spec.py ++++ b/osbs/build/spec.py +@@ -147,6 +147,9 @@ class BuildSpec(object): + kojihub = BuildParam("kojihub", allow_none=True) + koji_certs_secret = BuildParam("koji_certs_secret", allow_none=True) + koji_task_id = BuildParam("koji_task_id", allow_none=True) ++ koji_use_kerberos = BuildParam("koji_use_kerberos", allow_none=True) ++ koji_kerberos_principal = BuildParam("koji_kerberos_principal", allow_none=True) ++ koji_kerberos_keytab = BuildParam("koji_kerberos_keytab", allow_none=True) + image_tag = BuildParam("image_tag") + pulp_secret = BuildParam("pulp_secret", allow_none=True) + pulp_registry = BuildParam("pulp_registry", allow_none=True) +@@ -197,7 +200,8 @@ class BuildSpec(object): + sources_command=None, architecture=None, vendor=None, + build_host=None, authoritative_registry=None, distribution_scope=None, + koji_target=None, kojiroot=None, kojihub=None, koji_certs_secret=None, +- koji_task_id=None, ++ koji_use_kerberos=None, koji_kerberos_keytab=None, ++ koji_kerberos_principal=None, koji_task_id=None, + source_secret=None, # compatibility name for pulp_secret + pulp_secret=None, pulp_registry=None, pdc_secret=None, pdc_url=None, + smtp_uri=None, nfs_server_path=None, +@@ -240,6 +244,9 @@ class BuildSpec(object): + self.kojiroot.value = kojiroot + self.kojihub.value = kojihub + self.koji_certs_secret.value = koji_certs_secret ++ self.koji_use_kerberos.value = koji_use_kerberos ++ self.koji_kerberos_principal.value = koji_kerberos_principal ++ self.koji_kerberos_keytab.value = koji_kerberos_keytab + self.koji_task_id.value = koji_task_id + self.pulp_secret.value = pulp_secret or source_secret + self.pulp_registry.value = pulp_registry +diff --git a/osbs/conf.py b/osbs/conf.py +index bb92ca7..324fdbf 100644 +--- a/osbs/conf.py ++++ b/osbs/conf.py +@@ -211,6 +211,15 @@ class Configuration(object): + def get_koji_certs_secret(self): + return self._get_value("koji_certs_secret", self.conf_section, "koji_certs_secret") + ++ def get_koji_use_kerberos(self): ++ return self._get_value("koji_use_kerberos", self.conf_section, "koji_use_kerberos", is_bool_val=True) ++ ++ def get_koji_kerberos_keytab(self): ++ return self._get_value("koji_kerberos_keytab", self.conf_section, "koji_kerberos_keytab") ++ ++ def get_koji_kerberos_principal(self): ++ return self._get_value("koji_kerberos_principal", self.conf_section, "koji_kerberos_principal") ++ + def get_sources_command(self): + return self._get_value("sources_command", self.conf_section, "sources_command") + +diff --git a/tests/build/test_build_request.py b/tests/build/test_build_request.py +index add97d3..c10de7e 100644 +--- a/tests/build/test_build_request.py ++++ b/tests/build/test_build_request.py +@@ -1279,6 +1279,57 @@ class TestBuildRequest(object): + koji_certs_secret_name) + assert get_plugin(plugins, 'exit_plugins', 'koji_promote')['args']['koji_ssl_certs'] == mount_path + ++ def test_render_prod_request_with_koji_kerberos(self, tmpdir): ++ self.create_image_change_trigger_json(str(tmpdir)) ++ build_request = BuildRequest(str(tmpdir)) ++ name_label = "fedora/resultingimage" ++ push_url = "ssh://{username}git.example.com/git/{component}.git" ++ koji_task_id = 1234 ++ koji_use_kerberos = True ++ koji_kerberos_keytab = "FILE:/tmp/fakekeytab" ++ koji_kerberos_principal = "myprincipal@OSBSDOMAIN.COM" ++ kwargs = { ++ 'git_uri': TEST_GIT_URI, ++ 'git_ref': TEST_GIT_REF, ++ 'git_branch': TEST_GIT_BRANCH, ++ 'user': "john-foo", ++ 'component': TEST_COMPONENT, ++ 'base_image': 'fedora:latest', ++ 'name_label': name_label, ++ 'registry_uri': "example.com", ++ 'openshift_uri': "http://openshift/", ++ 'builder_openshift_url': "http://openshift/", ++ 'koji_target': "koji-target", ++ 'kojiroot': "http://root/", ++ 'kojihub': "http://hub/", ++ 'sources_command': "make", ++ 'koji_task_id': koji_task_id, ++ 'koji_use_kerberos': koji_use_kerberos, ++ 'koji_kerberos_keytab': koji_kerberos_keytab, ++ 'koji_kerberos_principal': koji_kerberos_principal, ++ 'vendor': "Foo Vendor", ++ 'authoritative_registry': "registry.example.com", ++ 'distribution_scope': "authoritative-source-only", ++ 'registry_api_versions': ['v1'], ++ 'git_push_url': push_url.format(username='', component=TEST_COMPONENT), ++ 'git_push_username': 'example', ++ } ++ build_request.set_params(**kwargs) ++ build_json = build_request.render() ++ ++ assert build_json["metadata"]["labels"]["koji-task-id"] == str(koji_task_id) ++ ++ plugins = get_plugins_from_build_json(build_json) ++ assert get_plugin(plugins, "exit_plugins", "koji_promote") ++ assert plugin_value_get(plugins, "exit_plugins", "koji_promote", ++ "args", "kojihub") == kwargs["kojihub"] ++ assert plugin_value_get(plugins, "exit_plugins", "koji_promote", ++ "args", "url") == kwargs["openshift_uri"] ++ ++ assert get_plugin(plugins, 'exit_plugins', 'koji_promote')['args']['koji_principal'] == koji_kerberos_principal ++ assert get_plugin(plugins, 'exit_plugins', 'koji_promote')['args']['koji_keytab'] == koji_kerberos_keytab ++ ++ + @pytest.mark.parametrize(('base_image', 'is_custom'), [ + ('fedora', False), + ('fedora:latest', False), diff --git a/osbs-client-site-customizations.patch b/osbs-client-site-customizations.patch new file mode 100644 index 0000000..8ffa43f --- /dev/null +++ b/osbs-client-site-customizations.patch @@ -0,0 +1,314 @@ +diff --git a/docs/configuration_file.md b/docs/configuration_file.md +index 3ea34af..5876c9a 100644 +--- a/docs/configuration_file.md ++++ b/docs/configuration_file.md +@@ -110,3 +110,53 @@ Some options are also mandatory. + * `memory_limit` (*optional*, `string`) — memory limit to apply to build (for more info, see [documentation for resources](https://github.com/projectatomic/osbs-client/blob/master/docs/resource.md) + + * `storage_limit` (*optional*, `string`) — storage limit to apply to build (for more info, see [documentation for resources](https://github.com/projectatomic/osbs-client/blob/master/docs/resource.md) ++ ++ ++## Build JSON Templates ++ ++In the `build_json_dir` there must be `prod.json` and `prod_inner.json` which ++defines the [OpenShift Build](https://docs.openshift.org/latest/dev_guide/builds.html) ++specification that will be used to enable the specific ++[atomic-reactor](https://github.com/projectatomic/atomic-reactor) plugins that ++should be enabled and the config values for each. ++ ++There is also a third file that is optional that can exist along side the ++previous two in `build_json_dir` which is `prod_customize.json` and it will ++provide the ability to set site-specific customizations such as removing, ++plugins, adding plugins, or overriding arguments passed to existing plugins. ++ ++The syntax of `prod_customize.json` is as follows: ++ ++```json ++{ ++ "disable_plugins": [ ++ { ++ "plugin_type": "foo_type", ++ "plugin_name": "foo_name" ++ }, ++ { ++ "plugin_type": "bar_type", ++ "plugin_name": "bar_name" ++ } ++ ], ++ ++ "enable_plugins": [ ++ { ++ "plugin_type": "foo_type", ++ "plugin_name": "foo_name", ++ "plugin_args": { ++ "foo_arg1": "foo_value1", ++ "foo_arg2": "foo_value2" ++ } ++ } ++ ] ++} ++``` ++ ++Such that: ++ ++* `disable_plugins` will define a list of lists that define the plugin type of the plugin that is to be removed (`prebuild_plugins`, `prepublish_plugins`, `postbuild_plugins`, `exit_plugins`) and the name of the plugin. ++ ++* `enable_plugins` is used to add plugins or modify already enabled plugins by overriding args passed to the plugin, these must be defined as key-value pairs as illustrated above. It should be noted that plugins added here will be executed at the end of the list of plugins in that particular `plugin_type` (`prebuild_plugins`, `prepublish_plugins`, `postbuild_plugins`, `exit_plugins`). ++ ++ +diff --git a/inputs/prod_customize.json b/inputs/prod_customize.json +new file mode 100644 +index 0000000..e874211 +--- /dev/null ++++ b/inputs/prod_customize.json +@@ -0,0 +1,7 @@ ++{ ++ "disable_plugins": [ ++ ], ++ ++ "enable_plugins": [ ++ ] ++} +diff --git a/osbs/build/build_request.py b/osbs/build/build_request.py +index a890d0b..aeceeaa 100644 +--- a/osbs/build/build_request.py ++++ b/osbs/build/build_request.py +@@ -24,7 +24,7 @@ except ImportError: + + from osbs.build.manipulate import DockJsonManipulator + from osbs.build.spec import BuildSpec +-from osbs.constants import SECRETS_PATH, DEFAULT_OUTER_TEMPLATE, DEFAULT_INNER_TEMPLATE ++from osbs.constants import SECRETS_PATH, DEFAULT_OUTER_TEMPLATE, DEFAULT_INNER_TEMPLATE, DEFAULT_CUSTOMIZE_CONF + from osbs.exceptions import OsbsException, OsbsValidationException + from osbs.utils import looks_like_git_hash, git_repo_humanish_part_from_uri + +@@ -46,6 +46,7 @@ class BuildRequest(object): + self.build_json = None # rendered template + self._template = None # template loaded from filesystem + self._inner_template = None # dock json ++ self._customize_conf = None # site customize conf for _inner_template + self._dj = None + self._resource_limits = None + self._openshift_required_version = parse_version('1.0.6') +@@ -136,6 +137,20 @@ class BuildRequest(object): + return self._inner_template + + @property ++ def customize_conf(self): ++ if self._customize_conf is None: ++ path = os.path.join(self.build_json_store, DEFAULT_CUSTOMIZE_CONF) ++ logger.debug("loading customize conf from path %s", path) ++ try: ++ with open(path, "r") as fp: ++ self._customize_conf= json.load(fp) ++ except IOError: ++ # File not found, which is perfectly fine. Set to empty string ++ self._customize_conf = {} ++ ++ return self._customize_conf ++ ++ @property + def dj(self): + if self._dj is None: + self._dj = DockJsonManipulator(self.template, self.inner_template) +@@ -649,10 +664,60 @@ class BuildRequest(object): + self.dj.dock_json_set_arg('postbuild_plugins', 'import_image', + 'insecure_registry', True) + ++ def render_customizations(self): ++ """ ++ Customize prod_inner for site specific customizations ++ """ ++ ++ disable_plugins = self.customize_conf.get('disable_plugins', []) ++ if not disable_plugins: ++ logger.debug("No site-specific plugins to disable") ++ else: ++ for plugin_dict in disable_plugins: ++ try: ++ self.dj.remove_plugin( ++ plugin_dict['plugin_type'], ++ plugin_dict['plugin_name'] ++ ) ++ logger.debug( ++ "site-specific plugin disabled -> Type:{0} Name:{1}".format( ++ plugin_dict['plugin_type'], ++ plugin_dict['plugin_name'] ++ ) ++ ) ++ except KeyError: ++ # Malformed config ++ logger.debug("Invalid custom configuration found for disable_plugins") ++ ++ enable_plugins = self.customize_conf.get('enable_plugins', []) ++ if not enable_plugins: ++ logger.debug("No site-specific plugins to enable") ++ else: ++ for plugin_dict in enable_plugins: ++ try: ++ self.dj.add_plugin( ++ plugin_dict['plugin_type'], ++ plugin_dict['plugin_name'], ++ plugin_dict['plugin_args'] ++ ) ++ logger.debug( ++ "site-specific plugin enabled -> Type:{0} Name:{1} Args: {2}".format( ++ plugin_dict['plugin_type'], ++ plugin_dict['plugin_name'], ++ plugin_dict['plugin_args'] ++ ) ++ ) ++ except KeyError: ++ # Malformed config ++ logger.debug("Invalid custom configuration found for enable_plugins") ++ ++ + def render(self, validate=True): + if validate: + self.spec.validate() + ++ self.render_customizations() ++ + # !IMPORTANT! can't be too long: https://github.com/openshift/origin/issues/733 + self.template['metadata']['name'] = self.spec.name.value + self.render_resource_limits() +diff --git a/osbs/build/manipulate.py b/osbs/build/manipulate.py +index a6b64cf..a0fd1b8 100644 +--- a/osbs/build/manipulate.py ++++ b/osbs/build/manipulate.py +@@ -50,6 +50,21 @@ class DockJsonManipulator(object): + self.dock_json[plugin_type].remove(p) + break + ++ def add_plugin(self, plugin_type, plugin_name, args_dict): ++ """ ++ if config has plugin, override it, else add it ++ """ ++ ++ plugin_modified = False ++ ++ for plugin in self.dock_json[plugin_type]: ++ if plugin['name'] == plugin_name: ++ plugin['args'] = args_dict ++ plugin_modified = True ++ ++ if not plugin_modified: ++ self.dock_json[plugin_type].append({"name": plugin_name, "args": args_dict}) ++ + def dock_json_has_plugin_conf(self, plugin_type, plugin_name): + """ + Check whether a plugin is configured. +diff --git a/osbs/constants.py b/osbs/constants.py +index 649626c..282f002 100644 +--- a/osbs/constants.py ++++ b/osbs/constants.py +@@ -18,6 +18,7 @@ DEFAULT_CONFIGURATION_FILE = "/etc/osbs.conf" + DEFAULT_CONFIGURATION_SECTION = "default" + DEFAULT_OUTER_TEMPLATE = "prod.json" + DEFAULT_INNER_TEMPLATE = "prod_inner.json" ++DEFAULT_CUSTOMIZE_CONF = "prod_customize.json" + GENERAL_CONFIGURATION_SECTION = "general" + POD_FINISHED_STATES = ["failed", "succeeded"] + POD_FAILED_STATES = ["failed"] +diff --git a/tests/build/test_build_request.py b/tests/build/test_build_request.py +index add97d3..96c642e 100644 +--- a/tests/build/test_build_request.py ++++ b/tests/build/test_build_request.py +@@ -1348,3 +1348,97 @@ class TestBuildRequest(object): + pull_base_image_plugin = get_plugin( + plugins, 'prebuild_plugins', 'pull_base_image') + assert pull_base_image_plugin is not None ++ ++ def test_render_prod_custom_site_plugin_enable(self): ++ """ ++ Test to make sure that when we attempt to enable a plugin, it is ++ actually enabled in the JSON for the build_request after running ++ build_request.render() ++ """ ++ ++ plugin_type = "exit_plugins" ++ plugin_name = "testing_exit_plugin" ++ plugin_args = {"foo": "bar"} ++ ++ build_request = BuildRequest(INPUTS_PATH) ++ build_request.customize_conf['enable_plugins'].append( ++ { ++ "plugin_type": plugin_type, ++ "plugin_name": plugin_name, ++ "plugin_args": plugin_args ++ } ++ ) ++ kwargs = get_sample_prod_params() ++ build_request.set_params(**kwargs) ++ build_request.render() ++ ++ assert { ++ "name": plugin_name, ++ "args": plugin_args ++ } in build_request.dj.dock_json[plugin_type] ++ ++ def test_render_prod_custom_site_plugin_disable(self): ++ """ ++ Test to make sure that when we attempt to disable a plugin, it is ++ actually disabled in the JSON for the build_request after running ++ build_request.render() ++ """ ++ ++ plugin_type = "postbuild_plugins" ++ plugin_name = "compress" ++ ++ build_request = BuildRequest(INPUTS_PATH) ++ build_request.customize_conf['disable_plugins'].append( ++ { ++ "plugin_type": plugin_type, ++ "plugin_name": plugin_name ++ } ++ ) ++ kwargs = get_sample_prod_params() ++ build_request.set_params(**kwargs) ++ build_request.render() ++ ++ for plugin in build_request.dj.dock_json[plugin_type]: ++ if plugin['name'] == plugin_name: ++ assert False ++ ++ def test_render_prod_custom_site_plugin_override(self): ++ """ ++ Test to make sure that when we attempt to override a plugin's args, ++ they are actually overridden in the JSON for the build_request ++ after running build_request.render() ++ """ ++ ++ plugin_type = "postbuild_plugins" ++ plugin_name = "compress" ++ plugin_args = {"foo": "bar"} ++ ++ kwargs = get_sample_prod_params() ++ ++ unmodified_build_request = BuildRequest(INPUTS_PATH) ++ unmodified_build_request.set_params(**kwargs) ++ unmodified_build_request.render() ++ ++ for plugin_dict in unmodified_build_request.dj.dock_json[plugin_type]: ++ if plugin_dict['name'] == plugin_name: ++ plugin_index = unmodified_build_request.dj.dock_json[plugin_type].index(plugin_dict) ++ ++ build_request = BuildRequest(INPUTS_PATH) ++ build_request.customize_conf['enable_plugins'].append( ++ { ++ "plugin_type": plugin_type, ++ "plugin_name": plugin_name, ++ "plugin_args": plugin_args ++ } ++ ) ++ build_request.set_params(**kwargs) ++ build_request.render() ++ ++ ++ assert { ++ "name": plugin_name, ++ "args": plugin_args ++ } in build_request.dj.dock_json[plugin_type] ++ ++ assert unmodified_build_request.dj.dock_json[plugin_type][plugin_index]['name'] == plugin_name ++ assert build_request.dj.dock_json[plugin_type][plugin_index]['name'] == plugin_name diff --git a/osbs-client.spec b/osbs-client.spec index e98f02f..a98554a 100644 --- a/osbs-client.spec +++ b/osbs-client.spec @@ -22,16 +22,16 @@ %global with_check 1 %endif -%global commit 5021224746eb0e6ac12c54c397397b629167030b +%global commit 696505ae4da4898a4e3689ef18e1f6791b04ea09 %global shortcommit %(c=%{commit}; echo ${c:0:7}) # set to 0 to create a normal release %global postrelease 0 -%global release 1 +%global release 2 %global osbs_obsolete_vr 0.14-2 Name: osbs-client -Version: 0.32 +Version: 0.33 %if "x%{postrelease}" != "x0" Release: %{release}.%{postrelease}.git.%{shortcommit}%{?dist} %else @@ -51,6 +51,16 @@ Source0: https://github.com/projectatomic/osbs-client/archive/%{commit}/o # Patch0: rewrite-httppy-pycurl-requests-0.30.patch +# Upstream patch submitted to enable koji krb5 auth to be used +# +# https://github.com/projectatomic/osbs-client/pull/482 +Patch1: osbs-client-koji_krb5.patch + +# Upstream patch submitted to enable site specific customizations +# +# https://github.com/projectatomic/osbs-client/pull/475 +Patch2: osbs-client-site-customizations.patch + BuildArch: noarch %if 0%{?with_python3} @@ -168,6 +178,9 @@ This package contains osbs Python 3 bindings. %patch0 -p1 rm tests/test_http.py +%patch1 -p1 +%patch2 -p1 + %if ! 0%{?with_check} # setup now requires pytest-runner which causes the build to fail even when # we're not running checks @@ -234,6 +247,13 @@ LANG=en_US.utf8 py.test-%{python2_version} -vv tests %endif # with_python3 %changelog +* Fri Dec 02 2016 Adam Miller - 0.33-2 +- Patch for koji krb5 +- Patch for site specific customizations + +* Tue Nov 29 2016 Adam Miller - 0.33-1 +- Update to latest upstream + * Thu Oct 13 2016 Adam Miller - 0.32-1 - Update to latest upstream diff --git a/sources b/sources index c6f16c3..a3f88c7 100644 --- a/sources +++ b/sources @@ -1 +1 @@ -4beb395b88a3ee0e39a62fb56b510023 osbs-client-5021224746eb0e6ac12c54c397397b629167030b.tar.gz +bc77cae5c4fe899972b1e557beaff296 osbs-client-696505ae4da4898a4e3689ef18e1f6791b04ea09.tar.gz