From 4663bf4194b105704310cfb8bc12fcdbc4aa5b16 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 19 Jun 2017 17:24:02 -0400 Subject: [PATCH 1/9] Add additional test for tag expiration --- data/model/test/test_tag.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/data/model/test/test_tag.py b/data/model/test/test_tag.py index 8fa0eb852..1e7177310 100644 --- a/data/model/test/test_tag.py +++ b/data/model/test/test_tag.py @@ -1,6 +1,7 @@ import pytest from mock import patch +from time import time from data.database import Image, RepositoryTag, ImageStorage, Repository from data.model.repository import create_repository @@ -112,7 +113,7 @@ def assert_tags(repository, *args): for tag in tags: assert not tag.name in tags_dict assert not tag.hidden - assert not tag.lifetime_end_ts + assert not tag.lifetime_end_ts or tag.lifetime_end_ts > time() tags_dict[tag.name] = tag @@ -145,6 +146,13 @@ def test_list_active_tags(initialized_db): # Make sure they are returned. assert_tags(repository, 'foo', 'bar') + # Mark as a tag as expiring in the far future, and make sure it is still returned. + footag.lifetime_end_ts = footag.lifetime_start_ts + 10000000 + footag.save() + + # Make sure they are returned. + assert_tags(repository, 'foo', 'bar') + # Delete a tag and make sure it isn't returned. footag = delete_tag('devtable', 'somenewrepo', 'foo') footag.lifetime_end_ts -= 4 @@ -159,6 +167,13 @@ def test_list_active_tags(initialized_db): assert_tags(repository, 'foo', 'bar') + # Mark as a tag as expiring in the far future, and make sure it is still returned. + footag.lifetime_end_ts = footag.lifetime_start_ts + 10000000 + footag.save() + + # Make sure they are returned. + assert_tags(repository, 'foo', 'bar') + # "Move" foo by updating it and make sure we don't get duplicates. create_or_update_tag('devtable', 'somenewrepo', 'foo', image2.docker_image_id) assert_tags(repository, 'foo', 'bar') From c5d8b5f86baaa61ace97147913d3c9f5e505fe37 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 19 Jun 2017 19:03:10 -0400 Subject: [PATCH 2/9] Add support for tag expiration based on a `quay.expires-after` label --- endpoints/v2/labelhandlers.py | 36 ++++++++++++++++++++++++++++++++ endpoints/v2/manifest.py | 7 +++---- endpoints/v2/models_interface.py | 8 +++++++ endpoints/v2/models_pre_oci.py | 8 +++++++ test/registry_tests.py | 33 +++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 endpoints/v2/labelhandlers.py diff --git a/endpoints/v2/labelhandlers.py b/endpoints/v2/labelhandlers.py new file mode 100644 index 000000000..d179ff4bc --- /dev/null +++ b/endpoints/v2/labelhandlers.py @@ -0,0 +1,36 @@ +import logging + +from app import app +from endpoints.v2.models_pre_oci import pre_oci_model as model +from util.timedeltastring import convert_to_timedelta + +logger = logging.getLogger(__name__) + +min_expire_sec = convert_to_timedelta(app.config.get('LABELED_EXPIRATION_MINIMUM', '1h')) +max_expire_sec = convert_to_timedelta(app.config.get('LABELED_EXPIRATION_MAXIMUM', '104w')) + +def _expires_after(value, namespace_name, repo_name, digest): + """ Sets the expiration of a manifest based on the quay.expires-in label. """ + try: + timedelta = convert_to_timedelta(value) + except ValueError: + logger.exception('Could not convert %s to timedeltastring for %s/%s@%s', value, namespace_name, + repo_name, digest) + return + + total_seconds = min(max(timedelta.total_seconds(), min_expire_sec.total_seconds()), + max_expire_sec.total_seconds()) + + logger.debug('Labeling manifest %s/%s@%s with expiration of %s', namespace_name, repo_name, + digest, total_seconds) + model.set_manifest_expires_after(namespace_name, repo_name, digest, total_seconds) + + +_LABEL_HANDLES = { + 'quay.expires-after': _expires_after, +} + +def handle_label(key, value, namespace_name, repo_name, digest): + handler = _LABEL_HANDLES.get(key) + if handler is not None: + handler(value, namespace_name, repo_name, digest) diff --git a/endpoints/v2/manifest.py b/endpoints/v2/manifest.py index 5d480472b..d35c46556 100644 --- a/endpoints/v2/manifest.py +++ b/endpoints/v2/manifest.py @@ -17,6 +17,7 @@ from endpoints.v2.errors import ( BlobUnknown, ManifestInvalid, ManifestUnknown, TagInvalid, NameInvalid) from endpoints.v2.models_interface import Label from endpoints.v2.models_pre_oci import data_model as model +from endpoints.v2.labelhandlers import handle_label from image.docker import ManifestException from image.docker.schema1 import DockerSchema1Manifest, DockerSchema1ManifestBuilder from image.docker.schema2 import DOCKER_SCHEMA2_CONTENT_TYPES @@ -188,6 +189,8 @@ def _write_manifest(namespace_name, repo_name, manifest): for key, value in manifest.layers[-1].v1_metadata.labels.iteritems(): media_type = 'application/json' if is_json(value) else 'text/plain' labels.append(Label(key=key, value=value, source_type='manifest', media_type=media_type)) + handle_label(key, value, namespace_name, repo_name, manifest.digest) + model.create_manifest_labels(namespace_name, repo_name, manifest.digest, labels) return repo, storage_map @@ -268,7 +271,3 @@ def _generate_and_store_manifest(namespace_name, repo_name, tag_name): model.create_manifest_and_update_tag(namespace_name, repo_name, tag_name, manifest.digest, manifest.bytes) return manifest - - -def _determine_media_type(value): - media_type_name = 'application/json' if is_json(value) else 'text/plain' diff --git a/endpoints/v2/models_interface.py b/endpoints/v2/models_interface.py index bbfd51b2c..66118a95f 100644 --- a/endpoints/v2/models_interface.py +++ b/endpoints/v2/models_interface.py @@ -256,3 +256,11 @@ class DockerRegistryV2DataInterface(object): Once everything is moved over, this could be in util.registry and not even touch the database. """ pass + + @abstractmethod + def set_manifest_expires_after(self, namespace_name, repo_name, digest, expires_after_sec): + """ + Sets that the manifest with given digest expires after the number of seconds from *now*. + """ + pass + diff --git a/endpoints/v2/models_pre_oci.py b/endpoints/v2/models_pre_oci.py index a241c7259..264bf149d 100644 --- a/endpoints/v2/models_pre_oci.py +++ b/endpoints/v2/models_pre_oci.py @@ -250,6 +250,14 @@ class PreOCIModel(DockerRegistryV2DataInterface): blob_record = model.storage.get_storage_by_uuid(blob.uuid) return model.storage.get_layer_path(blob_record) + def set_manifest_expires_after(self, namespace_name, repo_name, digest, expires_after_sec): + try: + manifest = model.tag.load_manifest_by_digest(namespace_name, repo_name, digest) + manifest.tag.lifetime_end_ts = manifest.tag.lifetime_start_ts + expires_after_sec + manifest.tag.save() + except model.InvalidManifestException: + return + def _docker_v1_metadata(namespace_name, repo_name, repo_image): """ diff --git a/test/registry_tests.py b/test/registry_tests.py index ad6eda27c..5a069e34a 100644 --- a/test/registry_tests.py +++ b/test/registry_tests.py @@ -42,6 +42,7 @@ from image.docker.schema1 import DockerSchema1ManifestBuilder from initdb import wipe_database, initialize_database, populate_database from jsonschema import validate as validate_schema from util.security.registry_jwt import decode_bearer_header +from util.timedeltastring import convert_to_timedelta try: @@ -1535,6 +1536,38 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix self.assertTrue('text/plain' in media_types) self.assertTrue('application/json' in media_types) + def test_expiration_label(self): + # Push a new repo with the latest tag. + images = [{ + 'id': 'someid', + 'config': {'Labels': {'quay.expires-after': '1d'}}, + 'contents': 'somecontent' + }] + + (_, manifests) = self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images) + + self.conduct_api_login('devtable', 'password') + tags = self.conduct('GET', '/api/v1/repository/devtable/newrepo/tag').json() + tag = tags['tags'][0] + + self.assertEqual(tag['end_ts'], tag['start_ts'] + convert_to_timedelta('1d').total_seconds()) + + def test_invalid_expiration_label(self): + # Push a new repo with the latest tag. + images = [{ + 'id': 'someid', + 'config': {'Labels': {'quay.expires-after': 'blahblah'}}, + 'contents': 'somecontent' + }] + + (_, manifests) = self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images) + + self.conduct_api_login('devtable', 'password') + tags = self.conduct('GET', '/api/v1/repository/devtable/newrepo/tag').json() + tag = tags['tags'][0] + + self.assertIsNone(tag.get('end_ts')) + def test_invalid_manifest_type(self): namespace = 'devtable' repository = 'somerepo' From 977539bf08317b2a64eaa6e2bac5c8cabe44fee3 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 21 Jun 2017 17:03:02 -0400 Subject: [PATCH 3/9] Abstract out an expiration status view into its own component --- .../directives/ui/expiration-status-view.css | 23 +++++++++++++ .../directives/ui/service-keys-manager.css | 20 ----------- static/directives/service-keys-manager.html | 16 +++------ .../expiration-status-view.component.html | 10 ++++++ .../expiration-status-view.component.ts | 34 +++++++++++++++++++ .../js/directives/ui/service-keys-manager.js | 21 +++--------- static/js/quay.module.ts | 2 ++ 7 files changed, 78 insertions(+), 48 deletions(-) create mode 100644 static/css/directives/ui/expiration-status-view.css create mode 100644 static/js/directives/ui/expiration-status-view/expiration-status-view.component.html create mode 100644 static/js/directives/ui/expiration-status-view/expiration-status-view.component.ts diff --git a/static/css/directives/ui/expiration-status-view.css b/static/css/directives/ui/expiration-status-view.css new file mode 100644 index 000000000..1bfe18c4f --- /dev/null +++ b/static/css/directives/ui/expiration-status-view.css @@ -0,0 +1,23 @@ +.expiration-status-view-element .expired, .expiration-status-view-element .expired a { + color: #D64456; +} + +.expiration-status-view-element .critical, .expiration-status-view-element .critical a { + color: #F77454; +} + +.expiration-status-view-element .warning, .expiration-status-view-element .warning a { + color: #FCA657; +} + +.expiration-status-view-element .info, .expiration-status-view-element .info a { + color: #2FC98E; +} + +.expiration-status-view-element .no-expiration, .expiration-status-view-element .no-expiration a { + color: #aaa; +} + +.expiration-status-view-element .fa { + margin-right: 6px; +} diff --git a/static/css/directives/ui/service-keys-manager.css b/static/css/directives/ui/service-keys-manager.css index c7b1e54ae..b63b377f3 100644 --- a/static/css/directives/ui/service-keys-manager.css +++ b/static/css/directives/ui/service-keys-manager.css @@ -20,30 +20,10 @@ color: #777; } -.service-keys-manager-element .expired a { - color: #D64456; -} - -.service-keys-manager-element .critical a { - color: #F77454; -} - -.service-keys-manager-element .warning a { - color: #FCA657; -} - -.service-keys-manager-element .info a { - color: #2FC98E; -} - .service-keys-manager-element .rotation { color: #777; } -.service-keys-manager-element .no-expiration { - color: #128E72; -} - .service-keys-manager-element .approval-automatic { font-size: 12px; color: #777; diff --git a/static/directives/service-keys-manager.html b/static/directives/service-keys-manager.html index 1935846dc..cfd82507f 100644 --- a/static/directives/service-keys-manager.html +++ b/static/directives/service-keys-manager.html @@ -113,20 +113,14 @@ - + Automatically rotated - - - - - Expiresd - - - - - Does not expire + + + + diff --git a/static/js/directives/ui/expiration-status-view/expiration-status-view.component.html b/static/js/directives/ui/expiration-status-view/expiration-status-view.component.html new file mode 100644 index 000000000..780381518 --- /dev/null +++ b/static/js/directives/ui/expiration-status-view/expiration-status-view.component.html @@ -0,0 +1,10 @@ + + + + + + + + Never + + \ No newline at end of file diff --git a/static/js/directives/ui/expiration-status-view/expiration-status-view.component.ts b/static/js/directives/ui/expiration-status-view/expiration-status-view.component.ts new file mode 100644 index 000000000..b5f4859a2 --- /dev/null +++ b/static/js/directives/ui/expiration-status-view/expiration-status-view.component.ts @@ -0,0 +1,34 @@ +import { Input, Component, Inject } from 'ng-metadata/core'; +import * as moment from "moment"; + +/** + * A component that displays expiration status. + */ +@Component({ + selector: 'expiration-status-view', + templateUrl: '/static/js/directives/ui/expiration-status-view/expiration-status-view.component.html', +}) +export class ExpirationStatusViewComponent { + @Input('<') public expirationDate: Date; + + private getExpirationInfo(expirationDate): any { + if (!expirationDate) { + return ''; + } + + var expiration_date = moment(expirationDate); + if (moment().isAfter(expiration_date)) { + return {'className': 'expired', 'icon': 'fa-warning'}; + } + + if (moment().add(1, 'week').isAfter(expiration_date)) { + return {'className': 'critical', 'icon': 'fa-warning'}; + } + + if (moment().add(1, 'month').isAfter(expiration_date)) { + return {'className': 'warning', 'icon': 'fa-warning'}; + } + + return {'className': 'info', 'icon': 'fa-clock-o'}; + } +} \ No newline at end of file diff --git a/static/js/directives/ui/service-keys-manager.js b/static/js/directives/ui/service-keys-manager.js index d6a4da38f..636385019 100644 --- a/static/js/directives/ui/service-keys-manager.js +++ b/static/js/directives/ui/service-keys-manager.js @@ -80,32 +80,19 @@ angular.module('quay').directive('serviceKeysManager', function () { return moment(key.created_date).add(key.rotation_duration, 's').format('LLL'); }; - $scope.getExpirationInfo = function(key) { + $scope.willRotate = function(key) { if (!key.expiration_date) { - return ''; + return false; } if (key.rotation_duration) { var rotate_date = moment(key.created_date).add(key.rotation_duration, 's') if (moment().isBefore(rotate_date)) { - return {'className': 'rotation', 'icon': 'fa-refresh', 'willRotate': true}; + return true; } } - expiration_date = moment(key.expiration_date); - if (moment().isAfter(expiration_date)) { - return {'className': 'expired', 'icon': 'fa-warning'}; - } - - if (moment().add(1, 'week').isAfter(expiration_date)) { - return {'className': 'critical', 'icon': 'fa-warning'}; - } - - if (moment().add(1, 'month').isAfter(expiration_date)) { - return {'className': 'warning', 'icon': 'fa-warning'}; - } - - return {'className': 'info', 'icon': 'fa-check'}; + return false; }; $scope.showChangeName = function(key) { diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index 07f697bd7..014f2efec 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -31,6 +31,7 @@ import { MarkdownToolbarComponent } from './directives/ui/markdown/markdown-tool import { MarkdownEditorComponent } from './directives/ui/markdown/markdown-editor.component'; import { DockerfileCommandComponent } from './directives/ui/dockerfile-command/dockerfile-command.component'; import { ImageCommandComponent } from './directives/ui/image-command/image-command.component'; +import { ExpirationStatusViewComponent } from './directives/ui/expiration-status-view/expiration-status-view.component'; import { BrowserPlatform, browserPlatform } from './constants/platform.constant'; import { ManageTriggerComponent } from './directives/ui/manage-trigger/manage-trigger.component'; import { ClipboardCopyDirective } from './directives/ui/clipboard-copy/clipboard-copy.directive'; @@ -74,6 +75,7 @@ import * as Clipboard from 'clipboard'; ImageCommandComponent, TypeaheadDirective, ManageTriggerComponent, + ExpirationStatusViewComponent, ClipboardCopyDirective, TriggerDescriptionComponent, ], From 99d7fde8ee7a6d4c1e6346ca3543a1383d92c5b7 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 21 Jun 2017 21:33:26 -0400 Subject: [PATCH 4/9] Add UI for viewing and changing the expiration of tags --- ...f8f6_add_change_tag_expiration_log_type.py | 25 +++++++ data/model/tag.py | 44 +++++++++++- data/model/test/test_tag.py | 36 +++++++++- endpoints/api/repository.py | 8 ++- endpoints/api/tag.py | 67 ++++++++++++++----- endpoints/api/test/test_tag.py | 37 ++++++++++ endpoints/api/user.py | 2 +- initdb.py | 2 + .../directives/repo-view/repo-panel-tags.html | 34 +++++++++- static/directives/tag-operations-dialog.html | 22 ++++++ .../directives/repo-view/repo-panel-tags.js | 7 +- static/js/directives/ui/logs-view.js | 13 ++++ .../js/directives/ui/tag-operations-dialog.js | 58 +++++++++++++++- 13 files changed, 329 insertions(+), 26 deletions(-) create mode 100644 data/migrations/versions/d8989249f8f6_add_change_tag_expiration_log_type.py diff --git a/data/migrations/versions/d8989249f8f6_add_change_tag_expiration_log_type.py b/data/migrations/versions/d8989249f8f6_add_change_tag_expiration_log_type.py new file mode 100644 index 000000000..9ae981008 --- /dev/null +++ b/data/migrations/versions/d8989249f8f6_add_change_tag_expiration_log_type.py @@ -0,0 +1,25 @@ +"""Add change_tag_expiration log type + +Revision ID: d8989249f8f6 +Revises: dc4af11a5f90 +Create Date: 2017-06-21 21:18:25.948689 + +""" + +# revision identifiers, used by Alembic. +revision = 'd8989249f8f6' +down_revision = 'dc4af11a5f90' + +from alembic import op + +def upgrade(tables): + op.bulk_insert(tables.logentrykind, [ + {'name': 'change_tag_expiration'}, + ]) + + +def downgrade(tables): + op.execute(tables + .logentrykind + .delete() + .where(tables.logentrykind.c.name == op.inline_literal('change_tag_expiration'))) diff --git a/data/model/tag.py b/data/model/tag.py index 24926174b..57faea80c 100644 --- a/data/model/tag.py +++ b/data/model/tag.py @@ -1,13 +1,17 @@ import logging +import time +from calendar import timegm from uuid import uuid4 from peewee import IntegrityError, JOIN_LEFT_OUTER, fn from data.model import (image, db_transaction, DataModelException, _basequery, - InvalidManifestException, TagAlreadyCreatedException, StaleTagException) + InvalidManifestException, TagAlreadyCreatedException, StaleTagException, + config) from data.database import (RepositoryTag, Repository, Image, ImageStorage, Namespace, TagManifest, RepositoryNotification, Label, TagManifestLabel, get_epoch_timestamp, db_for_update) +from util.timedeltastring import convert_to_timedelta logger = logging.getLogger(__name__) @@ -570,3 +574,41 @@ def _load_repo_manifests(namespace, repo_name): .join(Repository) .join(Namespace, on=(Namespace.id == Repository.namespace_user)) .where(Repository.name == repo_name, Namespace.username == namespace)) + + +def change_repository_tag_expiration(namespace_name, repo_name, tag_name, expiration_date): + """ Changes the expiration of the tag with the given name to the given expiration datetime. If + the expiration datetime is None, then the tag is marked as not expiring. + """ + try: + tag = get_active_tag(namespace_name, repo_name, tag_name) + return change_tag_expiration(tag, expiration_date) + except RepositoryTag.DoesNotExist: + return (None, False) + + +def change_tag_expiration(tag, expiration_date): + """ Changes the expiration of the given tag to the given expiration datetime. If + the expiration datetime is None, then the tag is marked as not expiring. + """ + end_ts = None + min_expire_sec = convert_to_timedelta(config.app_config.get('LABELED_EXPIRATION_MINIMUM', '1h')) + max_expire_sec = convert_to_timedelta(config.app_config.get('LABELED_EXPIRATION_MAXIMUM', '104w')) + + if expiration_date is not None: + offset = timegm(expiration_date.utctimetuple()) - tag.lifetime_start_ts + offset = min(max(offset, min_expire_sec.total_seconds()), max_expire_sec.total_seconds()) + end_ts = tag.lifetime_start_ts + offset + + if end_ts == tag.lifetime_end_ts: + return (None, True) + + # Note: We check not just the ID of the tag but also its lifetime_end_ts, to ensure that it has + # not changed while we were updatings it expiration. + result = (RepositoryTag + .update(lifetime_end_ts=end_ts) + .where(RepositoryTag.id == tag.id, + RepositoryTag.lifetime_end_ts == tag.lifetime_end_ts) + .execute()) + + return (tag.lifetime_end_ts, result > 0) diff --git a/data/model/test/test_tag.py b/data/model/test/test_tag.py index 1e7177310..6cfa7366d 100644 --- a/data/model/test/test_tag.py +++ b/data/model/test/test_tag.py @@ -1,13 +1,16 @@ import pytest +from datetime import datetime from mock import patch from time import time from data.database import Image, RepositoryTag, ImageStorage, Repository from data.model.repository import create_repository from data.model.tag import (list_active_repo_tags, create_or_update_tag, delete_tag, - get_matching_tags, _tag_alive, get_matching_tags_for_images) + get_matching_tags, _tag_alive, get_matching_tags_for_images, + change_tag_expiration, get_active_tag) from data.model.image import find_create_or_link_image +from util.timedeltastring import convert_to_timedelta from test.fixtures import * @@ -177,3 +180,34 @@ def test_list_active_tags(initialized_db): # "Move" foo by updating it and make sure we don't get duplicates. create_or_update_tag('devtable', 'somenewrepo', 'foo', image2.docker_image_id) assert_tags(repository, 'foo', 'bar') + + +@pytest.mark.parametrize('expiration_offset, expected_offset', [ + (None, None), + ('0s', '1h'), + ('30m', '1h'), + ('2h', '2h'), + ('2w', '2w'), + ('200w', '104w'), +]) +def test_change_tag_expiration(expiration_offset, expected_offset, initialized_db): + repository = create_repository('devtable', 'somenewrepo', None) + image1 = find_create_or_link_image('foobarimage1', repository, None, {}, 'local_us') + footag = create_or_update_tag('devtable', 'somenewrepo', 'foo', image1.docker_image_id) + + expiration_date = None + if expiration_offset is not None: + expiration_date = datetime.utcnow() + convert_to_timedelta(expiration_offset) + + assert change_tag_expiration(footag, expiration_date) + + # Lookup the tag again. + footag_updated = get_active_tag('devtable', 'somenewrepo', 'foo') + + if expected_offset is None: + assert footag_updated.lifetime_end_ts is None + else: + start_date = datetime.utcfromtimestamp(footag_updated.lifetime_start_ts) + end_date = datetime.utcfromtimestamp(footag_updated.lifetime_end_ts) + expected_end_date = start_date + convert_to_timedelta(expected_offset) + assert end_date == expected_end_date diff --git a/endpoints/api/repository.py b/endpoints/api/repository.py index eb740ef44..f8f41de44 100644 --- a/endpoints/api/repository.py +++ b/endpoints/api/repository.py @@ -334,6 +334,10 @@ class Repository(RepositoryParamResource): last_modified = format_date(datetime.fromtimestamp(tag.lifetime_start_ts)) tag_info['last_modified'] = last_modified + if tag.lifetime_end_ts: + expiration = format_date(datetime.fromtimestamp(tag.lifetime_end_ts)) + tag_info['expiration'] = expiration + if tag.tagmanifest is not None: tag_info['manifest_digest'] = tag.tagmanifest.digest @@ -498,11 +502,11 @@ class RepositoryTrust(RepositoryParamResource): repo = model.repository.get_repository(namespace, repository) if not repo: raise NotFound() - + tags, _ = tuf_metadata_api.get_default_tags_with_expiration(namespace, repository) if tags and not tuf_metadata_api.delete_metadata(namespace, repository): raise DownstreamIssue({'message': 'Unable to delete downstream trust metadata'}) - + values = request.get_json() model.repository.set_trust(repo, values['trust_enabled']) diff --git a/endpoints/api/tag.py b/endpoints/api/tag.py index 8e65257a5..42e375de2 100644 --- a/endpoints/api/tag.py +++ b/endpoints/api/tag.py @@ -1,5 +1,6 @@ """ Manage the tags of a repository. """ +from datetime import datetime, timedelta from flask import request, abort from auth.auth_context import get_authenticated_user @@ -54,12 +55,15 @@ class RepositoryTag(RepositoryParamResource): schemas = { 'MoveTag': { 'type': 'object', - 'description': 'Description of to which image a new or existing tag should point', - 'required': ['image',], + 'description': 'Makes changes to a specific tag', 'properties': { 'image': { - 'type': 'string', - 'description': 'Image identifier to which the tag should point', + 'type': ['string', 'null'], + 'description': '(If specified) Image identifier to which the tag should point', + }, + 'image': { + 'type': ['number', 'null'], + 'description': '(If specified) The expiration for the image', }, }, }, @@ -75,25 +79,52 @@ class RepositoryTag(RepositoryParamResource): if not TAG_REGEX.match(tag): abort(400, TAG_ERROR) - image_id = request.get_json()['image'] - repo = model.get_repo(namespace, repository, image_id) + repo = model.get_repo(namespace, repository) if not repo: raise NotFound() - original_image_id = model.get_repo_tag_image(repo, tag) - model.create_or_update_tag(namespace, repository, tag, image_id) + if 'expiration' in request.get_json(): + expiration = request.get_json().get('expiration') + expiration_date = None + if expiration is not None: + try: + expiration_date = datetime.utcfromtimestamp(float(expiration)) + except ValueError: + abort(400) - username = get_authenticated_user().username - log_action('move_tag' if original_image_id else 'create_tag', namespace, { - 'username': username, - 'repo': repository, - 'tag': tag, - 'namespace': namespace, - 'image': image_id, - 'original_image': original_image_id - }, repo_name=repository) + if expiration_date <= datetime.now(): + abort(400) - _generate_and_store_manifest(namespace, repository, tag) + existing_end_ts, ok = model.change_repository_tag_expiration(namespace, repository, tag, + expiration_date) + if ok: + if not (existing_end_ts is None and expiration_date is None): + log_action('change_tag_expiration', namespace, { + 'username': get_authenticated_user().username, + 'repo': repository, + 'tag': tag, + 'namespace': namespace, + 'expiration_date': expiration_date, + 'old_expiration_date': existing_end_ts + }, repo=repo) + else: + abort(400, 'Could not update tag expiration; Tag has probably changed') + + if 'image' in request.get_json(): + image_id = request.get_json()['image'] + original_image_id = model.get_repo_tag_image(repo, tag) + model.create_or_update_tag(namespace, repository, tag, image_id) + + username = get_authenticated_user().username + log_action('move_tag' if original_image_id else 'create_tag', namespace, { + 'username': username, + 'repo': repository, + 'tag': tag, + 'namespace': namespace, + 'image': image_id, + 'original_image': original_image_id + }, repo_name=repository) + _generate_and_store_manifest(namespace, repository, tag) return 'Updated', 201 diff --git a/endpoints/api/test/test_tag.py b/endpoints/api/test/test_tag.py index 1d13d0eae..abc0adf9e 100644 --- a/endpoints/api/test/test_tag.py +++ b/endpoints/api/test/test_tag.py @@ -124,6 +124,43 @@ def find_no_repo_tag_history(): yield +@pytest.mark.parametrize('expiration_time, expected_status', [ + (None, 201), + ('aksdjhasd', 400), +]) +def test_change_tag_expiration_default(expiration_time, expected_status, client, app): + with client_with_identity('devtable', client) as cl: + params = { + 'repository': 'devtable/simple', + 'tag': 'latest', + } + + request_body = { + 'expiration': expiration_time, + } + + conduct_api_call(cl, RepositoryTag, 'put', params, request_body, expected_status) + + +def test_change_tag_expiration(client, app): + with client_with_identity('devtable', client) as cl: + params = { + 'repository': 'devtable/simple', + 'tag': 'latest', + } + + tag = model.tag.get_active_tag('devtable', 'simple', 'latest') + updated_expiration = tag.lifetime_start_ts + 60*60*24 + + request_body = { + 'expiration': updated_expiration, + } + + conduct_api_call(cl, RepositoryTag, 'put', params, request_body, 201) + tag = model.tag.get_active_tag('devtable', 'simple', 'latest') + assert tag.lifetime_end_ts == updated_expiration + + @pytest.mark.parametrize('test_image,test_tag,expected_status', [ ('image1', '-INVALID-TAG-NAME', 400), ('image1', '.INVALID-TAG-NAME', 400), diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 093824e13..f359c16d5 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -20,7 +20,7 @@ from auth.permissions import (AdministerOrganizationPermission, CreateRepository UserAdminPermission, UserReadPermission, SuperUserPermission) from data import model from data.billing import get_plan -from data.database import Repository as RepositoryTable, UserPromptTypes +from data.database import Repository as RepositoryTable from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, log_action, internal_only, require_user_admin, parse_args, query_param, require_scope, format_date, show_if, diff --git a/initdb.py b/initdb.py index 387cc50f8..c6b36e18e 100644 --- a/initdb.py +++ b/initdb.py @@ -350,6 +350,8 @@ def initialize_database(): LogEntryKind.create(name='manifest_label_add') LogEntryKind.create(name='manifest_label_delete') + LogEntryKind.create(name='change_tag_expiration') + ImageStorageLocation.create(name='local_eu') ImageStorageLocation.create(name='local_us') diff --git a/static/directives/repo-view/repo-panel-tags.html b/static/directives/repo-view/repo-panel-tags.html index 0e572a568..20fe43daa 100644 --- a/static/directives/repo-view/repo-panel-tags.html +++ b/static/directives/repo-view/repo-panel-tags.html @@ -57,6 +57,12 @@ Delete Tags +
  • + + Change Tags Expiration + +
  • @@ -105,6 +111,11 @@ style="width: 80px;"> Size + + Expires + @@ -133,12 +144,16 @@ ng-if="repository.trust_enabled"> + + Unknown + + + + + + + + + + + + + + @@ -254,6 +282,10 @@ ng-class="repository.tag_operations_disabled ? 'disabled-option' : ''"> Delete Tag
    + + Change Expiration + @@ -261,7 +293,7 @@ - +
    diff --git a/static/directives/tag-operations-dialog.html b/static/directives/tag-operations-dialog.html index 5f5f7b2be..e6a4ef480 100644 --- a/static/directives/tag-operations-dialog.html +++ b/static/directives/tag-operations-dialog.html @@ -111,6 +111,28 @@ + +
    +
    + +
      +
    • + {{ tag_info.name }} +
    • +
    + + + + + If specified, the date and time that the key expires. If set to none, the tag(s) will not expire. + +
    +
    +
    = count) { + callback(true); + markChanged(tags, []); + return; + } + + var tag_info = tags[index]; + if (!tag_info) { return; } + + $scope.changeTagExpiration(tag_info.name, expiration_date, function(result) { + if (!result) { + callback(false); + return; + } + + perform(index + 1); + }, true); + }; + + perform(0); + }; + + $scope.changeTagExpiration = function(tag, expiration_date, callback) { + if (!$scope.repository.can_write) { return; } + + var params = { + 'repository': $scope.repository.namespace + '/' + $scope.repository.name, + 'tag': tag + }; + + var data = { + 'expiration': expiration_date + }; + + var errorHandler = ApiService.errorDisplay('Cannot change tag expiration', callback); + ApiService.changeTag(data, params).then(function() { + callback(true); + }, errorHandler); + }; + $scope.deleteMultipleTags = function(tags, callback) { if (!$scope.repository.can_write) { return; } @@ -296,6 +341,17 @@ angular.module('quay').directive('tagOperationsDialog', function () { }, ApiService.errorDisplay('Could not load manifest labels')); }, + 'askChangeTagsExpiration': function(tags) { + if ($scope.alertOnTagOpsDisabled()) { + return; + } + + $scope.changeTagsExpirationInfo ={ + 'tags': tags, + 'expiration_date': null + }; + }, + 'askRestoreTag': function(tag, image_id, opt_manifest_digest) { if ($scope.alertOnTagOpsDisabled()) { return; From 7d4fed689215d6138098eadf7def5f301e37561d Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 21 Jun 2017 21:58:16 -0400 Subject: [PATCH 5/9] Change error message when trying to pull a deleted or expired tag Will let the users know they can recover the tag via time machine Note: This was tested with the Docker protocol, but the new error code is *technically* out of spec; we should make sure its okay. --- data/model/tag.py | 9 +++++++++ endpoints/v2/errors.py | 7 +++++++ endpoints/v2/manifest.py | 14 +++++++++++--- endpoints/v2/models_pre_oci.py | 7 +++++++ .../expiration-status-view.component.css} | 0 .../expiration-status-view.component.ts | 9 +++++---- test/data/test.db | Bin 1687552 -> 1679360 bytes 7 files changed, 39 insertions(+), 7 deletions(-) rename static/{css/directives/ui/expiration-status-view.css => js/directives/ui/expiration-status-view/expiration-status-view.component.css} (100%) diff --git a/data/model/tag.py b/data/model/tag.py index 57faea80c..d62ee2bcb 100644 --- a/data/model/tag.py +++ b/data/model/tag.py @@ -530,6 +530,15 @@ def get_active_tag(namespace, repo_name, tag_name): Namespace.username == namespace)).get() +def get_possibly_expired_tag(namespace, repo_name, tag_name): + return (RepositoryTag + .select() + .join(Repository) + .join(Namespace, on=(Repository.namespace_user == Namespace.id)) + .where(RepositoryTag.name == tag_name, Repository.name == repo_name, + Namespace.username == namespace)).get() + + def associate_generated_tag_manifest(namespace, repo_name, tag_name, manifest_digest, manifest_data): tag = get_active_tag(namespace, repo_name, tag_name) diff --git a/endpoints/v2/errors.py b/endpoints/v2/errors.py index 0ae998106..40d7b9529 100644 --- a/endpoints/v2/errors.py +++ b/endpoints/v2/errors.py @@ -57,6 +57,13 @@ class ManifestUnknown(V2RegistryException): def __init__(self, detail=None): super(ManifestUnknown, self).__init__('MANIFEST_UNKNOWN', 'manifest unknown', detail, 404) +class TagExpired(V2RegistryException): + def __init__(self, message=None, detail=None): + super(TagExpired, self).__init__('TAG_EXPIRED', + message or 'Tag has expired', + detail, + 404) + class ManifestUnverified(V2RegistryException): def __init__(self, detail=None): diff --git a/endpoints/v2/manifest.py b/endpoints/v2/manifest.py index d35c46556..6a1fc801d 100644 --- a/endpoints/v2/manifest.py +++ b/endpoints/v2/manifest.py @@ -13,10 +13,11 @@ from endpoints.common import parse_repository_name from endpoints.decorators import anon_protect from endpoints.notificationhelper import spawn_notification from endpoints.v2 import v2_bp, require_repo_read, require_repo_write -from endpoints.v2.errors import ( - BlobUnknown, ManifestInvalid, ManifestUnknown, TagInvalid, NameInvalid) from endpoints.v2.models_interface import Label from endpoints.v2.models_pre_oci import data_model as model +from endpoints.v2.errors import (BlobUnknown, ManifestInvalid, ManifestUnknown, TagInvalid, + NameInvalid, TagExpired) +>>>>>>> Change error message when trying to pull a deleted or expired tag from endpoints.v2.labelhandlers import handle_label from image.docker import ManifestException from image.docker.schema1 import DockerSchema1Manifest, DockerSchema1ManifestBuilder @@ -43,7 +44,14 @@ def fetch_manifest_by_tagname(namespace_name, repo_name, manifest_ref): if manifest is None: has_tag = model.has_active_tag(namespace_name, repo_name, manifest_ref) if not has_tag: - raise ManifestUnknown() + has_expired_tag = model.has_tag(namespace_name, repo_name, manifest_ref) + if has_expired_tag: + logger.debug('Found expired tag %s for repository %s/%s', manifest_ref, namespace_name, + repo_name) + msg = 'Tag %s was deleted or has expired. To pull, revive via time machine' % manifest_ref + raise TagExpired(msg) + else: + raise ManifestUnknown() manifest = _generate_and_store_manifest(namespace_name, repo_name, manifest_ref) if manifest is None: diff --git a/endpoints/v2/models_pre_oci.py b/endpoints/v2/models_pre_oci.py index 264bf149d..fa2e7dc49 100644 --- a/endpoints/v2/models_pre_oci.py +++ b/endpoints/v2/models_pre_oci.py @@ -37,6 +37,13 @@ class PreOCIModel(DockerRegistryV2DataInterface): except database.RepositoryTag.DoesNotExist: return False + def has_tag(self, namespace_name, repo_name, tag_name): + try: + model.tag.get_possibly_expired_tag(namespace_name, repo_name, tag_name) + return True + except database.RepositoryTag.DoesNotExist: + return False + def get_manifest_by_tag(self, namespace_name, repo_name, tag_name): try: manifest = model.tag.load_tag_manifest(namespace_name, repo_name, tag_name) diff --git a/static/css/directives/ui/expiration-status-view.css b/static/js/directives/ui/expiration-status-view/expiration-status-view.component.css similarity index 100% rename from static/css/directives/ui/expiration-status-view.css rename to static/js/directives/ui/expiration-status-view/expiration-status-view.component.css diff --git a/static/js/directives/ui/expiration-status-view/expiration-status-view.component.ts b/static/js/directives/ui/expiration-status-view/expiration-status-view.component.ts index b5f4859a2..e3a5bd4a6 100644 --- a/static/js/directives/ui/expiration-status-view/expiration-status-view.component.ts +++ b/static/js/directives/ui/expiration-status-view/expiration-status-view.component.ts @@ -1,5 +1,6 @@ import { Input, Component, Inject } from 'ng-metadata/core'; import * as moment from "moment"; +import './expiration-status-view.component.css'; /** * A component that displays expiration status. @@ -16,16 +17,16 @@ export class ExpirationStatusViewComponent { return ''; } - var expiration_date = moment(expirationDate); - if (moment().isAfter(expiration_date)) { + var expiration = moment(expirationDate); + if (moment().isAfter(expiration)) { return {'className': 'expired', 'icon': 'fa-warning'}; } - if (moment().add(1, 'week').isAfter(expiration_date)) { + if (moment().add(1, 'week').isAfter(expiration)) { return {'className': 'critical', 'icon': 'fa-warning'}; } - if (moment().add(1, 'month').isAfter(expiration_date)) { + if (moment().add(1, 'month').isAfter(expiration)) { return {'className': 'warning', 'icon': 'fa-warning'}; } diff --git a/test/data/test.db b/test/data/test.db index 2dde19399bb4c06e10d48ae38768671c4dfcd86e..e109172d6a86edc69ff686a6a5063c939d785ec6 100644 GIT binary patch delta 73307 zcmeFad6;EYbtqnA*RAfVy4BTPU0ppE&;*b^#hC}D?mN%(d=)Ndyz@L?l-np02S8#a zo+SEHKt)sr5$ys+5XWdRU=oeV)90L+I1yuf?>X_-y)}1rH|_J|uOEJXMc=*GUT2-X z*IIk;HJwwZlBYhc+eq&B$zYBO?|4~$f25$R;;eiFK`kVL7x|cMMp&IRt_Qz*$8zR+ve*i6aLCfB6 zeYkO$4wsis{Ih#$@5fLLm3YVR>`(0Y3-zY^0pzct1vNkaxZxdUqL>xb` z22JBMMvAP25CV=PqzJFa;Q%ct1jP|Fhby$QG;|u(>^m}JIP$B1z2WWOhKKE68;TGE+QljfSgFv2+Si&(yU04)S$T3c^uU&{(AJF?sNG&)JK75-u?utIq>?!KfLtN z-6z$F1`vAt6X@8Endn9=8kDbuk!wIOn?34(u1G8MRP>F&-kjh zzV`D=ANiJh>C}_Jen(F{Wxw$kUtfCs$y+r0{`xncIQsrCd{w>Z-+}Y)_|#56Nl6MP zkuoBX1S~qoDG0AH3_{B^uaG3c^cH<|jX(lNMO;ET1f&F@X$m-u#|1=DV4g8ikrk3# z`q8IQ&EZczvHyyD(r;Q?c)%N3ih@~GseY|TPG-Gq&yHNCy?w+xHM?Wa;HSsNr@l9I&De`O-!}P^VQf&{`S{qY$G$%CtC9P5 zz47|7hXxB1?;HJ(vD-BNJ^1fKGulVSEtBOP%Tw1+T8HC){=DjkJfqHii#t zG`{g4k3O^OH3RU!%ZEdQ>dDO9rI+{qMT$Hoa6FGFJcA-63v6*NA`v_+1;a_NR)3tILzX`G%=<1Sfy-q^WD$`^ z1z0qiMFd$;5K%@IOj2Z6HaVpFUOk6A_KDAs!!ALQY2bGpAtX@(IwS!RFib=wiXwQP zA_#%RPxJ_8MUKTqMFDxDIAB*w0p$VAfuS-kle7r@N-bUUg~dxj_b@1^3q+Je5&%&V z5d_c`gdi{gr4S;`U8^3=%_B>n{K6HNf>Pjd92lEJaUg)>3D8V1B_a)K0w)z;Qg*pFiOE>5k)Wpln;r@ zG$;f`01Zh{n2cd0i4$mlRZjhn*<(xh{mtT~EY6`U3VKFlBn7krtPij}{6qzY6$yr> zKv^t(=(#JFj(q8XV+<>xM2`#^9uBl%6ug84vdpkDL4n!x=q?pY0=ND7-`(uK5EZzihxbCX->6#j z&t0)}`*W{SM{{$RAaFcs^u}J$TE71I)6Wl%pu^YfeCuC~4b_KszV)%)H%;z5u>YFj z0Y*&mg`}LQ@yUeTDy1uYEnO@uzklaYV)V#0JMS3K{)2W|`|sNKPE~jP)6{=Wj89!L z^^K`JrtaH$$MVATP-f?W`C(@8tVpg|zW=~bd~|FQ6xe~m*Q#Up%v12-lY>{hUVC8Y zz~K16pl;v|iMa_J?QQUIR3a8I;%bz-nqn3~3lJ2gzW?6&&tA{tk|GE^XcvV631Ru( z=>U@vg(GP}lzE2YrHfzZnFoY}1B1jsetbl;juTh!zi-~P_pI2z`o8%C>a+LF?@`J3 z&hI-r%&GUyU#{N%-ucVV-VG>@sNZ?-{MBdghS683k@wBdp0oQp=)t&cFAjK5^m^EB zJ$-_l@1MVH_H5lZtL0D5&#U*of1d6QXSNLo#Ix_XfBwp|_nx@_T;tw+|NLcVA5;3! z{9*N-ADFj{Q8a;J6r*1Bq4{HK-~;n>f6#!*2hKa7`_TNjdgBB0*Nm|kK{IQQ`P~20 zsGC37f7V$z4&J6V#=)s&^$FL&J6r(Ruje>BB1X@O<~2b8=Yyw}+dbp~YpFcjy-@?YDSlcB*eo40Tt= zO?_m~VfE6RhUOmIeG{Wy!P+bi-8p<<;(O{?X{e`$?w?Rmmv(1&GpTE8=fK=BF}R^Y z7BKc|6ovoK9~stdv~SZ^`_JD!ced{ChO*mtw*HOo+4Fbr-Q2xhq3qexy}_g1RZTl{ zj{eo|-CMf1X_e{ocWckx-R-IQsdM-Dx+l-yJ#qf-UFYrYQad;IZ)f`P^LJ}DcW;;d zvGaHDIMUt3QQNik=$7HzrhQ~{_jb~U&)+?Ccvv@n(K?&2v-7IJQhiyScv2lZHFRY8 zx>G|h?tERIkwM+yc{cj}F9k5S6`;HQZTD@3 z*bXrJQUH6m0-QyN)&aWx1IzaHo>_-D2f*$#0Jg2^^h*I~djQ>y$=uF0Q%6U}H}ur@ z{>hiT}MZ{=Y7D=EeN)+#Q00}Yqs=npXITm-7Rmpq0Uy-)sC$QwoUTrOZ1Oy z>EHeW!{(O&7&-z3j;~9?Ib`c|>e$^w$g=b9q5r)iTiv500~;&1{kaGBj|`lBXL^Z& zEAq7=n(G9uZFzHj1l{cvuK}!x*Lg6kh}U@l_HLQ8vq;w&Y_)AwSA=W*3ETN%_tqzD zTlVRf=-0yhpO>*;ZprF`Xq}gUmszs9)Yw)6wynX-D_J&*8s0*{b`iKR$%0+$p8hfK z6ddzDJT%%p&)##Ozin;0F`lt)O`cmQR>bN&04v+pB;Gye6?a9h&Vyn_uFeCnvTdyc zoJFt}M!K;5Za{2X)fLG)55UT{wE=K8(b}ZBw(U7xO1nivZ3Cb{w9bcsf=%nZ{jh1B zr@u>4AX?`EpePWnP5s*$4>qmy_QR&Nss9Y+)P-X#h}O9QpdUnQbN{v}hE40d{jh0m z>fibT&fYm1MC+UYz5b!2aIzR5+RpA-%Bb1w-pzVD?$X1gy5quF}~$acZnyXQ=hZIU%T zb9RvKCM(zU?lWPwt^L^aMWe8G>zB{%@^g{)r6BfCZGz~6y=^-Zd~y@WSs{ptmx91| zy%Yqo^P4b}ic@e*!NI@6$o)Z;-bHpqd&7YC2JIu- zZ)k6SZ1+trZMSDWzmOLk=>cBI`wjOE@)z(bLzfMQ#*b}%fwR+pdSS<#P8`G;A948lqm8d2CgMVWYh-}I}n1yF`6X>gcfN4ggF35FZh@k9-QH%B8U`C zr3*^2Hz7mXM+SP+0h94wt*=M=g*>KDI=K8_@-e4NJAU%E&1OQ9UF;ccDdzOT$c}~O z9SdXss@-fSTp>QzWcD-*a&j z;N%oo0%8s*9(Ey8#4uVoebd;tuDawx0Pi0A@l~s|Z+*`4i%*X|r#*Yu_nsR&@Udec zFce3DKb}Gb@GN)-WJ2n4ztl8r^4nKZNYGx zU&lsuByHxLW>3r+_uwWD_4y;NI7#XwXxJHZNBtD#pf_*RKqc%aj-|OG3R}$nlk=d}Xm&kSjv5W^o5x1}ARxl2)C; z8n+Oh7-KdVnl{#1=G|Fv|u}-34r&Du%@= ze5O8d>#KI}9SAW4w@zvXG~*hLZg?CxaR8HLf`GUMh?=1wo`T?eerEz)zZ43gB{C~2 z1ie9OL2ec+$x0EZMZKX|*c5Tt?KYU4SdcNrJa)ZVr$YnIt`T#vmS`-XkGT9Ehcjp~ zqjWTCis|EyklE=8V=?R5=a3<28tqYg`7}^VoJlPS3yV^~hTzrh`KWmmVM$nKh-<(& zN+MYnf>i45)auNN#bT~ctSin?#u%n|TO(x59`gGz&SY}BaLj4)JDj-5t7qNr01=E? zbhw8y1&t zgB&;=I8;%ey}Ig3dfKns9t#6!qlFD(Ch%GfdHZPl_3REfzjYwmwMcc;{?Le zunQ18O2KQ&tU!phbS+)3^X)5p|4;|-)a;3z@d$zhM?E~EHVKb7IB;UiBX~^eX>19B zO#t{*z-Wm5kSWk*C`HSZC_!*RE-h5}N?R>GteID*?$qqqkieCffZc>6)t~&SX8xGO zFn|ze5v3PL0DDOfAIBgJMdKJnq6!3KsaL#7gP(~5LkSL3i*M2#=t97gL{I{Opu7z4 z4gm`gA0>&1$YLVwa|9{LY_VRerX?9z@fgk@!|-GAVnfcjQkiz1E;~tsC!Y<*Ds?K( zw3N6h)uR1{vdt9}T(%Ao4u>rbGA5Yog(7~k1|HaKbx|zlSC@EC-{G_(J?J7!F)T)@ z2jAQyOc7`bg)k;i=Ahsy5E71}F$5JPo}eXBf!H&#BJ;IkWtFOIjqxjKw#=2>Njd4# zyVE(5s>gGLy=n@Fv!0;C6lExjWGDQ-tAcJr#A$ad7K-9_s(`3*y8Z1Hd?IGL175L+}KKz!u$$4Pii>8Q@Hr6Ilr9p;3iUfKW+ZBUG1e8f;#3 zh3^=B;d<3pZY&CE53Q4|S@4>5tZ7#?%>{6_Z3~yHaw{vB7M(szp@^N8(lrr57jc3? zDO}CHMROp-gETh74lTS$Vi5bG!1T%WLPM^km9#9a&fucMktGav zF^yNvu|Ovh%+|A9-ENEp0tO7zOAWM%N@+*?Dxo16>dwVkJSfa27`_xEHN9QBm8EM6^m1hFO0jXFcUNe9Gt_z*vKI1 zV?7qs5D0Hmgne=$YD_KP0sEe);3NS7NQlINIwV=pDy#x`84(j?9D-{INg>w>TV3Pg zVyalNnDJJs*yt!8PaY7u8*NYR1o4c>+c8z6QMYs*x38xo(TF@Pep-V^~2H45A!BwqZ8`WsXn^ z&Jz%?C{qMhstdWaxJqx3ZqW^Alpx)4f4OB(UKL@hoQxLysG&fnnvU7Lq@)y2%3n^o zXx&vZ88tQ2xcb1Inj@Rl9-O&22%RDs^Q=9PDWu67K0Eh@ZW;O z@(fD?Bd)&Zl~S$0m`>KrSgedM7!G-TScJ0~>=1PsHbpVkVQ_?EP8zl!+8OpSdYp8# z0fRYUu+W?x_q!=N5({lw%bhWo0Yf@Y+WbH7ITwSn@| zY$%bi)tW`E(pBMLMl>v{pL(0-_!-Mbz;T~smw)#*&BXMs4^1AKl(eTNUzmDvdG9?M z?PT|lJ!9@Mjodmp`2Tz;?5>^)XZmQ)cUrrU!`kjXmOtbewfAi2D3y9x^W4clRtc%b zM>M*Ve=PSefBq3o>#jd~nQnfaV{{CHpKhJpJv01=uU2>K86L2WtYX_e{KqaDARAsq zHa&cfwd-E+85ppE&7l9HJBI(*B?M$(OaNrY$=$=lfAmuR{>1pMg2zuQ^GEj*MgrI4ZrZ1}GbX=1T5Fez{oBo|2JYMM`a1!{th1-@ z9)CeQedd_c_l*DM$mN91WhcBbE=u_PG;4?jVj;@s)_e7&Tc@LSevEL^3~G+4Pd`0= zscLXW}S>#bYwTQXZ(~u+?8Gp)iw)Vyh)K`t{uG8RKmGL1!zYhn z3Rrn~Swt8C4m%_dfz}+0kzfEKNl5AtF-D<3wySlbi=tpmf^!h$RrG_gaR}1KSlHz$ zR74@zRv{#6Waz3P&A?!&7g0F@Rydl4vne9Lr3g;VJdMEK2LaI%O-a0fqqIWp7`|$- zKL7$SCr~i-&~W}ocp6T#B-jdJdj?AcLvS=XhGB4V;MnEgpV;+i*9Rf%60|S~SlHkk zDMQph3%6B77BHR=cm^WJUd>DSbfI_Dsukh?&VkV2_?xcLX-p`}(bR33m6kv4>NGbHqQbK%1eark z3}-YRg~Sw`Q4j)x;(=;7EHmu#z5^3q9Gqm~&H=a0;|mvO(~^4lz{Gxa=+MO84aQlq zTtN^cjj0b6Coacsiw*am{ik(!w!kW^EcLQE7!(W)JVdqgG7jKhJC z@Ljs*b3|&0ryLHHYF06k;7VA~m*)hp$OZ~*y(Bb4#&j~6NqSN)GT}|;s~txvdx0}9n2qZBUP8J zWQ^A%-bR<`SVL7;Ht31f1lBLoc(yDP23pcZ43(D1H;M(Dv&mph$YJq&*@D}u_fu^p zEQLa5FWa zpB1yl%?3FcFPP<+UE)|d%bP7WTQHl*2U)yf@kcVgi(dz8*S|;0QbOWu^2Jv#fKj@? zt`xVjuF$0NMZ3rjm zUS0!(0qaB70i}NX$OPIoSJKB9z^Gi79jfxMK6Zu){1N7T)Z6`f)s&- zHR#uE7LgTa1Gb2P8HgZn9Ubg*+pVKI%>cvkV90^P^FfA&)CiKpAa4QZ1jwYIdU-@L z3KFh<@aTl$*os+a0~dpHbkCGLti62z?COtbZ{KWNzYu}x+tx3H-}|=p3z^Cl+xq%T zo#&lSqZ4ntYQqh=NhZ$`R=2je=LoA?o85j`-PtF9W5T{Zp#81(e`&wIJayB=y=y!K zhJsZpSem2M&%SQr##KA`dJhf})+%|+gmIPa);drcEM1Ix$1M|vEj`#G(MzYI)$iXj zVLqb=yeA~Vsn>Nkj{y#17h%f)lcxITXZA3Q@CJ~!vi#%j#P2TNf(|V4VCDlO+;8uj z2zBc=T_mB`SKJ=1+)C?=SkVz^IcW!)#_TzTVUvzX%O@p@mbNt}HLJ2Y9jc+u5LK(W z8z8vt7H3MEMLroK+^KjdScs9jI_E|T^^imGw!JP-r(R2&xp>~?L;N*{3k2)3JBww_ zMK@h^RlBy1WKYsji&$zVIGsbvXX=q!Go?7Vy3uI132r_s8?cHwUCrstrKrDANiuPh zj%gPIZPaECS-7}?5Nw7(UeWWS)01<>6mJR-RuwUja29JF(wzilkvHx){Y_olR1eskzF3E1bPAn{nS`>xN*H7DfEf)~YQTF$Y=wlLNw$%H1c{}XA4$T-)aj?xp9!W2cfuQQmc49F9)TV>uIjI>QitLdwy zjFzmc6EdO}pFUW0)(uEquLO&9Al>Gupe;|e-O)7V?(ijn%c7Q)Oa zY`$9CsW1k8*1!~!NWMV_i#a#|dE*T#q}MaisMEqbY$0dN8Z^evxh!D8CTTGJtfsf@ zhJe_uZeR83`zPdXDH-RIp@PB7hjkJwmrF@oq0($MWW?pM`{*GX6k;q3&35L)AM7KZJ%T1nCg2AZ1B9^oQZYp}&PnDxD)ACt8t4%&=>NuE*MsjETcz|blr^l$$TAen4JX|yr$E` z=n}zvGoD5|ZqhDUQno?_$8u?hzAT431}RC)b;#wb7bIIAx9k0ub1Vy}M1o_%RMTG; z5SZQDoc`)ZAD9Sqv#h5j2hAL62zgwFh9#M9l0M3(a60fYB|~;^-C}A7JLz^p6lt3y zZHQDnc*pMzhd4rC^_zus!V=B5byUpgZW=6{n+!E5XU1PimhBlrpNa~egiB#<{+78# zn!sD@2&N0UoI*;)Qk3>}5k&M6DWgzzQf{Id^muY!yBBRV0#-EIu2=;&Yr=(Es7=M& zp+eMcc4neZZ-A+H61u1)2Gfd*s}ylXY1xBG&J^XufQ(g)IicaC1tLuwbFP5c^0Rhx z7%vDqkVB+ymPk<_iCUF>6oq9mmEgo_&Ep2S)vR`0Q5!}{Y*unOJGDCEX9H2(XGWU3 zd^yR|Wkb}{ZdQywZ`7lsi9|{tmgAx+;4uo`~UZtA3 zV%VIQ<3iY+^d;N0pO5>Ts9-Y^t%f6IqT3z5?5T(yMdpr%U>~u@7`v0_14zS*v(21T@mO(xryV7XaX(@=70^tn zXyr|4wm>(^Rf#T`ns^}X>Xa;5i{vmj>J*kU6mzsQY3j%cn?)av@vgAFOAtkKOeD-6 zUE9KYVgl}IDpj)O^XLn_gfcu^%M@%>HP*@ym53?iPeUDpP9d03%%VNap<&8t2{l*+ zi$i>KscMqS~-zHBn$zm&b^)F>?-S;hbMg9lDBF@?DlFpmGK0tp-?7O zOoyWhS4SsMdCHiMSlWsMT%a|~S}HU=Igv1B(1ENM^yHS-paT?^*1&67gq-@bx!;-UCOQPBAYZ`(XP;GWShZZ?UE>hH? zgklTgXj|lUDQDa&=FYJ!z2h(r8IQd?kVgB^fOdKOBNM;*`LK4^#Snfxc@zwQzun0( z+VO$$qk{vXaZR;ek}FN{8V{j3jnX2?AQ%l<_9O)GK!gb7wSu=Bg`yJ>9z{s%t3Nz> z{UtI-U^KXW5rKe}hhh@~7+$~+uMi9;kfO|j-|NW_Pol?QQ-k9hc+Me_y%%{Rf`tZ$ zjB*N!Z{R4DBB4n|oqlkVyBIh#SMe zs01{?C08U=>Y>SF$6y>(Bml>&L@@w~@x5vwIEhFQPllm5BTy`^zV4yki=#3I7A$xb zs4)OhAsh>Ssh(4eh1wT5MZmRM{htp_UUnl679li*QkY5t0z_!WU|FzA(h0P$~mFl?a1F&=g4n4i@s*!O*4997oF%gNmy7@TC42 z#3evjj|@H@m?nr)pm{K9L3I(RO(9A$#Xz9}LH*LhlUE#Ld6@&d4%7}od+`SZxE~<` z2kbb61ok=bIZG6(?t5hN_%TXmB)Nw~L3=SvDAZMfa0{r4K#{xzcPR;FSyg!iCXL~t z(uycTxdMo!gZW@#dLd{6RDsBVvrB*~2dw(=Ba@3KAqbD<2_7oDh`m}ZEReu5Flh|% z8mh3s*_hB{SFQHZ0qw7}|Ec|n_TRMM(|$|)toDoA&uAZAKJt;tyR?7i9aA5DdNMGq z)jpt(eY(%dukYtkj;aqmHyIhxYESpGGmij^->p?oKQic^&}t74Xg%6bXg{R=;s<8F zGYMEGmkL7M6*GDF(w!FN}mDz*h`)88B+-X1Lp&{?635w3l1} z^y%rTpLkX;pesje5<Y~h`oWU$?A1soelt?X)qQp?Cj&% zc~%H0#Y31B}VN09hVhg;7h-O}%ytOGDT_(11~=pP%~X(F;Mb{Nm52etPumIHtAV)LwZ3 zWH7yQ4M!$^rH(CVkIjE&@{^MvoKz>@I9Z((CcTr_PadBbGo24?cac-?AYs`1FB(%f>?B)ARk74SB(}`+MTCk*|tR&vpAS z8}fip@9Vd0@IO91+i%(6cYJy;w2WW0!QS}vo<7V5JLA(c{gw^(#iw`oTXwATB0fD0 zLG1AI8|;a1lZ*8WKeY&PM=Yq4GnLII2w(~nIPgpXU55Y<2dGbalRAQkiGJOTO&~0= z47pv~Y=WR^D4&3B(wz6iPo=df*RU>!#nUnFRV=c4**PHtFXP^qJ2TjYySgwpC4%N)BaEGyTD5R4(;D- z-vZlhNc#nCOZ#bU9(K%EYd@yFP5UtHo)2j62FcpBQnSE4b!2pSaB%!^@9)H+-rwPa zy}u&|dVhD#j|`6w?(h9`=Unga(7xW^$=Tjt&EDQ$?VjG>v61*iui4c6Ko(S7FXo1UzuQI^IadlCUrZu z6c5O3NfytY- z-<^Dy_S2JA?RzF;+FPcEv^Pxtr}o;(?`RKBJ~j30$v>I;=gGHDJwBP4QYV?I#^gg& zp~=}Pa`HI{cJBS}v3)l^3%l6J;6;O*_OK)UhD}@8;eNxW9qdrQVbcb7u-~w0|2oib z*tC7kYe(P$vZ3Yn_W^o)*V-h^^;_-ln&~%e z+O~H08#ZlP)7^f>flWx(R%30`>Q@}tJV6@(t5VX(u!+z$fOR?P18kbW4FHzv_I9j^ zKE$T^+JIOUmOj9yy?z5=Ra*K0n^tWDU{zlF0GnoS6`)J4i%cJ4)9kIGSQnZ;z^2(; z16Y@uKES5g-2hmXn?ArMMp?kRYaZ2=oob{Hv5B|VP^=42A7B%oYyhlE&(`7t+r+1L z!W)?G{ptU2S$p^JFCd+)q+LxXTgwaU=Z3xGp5YxI-!c7e?W^f{+(VF2uihO4hq<3I z`hpP#4YEwgWQS}KKNt3qnB9#N5cnCU%n-gynIQNYb77=|qge08@~<4T!LS9FHU_F? zV#_aTr=QWD{9~VRdSN!a!>|Vg^L%&QttiA^bLb&bisWDf@PN9 z_;+XP!XfKs`B%?PKko5w_K>e3T+?+cN43qb-bcDs)otnSAwIpZd+Ee=XT#g%A>X2q z9KL*XP8)|C{eZ4tRzgpbA z_uy6xE67*ryB`u3S5p!{y}L9#uN_s#&=~@bAAdWgj-Hr#ab#ZmhQ2Z3BK3-sGt+}_ zf-aj@eS>G_)&uj}#(?(bzMbvi9?EQhKlU?b^ojf_KKJ?HMW>iJ7|qZLW72 zBh=57_IaR8&G)BPsy>y9Gk<<$UfWu6xBo)>UAP7PEu6fb(LM=f?e5q9g|@YPtTf~4 zV`_D0?f^`FsCYAj55|Cw{aV%kwwaFt9i{=TQ~O_FF8P}F?_lIdw11_2y*8^gEic?X za|A|@tETtN{F{3DJu^QVx$WjJzfs+L@60sTR)#! zM(>+>&EUw@FMi;S%Xhzbh8r9qf9n3t@>kwBlNcQ7m3&pv`)8bcWX()(^g$SX>y;y~ zzivP?sNVC#eLD5|`)BGSMc3lJs^bGQ@7%L6)_=+zz+JWiHmKhFftiyk_rMG`k~#dX zCznePz|2lKK5TI`=1Mc3`jVckH_qP;DMT z-n`O&?2LAzz0%&j@Vvbvg zUTNO5zYp8(O>To;M6Ek|zr}1 zM%7;n1#BsQE9{RqB|2AZI=!R;aW@rnI8IBgyd#v4W7ed}5;CWOqD&i7$J*AYqeVO7 z#tisHyh$JFMM7A%kSCjhJ6sRRs3#>O(VE5Pt~6_0*u~my&N$@^r(Ds5BUkRoamu8F z^dcMG&U38QnRQdLzY$cVQk%;AGGfJ_x0wBgjuVSS!$cxc^5{wyOCwl}ifLU_efJmk zCsp@Tdk*c!ok3eCY%~#`j1Kc;LM`>y!*fTwx?nOxNG)?jO7f9NJlx8~^QeWzT}gjD z<+jJEQXs3#b2 z1X6BO7^7l#L&ooPN*2m2xk_f62kWqjG>T&-9nE%*CX0&KW==(&p;pb3Eg@Vf9%tie zzuU_}5UjAwKE3Cy-?oCDC3UB`Zl{dRD z!J7??^3}qYSUp&fTx~g*tt5&ODs2v0Qe@px%DIVB)ECGp0%ODyHox1ZC+Jwp(#C64 zFdFm5;~166$aa<&Gl^i(XRoxvS-F{Lw<$iMWNK^_kK6U7Y{6+(tWHr64u@K*Q6dsi zlU^uw{6xJ8ZvUuN9~LX^f}1xO-5Dxwi5H4KbJa^pWoJHW33-d4PNN~p;vj_tf$Qq# zxNrG|Z|}W#;uXezc^Ug>d(WwK{ba%KtlJ`?1h|W_dM52pCXFef0kM#EM@1is77JXW zQ7h&xk)WxNHJc?o;wh7cc-Tq$%^^%E7IkvGE@b@$SKH*W*_m_%@yG2xuik7X4Y(ag z8ZO#{Hp)3qwqr>+^+~JVnsMsPh=eS;sg6Y*&qdsE|-)iOBNGYvf`b+*`2)^68qdEbw$YEQM*4;9%kgZw@NkxRsWi7Mo7o$KsX-TgB69ET&t2 z;peX~_RD^n`2D#VJ4)w0nW~U|lyQT_LlyEfRM` zGF%|zDbv|Ryb!bGgN8iawKd&D&RF6lBvMQ{>0nrFo2=0a9nVr(3)N}Yhy=}WXv*Fy zMG|dYIPS_tauDQRhO=A9SZDlpdm)KzpVjfdd=FEY5t&%V^Cc(|LkYcvMKxB1R zFt{a;&WJdvNF#p572cL>g=5l(vj88%+ZAnuyi z;>i2L1y7ap=wx#(uakMFGnsW;97apo5-|#fPAF=~GH^Vox7%)^g9YQfMej>j$cDXa zQy?He-lj_h#8nGL#fT}2hD%OcJ)4Bg13Kzz1?m|e8EhaC3vP{tN*TXQWZ4pKrp3?M0Msvd}%ifr0K3q`DHt-G348aITx zO+lZ@TiLKXo>xi%%$`hA3i&88hGrF>$vQZR+0{)#RLTw37KKuL^dY^ECLuEiP&A&GeP zh_&gIl8z1wdsDzp`hvzphm7kJRs+`-Kw}?$g|T0DjeQ=*Ziq3(7sKxBUD?QH z)8ip6tx%xM%3%kd$oVtzgtJx-*_|X_&Ey29G1Ms(b+NEVu$O}H(X5E0!qZsIX$>kV z9nRxnr=!-vEtD}3wq+AmtXc^=J6Q!%qr^fGDdm%$fL#`hk#-=~?#hv(Il%@X@u-=v z)MQt?*&%iDqPG&MC{5hj3MEr?yY4i%I(ddq5Mq^}BwJZ9Bqb~<==6D$70o(wK_Ao& zBCU8?A)8)zwu5Cm3|cES_^P)Wk(wUa*lg9(nS8nujJrxLF67tQfSanGJU5;p;%&0(lN}$Ex>2@lB7#lp--UY!1aN zfr77IY_v^yltWrfyH*LMDs{eCG)1#z7izV#gfnRO>mf-Z@AehyV$=wELTo~a>GeUV z-9?&$45VjLCW_N@o*Q@q0guub^157P1xVg`5-VM6g7RCJY8gp%9P~aITMXGSdp`GcuG5 zi-zc!Nf+Y+CA%(>#${*6p^Nx%T_(%t%K^~coiJ;pad)l4MqM3asZLVql8|Iud25dL zrPC=NNBh&^vYvu(zj3KjzJ&WiVKHDU7hFo3uvX&-bZeca4#wUl@7a@b1C? z9C&6R3zKM5tKQkj3^L#MeS5pPwcxaT;>PG9^#?*m>2 zfCH<5@A(>dHy!@j;{f>9=YMuWz1cte&b{fmzUQ@v@z%pDkb~+^1G6X9OklP-Ixz6x z|9G$ZgTU;&Ce8cSQ6@&!JApzV|`(cnA<>XZwgQ>LEhb5Pj7Thz^El+Ym1J z=I`C`{oaeKw}fU*%ij&p9vmF$+_vY+)6v-@gA?-o$&as3%hTV6Y01Q9zcA7J((8Kj z^D)5o+W733N8WPRzl<;2Up;FaoVcU)|NVF!qWBLm@Lygt8<NPja zJ~h&azx9FT7jKvq2Q}_bOk4WI_-+JZ9!bo8?-l+0GVABUxewIM7#DWP;fTpw2)pf} zY{(GF!8NNcS(Kuk@>xW?KAlOnORZ`qY1S1>ZN!{)1h|yZi^;K8JB@l`K7Tm`hi}G} zjG2lNAwc3(DqOZTXh$@rq*``Y#tvoPq(aycr6@BSuIs}_*s^+riK=}72xb_7!_GL(}kWt((B7BhixB||t;sL@lk+pO>vWI3M8G66A`f|Q(6 zit)C?v5doDPRb=X^iw>Ss#pS$z!ff;GMv(?w#rqCF8rk+9WJVyvBr394`w z5~+MFml5^$xFZYpJr8c_RP3=pCgU~ui^)JMN23Ny4f3WaylK%{V$K>bro84xt7h;N z^=_TB5=Q-{dRiAG8K~%%aL1N^UVP=;@v@(%&%@Ulcayifpa4V58WQxn6qL^l*O8`- zC9Gkn-xo8=ZJf>)iHwdnrA&M=VDO@)bS$h`-2tJIvs5_Q59wohD1;P>WDsDhl#k7) zbDS>V@p}A3ED{PBjA8JFl-&BP-bfjxju0{G+)!OItdDg=xFL%8YVl&M>M3Ludo+j! za=r>*Qe1I7YHi_WUQfkrS$nY=^GB5&6jsC&W?RNr$jWKnoh@f;c^pA&(Kg&)OT~E2 zYt_)fuyJGHwX>`E@vo{k6R7J@s1M1=PQPNX5_kbE@&$tr3Md2& zNnNZ=8jU%fTd1L_W+Fp{%6LlO@VL=hu$!%S&~(WhG-hgYutujTu9gi(B*BwTa!o5i zxzq8m9;y_x4qcG4gu*gg)=N5Hz-**D;A;tWI@zqw=pcf6TQb-TWW_)&rL%e<}RN zV~&a?-Y6v!9(_#aTU;*a_a@p&zqJ*ibA=9Xprc3#hY~BzlrBz%99Yv{ppuQ0n~Mo# zm~|U{21#tw@X2x1E5==lJt(;%&Zs9DO}gEpmkI@2?F@^@DsEc@3k3vs5$cemsDs3k z%P+j`6~=zq8T;MmK8d7|HhdPm;&Oy*LSE3v(TwB?o2Y0(=QCP4*~_;b?PSZ{Fo1(G zY&8a(0#eTfQhtb_FfxvgUPwf0wz^Rgthl3=%V04iL&ti!}q@oEQaBaKE$6oOUM zT&>HTQi%pbMSC5sWde|QPj(&lq#>FOL~;$Tk|{a-)<`T2-{*Ep5g}Cw(iE3))KZCb z*+jWQMJkuCl+uD^aM%rQuPl&$e>>iY`9p5Rr1#=kyd`A`R;Rc{DA1#` zc?7)`%LE!xI%e@zeNrsdD6s8{y_|!iS%J?(m4784#&IENq7*6NiDvv3VC?t2!q_i6 zV?TTr#%`~~EV+ZG44szZXjrty-F0hIpCG+H-c3j8Vu>v`5U8C)_+t!X42j;brvi0s za(p37)?%(yGfU!LqERql8Mlt_n636gxl(kRP@~1>fT}-Yxq(9|!#qRhT(+uTrW>hJ z#NsJoSxnT&TOzGkyNOc7z*f+D%NOObR>oxQ1mhMTZBOYLBaJo^j$qsE*Cmod(3mTW zuBaW!VW^n%7US(;K_Kz zl5pjM79=lON=R8`oKPpA8ch@&dMcEP22&=FUaWa3doT-<8nQS$4Y9z{yhAFL!UY{n zK-9(KcF@%}zbS9R5GXR<^0=`q)d@7Cv{5pME>E^dd1@uPYzM}EWcE{oBbINPFI|50 z(b=iNT~}Q8(_gM$NY&j3=5*?=$7a7Y;(xF2zm~u8*sN)APA=~-z|<9P7-VE;z??bEX+_4>zWzcX=a>N#osYJ0~rggIxPn0;OPAE?NHOld}+4edrN_Tt4yC>?MN}zofMr*Xs8?HG6XT z;iuu&tU>Nff9u5OfBjDNhM&*5m#mMXn!%sl{hFbVi62`!^=R}M37PgHE%rW61G)Mn z#q?@b_P(*eKms?;iID4nE&UcoUGwgzeiCSYV@_qi3wU1tQB;ElZu^4afd#AjoA=GS zmo$%|8tskt$7gRFBGr3;04;Yx%ieE&xN(>cmzPfbvwLap$50KGc*pPTPwe;$^``p) z@k^fxeD>5g)$)JLxnFbqpk^R60BN!V5|s3lWccWZ zByd2h0=dRee1Jh@oP%nhq$qI`as0p<^o4vzerf15s@ZpB#&G0U|9ZpQzYP!Dzcw7@ z;adwl4WCdA~Mxj?a9)r>-EYr(m zMd9-{P%1$d2w~|5@UYA7xtV7==HSwyDrSoCZ3GI+7m|W3VK^-w-?R2qsB9_=3`rwU>jIt%ONsC(6s(788-ygu z1kSU<@tHMfhUHm|BcWC!hr=gHZ~){)nnqwAQ7A_tLek2hxYT(Z)hzyc^q}r@`8(7{ zfoIiEPO1*`yNE+i-;F2ahzoI>eU9&*KdpF4t2r9j;ZnJ16$ zT7wpqew_;zL8V?T2@xh%fUGC@a1+7u6oaCI$lp2)5y7Dmh%CPK3U$yocT6*SmG_5Z z`s*i`9{l7jnj_+U##g=dwVz-5$hX`}r=A4%J9^?N`;EW&`qJZ1-lFNH2poO?7rv@q z^zXoVcYJE6pQI#(lSrr@+xwyd$w55|9zJ1!&@v65LLmvr;HQs|t`P{u=|o(DG9n-) z01Y(>;jcFJ>P_x> z_tGz)0-o6Q77hKuXRcp5{xlE;R>Py+EB{+MS}ok@#hCci)A*08j9f{+s=;>n>5f{|LW!efH7or&~9^MV*tL z>wsY7{ntLbZ~`A@SukAZaZP*nvgpg=_zSZ=6F z%R%}5-lsI6_$|o@Py$6Q{1agMlkfj}ny%cqPkol^twFD8?8fi?YNzl2viGLpl9OfL z_&MFDyAR!`y8+?UX=JB?W+_N2NhRe5seMhVvQ&~%2$ijtRHdqt+ER`ja2<6NwACoM zjEJBhA|r6zad%V%opG5NT*p_&SJ~82T;aXz$nwA5SG4~h`kF8O1*)I(A|P|eSQyqbLG2^y6dH>;N!)8aE_5D&k^K9H1IS6w+s@zuJAezM}~u3F`?J3Iss}p zVC@ln`afVig5JGlG!RTu$zas&&7)uag-e-wlF5DrKocsE#Jumy@4D5&w^ zfRqo0_rVCyzWk`Q9z(WE5L^QAq!pJCy*4*;l@{`V^?!QH<;o(b^p!PtA^JGp25W{-RD zK^X00^Ond}|8jNu%gP@Q(%*)=@O^06zWVEjmtXnXD+BaV*zAk*ruld6z=7F+29JIJ zczHhwzGq;5fPy(#9S#75Rp5q!3J^|$m9kd6q{Y;tS+$F-Gh#|plPGzF z%`nlF8q2aQnW4s&Q5ixbw8D{{c(ue2xM@Bt- z09XbW!`O12PsF(XdZuP}i-3YOW{p~&>W8K3tmsan#;~0s<#y8OgvDyoaKy0{ACDY ztYvIxK=fsngDgT~Bx_oea~gU|ln{ojHQ8=$Ri&8AH_2$7Lv=MJcN>aaRKlhp4hX3(<#F zD>ZJclu}h)Pt{Vc&?(zIGqm#DK#JOoTJKVppKX{OhO2=_<}@Oibgkb=l`>d$U=saF z#>dK-*^7?da5K$EoHWUTvS}_0D)(eWD#iz?2I;Fc4{h3g93kqn>MX){3JsK(rAW8w z*W}z(Hk5K1%|ypuWSAv6S*lO5Yz&bGCEtnWikU6~W;9qbYt-XRbI>qoB?^M9Rm0x&EY3uT4OQ)o7Jtz{u*kq82HUU53RW%8nOxq`j^eoHT+p?XIbevoVE9A-;es z6~T6yfzis2ACRQ3_s#^+EUZx0(rP=Lcw)F)=9mx;+ViK%Oy>E#B0)c8f_Iykj74Cqv>95AnD~1 z&sAfjAL(|IU}%|wZ_8MTv?KxPXU7gnHr;-rXZHJ+sMXLq)ko<8-KIs4^>D5b0d;>) z>r^%#-n)7P6?C{^oPXtoJ&t0tVI^+VrenP~;Y+Mlp`=`gm1-%p=SB*?f(+_~jN8mJ z@k%6J&EUpp)JRNn+1h~SnS2=WvNou9x+86rrn3X0)T&ciRpeVt)GPL4c@DKBX3c3$ zVwQqNII02g$@q*bm7TnpE3K0r)5oXN=1`~%RIy$YlNK=>ImvJp?Q$-HLzZmFTi$e3 zt0%a=GKtCQs)k0o6Svz@Mw3jvHxc_0Wk$tg?MRlhbgVsT=jm>E=nlGrX)5N}>?ocW z%@!UP`3}O51}))t@V2GT8DNsagMSDHSMyh0tP6K*sNEIDU;yjST)^5 z$+SM?Pf!>3ymj~a zyMD6ssY6?+rs6m!Gj3UtGQs-#OS!mwo+=)l{(i%vElW6@FSo z03aXTGgm*c)PK#=2^$xlwc1|Xn*P$Rjklk@dd}j~jgLIeY<&5^>b}M0D-LjP+Z^WE z=d7kSEzk2Pn zT4(=%kS~8$C3wp9>JU(?x+7R zl`x<9s%a$e+EP6)Dq=HJ?RKKWRJJ;9^9jju7~1O!y||$uj8&cWdlNjWSr#x^vTPdT z$;`NCdtpA66Z|}rOX67|#xz?cvN&kvhb5_!%+=$Oah|Otl`J?S%Nod?12t?GD|EdW z7MoSGn7EF%ZCH@y8K%yqgoGUD#Hb4nGEB>qXMLi@4NIDWWe3&vNN9x_sWve)uIP?z zki^Hy5y@t$ZZ(q^Q={snuFJ%T#%pZ6;1akhB^{$oHyW94bzGh)i3ZTSWvL|%A|)x; z5-YtZE4nghwDwzc4$3zmnp$h-v*Ij=u*Rq{C=cpYsR)sbU@1f=ovTb)HC;_+dYNKB zJtn$ppV#yNTYHJCR zuxUg$GIb%thixS7i@4wB#ZOnJglAz^H zEHjA+2+C{u32$sX%pO^;9riHf(q|4sOgSsaQlkigF{jtsSv>>d4Y-0tWUgJdI|$h8 z0w;V38K9OwAOV0Kn>4kS#k;jsv>%B}q6{Xngb+niViC9uR@`=k3RB3YSc0Ilx;$gW zk)0!Eb{uijXuM;WJg%A_I1`VG)N8qQ%_8DCo~>~Pq3XqIyF!9#dwix12oXGOwbW3y zQQ2S;{LLl+5Pg{V8oRXf8Ph^Lu9KrWIT`olxw-im*@_lTSDI|0k z5u$=^nU#T;9$7sP5HuWHcVhYKAnCGQu3$$82t2cPH6KCEP9+aY#srn?a7jkr|CbyAu8Wo0l!--s$||rO4c?#qXrkdxh(;Fal&U6O;yM~ZW}u2Vw7E>4lA-F( zCQCiEl@W5D+jlZf!KhlrR8{K`YzK>A{j9``j6%6RJ=z!ORIOR=XKHd6zC%%X)ln+FvWt6tLvK=d?RVBYoA z7UXqN(5UCiq5}>#;GW@u*RWmefTK06M0%?yowl-gQ}h+H`MP`VV|V^ah~B8ZVfEt0 zZHwM7ANbP7yWY5JEiSXh&41Wjn*ZXBur%l11QEtkWO&1iH{SZ@)mJU>x=3ck)j<>CjLD1({*WS?B9Qfh4!NB!v zS9gM}_Wyn}?QHblzB+~9|1SOZPa6-uV|56fzwqT#HigE5i{3d85Z<*~SX|z_hdBQD zXZ`qR!G$k_T2cAkt57|^4PaI7%5mFl23q#16W%l7?jMvFf zC|D@Qf{$DWd$}kVI!}JvE58*ya^33X%Uf>0=De+6zW%yk*X!XhKL4KiVYFXy&3S)# z)pvqFz6Uyx7oSuqDWAPQ`2LlvrQqJ{S9k4)f_OwDFz^Tfi%F;n>fl(e&xQUGJO-Y8 zM5Jz+!DH9MP{-c5=fM+BI_@Wd{hC$gIp<&c=$>9`~SSb)t59{EA ziBVC6Fe2!cTQ7O4{Cn`!&+K2`ku{~Ak+c2aXYYmMT08D-*PV7}|JI;?!>YVri%}N% zG=QZx)X(7b!PvTvqOmX;vka^b6<#uC1P|N*hqUjkk1d=LCEpf2?+mDnx;M^`a_g~o z?IW*vZ*bF%Fz|`5NIw7U2fy%eaOF>+!{zUT4m++p^Vd6Xz2}p`yZ1xW()*!l|MnCW z>4k3(j(!lDUi1FdodCC>=5;*^GQJ2 z1%C=2c|YuMHN5v{r|kOiaY6kd80zykL5JPP-T#AQ%VRs(JA%8CzZsg&Qa`Yb_~`x} zZ?>V+*XJ$K#9KdH`0ew;!CfA7s(t{P_P_q(yRq==eir-)fE>Yh=S?R*vh9ikFMH1Y z!Pgg|NxKD_wq78kOT*)jIe6O-nBb@Lma|J2y_wv0S35ZMgK$8*zVP;E9sT0nw+FYe z`=B8DLFlj?-2L2@9UHF*&c78p?A&hrZqM?sJ{z3iLMiS;tJ^O=;r4|+@87*^SL>(; zmS4N0zdf<^^{vX5wT0UkE`>zF3xXFtvMTMVXPyqRPg)8-^T_JP5;#4bym9$2R-eDP zq(1V4FKyiQ%T;f2<>T0AnI|6A(}N4|h19|2|Gj$l;?i}ymzj)j_`0F9i$zB&Q3_ZmFH7k zP&LxQ5EqOTONBNXW>X9;rE=3X_#Jgf@SS+|qxGl^IC8JSm=;O-7 z!#%cGB?=x9Q7b8lN|a-4XJ(ZkGV1{{uo7w5;aauqH)E6^Tzzk-fAG}HuD$cr(2+{| z;SThnlKu?SJ2#V4B|DuVu(n<=j(Qc|)pDpHaJ6*HM!`NYN))@GbJY>l*@RchMpYo? z8Jz?_Ag(+b8>v>gl2M1U!`X=%AnY4l%C9>fHX9db(Z17SW2>v8c3bJ0lQYqN9ZZ*nmXwOeFzcKVc5h zq&k;TBeX`V2@sU&5JfsK7G~*k!4Vh@pOV^m1jRyu?oRtfP3vW1VXhg|BnQQ*Oiz)( zm9Sk>@q*u;_=;SPGDA->1;02;O+>3%1q-!C*#uTXs$@36klC0-J&iA>l|&|7%+HEW zZm5`COm5ih5ImZ}$Wv|7@$9fPEoD$^M8*}H@D->xUKl!(z&hOX8tRSDFt01)B5QIe zH7E#-;pAIHS(><7N2~Av!0Mwp+anuIqyo0xO+e-e1r{vL(ydlltcz1J2PCCT0vYkW zezTd&Qa}TN0!jtLP8W18d|=$63Bnq4-bCwy@ACSvCH6TB z8L?Br&p`;?6v(L1u2iO_WTYJ#L~|8Coi4TYw3D{0dNj?nq9V^V>VB9radZHW8(4}< zW@?fdMPg-M7x{uSObSY?TJ>8)BRlfytwukgaXexOOH=%{ZQ&7$g!3k8&IScGKE^lYY6caa`T^9cM|Nz5XSR0$XA_6RAfMm|c< z8d64TMW(|{hHg;RVmWyD*5gAb9$47(lRd|;d}vo@=i>6Ocl>bs$G4TY{%A|EcwpgQ zV7c|7jk_-n!D{K{=e!dQ9;HLq?#^(Vm5(_;SX~O<6%W00Wg0r+gr`gHenln0+&xo7w8|F!g5a0z<%^bAcRaY`^EZ!YcLcJsEEZd0})+jcB{f9Z~`_iz2g)`MHyTj{N9TOQu>g)P@^aku2Q zoVxhv;ysJkFTQw@TRdms@r7?csdfMK|AV0x3I~hz&{+ph`a(D$8liK7b1R`U*U#B1 zZXq=ttk}S$2?6WNFb-H^fD{6Hx()%A76IS9I%O_=^1xPc@lfz~XaBk5-$WGa$3n+| ztAt*1CSiepp00-pat@KMV>;{_4ua4ubnejw8hFUUdxKA!At`u|4uIUN?+FKMeCWLW zWCS44R@eZoNC+n(Zqle20=9h!4&H0Hj?|5XcLbj{LT3j*R9$qOQ4zpa}+a5lRO^;e{{GFG+CE+P?k4i-Zuu914ItG(YD<32ZMr_}J&d2QU8O zIl&!5h!0RP^t&_ub})PC!RLP^ynZNv{4?eUawwssI6q5p1%q4w;F2{k7=s%R7ZCj2 z00AD;NC1;+bqGZluG<`vw1OL}q0@sCrO<`JPzs$>Is`!U_RUQX0hIf^gKztCIQW7T zdUvp*gpjlUHq`yN`Ot5BH>YrAOr>od>xNr3x9tw{e|$sw}0Wx;2$o9z5d|R z&`X&^U=pm&Zxj-Xp#ajy!*vkyg(n07n?-XDl#T(EoQRN^rE3eX4_>5(E?B2xbrO7# z!zSR>!H1E+0Rs&{b?{LHe?NoLqd4-C%`xE?BM2m3G!h&X0nrW=0Z6Qbp$l$enh6kX za^dF9o&+Qyo=6_eVZItzHzJU`!RDZNfZ2lMpSEz-W>3^uH%Nk*o4z4%px_rd$0% zxz&PX9WiH}P#_bjgOs`j8M}FUaA9Lhe*Tg@C%Bpqoh5;#;(RX}f#Zg5>EQ@r!R*_7 zCg$`N3bX3q1`9tebm6wma|AxoA-u-Fq=n91+!AyR_>lct3!Q)P&G+mNdV1*G?aOB` zY}ca!TM1q9|MOGA1t(|M_Mg9S?dEkcGyrM=+#dnr6C5$5OewHR1sDpFaF)r z-}#mK$Ntj!f2LaK%-{o!5Vtg+F$W*HbN>aa^IwOO{xxA9pA+nD?K?9lH$&$yzWU&0 zUp;eaVPWelgL|7He*Ynm3Xp@#p9=?GD|BG-qrv6&zWL@`u0HsZ&u{){1Q&n%`280= z7usHMD15GGL&xKTi`y{T1CZgJkFofn)fF~?i;KI`YPN(1o9NRSC)xgko z;nTsNE(@Kz`TaQ*_RK}Wfllax{fCgieqm67o(IEsoDqDr6S{E!Auzyu+AIUu(_L}80$LMKE0WbQgU+q;mu7H8gqckfyni;jw2_BP;Dv%#oDa)9wMs=BT zD#@#z3|gytNsxOj@T4)0GU+LSR&>tHhy{9R@rpA^6Hv3y(4gDLrvzqNmy;bMn)XP~ z*1bfslNkz~T7TM|TI-cA0c7@Z%o(S95HYv2vM=YxDilHcoLF+qi6_+PieJcXtOtM^dvS8`czaMiVx|`ZJ7+#)bf=(WU&v9f8V4hipWqVGi)V6+b(w)!bH2 z4x7}ZGRT&y`9_Uy5-kPDBh!+AIm);>L74`jYvXobX|YhUTtAX~I_&fM^`U3VJ#{&% z(@Tky^l&g$+jv-zqnNENda zxu}-3D=vufTP>v~wP|Khf}HAAN9OfmpV#kv#(6z8n}q3!{YJ|YCnH-0sij$|=|rmv z>zAriS?s9EN&$7oR%Ymo5=6XeJMiBg#rU&{l^51!$gE)Bc|F_o{skA_P}w4D&-qu zp;$({*@p~@MjA2p+s=!U%C8bca-?sse~)HUI{)`PjTDp zz-?a>ivM!@-gn*=eDB?%^<``&{FBVY^Mco&y~YIpbX^EtUU>M_+#>VEyMwR)7Zmbi z?}4VX-udlcM2|UG310t0X!`VfLc8`KN?;m1Jm2`#V|Sf={{4He4lV=Ue=xotnnKNc zww(Hf2cyBQ*TXS9@4ov)5xe`Q;A5+B3@?9g2w6V!H$sIt^ZhRXH2`SZc>^>>&bs9v z)6uii!E3IBVXnR*1k~$8NMHn?ns0n=>1PiG2X}9S@sc+})3M*q9e;v~ofQ1t zjiJkzmp(D!w!ho^_uymOp+ot7&|%loC9mGwdFd|$bO$tjd){>5E7uQV&;IaIu;=|D zdB2I*Ei{He)&!p?ePTF7#};HfVundYEfWC+2NDn)u-)@sx_pCh=gPB#yT1k3@93ML zDSY`Ir(SfM{=4AgLpYtc%$rs(dtUw&{#Dlm_uK?WbeeL^3op3!C;I~QJFv}lH$#WB z_W#G52+nu!If!7?k{Ar|$~h`DPec zy9GKNZQXmy>TksJf@^Mp4lBpLJaqK^A9)Z!8t|{|*wh%xvsMq?%jolv(X^YF)y^Fu!%+jkL zhAjOjZVw&5xH7-)Pi5cPFF_6Aq&q`zT=~-XfAaWK&FZy~l>ft>p>Hg`_C?=Qg0F9c zuHAE~@-)W-fFS-yLbtD6^8Ev`r#9OA1sLna9}WF-Df5z_KfE#cSV&!5nP29odeGm5 z9w&Yx^yA$N-~HUe)~DL<`XaO!KN)&p+rnKRf0n=T{7*qBebmC6uI`rSeS)h#1xJ7D zU7?p9VUHhP_V~kJ3;n-bMC8vhMs{u}H3d0p&qOB^iB51tb=fR7h}Bet8$0PjGg^ys zu}XNF!wDW&eId+yjOTOZ3?;>kaZSi)s}wt~j%)QPZ&6fuY!pF-W#D0kobKCFO{kU_ z8|Z?J5uYYGEzkC}fis9DiUUq5u6OGaEUW}Rm5StX%^##E3Dfq<0)hy$TCq)uv6RM2 z{U+aGM?9VXue&VPTO>Ds{83yO7@xXP=*|;qcVdkO_ZbL*tQ&XG)hFrrB-F6bQ1|}AhoO> z6-icFv=CNC^IQl02f)FukP9M)Mhh!w=y12hBZZEvqQe@Rwi}H)|p1l zCF*!SLeM%sj?k7I&qV1u2f9~!-sntSX+a62EJB?rxf-s%8< zuURV~-7rpb>nXMaQAM3Uz%ivY{%`=Jpy-ppC9HMxh%x{AifZ*S~lOT z8&Q?$lQ>y)5Fr(wB?_SFspyDOlOv<>I82GH$(YM$$G%|XQFYWY3LTOyp+ssjwnefr zESC~(MUNNFtQ@X(J)Wwl>J;Qe6c(77O4=b9l5basGq7OOd#;jF`Vm$dB6)M5_Bb6A z%+9Fk_Yi)VNK!q-tAyEVKE}ASSw&2`BQIOWAn^D>=#-<6p^y6g@>M&&y8Xs&&)a(U zmLDvx2d6#~Nvso5CWy`!Qyy*eF7(Dz)=;GibkA%)ya!=7t*!ZtULf~JVx_gJa zk@;naf^P{d_MZA?obnp@M1A#Fp>M6Y4-J0tRP()Gg-Z7W|2?mC*RDD9l#RW=37xvQ zeD>=eea_~Wr2ZSIbpPF>p)an?mtjwB^`l2&&#(V&=<6$Ydv{t-H9veee3x$eUFZ)> zAE@7)4c`Cz5U6(RfBp=@fj=#_{rK-;`?vlfGy$>gH~sc|>CJup;rH;>deI-DGQJ|8 zwD|PS20w;TKmDiBzb?&}VL|X%=&LKETW@(9JGu8BIEN2D9{TZ-&)s2uE$mr)#?M_w zE%VtJM+^Uhk(soJ7G;7y(G`PRJmx&Dq0E`^3!%P4dP&vv6k(0D{8z_wP~l3 z8KfvqSJc^bjG~Yq8<~|}svFZX+GJYQoPoy(@p4^jdMJ@d=elU6pHC!hA5w7T@{FL? zdsK}=>Vq;H9uj56P<1dUt){@7(aEQ)gQmgqbLWnzI13lMD0tckDby7!h7Ek>Y#(Q8 zYOR48d{`mrDqqdZ@j9bd3^~@1w7LUZl2lEc#sq9;*HD&{2ARI1=Bl-hiaJ?@N>;n_ z%pS)0L4OhzM-@O`3T=h$mUya~h^9MDQotmztcj;BH93<8VLXK*ZM8$xzzsoSy>Uv- zxY{Hc7Kf6X%r_B&S13dkss$A|FMHRHV7v}D#_Qx~p4iF=p@gbJ_ooFGC9MQkv}z#N z7`MArg7b=Oqmx%Dx{WEe-YgOLxZbPfO|qHC`%r=qn6TKHRp|^7R*b2u^k_mK$B|~6 zDaZm?29_V$5x2!-Fy)WsO=r=eH|dN{;ut=on3>ovAXDqu5A|w+WuSyR?t+1?njmZ!X?kQ9QiOcH-?0W0yFVG1Mx$;a+9a6`F*4G?52;S6Y1)*8)3XOyf-S-p~Mi(`Ml#o19+=fKUHY;~iClplsw zCPu5BS`v$DMm%4&WG-K7Oj{XpM0+-f#^#(7Z5acDCB~yTIt9BXcw=99WMUuoiT#&s zN}rh6NU8x?b0I&$nV8$G=lP;k>(oRc>W8a#92{w9WDD4+g+g4i8+|Bm)Y;)EV?}0( zyqm$n1tUu)bYU2#sgjiRM^>fLt5;&lNj{ylIHKGMo7iNU0rhn%sS9x{HOx6v)U^n$ zNo8x@%sMsgg<+DDmWd^%(*$0WX_PTrC^lvXNrS?KN(pf9rLf*+T79t@6R7g6PeZ6= z@!1}DlXj65GIE`4C!9*AWHo|U3UaO2o>l8jtLDc>+s!9y3VyonBbmQt}+*%f8}{kW|EOZ zz;^{dq+vkxTOYhE_4!Xf5WMi0kP~ghq3M`Q(xYautY=V5!gjm1-F@3$=jD$EulzBj zD1S6>I{t$X>^b>`c|Uj@RB_>4Pk-`U4+Y2=1SuHe#GigbIpFWOH5d%w08dV@?LHj` zX;A~991&1B#&H5PKO+>#Yg))0qXl}hQ6ea%Vbew9&3{<@6{QC1ob33`dDB@tj=79@ z{u&?b&cL}v4*|#&ye0$N?S4+7d+f{aef*#d9J*t&&~o|}C)JVSuOA9d{L0$=)SeP6 zt{(y_DfrTSh_z3YiPPWx{7`V(2yP>jgQjCYXutP_v-K;2x6YfsHE$yS`T2X8HE|Z~ z$;0fyvk$#77x}NZ1)nScS?|Yr=&)6qJiK(vjXw=eEI^0Q`(M1}v{U^pLG9fz!Y>NY z;kkEkMLzJz;2ZO%X%U*9-F|>POMA)v!EHs@CU(~AE-!ESB20qopu;Om&|&|hZ{3G| zX8DZZuF_hKI|Mx8`B(0F^5z~2_%W~<*vs1Fmk*szzp5161oI>K<-F;9zV^Ns>?v#n ze|j-A`7AV@{0Q}p^UCaNgQGwS^eyGJ?I+%}u;*WPe`=+=>)4~-xBRjlrEOndVzxZ8 z=q%i{z(Z;q4W#ng-AnUZv~hN2?LyG({`d=Cu~FmJYKu$EV-J-#KEkiP1ZaTIE5G#S zjpvAKQ=kDZ{e9{ko6+|_h-<00NNfLl>FXCsyMhO$wQG+(-QCPNTsY4;JmJiRJg`a! z_sMH07;4@9Uq3i8I9ge|_PEC2&v^QIjQ^A~*A+ELI6SVbjiBotFECEnm_ozilK<*s z?%DXn3)etp=+cwV3^w<@=S6F&jqW9DtBXtXI}p6;(zW|mUK5DRPh}Radi5HF`0Cn` z5dUyTLe5`^gr4}WCS$JCpuprB0}4{USJm34uN12u2zU>~6xg%Q5@{h*4c9B-YO8@o zYd}+uv?5u#?#DpWF(1Q=iNLk!y-`qSGlH=h|LlN>x$ASQKa;E*DNHPYI&R=#Udcji=`3;FcccO zi9}Iz5V|goB-<-0a&`vn-m%w=sL2Ks6VT{%)E$H;`nWxo^CL&qvS`5tjSZtTl=N;& z=#BYa+RQge8=u6e8rUp5(;hNp8!;X46H;xa*T)(~cg!JS^rN;lbE}oK!Wz9wp0e?9 z*kZ?aUlmgsy^tO1WFFR-8xOCqrOrP>DnE>*@<#BBTb_AdcWs*EOAZI{Dlu->J(evd zBk{a$;qi(D1nqcF4d)2W3!C8j#8)8psOKct;D`d*qtc^JBVL(EJzY)oBiWKMouzZd z+F+Jz3{xXSEg&R8k4ke7Wh3f1j?nGM1Aj>LlXky@_Pq(Ro&foFSM$k!R1mwq(V|*q zh+#~;*N|O8$w&E~BS~HnvcI{BH4#z4RBPM}tP9niHMM41#0De6R6pG{Kz1@0OLr1V zzmx1|%2`kgnU?yqHjX(Xk?ceEl&J|xx-cm+eK!`LkOIO~rS3p4v@NF20BSNxW0h83 ztP#1CKa9~)6)cdW)F`jh6{N<;$BF#Nt7&zuoR1f>vq({lq`~ zzCY>IOLJDabLWTF-nh7H^~IMzS*;J~rG0ep{oB@FvUK*}$G^6r{?mNfH4l4&Pkwmq zp`}FRzpvc5_4YMD8O)=W&2`V>9c!83vOCwlvQ+!~MRntXjWs|f%ma~t`3S7!vZr2l z!p6xTUAt^?`S~XtY;A7R|L9sO`0B^j9$iTa`qQjHT1-9isrh2+uC;5n9eFqp$LIS+Q`D&`(A1h^{+;TXk;^lB0o*~|fXM3RBIv&}rW|Az9 zdxbDFs#{nqUd+HZ1nMQU+O|@9R&5T^cGjJ?$33}OoK6{^?nDS1AC=U0FVfQ3Lex*9 zl~O+HwdCTYYgTI&BQee;CT^A zNOlN~$;Vy;MC_D8`U=EisEl%MB1f9g@nV zBNACqtsmk!yERK>%9w0t6u%X2XyC=rEX*RM$gqMp-J)yvx^V|BjUZ6zFhX(6*j6c; z*AS{)A93j@Mpx8|ph;}g)uKXggg5}Zv(rqnno#jP>oOyrvf9<86vk{TZp_SPq~W?u z5+hMTbn9kPc1Tv_IMHbpat>=o_))u`9uTUQETwYwYPE#dNF_VYalRKUM(ZHy)i*J zeTQwUP&(*|4W*MEhyc@KQ^W+6&6qq3hpCVO_(Ili0krhSs zDS9GIJ<>?JWVwk%+8|KW8x8z)WF+^fs7p8MJHR$P&3v=D;p30;K;l_?DP6B z3GY6U$?5j9?LJRMxrpyqS_8$aCL&0Zv%*5pZ>1*LnlRKxV;_WWC2m4{rr7e_c-{!t zo1Q(XjT)4dDHJ9X&ckyo6D?-D<9x2Jci|&kkKhS>&`YWC&U6z_yH2oU&r(&h7vn(s zM#u^y)Lbw2a}qY9NOlrLb=uGB`Sj(nM)nT`w zA@OO?7+JDxI1>lW+Swl2Eo7J~4kFo8r;3!c956;|b2wzD7PV_GHc8g}YE~8^StJKa zyK$|HD`7m5jTR$ zc|lOyWV8(aGrotF|oy4d4}Du5!2dMq*=IDWhxt}sP=>^TV#qUCEXA2@v(SH>~LuuHLcavcG* zAH>#87M+6^?OeY+p{A%(|bDMwp_ zT*;%%tVoG;OfyZlpiosMEt7m|pim7S(o#v2kg8K$rF(3OE*Ft@xjJaY6*0j$GSA8l z%o|MOW*#byt%;bF28eIfikTrp3>)oC(@Hvml+U zY~3LeT&a@Rx<#cI;gxjUqc?8&?U7~m;eA;@)2Qk1GVPhurH8$EqM{{OR*!ajtlg}m(m2Th z_UQ2=@9F=cIo)p!r#t{8nzq9tsc^-rO>mDXGR(LEwz7S&G%nXo-YT+I$@a6TF@=S2 zp;6YFG}mG^ClR0cgD~bWJ=PqReW5nX5EKPc>a%2=9SU4@09d;s2D}O{QdL1O-4X_U zl%+?#G+4&)>UuN5;fyb)Nf8@Y+IXslRfl$^RN~?VQF6MyQaTxL(mj4^+3Idw~x7)vr6IVx`xg+U8k z$Mm$t$#K1nO7hTZLE6&Kr^%7ZHwLDd9e2qV(oz}R@5HbKupZlG)POwwtWl{A+D5mN zNy-zU?Nzy2!o%ohvjQa~J2gThuF3m!(*x%3#(S6dJ$NMh@jtTku&`Jbr1sTL%a2Yt zp)vy(`ubp&jWqz*JQgx;9#=*_m_53~u9vedH{W!O3XnQ|Vb(|$5!voV`(rv=6*{^- z&0AQ-PYwmFCAyf-jEn#uJxut!#JLIa?`ltt}(sI)_EQz70 zi0?F$X}`pKPy+3>RE6aMfRsr^!49vGPB6`eiV9t^*HBz)z(*Qv%9sg7xm-dAF&fKr zdLfMuN?Flk>Ut3a_4QP>i`7birmFc3O%zm$QwC+EX@==hqvGikX~x!~V*|>r`EE)d z=enlWVgS|}>%_@K6)m|8A!3PAv`ywF5|8K535~NxL_=NZ)Y%c7I7iLAVcvAiajRVDrw^(had6v-aFRzq{N1(Co%m_N$NpgxV}(C? z)$}^}FPnr39?0MwjFTYkMd6^7qk%iJVOn*N&?AF8PJ#iCBY)tX$WY%59y)10;D_$` zdFYa_{$217peha%1-Qshm_r|eLR9d}lVQM%9)HnY%3JT)6`Xep3^=b1o#Nj{1V0Ae z@|~yddvI|n^D6fB8^1Vh-^q)+=6C&xf=uuk4=P75JY(NWmO^LVc;m)*&)5f*qxoG7 zo^$5DhnCd$K6dTK`Dg8e%F+COY?gs8KWkqm_}%Fz8jYI|Mbn5Zrp!9jK4I$2ph!< z_N^}NoZZJi*|onFM!4m|eb*j|Y!Bb(CA#k!qX3qYDJ*X{(=n&-C;1L$fijHe6i8%N z=aHB>?Qq#rz0itfd)bjP8}^u)$oIUF&Cr!WMU1x{wO!~Z7_=oI`KZJH8C1wLOZ(73NP+vcd( z8WX+hFg{5nkV%xzG&{Z{0lx={H>+7Xlc2gKZWJSAB%bn96DZa;og&d_SOe0i7MQev zCSuX4tePs>D-oc1pyPbF3;q#_g2_`DKGDT6pPw4iq&4m!OxLQ1omsYNV6!4NLbyg_ z;|6Nqk%G}-1>QX4oL1QW&}{U3B|10h01_-i$2@-EB*tw~V8nQR#^?DS)-TB}K13KU z0YZ{af5SdrZfDA8Ao~yH_1s2D;c+0vSZ1#?DfV|9?M5TM;jR`1|Rc@smXZ#3MCJUT~~7m zj4d@8FmsER2Wp{=C28=ilZj?K-i*1dpoY5%hDakvhw z#)j>Nlf6WH5}mkVm(281)r^y@m(n~}soPkq;S9^U(g>*zd%a8rN~ATGO?N${ij~Eo zC!}yP<5TG-z#tx8i4I?zWJ+4Y7*r!Uz8^1Ql}Re0Rtp1XYB5rn@5861KtVdE*>}K^ z)tDsBn4=}2Akd4Hc(FZ=MIC&y@m~2zXnQ!bHGgGJJES}9n4~nWq_f31+Z#;kQ(v$| zcH;E}HjRKiTLz70xv=3yiGH?_L@j}Lx-BT4)ElakaOjE@Yk~uOF@Y4?F{1%Q95Dhs zFs2E{4@HJDHDNRY+bwwlNp0W-j*8`czTF}Wl&d5f>jYN}lUQbo_}K(h%4Z~@_cl&0&A^KktxO4do; z*EM500MI7iDo3koMHh6b(NSwn5zQ2!SXL=uOil5l@tK~=7?W(=o~V8;rQ=ODG1Hh@ z7MzFGeWxfd0-4{V_WW?q#-7*jF_z6e&)swE|7q{seO+kpTz53|ty{lhb7Ty(!w+9NznTml zpWXf7d*^2f0HXdo8N7M@`8ARUzdpZ{4t{iRRO-!}+2Fe`eCP9dMD6+g6u=7Zc%$#I z-?%=1Gao!T|1}>xUficv^x*fm1kU>~S8sl>9lZX+;y!HE+nc$MWPI@F^S`pM&(B}k z53D;Z?a$x4KL2h%`2O`h7R=&jAHDzh*&qIK{qAAFKNnG7iip~p_?DA=y#RO-UE7yq zJ~9n?48(~QY>_aV`C-;1lm&X7S&B12{E%j8(F)me`tx?M&cHPT1k7NBCNpEPD#Ph?wC!faH0)C!mB}k|j+_N>2w%htgu& z()a_q9-b)0#jJ+d*_#4)z=RB>Zz9tSl&&p=T|0%2qwP}T2+rM^8E%XqOe(zuV28U?sEh=7~90GJf zHv0p^`X~urGa@`*_k=m02o0$Qj2FR)KVOJgvnbG6IUCh>p2LMiPGQxUSUyqmqno!+ zga1CaoxhYLeV928e*zSHQ4K&uTTK}hvR0e{Pz`j#;l;(7UXwcI8q9fWZPu_zYTY z)=PNx6cBEw%q~f=QxF%1RPmj>H-IrMP#u7Wfgvjj)VwhaifH`NX z0ppjQ6-?J6w>(WEOjT{Z8|8gB3kbgK!YZO#KoRu>JP%aGrJdSYug0|J#4?oIV0dOv zn}uma)d`?LN6eVo9tC9KRb!ZR`k`!rJsdivQ>rr21OPzzz8PEVfIixt4<5Iq+eOVh z1JoW2&U)P#nstws>&VkiIs=->ig`*N;ZrXqO4M&AVnGs&u8SGa$Ki|+ z84_)!5fU|asJb4VPZhCVP8(;pnt)+szD^;yvB&dhNrn4PQ1Dg6%uT z6O`Mn%|ryj(`8Hh%yPQzEl8GIz9AvgibxgTNBweU(R6e$F|r<>W?0a#_?d{YU?{*K zT&fV9W3|M6+oL=YnMRV~2yzR9DS@H3wYAM~zS`oAxCW~0m(Nw#7k#Eb%QD$EyB;Pg zzR3b=ZN=kpP-ig^H_2#7GYiFboZKfmo5G4BOwx8E_~K#3XVngxPaMCVl8hA$^NuA@ zP+nu)M9W~4E2ZN)I!4Y^mLb{Z4~U8s@sh1=nOO3~k)r892E-&O#-E}I*{wZ*idtl_QkjEt|zyjuU$O(9~7xGK>z>% delta 79536 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 Om+zaw;(NQl+y4v2VI&Fw From 1d600abc5d7465690ee960c72546e99d7583a8a7 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 27 Jun 2017 14:15:19 +0300 Subject: [PATCH 6/9] Fix bug in date time picker that prevents subsequent selection --- static/directives/datetime-picker.html | 2 +- static/js/directives/ui/datetime-picker.js | 26 +++++++++++++--------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/static/directives/datetime-picker.html b/static/directives/datetime-picker.html index 25f57d701..653c3869a 100644 --- a/static/directives/datetime-picker.html +++ b/static/directives/datetime-picker.html @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/static/js/directives/ui/datetime-picker.js b/static/js/directives/ui/datetime-picker.js index e498850af..9b22c451a 100644 --- a/static/js/directives/ui/datetime-picker.js +++ b/static/js/directives/ui/datetime-picker.js @@ -12,7 +12,7 @@ angular.module('quay').directive('datetimePicker', function () { 'datetime': '=datetime', }, controller: function($scope, $element) { - $scope.entered_datetime = null; + var datetimeSet = false; $(function() { $element.find('input').datetimepicker({ @@ -24,11 +24,15 @@ angular.module('quay').directive('datetimePicker', function () { }); $element.find('input').on("dp.change", function (e) { - $scope.datetime = e.date ? e.date.unix() : null; + $scope.$apply(function() { + $scope.datetime = e.date ? e.date.unix() : null; + }); }); }); - $scope.$watch('entered_datetime', function(value) { + $scope.$watch('selected_datetime', function(value) { + if (!datetimeSet) { return; } + if (!value) { if ($scope.datetime) { $scope.datetime = null; @@ -39,14 +43,16 @@ angular.module('quay').directive('datetimePicker', function () { $scope.datetime = (new Date(value)).getTime()/1000; }); - $scope.$watch('datetime', function(value) { - if (!value) { - $scope.entered_datetime = null; - return; - } + $scope.$watch('datetime', function(value) { + if (!value) { + $scope.selected_datetime = null; + datetimeSet = true; + return; + } - $scope.entered_datetime = moment.unix(value).format('LLL'); - }); + $scope.selected_datetime = moment.unix(value).format('LLL'); + datetimeSet = true; + }); } }; return directiveDefinitionObject; From a8b340feb61466c1e448f09581411debfe9a3445 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 27 Jun 2017 14:15:33 +0300 Subject: [PATCH 7/9] Have tag ops dialog set the expiration date to the current date for the tag, by default --- static/directives/tag-operations-dialog.html | 3 ++- static/js/directives/ui/tag-operations-dialog.js | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/static/directives/tag-operations-dialog.html b/static/directives/tag-operations-dialog.html index e6a4ef480..12e1b7d21 100644 --- a/static/directives/tag-operations-dialog.html +++ b/static/directives/tag-operations-dialog.html @@ -126,7 +126,8 @@ - + If specified, the date and time that the key expires. If set to none, the tag(s) will not expire. diff --git a/static/js/directives/ui/tag-operations-dialog.js b/static/js/directives/ui/tag-operations-dialog.js index 9880c4747..65ebb988d 100644 --- a/static/js/directives/ui/tag-operations-dialog.js +++ b/static/js/directives/ui/tag-operations-dialog.js @@ -18,6 +18,7 @@ angular.module('quay').directive('tagOperationsDialog', function () { }, controller: function($scope, $element, $timeout, ApiService) { $scope.addingTag = false; + $scope.changeTagsExpirationInfo = null; var markChanged = function(added, removed) { // Reload the repository. @@ -346,9 +347,11 @@ angular.module('quay').directive('tagOperationsDialog', function () { return; } - $scope.changeTagsExpirationInfo ={ + var expiration_date = null; + expiration_date = tags[0].expiration_date ? tags[0].expiration_date / 1000 : null; + $scope.changeTagsExpirationInfo = { 'tags': tags, - 'expiration_date': null + 'expiration_date': expiration_date }; }, From 9679ec91ecdd66723f7f6745c4172b08775f6bf2 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 17 Jul 2017 16:36:05 +0300 Subject: [PATCH 8/9] Fix for hard merge --- endpoints/api/tag.py | 18 ++++++++------ endpoints/api/tag_models_interface.py | 24 +++++++++++++++---- endpoints/api/tag_models_pre_oci.py | 21 ++++++++++++---- endpoints/api/test/test_tag_models_pre_oci.py | 17 ++++++------- endpoints/v2/labelhandlers.py | 2 +- endpoints/v2/manifest.py | 1 - 6 files changed, 54 insertions(+), 29 deletions(-) diff --git a/endpoints/api/tag.py b/endpoints/api/tag.py index 42e375de2..106d051f0 100644 --- a/endpoints/api/tag.py +++ b/endpoints/api/tag.py @@ -10,7 +10,7 @@ from endpoints.api import (resource, nickname, require_repo_read, require_repo_w parse_args, query_param, truthy_bool, disallow_for_app_repositories) from endpoints.api.tag_models_interface import Repository from endpoints.api.tag_models_pre_oci import pre_oci_model as model -from endpoints.exception import NotFound +from endpoints.exception import NotFound, InvalidRequest from endpoints.v2.manifest import _generate_and_store_manifest from util.names import TAG_ERROR, TAG_REGEX @@ -53,7 +53,7 @@ class ListRepositoryTags(RepositoryParamResource): class RepositoryTag(RepositoryParamResource): """ Resource for managing repository tags. """ schemas = { - 'MoveTag': { + 'ChangeTag': { 'type': 'object', 'description': 'Makes changes to a specific tag', 'properties': { @@ -61,7 +61,7 @@ class RepositoryTag(RepositoryParamResource): 'type': ['string', 'null'], 'description': '(If specified) Image identifier to which the tag should point', }, - 'image': { + 'expiration': { 'type': ['number', 'null'], 'description': '(If specified) The expiration for the image', }, @@ -71,8 +71,8 @@ class RepositoryTag(RepositoryParamResource): @require_repo_write @disallow_for_app_repositories - @nickname('changeTagImage') - @validate_json_request('MoveTag') + @nickname('changeTag') + @validate_json_request('ChangeTag') def put(self, namespace, repository, tag): """ Change which image a tag points to or create a new tag.""" @@ -106,12 +106,16 @@ class RepositoryTag(RepositoryParamResource): 'namespace': namespace, 'expiration_date': expiration_date, 'old_expiration_date': existing_end_ts - }, repo=repo) + }, repo_name=repository) else: - abort(400, 'Could not update tag expiration; Tag has probably changed') + raise InvalidRequest('Could not update tag expiration; Tag has probably changed') if 'image' in request.get_json(): image_id = request.get_json()['image'] + image = model.get_repository_image(namespace, repository, image_id) + if image is None: + raise NotFound() + original_image_id = model.get_repo_tag_image(repo, tag) model.create_or_update_tag(namespace, repository, tag, image_id) diff --git a/endpoints/api/tag_models_interface.py b/endpoints/api/tag_models_interface.py index c54a4cd86..a005aed9b 100644 --- a/endpoints/api/tag_models_interface.py +++ b/endpoints/api/tag_models_interface.py @@ -61,7 +61,7 @@ class Repository(namedtuple('Repository', ['namespace_name', 'repository_name']) class Image( namedtuple('Image', [ 'docker_image_id', 'created', 'comment', 'command', 'storage_image_size', - 'storage_uploading', 'ancestor_length', 'ancestor_id_list' + 'storage_uploading', 'ancestor_id_list' ])): """ Image @@ -71,7 +71,6 @@ class Image( :type command: string :type storage_image_size: int :type storage_uploading: boolean - :type ancestor_length: int :type ancestor_id_list: [int] """ @@ -91,7 +90,7 @@ class Image( 'command': json.loads(command) if command else None, 'size': self.storage_image_size, 'uploading': self.storage_uploading, - 'sort_index': self.ancestor_length, + 'sort_index': len(self.ancestor_id_list), } if include_ancestors: @@ -116,9 +115,9 @@ class TagDataInterface(object): """ @abstractmethod - def get_repo(self, namespace_name, repository_name, docker_image_id): + def get_repo(self, namespace_name, repository_name): """ - Returns a repository associated with the given namespace, repository, and docker_image_id + Returns a repository associated with the given namespace and repository name. """ @abstractmethod @@ -157,6 +156,13 @@ class TagDataInterface(object): Returns the repository associated with the namespace_name and repository_name """ + @abstractmethod + def get_repository_image(self, namespace_name, repository_name, docker_image_id): + """ + Returns the repository image associated with the namespace_name, repository_name, and docker + image ID. + """ + @abstractmethod def restore_tag_to_manifest(self, repository_name, tag_name, manifest_digest): """ @@ -170,3 +176,11 @@ class TagDataInterface(object): Returns the existing repo tag image if it exists or else returns None Side effects include adding the tag with associated name to the image with the associated id in the named repo. """ + + @abstractmethod + def change_repository_tag_expiration(self, namespace_name, repository_name, tag_name, + expiration_date): + """ Sets the expiration date of the tag under the matching repository to that given. If the + expiration date is None, then the tag will not expire. Returns a tuple of the previous + expiration timestamp in seconds (if any), and whether the operation succeeded. + """ diff --git a/endpoints/api/tag_models_pre_oci.py b/endpoints/api/tag_models_pre_oci.py index 1002c78c8..99adf9a57 100644 --- a/endpoints/api/tag_models_pre_oci.py +++ b/endpoints/api/tag_models_pre_oci.py @@ -27,12 +27,12 @@ class PreOCIModel(TagDataInterface): return RepositoryTagHistory(tags=repository_tag_history, more=more) - def get_repo(self, namespace_name, repository_name, docker_image_id): - image = model.image.get_repo_image(namespace_name, repository_name, docker_image_id) - if image is None: + def get_repo(self, namespace_name, repository_name): + repo = model.repository.get_repository(namespace_name, repository_name) + if repo is None: return None - return Repository(image.repository.namespace_user, image.repository.name) + return Repository(repo.namespace_user, repo.name) def get_repo_tag_image(self, repository, tag_name): repo = model.repository.get_repository(str(repository.namespace_name), str(repository.repository_name)) @@ -73,6 +73,13 @@ class PreOCIModel(TagDataInterface): new_tags.append(convert_tag(tag)) return new_tags + def get_repository_image(self, namespace_name, repository_name, docker_image_id): + image = model.image.get_repo_image(namespace_name, repository_name, docker_image_id) + if image is None: + return None + + return convert_image(image) + def get_repository(self, namespace_name, repository_name): repo = model.repository.get_repository(namespace_name, repository_name) if repo is None: @@ -102,13 +109,17 @@ class PreOCIModel(TagDataInterface): return convert_image(image) + def change_repository_tag_expiration(self, namespace_name, repository_name, tag_name, + expiration_date): + return model.tag.change_repository_tag_expiration(namespace_name, repository_name, tag_name, + expiration_date) + def convert_image(database_image): return Image(docker_image_id=database_image.docker_image_id, created=database_image.created, comment=database_image.comment, command=database_image.command, storage_image_size=database_image.storage.image_size, storage_uploading=database_image.storage.uploading, - ancestor_length=len(database_image.ancestors), ancestor_id_list=database_image.ancestor_id_list()) diff --git a/endpoints/api/test/test_tag_models_pre_oci.py b/endpoints/api/test/test_tag_models_pre_oci.py index b7c750854..5ddb34ab3 100644 --- a/endpoints/api/test/test_tag_models_pre_oci.py +++ b/endpoints/api/test/test_tag_models_pre_oci.py @@ -106,20 +106,18 @@ def test_list_repository_tag_history(expected, namespace_name, repository_name, specific_tag) == expected -def get_repo_image_mock(monkeypatch, return_value): - def return_return_value(namespace_name, repository_name, image_id): +def get_repo_mock(monkeypatch, return_value): + def return_return_value(namespace_name, repository_name): return return_value - monkeypatch.setattr(model.image, 'get_repo_image', return_return_value) + monkeypatch.setattr(model.repository, 'get_repository', return_return_value) def test_get_repo_not_exists(get_monkeypatch): namespace_name = 'namespace_name' repository_name = 'repository_name' - image_id = 'image_id' - get_repo_image_mock(get_monkeypatch, None) - - repo = pre_oci_model.get_repo(namespace_name, repository_name, image_id) + get_repo_mock(get_monkeypatch, None) + repo = pre_oci_model.get_repo(namespace_name, repository_name) assert repo is None @@ -127,14 +125,13 @@ def test_get_repo_not_exists(get_monkeypatch): def test_get_repo_exists(get_monkeypatch): namespace_name = 'namespace_name' repository_name = 'repository_name' - image_id = 'image_id' mock = Mock() mock.namespace_user = namespace_name mock.name = repository_name mock.repository = mock - get_repo_image_mock(get_monkeypatch, mock) + get_repo_mock(get_monkeypatch, mock) - repo = pre_oci_model.get_repo(namespace_name, repository_name, image_id) + repo = pre_oci_model.get_repo(namespace_name, repository_name) assert repo is not None assert repo.repository_name == repository_name diff --git a/endpoints/v2/labelhandlers.py b/endpoints/v2/labelhandlers.py index d179ff4bc..67596f404 100644 --- a/endpoints/v2/labelhandlers.py +++ b/endpoints/v2/labelhandlers.py @@ -1,7 +1,7 @@ import logging from app import app -from endpoints.v2.models_pre_oci import pre_oci_model as model +from endpoints.v2.models_pre_oci import data_model as model from util.timedeltastring import convert_to_timedelta logger = logging.getLogger(__name__) diff --git a/endpoints/v2/manifest.py b/endpoints/v2/manifest.py index 6a1fc801d..b79111f39 100644 --- a/endpoints/v2/manifest.py +++ b/endpoints/v2/manifest.py @@ -17,7 +17,6 @@ from endpoints.v2.models_interface import Label from endpoints.v2.models_pre_oci import data_model as model from endpoints.v2.errors import (BlobUnknown, ManifestInvalid, ManifestUnknown, TagInvalid, NameInvalid, TagExpired) ->>>>>>> Change error message when trying to pull a deleted or expired tag from endpoints.v2.labelhandlers import handle_label from image.docker import ManifestException from image.docker.schema1 import DockerSchema1Manifest, DockerSchema1ManifestBuilder From 05194664a9bbe40b8d2d89ccb773069e0f5f5f91 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 17 Jul 2017 17:28:11 +0300 Subject: [PATCH 9/9] Better typing on expiration status view --- .../expiration-status-view.component.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/static/js/directives/ui/expiration-status-view/expiration-status-view.component.ts b/static/js/directives/ui/expiration-status-view/expiration-status-view.component.ts index e3a5bd4a6..21092b951 100644 --- a/static/js/directives/ui/expiration-status-view/expiration-status-view.component.ts +++ b/static/js/directives/ui/expiration-status-view/expiration-status-view.component.ts @@ -2,6 +2,11 @@ import { Input, Component, Inject } from 'ng-metadata/core'; import * as moment from "moment"; import './expiration-status-view.component.css'; +type expirationInfo = { + className: string; + icon: string; +}; + /** * A component that displays expiration status. */ @@ -12,12 +17,12 @@ import './expiration-status-view.component.css'; export class ExpirationStatusViewComponent { @Input('<') public expirationDate: Date; - private getExpirationInfo(expirationDate): any { + private getExpirationInfo(expirationDate): expirationInfo|null { if (!expirationDate) { - return ''; + return null; } - var expiration = moment(expirationDate); + const expiration = moment(expirationDate); if (moment().isAfter(expiration)) { return {'className': 'expired', 'icon': 'fa-warning'}; } @@ -32,4 +37,4 @@ export class ExpirationStatusViewComponent { return {'className': 'info', 'icon': 'fa-clock-o'}; } -} \ No newline at end of file +}