From 1e683b3f1c5e70d11736cd26f401a39c71d99108 Mon Sep 17 00:00:00 2001 From: igorski-r7 <99184344+igorski-r7@users.noreply.github.com> Date: Wed, 24 Apr 2024 16:40:22 +0200 Subject: [PATCH] RuntimeValidator - 16777 - Added SDK version validation (#185) --- README.md | 2 +- icon_validator/constants.py | 1 + .../plugin_validators/runtime_validator.py | 70 +++++-- .../.CHECKSUM | 23 +++ .../.dockerignore | 9 + .../Dockerfile | 21 +++ .../Makefile | 53 ++++++ .../bin/komand_base64 | 50 +++++ .../extension.png | Bin 0 -> 16994 bytes .../help.md | 173 ++++++++++++++++++ .../icon.png | Bin 0 -> 7241 bytes .../komand_base64/__init__.py | 1 + .../komand_base64/actions/__init__.py | 3 + .../komand_base64/actions/decode/__init__.py | 2 + .../komand_base64/actions/decode/action.py | 29 +++ .../komand_base64/actions/decode/schema.py | 76 ++++++++ .../komand_base64/actions/encode/__init__.py | 2 + .../komand_base64/actions/encode/action.py | 18 ++ .../komand_base64/actions/encode/schema.py | 63 +++++++ .../komand_base64/connection/__init__.py | 2 + .../komand_base64/connection/connection.py | 11 ++ .../komand_base64/connection/schema.py | 15 ++ .../komand_base64/tasks/__init__.py | 2 + .../komand_base64/tasks/test_task/__init__.py | 2 + .../komand_base64/tasks/test_task/schema.py | 114 ++++++++++++ .../komand_base64/tasks/test_task/task.py | 18 ++ .../komand_base64/triggers/__init__.py | 1 + .../komand_base64/util/__init__.py | 1 + .../plugin.spec.yaml | 116 ++++++++++++ .../requirements.txt | 3 + .../setup.py | 14 ++ .../test_validate_plugin.py | 15 ++ 32 files changed, 896 insertions(+), 14 deletions(-) create mode 100644 icon_validator/constants.py create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/.CHECKSUM create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/.dockerignore create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/Dockerfile create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/Makefile create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/bin/komand_base64 create mode 100644 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/extension.png create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/help.md create mode 100644 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/icon.png create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/__init__.py create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/__init__.py create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/decode/__init__.py create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/decode/action.py create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/decode/schema.py create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/encode/__init__.py create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/encode/action.py create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/actions/encode/schema.py create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/connection/__init__.py create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/connection/connection.py create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/connection/schema.py create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/tasks/__init__.py create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/tasks/test_task/__init__.py create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/tasks/test_task/schema.py create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/tasks/test_task/task.py create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/triggers/__init__.py create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/komand_base64/util/__init__.py create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/plugin.spec.yaml create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/requirements.txt create mode 100755 unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/setup.py diff --git a/README.md b/README.md index 7c88f430..27ddd3b9 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ be on your way to contributing! ## Changelog -* 2.47.18 - `HelpInputOutputValidator` | `SpecPropertiesValidator` - Update to enable `placeholder` and `tooltip` validation +* 2.47.18 - `HelpInputOutputValidator` | `SpecPropertiesValidator` - Update to enable `placeholder` and `tooltip` validation | `RuntimeValidator` - Added SDK version validation * 2.47.17 - `SpecPropertiesValidator` - Added new excludeProduct field validator * 2.47.16 - `HelpInputOutputValidator` - Update error message from `icon-plugin` to `insight-plugin` | `DockerValidator` - Print full error message and change instances of `icon-plugin` to `insight-plugin` * 2.47.15 - `TitleValidator` - Change validator to print all issues rather than break on the first diff --git a/icon_validator/constants.py b/icon_validator/constants.py new file mode 100644 index 00000000..8017240c --- /dev/null +++ b/icon_validator/constants.py @@ -0,0 +1 @@ +DEFAULT_TIMEOUT = 3 diff --git a/icon_validator/rules/plugin_validators/runtime_validator.py b/icon_validator/rules/plugin_validators/runtime_validator.py index 04290395..9ba71258 100644 --- a/icon_validator/rules/plugin_validators/runtime_validator.py +++ b/icon_validator/rules/plugin_validators/runtime_validator.py @@ -1,8 +1,12 @@ -import os import glob +import os +import re +from typing import List, Union -from icon_validator.rules.validator import KomandPluginValidator +import requests +from icon_validator.constants import DEFAULT_TIMEOUT from icon_validator.exceptions import ValidationException +from icon_validator.rules.validator import KomandPluginValidator class RuntimeValidator(KomandPluginValidator): @@ -45,31 +49,71 @@ def validate_caching(spec): for path in paths: for root, dirs, files in os.walk(path): for file in files: - with open(os.path.join(root, file), 'r') as open_file: + with open(os.path.join(root, file), "r") as open_file: file_str = open_file.read().replace("\n", "") if "cache" in file_str: - raise ValidationException(f"Cloud ready plugins cannot contain caching. " - f"Update {str(os.path.join(root, file))}.") + raise ValidationException( + f"Cloud ready plugins cannot contain caching. " + f"Update {str(os.path.join(root, file))}." + ) - @staticmethod - def validate_dockerfile(spec, latest_images): + def validate_dockerfile(self, spec, latest_images): if "setup.py" in os.listdir(spec.directory): with open(f"{spec.directory}/setup.py", "r") as setup_file: setup_str = setup_file.read().replace("\n", "") if "insightconnect-plugin-runtime" in setup_str: with open(f"{spec.directory}/Dockerfile", "r") as docker_file: - docker_str = docker_file.read().replace("\n", "") + docker_str = docker_file.read() + docker_image = next( + filter(lambda image: image in docker_str, latest_images), + None, + ) if not any(image in docker_str for image in latest_images): - raise ValidationException("insightconnect-plugin-runtime is being used in setup.py. " - "Update Dockerfile accordingly to use latest base image.") + raise ValidationException( + "insightconnect-plugin-runtime is being used in setup.py. " + "Update Dockerfile accordingly to use latest base image." + ) + + current_tag = self._parse_image_tag(docker_image, docker_str) + latest_tags = self._get_latest_runtime_tags(docker_image) + if current_tag and current_tag not in latest_tags: + raise ValidationException( + f"The current base image tag ({current_tag}) set is not latest. " + f"Please update Dockerfile accordingly to use latest base image." + f" Current latest tags are: {latest_tags}" if latest_tags else "" + ) + + @staticmethod + def _parse_image_tag(image_name: str, dockerfile_content: str) -> Union[str, None]: + match = re.search(f"{image_name}:\s*(.*)", dockerfile_content) + if match: + return match.group(1) + return None + + @staticmethod + def _get_latest_runtime_tags(image_name: str) -> List[str]: + latest_tags = ["latest"] + try: + response = requests.request( + "GET", + f"https://hub.docker.com/v2/repositories/{image_name}/tags", + params={"page_size": 3, "ordering": "last_updated"}, + timeout=DEFAULT_TIMEOUT, + ) + response.raise_for_status() + return latest_tags + [tag.get("name") for tag in response.json().get("results", [])] + except Exception: + return latest_tags def validate(self, spec): - latest_images = ["rapid7/insightconnect-python-3-plugin", - "rapid7/insightconnect-python-3-slim-plugin"] - RuntimeValidator.validate_dockerfile(spec, latest_images) + latest_images = [ + "rapid7/insightconnect-python-3-plugin", + "rapid7/insightconnect-python-3-slim-plugin", + ] + self.validate_dockerfile(spec, latest_images) with open(f"{spec.directory}/Dockerfile", "r") as file: docker_str = file.read().replace("\n", "") diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/.CHECKSUM b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/.CHECKSUM new file mode 100755 index 00000000..06adccc0 --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/.CHECKSUM @@ -0,0 +1,23 @@ +{ + "spec": "7b971f1ccf8214be1b140fe212ade68d", + "manifest": "c4fe1b5757c0fbf72a28679cc6303e30", + "setup": "9d01465bd990dde0f93e26a9fbda8a33", + "schemas": [ + { + "identifier": "decode/schema.py", + "hash": "e8816b23112cbb301c9255c29323c4a8" + }, + { + "identifier": "encode/schema.py", + "hash": "70afbd79bd72035de62b32983ee57ba3" + }, + { + "identifier": "connection/schema.py", + "hash": "da5382221ca2a33a2f854e17b068d502" + }, + { + "identifier": "test_task/schema.py", + "hash": "930a8c07873126927dbcaf6d8e5226fa" + } + ] +} \ No newline at end of file diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/.dockerignore b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/.dockerignore new file mode 100755 index 00000000..93dc53fb --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/.dockerignore @@ -0,0 +1,9 @@ +unit_test/**/* +unit_test +examples/**/* +examples +tests +tests/**/* +**/*.json +**/*.tar +**/*.gz \ No newline at end of file diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/Dockerfile b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/Dockerfile new file mode 100755 index 00000000..4f5ebafe --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/Dockerfile @@ -0,0 +1,21 @@ +FROM rapid7/insightconnect-python-3-plugin:4 +LABEL organization=komand +LABEL sdk=python +LABEL type=plugin + +ENV SSL_CERT_FILE /etc/ssl/certs/ca-certificates.crt +ENV SSL_CERT_DIR /etc/ssl/certs +ENV REQUESTS_CA_BUNDLE /etc/ssl/certs/ca-certificates.crt + +ADD ./plugin.spec.yaml /plugin.spec.yaml +ADD . /python/src + +WORKDIR /python/src +# Add any package dependencies here + +# End package dependencies +RUN if [ -f requirements.txt ]; then pip install -r requirements.txt; fi +RUN python setup.py build && python setup.py install + + +ENTRYPOINT ["/usr/local/bin/komand_base64"] diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/Makefile b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/Makefile new file mode 100755 index 00000000..cb85f96b --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/Makefile @@ -0,0 +1,53 @@ +# Include other Makefiles for improved functionality +INCLUDE_DIR = ../../tools/Makefiles +MAKEFILES := $(wildcard $(INCLUDE_DIR)/*.mk) +# We can't guarantee customers will have the include files +# - prefix to ignore Makefiles when not present +# https://www.gnu.org/software/make/manual/html_node/Include.html +-include $(MAKEFILES) + +ifneq ($(MAKEFILES),) + $(info [$(YELLOW)*$(NORMAL)] Use ``make menu`` for available targets) + $(info [$(YELLOW)*$(NORMAL)] Including available Makefiles: $(MAKEFILES)) + $(info --) +else + $(warning Makefile includes directory not present: $(INCLUDE_DIR)) +endif + +VERSION?=$(shell grep '^version: ' plugin.spec.yaml | sed 's/version: //') +NAME?=$(shell grep '^name: ' plugin.spec.yaml | sed 's/name: //') +VENDOR?=$(shell grep '^vendor: ' plugin.spec.yaml | sed 's/vendor: //') +CWD?=$(shell basename $(PWD)) +_NAME?=$(shell echo $(NAME) | awk '{ print toupper(substr($$0,1,1)) tolower(substr($$0,2)) }') +PKG=$(VENDOR)-$(NAME)-$(VERSION).tar.gz + +# Set default target explicitly. Make's default behavior is the first target in the Makefile. +# We don't want that behavior due to includes which are read first +.DEFAULT_GOAL := default # Make >= v3.80 (make -version) + + +default: image tarball + +tarball: + $(info [$(YELLOW)*$(NORMAL)] Creating plugin tarball) + rm -rf build + rm -rf $(PKG) + tar -cvzf $(PKG) --exclude=$(PKG) --exclude=tests --exclude=run.sh * + +image: + $(info [$(YELLOW)*$(NORMAL)] Building plugin image) + docker build --pull -t $(VENDOR)/$(NAME):$(VERSION) . + docker tag $(VENDOR)/$(NAME):$(VERSION) $(VENDOR)/$(NAME):latest + +regenerate: + $(info [$(YELLOW)*$(NORMAL)] Regenerating schema from plugin.spec.yaml) + icon-plugin generate python --regenerate + +export: image + $(info [$(YELLOW)*$(NORMAL)] Exporting docker image) + @printf "\n ---> Exporting Docker image to ./$(VENDOR)_$(NAME)_$(VERSION).tar\n" + @docker save $(VENDOR)/$(NAME):$(VERSION) | gzip > $(VENDOR)_$(NAME)_$(VERSION).tar + +# Make will not run a target if a file of the same name exists unless setting phony targets +# https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html +.PHONY: default tarball image regenerate diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/bin/komand_base64 b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/bin/komand_base64 new file mode 100755 index 00000000..17ff2c3b --- /dev/null +++ b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/bin/komand_base64 @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# GENERATED BY KOMAND SDK - DO NOT EDIT +import os +import json +from sys import argv + +Name = "Base64" +Vendor = "rapid7" +Version = "1.1.2" +Description = "Encode and decode data using the base64 alphabet" + + +def main(): + if 'http' in argv: + if os.environ.get("GUNICORN_CONFIG_FILE"): + with open(os.environ.get("GUNICORN_CONFIG_FILE")) as gf: + gunicorn_cfg = json.load(gf) + if gunicorn_cfg.get("worker_class", "sync") == "gevent": + from gevent import monkey + monkey.patch_all() + elif 'gevent' in argv: + from gevent import monkey + monkey.patch_all() + + import insightconnect_plugin_runtime + from komand_base64 import connection, actions, triggers, tasks + + class ICONBase64(insightconnect_plugin_runtime.Plugin): + def __init__(self): + super(self.__class__, self).__init__( + name=Name, + vendor=Vendor, + version=Version, + description=Description, + connection=connection.Connection() + ) + self.add_action(actions.Decode()) + + self.add_action(actions.Encode()) + + self.add_task(tasks.TestTask()) + + + """Run plugin""" + cli = insightconnect_plugin_runtime.CLI(ICONBase64()) + cli.run() + + +if __name__ == "__main__": + main() diff --git a/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/extension.png b/unit_test/plugin_examples/good_plugin_with_task_sdk_not_latest/extension.png new file mode 100644 index 0000000000000000000000000000000000000000..abf154a23e36c6d87e1b294b3e7591cf20acf281 GIT binary patch literal 16994 zcmeHv^;=Y5^zNBqfT4$!W)M&jDT$#OLP{E?5fG8??m<$e5s(IHq?GQGP?YW#kS^)w z9zXZqKjHpxpXd9-Je+4Z=j^rD-fOLQy=(1xtF9_fginhP005Dqf~+P0Kq3D=a2)WP z!BtyR@DHA&f}RTi5Rm=*K!)f1uK)lED9TDb^Gw^BBS?Ebmaz8sd6QHg+RF+W^6pJ6 zTtYuGk?mvubFBp3R}$}D%XRHe&W*&&j(pFJ&-fmmzRUZ4C6IS6-ag`uKUtTuGz|d@ zDjk^U(AI}VSRq>;SQ2q3NWODSG=xcT1V4jE$*+Nj`S#l4FprGV-^`^_Eicr+Onlp$UT?d# zd2nmlKnjiV{2dvP7|yTbME+&t@BYR&1>}P|Q#FFy=jPF;EQ=5~6D$8;x} zXJ8GDkT_nTkvJ~c*qfcJxBYy>aFFpAp`K|)PerVuy7SuJ*{UVHfU_~2nb!3)^GL4R ztw-(^zMJSngtq>`@~VT8kI=#BQs0kg$Aq8h?6h`U#o|eJJEf}K1ai`7&*jYL?P(hF z5`N_Hn52&1x0t6m}hqf@_`!MOe*k}FUq)8Md6-A+^ZV zzWfoMqnls8lRjSS9rocEE2YCU)?q2h!`)#<7*TTm)bCN1yX)tn;^b$dt6z%cpx`|6 zsKOrZ+4pfBNYtMreWmq&rFgJL9I{|hd}RFNw~Q>h2m6Kq4iZB~u#PO$zJw}|Xb!C2 zkLBtQfv6GK`YidGW<8^Yz9a;^7)%lSErl$R9rqL1T8wdXrJqD*eWJQaw>EvXQy67f zBfzVLx|d)?zK8~5_Hn&TK`~W5B^2)D`^HdR=pCEL)t2o$W!|4+e)lZdj7i?^Oyb2{@$=yViKdgyhZnj;8}Q_Jd~aY zAj=pSzEECHCr%?M?zs%-Zxk?8G&x!mvn}6QTB{~_O$yBG3@czdAz2*W_^gKDxVW8( z`?88YPd;!MUU=M*lzqam^X)B*HxUF6AMn~SMP!Ybbns9DBu})36pjPR6aRcIJxbm9 z)U$zs;Q)9OEnGzd{F;V|VprH94hJDYoMZg7__y1dP;~8dr(Mj@lUinEHY97%=r~!f zl+vW2a0Ll#(~+_mb>uI3^gGk^rqxGedRUnO?ewGoOA`U$4{0Hpl?C9mPCwWmZ=}6_ z{PJ6Pv9V2W46`Q6-Y~k=VyD6FtJCs5uix0O1snM6w{>ZpJ7O3E267e2ef-khw#M&V zyLBYYelJ4;LPqj@j)O=V1JsM|v9a&HLuR?|lRU|ir=y9~iN;*rf_T z(ABYfH`m%NFd7D2yZKzoO;uW`*{*42pM!3uB;%%V3xamIuuw^(zH(VvAL_ZP4O_jb zw4M7p)orB?pM1Y8jjkKr^&!r%p;%{MtSFShNZ9#bZ~aXQ{n>mgKRkXCalE`4S=Vyo zENr34pMzAaaaxwsYqa-6;E$IXC4ErO%G4QoM`YGW3mkI0VBcbPpDMxMsCbmB&I)nq~IE>=Q90`6p9fz|E10s&BmH&3Bf#UpMa) z%!nApba$JUUA{;}HmY{E{kZcY=8kq*+>S%@pM$BIu)`^NCPPC4Kx8!FcUMyH3cHrM zrQJZmQRAJ7UhaUub&(^(TY?E9n0YrMh{VpEsv4w6mI0cQD#^~5GM7B&-C$Qg8*qyy z7o81RbZEISr72&iwf~t)&z!EG%Y|xT)<~;C0M9%xFSf2o!yMbZup0 zu=_GLdw%lfefX8pEcT4ZN;&HGi(B5FY*S@UkThEUm_vw@BfcOOw{Ej?&`=;Bt69Wdv0ulDD-5{5V>^7%bUGIjT)VrL2hAf!JyJZ#ZjRc=%=4d_AN+)jZVEu_ zP_4Mngwt}k;~*!Jw@k))by+zu$4uT|=KFS|hSR{76Z=C2A|?T#{1FDQTpvFr0z$YP zKHfvk``UDE@IhTR`6A`y6bB(9H zbxvkpn;vPZTZW$P=lwgB8u&&!DhAn=t&BTmZDDBF+}a7Px$yJsdUB) z{=1I2r6G`Kca3>0`QR&bkmLu2H9Yue<}A~Yr?AKumZgkF8GNY4EXnoEhTn1q9Jq|R z>IgJjmC4|1#GrN?3sJ;}O&qCEdB_7;)uJWbvX5(6v}#DVR;*T8u*QMsyfA%n++RLuf00wG6apJX9H{J3h-))T?BDQ;K$<{Vw zXuCSs7)q?D={l$Qy(EBmBy-^U@{~?gsuYVy1OC9&1KsHdNnwXoEq_D|CK}~--8oGW z`&x7Z32_~3dnL0``<^|&KtsKLuF=Egc=6GkYj5s@REV3o5bzJ{(Q<0{Tt?Z@&4N$n zK;(M7TrJRZ)V-|fR25Y(7aKstVM~V#jUk??b&#|#=zU-%gN!E{BUHs+Coc2NNCdGT zf9c}qCEDs;chWRUE;@c~`*Od0H?Pe8`$&0gLLWn-WMBxy369raCqvKek?vii}pd@kKDfwRgRxeVtPac zNM8*4e-3{HrZLqZ5?3nL*>b)4_zPs zaYy@mhPY3JA@aJ3ubLL=5MNLELK%yKRN}fE-)2xL57j)Dr%ZlSlTh*Z0#jJaz$K?c zs5>26H|~1+1o%Ea{@R|x#Cz3xdE)ifD(AfyH9;Df<&Cs}vm;|XPW%lw*{mJ}!kvs? zB7^a`rnAd3dr{9qjo&+B@WX{Ni{wurDhE4oYrGWYY}!2@iE3#qZubTx_!=$2R4m%0 zHKk#`%+7%Ya(;o~e&%&o{OcE6b-%0lUp3l zLgZlg1I+a6*A!VnL~o+_3VzaJ=3Ot<^)=z}U-%P_v^g`xsbwUC9mzDPS<548Y(`f2 zhY%8IZlWr-?d+@k_jxDS6{h9MV}_#|l%VW$G}`C#8x4-$zlGSs00YnfCH~g^C57y$ znOJpU5HKLTy$CSExAT62&aj7B0KX+Eh-U%@2K!KVE|dO5z*bN+4WeJQHbbM}lapp` znl`4)aDZowc~BeR36I6B<3oHjU6ry*{>`n;cBGL=X;S|zHf)V%u6X!mb{055fK?Du z>ofEyu$xMT1M8<9XDWj94%EG%Om_kERR&!+B}bY>MPNN+96v*{;ClFoM&IV3fhh)AB%ui|jug&WBKe8W!w_1EAK)FMJw3`nn_Oe8e-Imb<^>$%Ot36y_ifX3M;7 zaC@!4lOZsqv`0?;st{2j`pc*Q`yoEWnH9eN;-X<^pCP}$CWeY|GaN(wTb|RB^XmI+ zy|WQ)!4^tT8-EAsnk5ppA_L4o$a*twK$+RI*dCNuW}l{uD)X?Z*RfVEA&|doI**gPtrA0d-%xe_gG*Wy3)Eum@*5>@r-!l4*! z@8tjx>NQn3KYxz8E~~n|ZQxO_>g9fO>)npMhHJXmLC~K>$|485Ro#LpKO!0hOp4a< z2rEELoW9yYN%K4eqke;3s;s<0mVmo?M=7zo~wm_Jv`5}lk5#(5-b+s37HI5kA$ zI8uT-*dIr=j4%1W8iG^HnGlOZARf2uIP!{wm8X(Csdr_ALu(N*4C|nK)2Z*W!NK+3 zZCg=ltD@iu-h=MJb(v^`xqL;!5X{_L3D~4HhhaqjZyU4h2SJ!|y7f_vISi6TD6)4e z*(x}r%fhIWXZx`2MQsjsmo8j4A~qItRnuAf96>NC=rIi=LYUWC3W~XY`)$T-O?H+)_?}S{+$0t`R6jviPSv7 zUqh?H-dHLP^}Mw=U4IF84Rz_Xhwkfx)PTD*6q8~t0PDTP4jNg0NFDL@3qlGQ8>Ty+ zXb}{(bfaS{rGXc(lN+Kcgf2guD8YK(xlo{gj$;=Zbk-lqf^t{|E*mL|cx*AFqhNt~ zrtMkBf}Lc$vI+=CP1h|E^0p&qRJzPW{Shk;MBHNTEs1@KW6cl)A$k30Yw8zq?z0G&vuvBMQ(~msKD{J> zknN&Ch{ONuggDp>2Kn}f@%`AYH+^%_y*xSe7E2+PW2!11wY^cv2vpHJQ(~Yx3d2TA zWdrYhUuhD91ZI}%zHj_>jhb_7kbe1h>1@Eo*JzITnpVR13^Gx!XUNsGLN+4r&x1EZ zh$_o15&qdvrun$TEI3TK|Tbg{u%mlM<@X{V@{<6V>$yit6+PvGQx#3iu)D}+4 zlI9o9kDS`Yb=>^O>!T{UeAIQHkml>Qjq#22(B_fD!f9uNxX;PY_4Rej`3AR{4@5gV z^R@vq3`T4`j)O}I9uAzJo&=$}W;RzW421NZ!mtW7dGE_X50)NLM|jIF7XKWJP1AWx zO{xHI{@(Td&k#VQAc(?F5j&sNp`$Bre%UT_N^;)4eaoBE2xPS!)wfzs+}`Z8W{U4* z*WdDpCE1nMTVGT;tlQ{{n)ZNnf;v~z_zFhunes~AC^)XtD?k;w_m|~ZTh$x z_@dsl`ywVA%w5LHpCBIeg%%p`_elE7X4o4r`j|J(uN)Z~R@l#p-0#nc+0X!*d5yjP zP@=BqCRz??w0Z{Fv52sAj08%h1%9WG=5OKu5JUut4zaFZ z$vyZaIq_k0e6Wj(NRpuT@XIG!bC6|_6C5dT-kl1%?r`1Ssk}uw* zSSwq7u3lH_MH1Y=uoM3hG}FzNNu?Kq235q9h0Fo)NPQX_}Vp_0X@fdZUV^ zoaR|GgmcRFPRz4Wge;4z(K%(qz2i?Ik;86nsOTl-Wy5d+kd;!NNuw;~QSDY)FESz< z3U~!PT{^V0S4?|@9MsITa%~>bdylmS5L6-eLlRyma`<~5)C-r6t1Vwneql*8ntb{R zRbeCPmHulCT9i$+3Pu{EqZ!`G1Sgr7G8p<9-EgFO3j6PHOr;dEgdp5phTS-VPE_z@ z?-^EGbihHlZNOG(ud#tVq&tFU`Ns#%6&8ckX%=HeI`-yRz1@!~p790Bu>{`hwOM`R zL{l+^DVUf1@O9tXUu?m6<)j}q)maud>z2eep+AS{9Mg8*AF=@VpqM%ozRAkoBuLye z_*uKQ*RQGIS>Ji7EBH}Jo)&5}vTT)_237h=);=FhxvyR8qI`DHF7W$6>V6PfH+$=+ zAw!(Z%$eL2X@x$>*7r)ja_hY|Y|II#{6Adgc@K1Z=L`m^J**+bZlYB$+wf=s`=;~pxO`Va z+?H2&aHwPu4^sPBpq0y$+O%)SOdCRML+%>iD?Hp06`eI=Q&-4zb*R4{peBV|As-3= z;ZvW`ev?YZaKsxlTN)wsw^HEi)X$PJmStSr0>hU}3CFv3urcy{oG2JDM^TveNi~QC z(tgdGz>J1NLCyzmThUB7D#cKx#r2l_@|Q;fn2O^;=*aomVhW_%CjJD`V)%h0Z7g*K zoxqRVq)01b&?6%}tyGR#Ft9+Djtjsz2rYlsJR)0UaoE|mUfe4gY+BAL=w95j-WL+EAuqe z(WUT3h^UX=g5qO2DPRymVITf7zxjno+O9Jk%-?9H_E_1UV!s~(Mh+(5d@FG+l&!ea zcFf1IyRknw8xZyCRXyE^+lL?T5lC2|(ldHkhs_+lzY0h8TP^vs`E{a}yjnBCS1GT< z#M?%c8%y}_*hebu1>4Vo>6A!wZWUO%3b;chciljKPc?3J%OU*2ywuyY%jK(B?a!48 zk4LV2Y;#YuXA5q{IIphOE+e>NXv;FW=Ys)e__I$XNQA`S0b+-n1b2sRcxT&oh8(;@ zBhZVziGA}_QZ&gfedJh@hgSWAYJ^&(HItVXSPrvx#6e`&m5G2}OD-*-B?|M9gV zura_m%GCVkYQ;lKk5+q?tMnChBlE~NoTu{iekqD?9Pzi@$O}?dpzRs!zxF-2%iNt) z&|AE&^8vO-M+0K=WdN-K|rzoGPIp;VX*hfurkR4Mg(-8+B?$lv54bf9JpIR=(|{(IMWFt%4LiA`ef z7832W=#-yo-EH=!h#it=v!^1K&>;67?v}Fn=Xfh9AjR-`)^DZqBC5-cq)4c8Iu36W z_95?Qt0of2zr1BjrUZ`|>U1aw|Jd}F_1W+yG(rV$UB!C!SM5hns{$(>aR!FLxs#gE zRxT9v`Y(UW9iAcvZMrL$(T}|CTCF?J?}*@M^+AcalhR`QY8dS#vxd1wW310~SsaBTpqRbk8d9}sE16uKpY}9q^6)}g zdMPvt?=Jj9G!js#_(}8~B#G5RSlU8MH|(Q{q|tbA=yxduX3sTUH12n@`BN2pvS;gc zMv8}U2-{saE3OnEA!@V4I)4lE{Bycx_XzJp1+rqsyeJs+7y*l0=FtEchx9E>Xr(P|E?N;SZQVRS<_6W4QAz%2u0IF^N4?lMffSkuYj-QHi^;776C2>^r8& zy;>OxLNHVw(rF_z?tg+Y_@uChhvPj1Nk;KK`r`L^0jq-4x%C!^qj66ZvBBEp)&mRz zT1<`;=wrvoSBzA*v*=J-!NS~BE;~1>Jqm+E71*yYXMH=B3oaIOgzN}PW|$0y|Mx=^ zATjIL=TPbO(>e%aubazIY`ZA}tyn&I+umCFmfEpN!|$}wUFm_=;W1ySgV+0m>)kE^ zL6ql1D+)9mG6b)!QjtXr0{gk?s}%BXDC`&}%8C#$dyCxmX{*+frYZYj$gmxXTWbYW z)X}1q7_br2XxBId%n$^GCqY0A@qdVct0mZD#A&9+_6q|r4K_|p<=woNMh6=bL%DD; zVvSN5YQ+82RLZ<$D(H(tJbZ{z!6j7eqGIgAiQU7-P8$8qkPym+ixD%B!oV@PBhqcj zwYqbE=qU%;G1w#nwawWhUkz)jOT1o6V<1jAfn}18ig3WIrT_=S+VFSPKo4)Rj@lYv zeT0+te3$7ijG9ab;kokL3*?64py-T9WSb=rr{n_}q?)vQ`t|)X9I$rhn$bVGkdekn zIl%%;4QgOgi|3RB_qc$SWD@8%fOR0`j`7{K3{;3mzeWS6W1kvRWi`9HjtG(Za9mp% z91_|^30vC5Qs<2SPh_yvoZ$zd&*uQB#Q2S0=(Tc1#g|ypBFl|e3WAm`5rqT%o2~vl zkvcukI2NyLG7eUXEsg6O0)&Mgc=?n~(9d%Fo#@*Lr%>anQJbDVBVi(*4@;48_kOhA zkC`S#*#F;9=MFK+7Uf9B>1BcG#*HIQytx6to%oD^=Qy@=G;{A6XSu)LiVJ&oC${fQsV^AKnD5)D-Cr`an|4{H)Q(13jwlsPT_+}(VRHtS7qC^lcRe#+yD{^= z7)Z_RtLgu|iVcTQ<#B>cDxu|I_s$RwM*UaI+^~aj4*@g2Dmc7pI9#}@zg^J;&ZO&&AU+)m=!ogB?-M~rr{Ha` zCMVSW+Sd*TF#klAWhnBPXTQ22h!ny=vv2wi=%KB%%By}(qYp3qSK0I2h`;@@mO(^Y z(IvLGFm|PM32_ZWASyx1a!e5pt--=vEj^t*wgmPd6tM`` z2?8LCx+2^qBUqQ+FCO5A&Rn;ZO`T?pEuWVRWJH(0Jtnb4|zwnctB&Y8*}l*4ERe5C49!n_2fbK-=5V*rRpiQn!_Hq#*Hk+!&j zxlg5y-((@w>7aU#U}+ik-9Nr4kI=PBXn0JZ z&sE&3*bcy!kXNJYL+#~Z_K=a=kv6`$;rf$s|TRLC%vmlD{ z#7e)>hZ%)>cwppk)S-sCviBMZsts4uVoqK6FbUeG(VfIq6k>z{rulF@Ubvsi5+ES(7~lP4{ldU z_&WUoMrAwloBh$CTKtSHF2D?{yI+Eh1SlpoBsGW$ z8Mc3(yB3s}an#n85Dvlhp?j4hK~nD*Kwc>M%wrdXc*6R#VZ~OIn@85QurdnfHfB&% zQ<09>hY@juxajv-wSJpRi(CT=x3zVU(aWN#J_-lTu>y5 z9fYvly0+DNhrXJ{gccw2 zuehR_G+VzqZ&>pj3ti&!Fc^>z9s$Ph;m1$n@i4*Scm%RWp!&8{c@l}CC4MH22=!WQ zRT<{RH(_vqbdV&J25g~=6>R%+7glFhQ_834%wa+V4XCqAaW$0ql6oug>?BH@?d<00 zbiI5TpJ-YJ`2+NB^CDJ1=CsR`w&K!%@L-*Stpc;zq zzWk$gm&gaQ{-G_Z?>oG;gItleLQ(G z-qt{7(lOPwr5$&7f$XDn+t-cZtSP6`OYS?&NPjJr45{gS;8Xs@>$XHuuq9HLbrs`C8G?Q$Slzs;ZZ*o-Pt_wS1fYtE?C2_fER)V7=)!&9G)nKh}c+4*5&x zut^99@`gwQQ~Ix+m!76ZFmdQ6j@P!*Q@^md(iM8S|5?qz052;9Gr~8)dh?^TVo4Yu z@^pgn#4~yMpdtWtqkmIhFM;p)GqfdOViT6|QYxKFh~M&K(hN!T-h|b$C+N!sXar#l z5Ql#@19rk>L}Bt2N5N+w5Gpf6(dIR?u3m!{ZJ~ueaMmt7KVm&mt(g!E*v0ahuvEpp zVkEJx1hzi5Y}&qXmiy1ta-#*ihmH>(7+1)*6N>s!tborsUH)dRFoJ&!O#uE+?D%w4 zbW3p8*_&%-n%<)Pwvd-0=JB=rZtPI>Ba#g3_(6*WfoV0g0eQ6l3eCod)d`M~JxQRm z=<@l)DCsu-D%d!HW?tl-?j)L91$+8FGl-NckV2PgMK zwOhi_YiH3%7d}>WvyDhV7xZ_l&R47aAG2*-|ZijT!z+@6dY|tMVqeZ=brB|LMwOAEO5rXWrfn1ohVEg&e)F^+RZ|dh{Y&jK{>44VB}!ASuNpo-StOq=}uD63?533humP&rzw0rEqX$+QRn-q z*0jx?B(M_;hfb0>s(>?40LD5Ox2>g3069%}XAHsMwYy?`b|2}UTBprVKE(r!h^CJc zBm9qOgwH3q&-XJZ^2A}bJL5to1e`{8VCu4wY;#xKdacEe@^(xGPgA=p$??aIR$IyyO{TBj)`-$wUo z1FpMX8f8RzExbdXl@9BxE^3|ww=$NDT0;Lwf2F(FvuMO(RxUQg$Xn=OZ51`+l<MAv%Zi^)_{IzqdT1qu zF2D`tG~R1Vp0ub81AdMq0A^c1tL?|>yoDKn+Lk$H2e&4Jao#F~dJ=RRj30Rz3g|r& z<~CDB0%&#Lgih6m3io}4u*DJX$~(S+*w6hLkNEl}4U?*nJWUbO zQqAYMsGG`reYrzSF{Jm?SgV7gN^-`HGeunMe9x+!oR4$aoqtdAN_YA03Mcen3q+xpYBuTC-9y3&a$# zKy=caRQK16^80WWnk%mMNd682j?2Gl(}W1t4ewh+P>Bjt|kpi}4J`z=n{6 z^41Z1Lb{ykOnd#|UimLnA^{{b2s6gJCo(MV?j7JB?_7(!}mT#WSSwx)CoH0=hq9qK`iESHF zaD@jtL1;CUxSx0Q*WuP-E9_M8=CUtt%jVvzo8JPMz*O2?>k`MNXXFJ?0#hK7M!%`2 zhKcx0KF!))IXKba4#9yKX|Heo2weAu*NI-l%QldqMWNapAR5nW#L|9B0MRM)1P#Ix zFHNF^SV4#g8n)tiw*}AW#NrJX$7o*YfW4=l`<_cVol#2|#(sRo#>YGf^jw0lVsImR zfF#-L;0O_~Nhi_EjfNXkO|#MSK;t6tUzd~i1ckz` zag><~G!?g)V_wq&4>@6@_ix`z@Jp%Ec{vZ2`pKsoa;&L|8OdeZy4M*J{6my2LuM3U z>BgtW*m}paA`B5jRtC1``TZgh`ICgOmW15-19~}$2G0H9%KO@`zC-|1hPuvh%Z){( z=r2oz?*5>!K9q$l7(+3HM0ilFSEA;B+Y>>j?I*|Bg1L?n)MKX@EDO`+9Jh$Q`{ z9i%MgH<+kdf^cL5mVGzz8aV^mVah0J^pDZt+dD}&bK4AqU)IDwTjYMm=@*UQ;WkfA<`}6rp1>8dQ>f*! ztLz-VEY)7)lIU|!i@JQHwn#z@%d|0qDoFswTw$XKlfwbZ%Ksb=S|55Dh=_Sfi*@mh z=kcJ?%%bGBZ_h>7xD~`mcyWnL+Smu4X6r@!>HH%3aaB#j?`>y!Nb$9c(a!cdTm(8T z{nR92lwKk&;*Yt|Cy;(lg+G`0f(LATVM(%ct0WU*rp5z+HmzXr2%Fdw`2X|Ev1k;3j4kqUO`k#62r5&;a z7=7p&9S|;-fn(@tg%m_2Jw@)^y@wk{e1~`c_gNpMfb+~)7`YRXb`wf_TBq4`rCI>k zKO~~)aRm^SSTbSX+v7u@_>)Qg8tW}<7I^FFv*-7z^~=OP0GMNe0mPFotHQCF-^#@A zo^rb0&iSqXaxn+n{N-Nw4A8_!G;zgH1p{r6bS7$t6;}Uq2Z65_&pj4?qfRD%M6byk z7573&lym=2X8K*98aHa6>t+jC$qr~P9y8$hZI>ju z$YjDQQ1m8S&fTryc;b53+^stw!kUIZ=uPmSNKriX5j%fSvb&e{q8!*a6YogAmoqKA zI->R=8r1`=WZ(e(mw**&vuX(0IPUDBLge=Jojqe828iy(x=2pXU=gJ^7SQ}}ZoC5j z3RjC!m#m3JfgU78A8v0}beCBG>xpwFUVlO)5gdY%IocSL9RS%K0msgTcP0M6=8bOy zwy0TZ=R|cgF7NOW1>$c!Xe%oU2C^tXP4J3xr`ulzSLyB_O2(LYn;fdTM9M6P3#sqk z99tc9HP-$ZR}qb5D2R!ITHa9xnse-U7O5GCs4uq3HZFIGm+$9~V5zmbDwH`C#@cZ0 zaTjFNamP*gxhyCSUktw;KB(mUYBR8^@S>6;0t)uO8Dni`IbHqab;bc!KV|q|WQ8y}jFx zt?O5ZGk59yPZOK2x*?=fWHVi(36v#p;K#ouIPUL}V8dd%Dm$90MJ1AtLOYTc233O? z4%DIk3hw;Avw@XBTu2AHO+%#Qy1(c3{>cZEHMWr;34;Iqe7+C6^JsB%6-=E1HIpZhqhFmTtTsS-3@e^H~EG3O~d^0vKH3+S*0vhafe zB*eAlvNS){^E3XsGkO!iY$BkT;6d{s3Vg=wiNHS?^f4TQezP7qaEe3&z$Z{}vhKD2 zE}v8)k%32Yg&|Iz0o6P;ucZYyPKyNq7=Y9V-=oeu2+DEWUXvRtwja8xE);Nrqz^rU0e_10zB_G03_Q{e*Id6%gT!J zwc&+BZEaYnkQ+WPRO@@e{pZ`*?L(PNqqstSNc!c1*M{qqz%dJP z8qp%IG!P^0%>sIY^T$W^Y*I+adTr7>{oJ{uwftT6YxLUH@oM=o0eA}tQvtPgBAv{J zznnX!pFchus~$d`W9a6_%%4@42dZjVK7cIZfTqpNiJavOHqM2Dtv)puH?qkFag*(QioE*?D5OFN&s zdGz$)yHAOVto`Hj9TI;#qkU{3>Sga^s4_0=2+Fq=0Get?PYDp%(r741L2$oQ2-};0 zU?y6YFR{a+%*gti%)L7RrnUyGUfVTYkFmg5Cz6y)$pE0|THHaMUzZ`q1nd=|BCYQ~ z-2&P;8o^=XJ^OOn70_RQz2pZl7A@U$jGzwQghU42wEw$@E)^_G2{+9RF~e95xn?fB zIOG0aivSAZ>5VNZwZi(r7}vURD+s`T`peOa|E{d5i4u4qK!WA2i(e}-plRjCHRCPe zV0Q$E{5xXWU@Utx3ceQk{KYSGgZul5DebQ4cPn2@MQHa~7Rs?Oqn@H^yTQ!Cm=9pz zwZdDSCo4Z4DJcj|MUZ)5Z^5mW?2Yz<*+Mxs=36#S8<`wb^I?+RwJ538@UDJ~F#+&4 zTP<$^2~2h>U~s|v)xNY)VU`8A)+qXbox3nk5JElUzI*5z_3P9I(#C!%yHdUZqb8BY zSm1ZG(ToxR;jvU#e)rQFWR>BN46q$@EQHR9|6Q*o_=^DqC*id?#g+g7x%&6J0KH55 zs|(%O(M-%hAb8I8r=!8ri0mtT9c(mAFkB%le7$Ck0Mwj2SFr&c^6$_-+nfJ6y8r+8 g?-oSMuH0fhn?^#nR6ja_{vA+!qAFV^ZSv-S0rVD`p^XSq zB-GG@3WySrC@LioiqcDfgak-{FV2i}XRYsk_uswi{`j(zocHXrpS_>=dH0)h*3M0f zi)STw$m{?B0FtIAr!E5kn>gP;+eC#H+Lysw!ovZ=X8sq&iidt$g5E`NZRn&&7LA2md9nBMpP<03l ztgZ=$z(EiVgz&AYuK4FmNtn&g%^h+1)R{kX3D1y9o&m z+y76Ygpz@SQ9fXZnmQPV`#!JVz5NN7G5?_PPrd!ELVPgb%NT!rfFD}89`4FNz(Vc* zb)xT%LN*9YyMPkPZZc0%^L#p&%3* z3I}OvxVl0xt{NyfO#4Tie};!^K#VoCPoLC=oHm9)PQ#2~&@-pCv|wj6HMC9{>%e|s zO}+gIC~q|8M{cYz_n%m!e~CpH`(aQ7yq^^w@Aab!EIjc9yuT;jN72~$`>JcHX(=8v zL!q(W-_6IrFV!D2KZWtb24dXK_~CJiziW-a{u3Tr+87853ex~-pkZi`CJcfGp}v2? zU12(!Fm-Kp6dbPfC*JLE8vaqMLW^)NK0hcSv``u_jGItVS1q^>2m;ZDgLE_@njknz z2cqfbj&a9mW0b%`is0|%^S5&PVG`Ep_u*fgLwNGn_QH4z_mrP-^H@|6;Q+vX8`D!p zR>6HU18V_YCQrqNhBJ5@OV*M30Lx9en@?`lk=~+W@;uVQ=)%@R(k{Xw!15JQ_3i1@ z4|kc#womcBqzwPCNJmCl&If^e&WDM2v$Hq=Ads=qvjw0?-vo$~0&F7y46{Tc4aERk z5I=02z~5L16aVKl z_`fs#hc)=C82-1~jQ^72iJvuqy?UBRYPcBD;4ZCSzP_|NKV4a|5*c@7`%))@8u&_+ z3Tt%I4#e{_`kBnW9T879h~Y0*dW%Zdal1E5x4Sm5p>fQjk#R8E!?nn0#UlNQ_L;Il zfyR2xNgOnk3G8mswDiOwKk zb^}1~i0R#OVj*+mZ@;WP9RSngDH5U{fR6Mx>X}O=MQQ5Ax?W?iCfQCkxLH*1&^?<4 z#W2TXzdklK7du)~cckAbdvpMUFX)Ck&pV(BY?i`?1H@Ba;IRJ^>}d7Yd!I)mXwVRr%OLg0MOQz z9;Cgb&IC$d`E1Fo6(9PjsloDMjff_&^wWo#-lebFCy%s*J|Oglp5b;!quP$ZeS zl?_%?Fe`Q61VF6#MDv_j-?gNw7RrI)H!!&uhGIiW%#gNk$Lx|bSqp)0MMd79m(k!f zXX&g$lNz4n#YUvQ%cBVPzCm_i?9A0p^cF6l3rUU6GtamXoL#G(v1FbIE7aO(Ku)BjmP6+r=&0THrZty;ubi?6D<|Tqy?i1DtGkWUOdS& zf68cXTkJ}>5iX;;db)MG;TAvaCrXkE=?k0kxOvdZ`knSKFL?2`bJ`*)Y=!NTl?S;- zgQIIiTA;1>b`JUQ64TD7lL#u#26pX#s}hrxy&m?4y}Y8Zz~3uE?#2p00Sf0XLrYWi>XhMW%E2P(q##pErMaUA8mKo7MuzrY7(Mh>;is@Do^s95EG5bI zr_EcfZ*_*&Zwx;C1XAF<-?~b*CK@{qrgJSO6H7gaAB>fi@z0<2#nf#&+1pi_p*O#( zE6CWZSvRNiEt32|ej!Ocu^h?+5w|U+ZfCtCJ}*QTcT?`pJ8!-JR-z4eePDl6{)B1v z4cRUuN@DgBNRK4T)PwDJvhmu3nGV}kD|gU-w7uERMD}K8l2fPCcx+@FeoaQWWnc8V zv~`0Us|2Nd8hdRWngi6YQLSO@@MtNUca54 zuLr`X0zzLn#;n$!0c$Neja(Vl9zPhKQe703UB~Fnz3G80!*vnn<}hKP*Yl~`i{-nn zL^P?)-KOne0b?~}Zd>4}95rJhVBIE3zT!PRt3%Kx+MF@3 zfb(RK@^zZrJ<@&sUUmN@xx3Dpte};$)Jg94ikwX*qh)DnElVZ~#ZSXG)O_vDuR3{i z!<|*;*j;;5H!4TU$mkVM|GEv!1?>}}J-jO53_qj;O*zIFe0aI!WOv{ZW!W*$FPBNv z;;dsDC^id~MfCd)tzD&^BOZmE%QiUGJ1pFHRTdMBn3;^r};u#tMy60v<$ zWVk=~2p!l~D5c8Bv{($r^Ybike8tdc7xL+iyo&YX!z<;nOJZAD zB^p=%oORi}fpbs$!V_+ar7SEDnHp7W+)%WDrv^rmEK-nMhxe+#Zq?~sn0>O#8kf1~ zaI}6rq?0jR8s%pucHHdbDw`T#@Tsu>$ucExP&40x)H&BMr4KJH6jSOrF9R*4cOE{X z;-f$eihDl8jIZ$U8Rt<3XXd$%@uahYTq}y*Hg1TS@9xm4-WU-1Yhz<2J>kOak|ewq z9uebx&V23paH$TqRR37ob|3kILAzu}%ZLwtHLBC;vfJ|FNfvRzBXmSg=6aYeJ@E0K zs~Zfb`!NBt471m@OG`e#{)U-(QKh903J|=a1BXr0HNyK!7I^isYwQy_1G!I@!kk;e zYw}A-xJ2n=Y3&{dE+{>XC%MEG%;+EGULBL$NYty&jITS6oVF^M_R%)rb}o-wG`vo; zAQ3g}yUC^6l){HjZMaLvhd-SafnhRZmNO2vb=Y+t0;gG6ga>JMDd4tbT>U`{0X3U4JDwbfKr{v%$0vjVgSz;|XPp8q zd5q5{B$f(nZmjf768+z)vNso$OvpeDT2T^kzBlL`#&ov#{8vJ49{{ugk#Uc)W!~#9<156gjYUsT~iZ#f-Vw0Qd`RW#SpI~o--{X&aN+o zeR7itF&;W0I)xLIcVsNK4>NkoqS5LB4@d*AidpBlYlz@4HHHR;+PWOwDbZI-61=Pg z$b`Rp1@x@1v%vFs&rzx2H~K;I>G{0;?vO+yQtk_~hE#y=&MOMfROwi+^6^Gxakh9} zO==CT@X;HW#+_}^u!{1p-k(1N5SE=m1~E!Ax2RV{z~Jepq2h!4a}SZ743d>cR~wk&O};UmetIx;)hqQ?s+{)m z)aru7yCIK#W+)+Lo}4ZK1` zYwM1|$L9VkL7p4E6NUTd&`&6^h$94;tgU|ID!t*W-2~4Rjqcl5=%XFHP#qg*_Aq>< z;cLKo$>wm%UJc?zxOGn5qHe(jP|b5X`UMm>O07BJ6>V4KkQrA;6AR}Kb-remE8}b; z?QE%t5_!_TVdo%5XA{bP>lR{Cu1J-VrLl75Z|9H$6p5y!I63>BdX&3IZ{97&R?8Y& zcx>?xJf77SZTbb2LJpsnz9>C;Y1w=CHjaKrQ;BkxrTx6Y?!i<){Pej;HkSi5Iq@gI zPUXDTFJ8@oM5)n(va{Po9E@3SXmWO>bED~f_xFsgy}dZzSg`YBm&Gj9;u_0?zyGlJE|ksVcS-+1y}$Rz$w%elqEn-snq{?a`l1!Ca%g$$7Ee77Y|Oy(9eHv=r4} zAqPgt;90Eq^~Ki|%Jl)gZFWyY$4tF@E4w>i?992X(B>x3ToD|@%k_5o>uKUkpYyZN z3|e9-&c4)NhwxM&}g28E=T*Np((oo^$PM&Grg<;#uD;c{)!=1aH0QXEN zDD;jQcl=s4=gG*^ORhup@k2d>HSJp@I>( zfyIxs%zSJ0EMEW3EW#@CVpR;o^<@^8aV2&!(4=@5&BW$z$0yMTM(EAyiNwk9juOoz z5MvK5B|59qC~}|3(t8^5EMM~`4dc3E8`C87bJc|~XPZ>z%hVuc`c}k%s6}_)k#ZJ! zF>DGb%}n!KElxUf{)@(j%p1N-9CrWMr7}myFL1I2wNk?5Wdqm5ZX5XOjkufZmrN?QHm$ zLK9?ocgy1h)9fm(&%yjAQyPDsWM?}2OhNLKWhZSTK)`pS@%yex2yb4mJU}VYPn!ob ziK3gLE{f|@_FU}h*`0s&=9dG9pAgN|faNfi_t=?V1lyVrKogiKdHX9*sB#atV)>p#cD=*#qoVr&AUcSR)zgqOAq0Q+|9cT>au$6&XIxmLw zX?dp1XnM`RMZHkh=S4Ni1EuXT&{oydDiu0?J6Ye|wbuonW`PrZX*UDP3@l-}$Cg1j z>~Wi$tsg5;%6V<06=u(2us9^=8<+dVy2lAJ4r~r~(lOa5XSK6%na5ffI-%yzEE!K* zqC08!OKp5wcCeld7Z1Asuz!zAf$1_!EUDd|nF~y}d%MBl$=po~DRP*e8#K>enWPKa zTYh^D3;uGFJnI;|EH5a-fYIoZxdS(feyzVN*^s+wU8zEMHFTHkzI;@ugYSk-!yRAP zZ-(H|1bXYexVgosi_PbL-p)CqABQ>vssa@@J5^fbXU1_DBdaCY9`T}D$0u8e3)&tJ zSFbp%BOp3un8xA{$zJ968a?UBj;%XFuzA55L&-eicmJx1A-{VgWq+)_!ap z$njp$Xs?r*73kjyy*OL+n}K*^s}v_dyxZ3kw(QfLqFa&Qs=ovPF*BuazNT%e>OjOz z++LPJ3^C&a!TvbL99n{%mXUIhzLhvCZ*pH`Z0Z+Q*xtVIxp{=Ok}>Dp;;m=1m-41V zji|@5wx8tY2~KE*)P@LYRZe+^jCG6Cq=iO|^pvg-ZCU0C-~~HhRsbe2iWFyj7h}ANr None: result = validate(directory_to_test, file_to_test, False, True, [OutputValidator()]) self.assertEqual(result, 0) + def test_runtime_version_validator(self) -> None: + directory_to_test = self.GOOD_PLUGIN_WITH_TASK_DIRECTORY + file_to_test = "plugin.spec.yaml" + result = validate(directory_to_test, file_to_test, False, True, [RuntimeValidator()]) + self.assertEqual(result, 0) + + def test_runtime_version_validator_failing(self) -> None: + directory_to_test = self.GOOD_PLUGIN_SDK_NOT_LATEST + file_to_test = "plugin.spec.yaml" + result = validate(directory_to_test, file_to_test, False, True, [RuntimeValidator()]) + self.assertEqual(result, 1) + @staticmethod def replace_requirements(path, text): f = open(path, 'w')